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 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); } }