diff --git a/app/src/cc/arduino/packages/MonitorFactory.java b/app/src/cc/arduino/packages/MonitorFactory.java index 83d849dd6ef..1682eb34185 100644 --- a/app/src/cc/arduino/packages/MonitorFactory.java +++ b/app/src/cc/arduino/packages/MonitorFactory.java @@ -30,23 +30,24 @@ package cc.arduino.packages; import processing.app.AbstractMonitor; +import processing.app.Base; import processing.app.NetworkMonitor; import processing.app.SerialMonitor; public class MonitorFactory { - public AbstractMonitor newMonitor(BoardPort port) { + public AbstractMonitor newMonitor(Base base, BoardPort port) { if ("network".equals(port.getProtocol())) { if ("yes".equals(port.getPrefs().get("ssh_upload"))) { // the board is SSH capable - return new NetworkMonitor(port); + return new NetworkMonitor(base, port); } else { // SSH not supported, no monitor support return null; } } - return new SerialMonitor(port); + return new SerialMonitor(base, port); } } diff --git a/app/src/processing/app/AbstractMonitor.java b/app/src/processing/app/AbstractMonitor.java index 52c5b65b56b..1efbec7e2d6 100644 --- a/app/src/processing/app/AbstractMonitor.java +++ b/app/src/processing/app/AbstractMonitor.java @@ -27,6 +27,7 @@ public AbstractMonitor(BoardPort boardPort) { this.boardPort = boardPort; addWindowListener(new WindowAdapter() { + @Override public void windowClosing(WindowEvent event) { try { closed = true; @@ -41,6 +42,7 @@ public void windowClosing(WindowEvent event) { KeyStroke wc = Editor.WINDOW_CLOSE_KEYSTROKE; getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(wc, "close"); getRootPane().getActionMap().put("close", (new AbstractAction() { + @Override public void actionPerformed(ActionEvent event) { try { close(); @@ -165,6 +167,7 @@ private synchronized String consumeUpdateBuffer() { return s; } + @Override public void actionPerformed(ActionEvent e) { String s = consumeUpdateBuffer(); if (s.isEmpty()) { @@ -173,4 +176,13 @@ public void actionPerformed(ActionEvent e) { message(s); } } + + /** + * Read and apply new values from the preferences, either because + * the app is just starting up, or the user just finished messing + * with things in the Preferences window. + */ + public void applyPreferences() { + // Empty. + }; } diff --git a/app/src/processing/app/AbstractTextMonitor.java b/app/src/processing/app/AbstractTextMonitor.java index b6e196a16b5..64a0928e55e 100644 --- a/app/src/processing/app/AbstractTextMonitor.java +++ b/app/src/processing/app/AbstractTextMonitor.java @@ -40,17 +40,19 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { protected JButton clearButton; protected JCheckBox autoscrollBox; protected JCheckBox addTimeStampBox; - protected JComboBox lineEndings; - protected JComboBox serialRates; + protected JComboBox lineEndings; + protected JComboBox serialRates; - public AbstractTextMonitor(BoardPort boardPort) { + public AbstractTextMonitor(Base base, BoardPort boardPort) { super(boardPort); + + // Add font size adjustment listeners. This has to be done here due to + // super(boardPort) invoking onCreateWindow(...) before we can store base. + base.addEditorFontResizeListeners(textArea); } + @Override protected void onCreateWindow(Container mainPane) { - Font consoleFont = Theme.getFont("console.font"); - Font editorFont = PreferencesData.getFont("editor.font"); - Font font = Theme.scale(new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize())); mainPane.setLayout(new BorderLayout()); @@ -58,7 +60,6 @@ protected void onCreateWindow(Container mainPane) { textArea.setRows(16); textArea.setColumns(40); textArea.setEditable(false); - textArea.setFont(font); // don't automatically update the caret. that way we can manually decide // whether or not to do so based on the autoscroll checkbox. @@ -75,6 +76,7 @@ protected void onCreateWindow(Container mainPane) { textField = new JTextField(40); // textField is selected every time the window is focused addWindowFocusListener(new WindowAdapter() { + @Override public void windowGainedFocus(WindowEvent e) { textField.requestFocusInWindow(); } @@ -103,28 +105,17 @@ public void windowGainedFocus(WindowEvent e) { minimumSize.setSize(minimumSize.getWidth() / 3, minimumSize.getHeight()); noLineEndingAlert.setMinimumSize(minimumSize); - lineEndings = new JComboBox(new String[]{tr("No line ending"), tr("Newline"), tr("Carriage return"), tr("Both NL & CR")}); - lineEndings.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent event) { - PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex()); - noLineEndingAlert.setForeground(pane.getBackground()); - } - }); - if (PreferencesData.get("serial.line_ending") != null) { - lineEndings.setSelectedIndex(PreferencesData.getInteger("serial.line_ending")); - } - if (PreferencesData.get("serial.show_timestamp") != null) { - addTimeStampBox.setSelected(PreferencesData.getBoolean("serial.show_timestamp")); - } - addTimeStampBox.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - PreferencesData.setBoolean("serial.show_timestamp", addTimeStampBox.isSelected()); - } + lineEndings = new JComboBox(new String[]{tr("No line ending"), tr("Newline"), tr("Carriage return"), tr("Both NL & CR")}); + lineEndings.addActionListener((ActionEvent event) -> { + PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex()); + noLineEndingAlert.setForeground(pane.getBackground()); }); + addTimeStampBox.addActionListener((ActionEvent event) -> + PreferencesData.setBoolean("serial.show_timestamp", addTimeStampBox.isSelected())); lineEndings.setMaximumSize(lineEndings.getMinimumSize()); - serialRates = new JComboBox(); + serialRates = new JComboBox(); for (String rate : serialRateStrings) { serialRates.addItem(rate + " " + tr("baud")); } @@ -142,9 +133,12 @@ public void actionPerformed(ActionEvent e) { pane.add(Box.createRigidArea(new Dimension(8, 0))); pane.add(clearButton); + applyPreferences(); + mainPane.add(pane, BorderLayout.SOUTH); } + @Override protected void onEnableWindow(boolean enable) { textArea.setEnabled(enable); @@ -171,6 +165,7 @@ public void onSerialRateChange(ActionListener listener) { serialRates.addActionListener(listener); } + @Override public void message(String msg) { SwingUtilities.invokeLater(() -> updateTextArea(msg)); } @@ -189,6 +184,26 @@ protected void updateTextArea(String msg) { } } + @Override + public void applyPreferences() { + + // Apply font. + Font consoleFont = Theme.getFont("console.font"); + Font editorFont = PreferencesData.getFont("editor.font"); + textArea.setFont(Theme.scale(new Font( + consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()))); + + // Apply line endings. + if (PreferencesData.get("serial.line_ending") != null) { + lineEndings.setSelectedIndex(PreferencesData.getInteger("serial.line_ending")); + } + + // Apply timestamp visibility. + if (PreferencesData.get("serial.show_timestamp") != null) { + addTimeStampBox.setSelected(PreferencesData.getBoolean("serial.show_timestamp")); + } + } + private String addTimestamps(String text) { String now = new SimpleDateFormat("HH:mm:ss.SSS -> ").format(new Date()); final StringBuilder sb = new StringBuilder(text.length() + now.length()); diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 62ebfc2acaf..8544e6c4180 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -1877,6 +1877,81 @@ public void handleFontSizeChange(int change) { getEditors().forEach(Editor::applyPreferences); } + private MouseWheelListener editorFontResizeMouseWheelListener = null; + private KeyListener editorFontResizeKeyListener = null; + + /** + * Adds a {@link MouseWheelListener} and {@link KeyListener} to the given + * component that will make "CTRL scroll" and "CTRL +/-" + * (with optional SHIFT for +) increase/decrease the editor text size. + * This method is equivalent to calling + * {@link #addEditorFontResizeMouseWheelListener(Component)} and + * {@link #addEditorFontResizeKeyListener(Component)} on the given component. + * Note that this also affects components that use the editor font settings. + * @param comp - The component to add the listener to. + */ + public void addEditorFontResizeListeners(Component comp) { + this.addEditorFontResizeMouseWheelListener(comp); + this.addEditorFontResizeKeyListener(comp); + } + + /** + * Adds a {@link MouseWheelListener} to the given component that will + * make "CTRL scroll" increase/decrease the editor text size. + * When CTRL is not pressed while scrolling, mouse wheel events are passed + * on to the parent of the given component. + * Note that this also affects components that use the editor font settings. + * @param comp - The component to add the listener to. + */ + public void addEditorFontResizeMouseWheelListener(Component comp) { + if (this.editorFontResizeMouseWheelListener == null) { + this.editorFontResizeMouseWheelListener = (MouseWheelEvent e) -> { + if (e.isControlDown()) { + if (e.getWheelRotation() < 0) { + this.handleFontSizeChange(1); + } else { + this.handleFontSizeChange(-1); + } + } else { + e.getComponent().getParent().dispatchEvent(e); + } + }; + } + comp.addMouseWheelListener(this.editorFontResizeMouseWheelListener); + } + + /** + * Adds a {@link KeyListener} to the given component that will make "CTRL +/-" + * (with optional SHIFT for +) increase/decrease the editor text size. + * Note that this also affects components that use the editor font settings. + * @param comp - The component to add the listener to. + */ + public void addEditorFontResizeKeyListener(Component comp) { + if (this.editorFontResizeKeyListener == null) { + this.editorFontResizeKeyListener = new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK + || e.getModifiersEx() == (KeyEvent.CTRL_DOWN_MASK + | KeyEvent.SHIFT_DOWN_MASK)) { + switch (e.getKeyCode()) { + case KeyEvent.VK_PLUS: + case KeyEvent.VK_EQUALS: + Base.this.handleFontSizeChange(1); + break; + case KeyEvent.VK_MINUS: + if (!e.isShiftDown()) { + Base.this.handleFontSizeChange(-1); + } + break; + } + } + } + }; + } + comp.addKeyListener(this.editorFontResizeKeyListener); + } + public List getBoardsCustomMenus() { return boardsCustomMenus; } diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index dc0a5b7cbf2..49c092fd9d2 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -315,7 +315,7 @@ public void windowDeactivated(WindowEvent e) { status = new EditorStatus(this); consolePanel.add(status, BorderLayout.NORTH); - console = new EditorConsole(); + console = new EditorConsole(base); console.setName("console"); // windows puts an ugly border on this guy console.setBorder(null); @@ -495,6 +495,9 @@ public void applyPreferences() { tab.applyPreferences(); } console.applyPreferences(); + if (serialMonitor != null) { + serialMonitor.applyPreferences(); + } } @@ -1270,12 +1273,14 @@ private JMenu buildEditMenu() { JMenuItem increaseFontSizeItem = newJMenuItem(tr("Increase Font Size"), KeyEvent.VK_PLUS); increaseFontSizeItem.addActionListener(event -> base.handleFontSizeChange(1)); menu.add(increaseFontSizeItem); - // Add alternative shortcut "CTRL SHIFT =" for keyboards that haven't the "+" key - // in the base layer. This workaround covers all the keyboards that have the "+" - // key available as "SHIFT =" that seems to be very common. + // Many keyboards have '+' and '=' on the same key. Allowing "CTRL +", + // "CTRL SHIFT +" and "CTRL =" covers the generally expected behavior. KeyStroke ctrlShiftEq = KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, SHORTCUT_KEY_MASK | ActionEvent.SHIFT_MASK); menu.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlShiftEq, "IncreaseFontSize"); + KeyStroke ctrlEq = KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, SHORTCUT_KEY_MASK); + menu.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlEq, "IncreaseFontSize"); menu.getActionMap().put("IncreaseFontSize", new AbstractAction() { + @Override public void actionPerformed(ActionEvent e) { base.handleFontSizeChange(1); } @@ -2209,7 +2214,7 @@ public void handleSerial() { return; } - serialMonitor = new MonitorFactory().newMonitor(port); + serialMonitor = new MonitorFactory().newMonitor(base, port); if (serialMonitor == null) { String board = port.getPrefs().get("board"); diff --git a/app/src/processing/app/EditorConsole.java b/app/src/processing/app/EditorConsole.java index f656798f0cb..15c78df2dda 100644 --- a/app/src/processing/app/EditorConsole.java +++ b/app/src/processing/app/EditorConsole.java @@ -61,7 +61,7 @@ public static void setCurrentEditorConsole(EditorConsole console) { private SimpleAttributeSet stdOutStyle; private SimpleAttributeSet stdErrStyle; - public EditorConsole() { + public EditorConsole(Base base) { document = new DefaultStyledDocument(); consoleTextPane = new JTextPane(document); @@ -110,18 +110,55 @@ public EditorConsole() { setMinimumSize(new Dimension(100, (height * lines))); EditorConsole.init(stdOutStyle, System.out, stdErrStyle, System.err); + + // Add font size adjustment listeners. + base.addEditorFontResizeListeners(consoleTextPane); } public void applyPreferences() { + + // Update the console text pane font from the preferences. Font consoleFont = Theme.getFont("console.font"); Font editorFont = PreferencesData.getFont("editor.font"); Font actualFont = new Font(consoleFont.getName(), consoleFont.getStyle(), scale(editorFont.getSize())); + AttributeSet stdOutStyleOld = stdOutStyle.copyAttributes(); + AttributeSet stdErrStyleOld = stdErrStyle.copyAttributes(); StyleConstants.setFontSize(stdOutStyle, actualFont.getSize()); StyleConstants.setFontSize(stdErrStyle, actualFont.getSize()); - out.setAttibutes(stdOutStyle); - err.setAttibutes(stdErrStyle); + // Re-insert console text with the new preferences if there were changes. + // This assumes that the document has single-child paragraphs (default). + if (!stdOutStyle.isEqual(stdOutStyleOld) || !stdErrStyle.isEqual(stdOutStyleOld)) { + out.setAttibutes(stdOutStyle); + err.setAttibutes(stdErrStyle); + + int start; + for (int end = document.getLength() - 1; end >= 0; end = start - 1) { + Element elem = document.getParagraphElement(end); + start = elem.getStartOffset(); + AttributeSet attrs = elem.getElement(0).getAttributes(); + AttributeSet newAttrs; + if (attrs.isEqual(stdErrStyleOld)) { + newAttrs = stdErrStyle; + } else if (attrs.isEqual(stdOutStyleOld)) { + newAttrs = stdOutStyle; + } else { + continue; + } + try { + String text = document.getText(start, end - start); + document.remove(start, end - start); + document.insertString(start, text, newAttrs); + } catch (BadLocationException e) { + // Should only happen when text is async removed (through clear()). + // Accept this case, but throw an error when text could mess up. + if (document.getLength() != 0) { + throw new Error(e); + } + } + } + } } public void clear() { diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java index 8c5fb86a7d8..25c45d9d854 100644 --- a/app/src/processing/app/EditorTab.java +++ b/app/src/processing/app/EditorTab.java @@ -68,7 +68,7 @@ /** * Single tab, editing a single file, in the main window. */ -public class EditorTab extends JPanel implements SketchFile.TextStorage, MouseWheelListener { +public class EditorTab extends JPanel implements SketchFile.TextStorage { protected Editor editor; protected SketchTextArea textarea; protected RTextScrollPane scrollPane; @@ -110,7 +110,7 @@ public EditorTab(Editor editor, SketchFile file, String contents) file.setStorage(this); applyPreferences(); add(scrollPane, BorderLayout.CENTER); - textarea.addMouseWheelListener(this); + editor.base.addEditorFontResizeMouseWheelListener(textarea); } private RSyntaxDocument createDocument(String contents) { @@ -182,18 +182,6 @@ private SketchTextArea createTextArea(RSyntaxDocument document) configurePopupMenu(textArea); return textArea; } - - public void mouseWheelMoved(MouseWheelEvent e) { - if (e.isControlDown()) { - if (e.getWheelRotation() < 0) { - editor.base.handleFontSizeChange(1); - } else { - editor.base.handleFontSizeChange(-1); - } - } else { - e.getComponent().getParent().dispatchEvent(e); - } - } private void configurePopupMenu(final SketchTextArea textarea){ diff --git a/app/src/processing/app/NetworkMonitor.java b/app/src/processing/app/NetworkMonitor.java index b7f08026ace..3ab20314ba4 100644 --- a/app/src/processing/app/NetworkMonitor.java +++ b/app/src/processing/app/NetworkMonitor.java @@ -31,8 +31,8 @@ public class NetworkMonitor extends AbstractTextMonitor implements MessageConsum private Channel channel; private int connectionAttempts; - public NetworkMonitor(BoardPort port) { - super(port); + public NetworkMonitor(Base base, BoardPort port) { + super(base, port); onSendCommand(new ActionListener() { public void actionPerformed(ActionEvent event) { diff --git a/app/src/processing/app/SerialMonitor.java b/app/src/processing/app/SerialMonitor.java index 45adbd7d54b..d4f59019eae 100644 --- a/app/src/processing/app/SerialMonitor.java +++ b/app/src/processing/app/SerialMonitor.java @@ -21,9 +21,8 @@ import cc.arduino.packages.BoardPort; import processing.app.legacy.PApplet; -import java.awt.*; +import java.awt.Color; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import static processing.app.I18n.tr; @@ -33,41 +32,33 @@ public class SerialMonitor extends AbstractTextMonitor { private Serial serial; private int serialRate; - public SerialMonitor(BoardPort port) { - super(port); + public SerialMonitor(Base base, BoardPort port) { + super(base, port); serialRate = PreferencesData.getInteger("serial.debug_rate"); serialRates.setSelectedItem(serialRate + " " + tr("baud")); - onSerialRateChange(new ActionListener() { - public void actionPerformed(ActionEvent event) { - String wholeString = (String) serialRates.getSelectedItem(); - String rateString = wholeString.substring(0, wholeString.indexOf(' ')); - serialRate = Integer.parseInt(rateString); - PreferencesData.set("serial.debug_rate", rateString); - try { - close(); - Thread.sleep(100); // Wait for serial port to properly close - open(); - } catch (InterruptedException e) { - // noop - } catch (Exception e) { - System.err.println(e); - } + onSerialRateChange((ActionEvent event) -> { + String wholeString = (String) serialRates.getSelectedItem(); + String rateString = wholeString.substring(0, wholeString.indexOf(' ')); + serialRate = Integer.parseInt(rateString); + PreferencesData.set("serial.debug_rate", rateString); + try { + close(); + Thread.sleep(100); // Wait for serial port to properly close + open(); + } catch (InterruptedException e) { + // noop + } catch (Exception e) { + System.err.println(e); } }); - onSendCommand(new ActionListener() { - public void actionPerformed(ActionEvent e) { - send(textField.getText()); - textField.setText(""); - } + onSendCommand((ActionEvent event) -> { + send(textField.getText()); + textField.setText(""); }); - onClearCommand(new ActionListener() { - public void actionPerformed(ActionEvent e) { - textArea.setText(""); - } - }); + onClearCommand((ActionEvent event) -> textArea.setText("")); } private void send(String s) { @@ -93,6 +84,7 @@ private void send(String s) { } } + @Override public void open() throws Exception { super.open(); @@ -106,6 +98,7 @@ protected void message(char buff[], int n) { }; } + @Override public void close() throws Exception { super.close(); if (serial != null) {