diff --git a/app/src/cc/arduino/LimitLinesDocumentListener.java b/app/src/cc/arduino/LimitLinesDocumentListener.java new file mode 100644 index 00000000000..9ccd99d2674 --- /dev/null +++ b/app/src/cc/arduino/LimitLinesDocumentListener.java @@ -0,0 +1,136 @@ +/** + * This file courtesy of Rob Camick + *
+ * https://tips4java.wordpress.com/2008/10/15/limit-lines-in-document/ + *
+ * About page at https://tips4java.wordpress.com/about/ says something + * like MIT + */ + +package cc.arduino; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Element; + +/* + * A class to control the maximum number of lines to be stored in a Document + * + * Excess lines can be removed from the start or end of the Document + * depending on your requirement. + * + * a) if you append text to the Document, then you would want to remove lines + * from the start. + * b) if you insert text at the beginning of the Document, then you would + * want to remove lines from the end. + */ +public class LimitLinesDocumentListener implements DocumentListener { + private int maximumLines; + private final boolean isRemoveFromStart; + + /* + * Specify the number of lines to be stored in the Document. + * Extra lines will be removed from the start of the Document. + */ + public LimitLinesDocumentListener(int maximumLines) { + this(maximumLines, true); + } + + /* + * Specify the number of lines to be stored in the Document. + * Extra lines will be removed from the start or end of the Document, + * depending on the boolean value specified. + */ + public LimitLinesDocumentListener(int maximumLines, boolean isRemoveFromStart) { + setLimitLines(maximumLines); + this.isRemoveFromStart = isRemoveFromStart; + } + + /* + * Return the maximum number of lines to be stored in the Document + */ + public int getLimitLines() { + return maximumLines; + } + + /* + * Set the maximum number of lines to be stored in the Document + */ + public void setLimitLines(int maximumLines) { + if (maximumLines < 1) { + String message = "Maximum lines must be greater than 0"; + throw new IllegalArgumentException(message); + } + + this.maximumLines = maximumLines; + } + + // Handle insertion of new text into the Document + + public void insertUpdate(final DocumentEvent e) { + // Changes to the Document can not be done within the listener + // so we need to add the processing to the end of the EDT + + SwingUtilities.invokeLater(() -> removeLines(e)); + } + + public void removeUpdate(DocumentEvent e) { + } + + public void changedUpdate(DocumentEvent e) { + } + + /* + * Remove lines from the Document when necessary + */ + private void removeLines(DocumentEvent e) { + // The root Element of the Document will tell us the total number + // of line in the Document. + + Document document = e.getDocument(); + Element root = document.getDefaultRootElement(); + + while (root.getElementCount() > maximumLines) { + if (isRemoveFromStart) { + removeFromStart(document, root); + } else { + removeFromEnd(document, root); + } + } + } + + /* + * Remove lines from the start of the Document + */ + private void removeFromStart(Document document, Element root) { + Element line = root.getElement(0); + int end = line.getEndOffset(); + + try { + document.remove(0, end); + } catch (BadLocationException ble) { + System.out.println(ble); + } + } + + /* + * Remove lines from the end of the Document + */ + private void removeFromEnd(Document document, Element root) { + // We use start minus 1 to make sure we remove the newline + // character of the previous line + + Element line = root.getElement(root.getElementCount() - 1); + int start = line.getStartOffset(); + int end = line.getEndOffset(); + + try { + document.remove(start - 1, end - start); + } catch (BadLocationException ble) { + System.out.println(ble); + } + } +} diff --git a/app/src/cc/arduino/MessageConsole.java b/app/src/cc/arduino/MessageConsole.java new file mode 100644 index 00000000000..9981bb67574 --- /dev/null +++ b/app/src/cc/arduino/MessageConsole.java @@ -0,0 +1,204 @@ +/** + * This file courtesy of Rob Camick + *
+ * https://tips4java.wordpress.com/2008/11/08/message-console/ + *
+ * About page at https://tips4java.wordpress.com/about/ says something + * like MIT + */ + +package cc.arduino; + +import javax.swing.*; +import javax.swing.event.DocumentListener; +import javax.swing.text.*; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/* + * Create a simple console to display text messages. + * + * Messages can be directed here from different sources. Each source can + * have its messages displayed in a different color. + * + * You can limit the number of lines to hold in the Document. + */ +public class MessageConsole { + + private final JTextComponent textComponent; + private final JScrollPane scrollPane; + private final Document document; + private DocumentListener limitLinesListener; + + /* + * Use the text component specified as a simply console to display + * text messages. + */ + public MessageConsole(JTextComponent textComponent, JScrollPane scrollPane) { + this.textComponent = textComponent; + this.scrollPane = scrollPane; + this.document = textComponent.getDocument(); + this.textComponent.setEditable(false); + DefaultCaret caret = (DefaultCaret) this.textComponent.getCaret(); + caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + } + + /* + * Redirect the output from the standard output to the console + * using the default text color and null PrintStream + */ + public void redirectOut() { + redirectOut(null, null); + } + + /* + * Redirect the output from the standard output to the console + * using the specified color and PrintStream. When a PrintStream + * is specified the message will be added to the Document before + * it is also written to the PrintStream. + */ + public void redirectOut(SimpleAttributeSet style, PrintStream printStream) { + ConsoleOutputStream cos = new ConsoleOutputStream(style, printStream); + System.setOut(new PrintStream(cos, true)); + } + + /* + * Redirect the output from the standard error to the console + * using the default text color and null PrintStream + */ + public void redirectErr() { + redirectErr(null, null); + } + + /* + * Redirect the output from the standard error to the console + * using the specified color and PrintStream. When a PrintStream + * is specified the message will be added to the Document before + * it is also written to the PrintStream. + */ + public void redirectErr(SimpleAttributeSet style, PrintStream printStream) { + ConsoleOutputStream cos = new ConsoleOutputStream(style, printStream); + System.setErr(new PrintStream(cos, true)); + } + + /* + * To prevent memory from being used up you can control the number of + * lines to display in the console + * + * This number can be dynamically changed, but the console will only + * be updated the next time the Document is updated. + */ + public void setMessageLines(int lines) { + if (limitLinesListener != null) { + document.removeDocumentListener(limitLinesListener); + } + + limitLinesListener = new LimitLinesDocumentListener(lines, true); + document.addDocumentListener(limitLinesListener); + } + + /* + * Class to intercept output from a PrintStream and add it to a Document. + * The output can optionally be redirected to a different PrintStream. + * The text displayed in the Document can be color coded to indicate + * the output source. + */ + class ConsoleOutputStream extends ByteArrayOutputStream { + + private SimpleAttributeSet attributes; + private final PrintStream printStream; + private final StringBuffer buffer = new StringBuffer(1000); + private boolean isFirstLine; + + /* + * Specify the option text color and PrintStream + */ + public ConsoleOutputStream(SimpleAttributeSet attributes, PrintStream printStream) { + this.attributes = attributes; + this.printStream = printStream; + this.isFirstLine = true; + } + + /* + * Override this method to intercept the output text. Each line of text + * output will actually involve invoking this method twice: + * + * a) for the actual text message + * b) for the newLine string + * + * The message will be treated differently depending on whether the line + * will be appended or inserted into the Document + */ + public void flush() { + String message = toString(); + + if (message.length() == 0) { + return; + } + + handleAppend(message); + + reset(); + } + + /* + * We don't want to have blank lines in the Document. The first line + * added will simply be the message. For additional lines it will be: + * + * newLine + message + */ + private void handleAppend(String message) { + // This check is needed in case the text in the Document has been + // cleared. The buffer may contain the EOL string from the previous + // message. + + if (document.getLength() == 0) { + buffer.setLength(0); + } + + buffer.append(message); + clearBuffer(); + } + + /* + * The message and the newLine have been added to the buffer in the + * appropriate order so we can now update the Document and send the + * text to the optional PrintStream. + */ + private void clearBuffer() { + // In case both the standard out and standard err are being redirected + // we need to insert a newline character for the first line only + + synchronized (document) { + if (isFirstLine && document.getLength() != 0) { + buffer.insert(0, "\n"); + } + + isFirstLine = false; + String line = buffer.toString(); + + int offset = document.getLength(); + SwingUtilities.invokeLater(() -> { + try { + document.insertString(offset, line, attributes); + } catch (BadLocationException ble) { + //ignore + } + }); + + Timer timer = new Timer(200, (e) -> { + scrollPane.getHorizontalScrollBar().setValue(0); + scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum()); + }); + timer.setRepeats(false); + timer.start(); + + if (printStream != null) { + printStream.print(line); + } + + buffer.setLength(0); + } + } + } +} diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index ba95406e3a7..557c43fd6d8 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -639,9 +639,6 @@ protected void handleActivated(Editor whichEditor) { // noop } } - - // set the current window to be the console that's getting output - EditorConsoleStream.setCurrent(activeEditor.console); } diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index 1eb3b6a8f95..879d47b41b9 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -263,7 +263,7 @@ public void windowDeactivated(WindowEvent e) { status = new EditorStatus(this); consolePanel.add(status, BorderLayout.NORTH); - console = new EditorConsole(this); + console = new EditorConsole(); console.setName("console"); // windows puts an ugly border on this guy console.setBorder(null); diff --git a/app/src/processing/app/EditorConsole.java b/app/src/processing/app/EditorConsole.java index a2116f8b436..7e7e1cf4ca5 100644 --- a/app/src/processing/app/EditorConsole.java +++ b/app/src/processing/app/EditorConsole.java @@ -21,236 +21,90 @@ package processing.app; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.List; +import cc.arduino.MessageConsole; -import javax.swing.JScrollPane; -import javax.swing.JTextPane; -import javax.swing.SwingUtilities; -import javax.swing.Timer; -import javax.swing.text.AttributeSet; +import javax.swing.*; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; -import javax.swing.text.Element; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; - -import processing.app.helpers.OSUtils; +import java.awt.*; /** * Message console that sits below the editing area. - *
+ *
* Debugging this class is tricky... If it's throwing exceptions, * don't take over System.err, and debug while watching just System.out * or just write println() or whatever directly to systemOut or systemErr. */ @SuppressWarnings("serial") public class EditorConsole extends JScrollPane { - Editor editor; - - JTextPane consoleTextPane; - BufferedStyledDocument consoleDoc; - - SimpleAttributeSet stdStyle; - SimpleAttributeSet errStyle; + private final DefaultStyledDocument document; + private final JTextPane consoleTextPane; // Single static instance shared because there's only one real System.out. // Within the input handlers, the currentConsole variable will be used to // echo things to the correct location. - - public EditorConsole(Editor _editor) { - editor = _editor; - int maxLineCount = PreferencesData.getInteger("console.length"); + public EditorConsole() { + document = new DefaultStyledDocument(); - consoleDoc = new BufferedStyledDocument(4000, maxLineCount); - consoleTextPane = new JTextPane(consoleDoc); - consoleTextPane.setEditable(false); + consoleTextPane = new JTextPane(document); + Color backgroundColour = Theme.getColor("console.color"); + consoleTextPane.setBackground(backgroundColour); - // necessary? - SimpleAttributeSet leftAlignAttr = new SimpleAttributeSet(); - StyleConstants.setAlignment(leftAlignAttr, StyleConstants.ALIGN_LEFT); - consoleDoc.setParagraphAttributes(0, 0, leftAlignAttr, true); - - // build styles for different types of console output - Color bgColor = Theme.getColor("console.color"); - Color fgColorOut = Theme.getColor("console.output.color"); - Color fgColorErr = Theme.getColor("console.error.color"); Font consoleFont = Theme.getFont("console.font"); Font editorFont = PreferencesData.getFont("editor.font"); - Font font = new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()); - - stdStyle = new SimpleAttributeSet(); - StyleConstants.setForeground(stdStyle, fgColorOut); - StyleConstants.setBackground(stdStyle, bgColor); - StyleConstants.setFontSize(stdStyle, font.getSize()); - StyleConstants.setFontFamily(stdStyle, font.getFamily()); - StyleConstants.setBold(stdStyle, font.isBold()); - StyleConstants.setItalic(stdStyle, font.isItalic()); - - errStyle = new SimpleAttributeSet(); - StyleConstants.setForeground(errStyle, fgColorErr); - StyleConstants.setBackground(errStyle, bgColor); - StyleConstants.setFontSize(errStyle, font.getSize()); - StyleConstants.setFontFamily(errStyle, font.getFamily()); - StyleConstants.setBold(errStyle, font.isBold()); - StyleConstants.setItalic(errStyle, font.isItalic()); - - consoleTextPane.setBackground(bgColor); - - // add the jtextpane to this scrollpane - setViewportView(consoleTextPane); + Font actualFont = new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()); + + SimpleAttributeSet stdOutStyle = new SimpleAttributeSet(); + StyleConstants.setForeground(stdOutStyle, Theme.getColor("console.output.color")); + StyleConstants.setBackground(stdOutStyle, backgroundColour); + StyleConstants.setFontSize(stdOutStyle, actualFont.getSize()); + StyleConstants.setFontFamily(stdOutStyle, actualFont.getFamily()); + StyleConstants.setBold(stdOutStyle, actualFont.isBold()); + StyleConstants.setItalic(stdOutStyle, actualFont.isItalic()); + + SimpleAttributeSet stdErrStyle = new SimpleAttributeSet(); + StyleConstants.setForeground(stdErrStyle, Theme.getColor("console.error.color")); + StyleConstants.setBackground(stdErrStyle, backgroundColour); + StyleConstants.setFontSize(stdErrStyle, actualFont.getSize()); + StyleConstants.setFontFamily(stdErrStyle, actualFont.getFamily()); + StyleConstants.setBold(stdErrStyle, actualFont.isBold()); + StyleConstants.setItalic(stdErrStyle, actualFont.isItalic()); + + MessageConsole messageConsole = new MessageConsole(consoleTextPane, this); + messageConsole.redirectErr(stdErrStyle, System.err); + messageConsole.redirectOut(stdOutStyle, System.out); + messageConsole.setMessageLines(PreferencesData.getInteger("console.length") * 2); + + JPanel noWrapPanel = new JPanel(new BorderLayout()); + noWrapPanel.add(consoleTextPane); + + setViewportView(noWrapPanel); + getVerticalScrollBar().setUnitIncrement(7); // calculate height of a line of text in pixels // and size window accordingly - FontMetrics metrics = getFontMetrics(font); + FontMetrics metrics = getFontMetrics(actualFont); int height = metrics.getAscent() + metrics.getDescent(); int lines = PreferencesData.getInteger("console.lines"); int sizeFudge = 6; //10; // unclear why this is necessary, but it is setPreferredSize(new Dimension(1024, (height * lines) + sizeFudge)); setMinimumSize(new Dimension(1024, (height * 4) + sizeFudge)); - - EditorConsoleStream.init(); - - // to fix ugliness.. normally macosx java 1.3 puts an - // ugly white border around this object, so turn it off. - if (OSUtils.isMacOS()) { - setBorder(null); - } - - // periodically post buffered messages to the console - // should the interval come from the preferences file? - new Timer(250, new ActionListener() { - public void actionPerformed(ActionEvent evt) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - // only if new text has been added - if (consoleDoc.isChanged()) { - // insert the text that's been added in the meantime - consoleDoc.insertAll(); - // always move to the end of the text as it's added - consoleTextPane.setCaretPosition(consoleDoc.getLength()); - } - } - }); - } - }).start(); - } - - - /** - * Append a piece of text to the console. - *
- * Swing components are NOT thread-safe, and since the MessageSiphon - * instantiates new threads, and in those callbacks, they often print - * output to stdout and stderr, which are wrapped by EditorConsoleStream - * and eventually leads to EditorConsole.appendText(), which directly - * updates the Swing text components, causing deadlock. - *
- * Updates are buffered to the console and displayed at regular
- * intervals on Swing's event-dispatching thread. (patch by David Mellis)
- */
- synchronized void appendText(String txt, boolean e) {
- consoleDoc.appendString(txt, e ? errStyle : stdStyle);
}
-
public void clear() {
try {
- consoleDoc.remove(0, consoleDoc.getLength());
- } catch (BadLocationException e) {
- // ignore the error otherwise this will cause an infinite loop
- // maybe not a good idea in the long run?
- }
- }
-}
-
-
-// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
-
-
-/**
- * Buffer updates to the console and output them in batches. For info, see:
- * http://java.sun.com/products/jfc/tsc/articles/text/element_buffer and
- * http://javatechniques.com/public/java/docs/gui/jtextpane-speed-part2.html
- * appendString() is called from multiple threads, and insertAll from the
- * swing event thread, so they need to be synchronized
- */
-@SuppressWarnings("serial")
-class BufferedStyledDocument extends DefaultStyledDocument {
- private List