package dasherJava.gui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.IllegalComponentStateException;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.plaf.metal.MetalComboBoxUI;
import javax.swing.text.BadLocationException;
import dasherJava.DasherJava;
import dasherJava.DasherJava.ViewPanelBounds;
import dasherJava.core.input.InputProvider;
import dasherJava.core.output.PauseDasherTarget;
import dasherJava.core.output.TextCharOutput;
import dasherJava.core.settings.Settings;
import dasherJava.core.startStop.StartStopHandler;
import dasherJava.core.world.SquareWorldView;
import dasherJava.core.world.WorldUpdateThread;
public class MainFrame extends DefaultLocationJFrame {
private final TextCharOutput internalTextCharOutput;
private final SettingsDialog settingsDialog;
private final JScrollPane textAreaScrollPane;
private final JTextArea textArea = new JTextArea(3, 0); //show 3 rows by default
private final JPanel statusBar = new JPanel(null);
private final JSpinner movementSpeedSpinner = SettingsDialog.createDoubleSpinner(0.0, 10.0, 0.001);
private final JComboBox<String> alphabetComboBox = new JComboBox<>();
private final ChangeListener movementSpeedSpinnerListener = e -> {
float newValue = SettingsDialog.readDoubleSpinner((JSpinner) e.getSource());
DasherJava.getSettings().setMovementSpeed(newValue);
};
private final ActionListener alphabetComboBoxActionListener = e -> {
String name = (String) alphabetComboBox.getSelectedItem();
if (name==null) return; //no selection
DasherJava.changeAlphabet(name);
};
private volatile boolean isCurrentlyShowing = false; //field to be read by other threads
private WorldPanel worldPanel;
private WorldUpdateThread worldUpdateThread;
public MainFrame() {
super("DasherJava "+DasherJava.VERSION_STRING);
settingsDialog=new SettingsDialog(this);
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
DasherJava.doExit(true);
}
@Override
public void windowIconified(WindowEvent e) {
isCurrentlyShowing=false;
}
@Override
public void windowDeiconified(WindowEvent e) {
isCurrentlyShowing=true;
}
});
addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
isCurrentlyShowing=false;
}
@Override
public void componentShown(ComponentEvent e) {
isCurrentlyShowing=true;
}
});
setLayout(new BorderLayout());
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textAreaScrollPane=new JScrollPane(textArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
internalTextCharOutput=new TextCharOutput() {
@Override
public void outputChar(int unicode) {
SwingUtilities.invokeLater(() -> {
textArea.requestFocusInWindow();
int caretPosition = textArea.getCaretPosition();
String toInsert = Character.toString(unicode);
textArea.setText(getText(0, caretPosition)+toInsert
+getText(caretPosition, textArea.getText().length()));
textArea.setCaretPosition(caretPosition+toInsert.length());
});
}
@Override
public void deleteLastChar() {
SwingUtilities.invokeLater(() -> {
textArea.requestFocusInWindow();
int caretPosition = textArea.getCaretPosition();
if (caretPosition<=0) return; //caret at start of text
textArea.setText(getText(0, caretPosition-1)+getText(caretPosition, textArea.getText().length()));
textArea.setCaretPosition(caretPosition-1);
});
}
@Override
public void deleteText(TextRange range) {
SwingUtilities.invokeLater(() -> {
textArea.requestFocusInWindow();
switch (range) {
case TEXT_RANGE_ALL:
textArea.setText("");
break;
case TEXT_RANGE_CHAR:
int caretPosition = textArea.getCaretPosition();
if (caretPosition>=textArea.getText().length()) return; //caret at end of text
textArea.setText(getText(0, caretPosition)
+getText(caretPosition+1, textArea.getText().length()));
textArea.setCaretPosition(caretPosition);
break;
case TEXT_RANGE_WORD:
case TEXT_RANGE_SENTENCE:
case TEXT_RANGE_LINE:
case TEXT_RANGE_PARAGRAPH:
break; //not yet implemented
}
});
}
@Override
public void moveTextCaret(TextTarget target) {
textArea.requestFocusInWindow();
SwingUtilities.invokeLater(() -> {
switch (target) {
case TEXT_TARGET_START:
textArea.setCaretPosition(0);
break;
case TEXT_TARGET_END:
textArea.setCaretPosition(textArea.getText().length());
break;
case TEXT_TARGET_PREVIOUS_CHAR:
textArea.setCaretPosition(Math.max(textArea.getCaretPosition()-1, 0));
break;
case TEXT_TARGET_NEXT_CHAR:
textArea.setCaretPosition(Math.min(textArea.getCaretPosition()+1,
textArea.getText().length()));
break;
case TEXT_TARGET_PREVIOUS_WORD:
case TEXT_TARGET_NEXT_WORD:
case TEXT_TARGET_PREVIOUS_SENTENCE:
case TEXT_TARGET_NEXT_SENTENCE:
case TEXT_TARGET_PREVIOUS_LINE:
case TEXT_TARGET_NEXT_LINE:
case TEXT_TARGET_PREVIOUS_PARAGRAPH:
case TEXT_TARGET_NEXT_PARAGRAPH:
break; //not yet implemented
}
});
}
};
statusBar.setLayout(new BoxLayout(statusBar, BoxLayout.LINE_AXIS));
statusBar.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
JLabel speedLabel = new JLabel("Speed:");
speedLabel.setAlignmentY(0.55f); //move label slightly upwards to improve appearance/alignment
statusBar.add(speedLabel);
statusBar.add(Box.createRigidArea(new Dimension(1, 0)));
movementSpeedSpinner.setMaximumSize(movementSpeedSpinner.getPreferredSize());
statusBar.add(movementSpeedSpinner);
statusBar.add(Box.createRigidArea(new Dimension(5, 0)));
JLabel alphabetLabel = new JLabel("Alphabet:");
alphabetLabel.setAlignmentY(0.55f); //move label slightly upwards to improve appearance/alignment
statusBar.add(alphabetLabel);
statusBar.add(Box.createRigidArea(new Dimension(1, 0)));
alphabetComboBox.setMinimumSize(new Dimension(0, 0));
alphabetComboBox.addActionListener(alphabetComboBoxActionListener);
//System.out.println(UIManager.getUI(alphabetComboBox)); //to find out the class name of the combo box UI
alphabetComboBox.setUI(new MetalComboBoxUI() { //adapted from https://stackoverflow.com/a/50380449
@Override
protected ComboPopup createPopup() {
return new BasicComboPopup(comboBox) {
@Override
protected Rectangle computePopupBounds(int px, int py, int pw, int ph) {
return super.computePopupBounds(px, py, Math.max(comboBox.getPreferredSize().width,
comboBox.getWidth()), ph);
}
};
}
});
statusBar.add(alphabetComboBox);
statusBar.add(Box.createRigidArea(new Dimension(5, 0)));
JButton settingsButton = new JButton("Settings...");
settingsButton.addActionListener(e -> DasherJava.showSettingsDialog());
statusBar.add(settingsButton);
}
public boolean isCurrentlyShowing() {
return isCurrentlyShowing;
}
public void setContent(SquareWorldView worldView, SwingWorldGraphics worldGraphics,
StartStopHandler startStopHandler, InputProvider inputProvider) {
boolean wasStarted = false;
if (worldUpdateThread!=null) {
wasStarted=worldUpdateThread.isStarted();
worldUpdateThread.terminate();
}
worldPanel=new WorldPanel(worldView, worldGraphics);
worldUpdateThread=new WorldUpdateThread(worldView, startStopHandler,
inputProvider!=null ? inputProvider : worldPanel, worldPanel, wasStarted);
worldPanel.setWorldUpdateThread(worldUpdateThread);
worldUpdateThread.start();
Settings settings = DasherJava.getSettings();
updateStatusBar(settings);
getContentPane().removeAll();
if (settings.getTextOutputTarget().equals("internal")) {
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, textAreaScrollPane, worldPanel);
splitPane.setOneTouchExpandable(true);
add(splitPane, BorderLayout.CENTER);
} else add(worldPanel, BorderLayout.CENTER);
if (settings.getShowStatusBar()) add(statusBar, BorderLayout.PAGE_END);
setAlwaysOnTop(settings.getWindowAlwaysOnTopResult());
setFocusableWindowState(!settings.getWindowUnfocusableResult());
setSize(settings.getWindowWidthResult(), settings.getWindowHeightResult());
int windowX = settings.getWindowXResult();
int windowY = settings.getWindowYResult();
if (windowX==Integer.MIN_VALUE && windowY==Integer.MIN_VALUE) {
if (!isVisible()) //may not call setLocationByPlatform(true) while the window is visible
setLocationByPlatform(true);
} else setLocation(windowX, windowY);
if (isVisible()) {
revalidate(); //must revalidate after adding/removing components
repaint(); //must repaint after revalidating
}
setVisible(true); //brings the window to the front if it was already visible
}
public ViewPanelBounds getViewPanelBoundsOnScreen() {
if (worldPanel==null) return null;
try {
Point p = worldPanel.getLocationOnScreen();
return new ViewPanelBounds(p.x, p.y, worldPanel.getWidth(), worldPanel.getHeight());
} catch (IllegalComponentStateException ex) { //not showing on screen
return null;
}
}
public TextCharOutput getInternalTextCharOutput() {
return internalTextCharOutput;
}
public PauseDasherTarget getPauseDasherTarget() {
return worldUpdateThread;
}
public SettingsDialog getSettingsDialog() {
return settingsDialog;
}
public boolean doesTextAreaContainText() {
return !textArea.getText().isEmpty();
}
public void writeWindowParameters(Settings settings) {
Rectangle bounds = getBounds();
settings.setWindowX(bounds.x);
settings.setWindowY(bounds.y);
settings.setWindowWidth(bounds.width);
settings.setWindowHeight(bounds.height);
}
private String getText(int start, int end) {
try {
return textArea.getText(start, end-start);
} catch (BadLocationException ex) {
DasherJava.showErrorMessage("MainFrame: getText()", ex);
return "";
}
}
private void updateStatusBar(Settings settings) {
//Need to remove the listener before setting the value because ChangeListeners apparently fire before
//the value actually changed, so the listener would always reset the setting to its previous value.
movementSpeedSpinner.removeChangeListener(movementSpeedSpinnerListener);
movementSpeedSpinner.setValue(settings.getMovementSpeed());
movementSpeedSpinner.addChangeListener(movementSpeedSpinnerListener);
populateAlphabetComboBox(settings);
}
private void populateAlphabetComboBox(Settings settings) {
alphabetComboBox.removeActionListener(alphabetComboBoxActionListener); //prevent the listener from being called
//while the combo box is being modified
alphabetComboBox.removeAllItems();
for (String alphabetName : settings.getAlphabetHistory()) {
alphabetComboBox.addItem(alphabetName);
}
alphabetComboBox.setSelectedItem(settings.getAlphabetName());
alphabetComboBox.addActionListener(alphabetComboBoxActionListener);
}
}