From e15ba64ee2b99f7ba72984922073dbadd400035f Mon Sep 17 00:00:00 2001 From: Federico Fissore Date: Mon, 29 Jun 2015 14:12:51 +0200 Subject: [PATCH 1/2] New console: replaces previous EditorConsoleStream with one that's faster and doesn't discard end chars. See #2798 --- app/src/cc/arduino/ConsoleOutputStream.java | 131 +++++++++ app/src/processing/app/Base.java | 2 +- app/src/processing/app/Editor.java | 2 +- app/src/processing/app/EditorConsole.java | 252 +++++------------- .../processing/app/EditorConsoleStream.java | 65 ----- app/src/processing/app/EditorStatus.java | 2 +- ...ngEscapeOnCloseConfirmationDialogTest.java | 2 +- .../ReduceIndentWith1CharOnLastLineTest.java | 2 +- 8 files changed, 197 insertions(+), 261 deletions(-) create mode 100644 app/src/cc/arduino/ConsoleOutputStream.java delete mode 100644 app/src/processing/app/EditorConsoleStream.java diff --git a/app/src/cc/arduino/ConsoleOutputStream.java b/app/src/cc/arduino/ConsoleOutputStream.java new file mode 100644 index 000000000..a1c1e959c --- /dev/null +++ b/app/src/cc/arduino/ConsoleOutputStream.java @@ -0,0 +1,131 @@ +/* + * This file is part of Arduino. + * + * Arduino is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As a special exception, you may use this file as part of a free software + * library without restriction. Specifically, if other files instantiate + * templates or use macros or inline functions from this file, or you compile + * this file and link it with other files to produce an executable, this + * file does not by itself cause the resulting executable to be covered by + * the GNU General Public License. This exception does not however + * invalidate any other reasons why the executable file might be covered by + * the GNU General Public License. + * + * Copyright 2015 Arduino LLC (http://www.arduino.cc/) + * + * Original version of 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 processing.app.EditorConsole; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.SimpleAttributeSet; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/* + * 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. + */ +public class ConsoleOutputStream extends ByteArrayOutputStream { + + private final SimpleAttributeSet attributes; + private final PrintStream printStream; + private final StringBuilder buffer; + private final Timer timer; + private JScrollPane scrollPane; + private Document document; + + public ConsoleOutputStream(SimpleAttributeSet attributes, PrintStream printStream) { + this.attributes = attributes; + this.printStream = printStream; + this.buffer = new StringBuilder(); + + this.timer = new Timer(100, (e) -> { + if (scrollPane != null) { + synchronized (scrollPane) { + scrollPane.getHorizontalScrollBar().setValue(0); + scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum()); + } + } + }); + timer.setRepeats(false); + } + + public synchronized void setCurrentEditorConsole(EditorConsole console) { + this.scrollPane = console; + this.document = console.getDocument(); + } + + public synchronized void flush() { + String message = toString(); + + if (message.length() == 0) { + return; + } + + handleAppend(message); + + reset(); + } + + private void handleAppend(String message) { + resetBufferIfDocumentEmpty(); + + buffer.append(message); + + clearBuffer(); + } + + private void resetBufferIfDocumentEmpty() { + if (document != null && document.getLength() == 0) { + buffer.setLength(0); + } + } + + private void clearBuffer() { + String line = buffer.toString(); + buffer.setLength(0); + + printStream.print(line); + + if (document != null) { + SwingUtilities.invokeLater(() -> { + try { + int offset = document.getLength(); + document.insertString(offset, line, attributes); + } catch (BadLocationException ble) { + //ignore + } + }); + + if (!timer.isRunning()) { + timer.restart(); + } + } + } +} diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index ba95406e3..19782e96a 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -641,7 +641,7 @@ public class Base { } // set the current window to be the console that's getting output - EditorConsoleStream.setCurrent(activeEditor.console); + EditorConsole.setCurrentEditorConsole(activeEditor.console); } diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index de5688bd0..8871a214a 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -264,7 +264,7 @@ public class Editor extends JFrame implements RunnerListener { 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 a2116f8b4..b922b1b2d 100644 --- a/app/src/processing/app/EditorConsole.java +++ b/app/src/processing/app/EditorConsole.java @@ -21,236 +21,106 @@ 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 javax.swing.JScrollPane; -import javax.swing.JTextPane; -import javax.swing.SwingUtilities; -import javax.swing.Timer; -import javax.swing.text.AttributeSet; -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 cc.arduino.ConsoleOutputStream; +import javax.swing.*; +import javax.swing.text.*; +import java.awt.*; +import java.io.PrintStream; /** * 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; + private static ConsoleOutputStream out; + private static ConsoleOutputStream err; - SimpleAttributeSet stdStyle; - SimpleAttributeSet errStyle; + public static synchronized void init(SimpleAttributeSet outStyle, PrintStream outStream, SimpleAttributeSet errStyle, PrintStream errStream) { + if (out != null) { + return; + } - // 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; + out = new ConsoleOutputStream(outStyle, outStream); + System.setOut(new PrintStream(out, true)); - int maxLineCount = PreferencesData.getInteger("console.length"); + err = new ConsoleOutputStream(errStyle, errStream); + System.setErr(new PrintStream(err, true)); + } - consoleDoc = new BufferedStyledDocument(4000, maxLineCount); - consoleTextPane = new JTextPane(consoleDoc); + public static void setCurrentEditorConsole(EditorConsole console) { + out.setCurrentEditorConsole(console); + err.setCurrentEditorConsole(console); + } + + private final DefaultStyledDocument document; + private final JTextPane consoleTextPane; + + public EditorConsole() { + document = new DefaultStyledDocument(); + + consoleTextPane = new JTextPane(document); consoleTextPane.setEditable(false); + DefaultCaret caret = (DefaultCaret) consoleTextPane.getCaret(); + caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); - // necessary? - SimpleAttributeSet leftAlignAttr = new SimpleAttributeSet(); - StyleConstants.setAlignment(leftAlignAttr, StyleConstants.ALIGN_LEFT); - consoleDoc.setParagraphAttributes(0, 0, leftAlignAttr, true); + Color backgroundColour = Theme.getColor("console.color"); + consoleTextPane.setBackground(backgroundColour); - // 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()); + Font actualFont = 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()); + 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()); - 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.setParagraphAttributes(stdOutStyle, true); - consoleTextPane.setBackground(bgColor); + 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()); - // add the jtextpane to this scrollpane - setViewportView(consoleTextPane); + 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)); + setMinimumSize(new Dimension(1024, (height * 5) + 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(); + EditorConsole.init(stdOutStyle, System.out, stdErrStyle, System.err); } - - /** - * 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()); + document.remove(0, document.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 elements = new ArrayList(); - private int maxLineLength, maxLineCount; - private int currentLineLength = 0; - private boolean changed = false; - - public BufferedStyledDocument(int _maxLineLength, int _maxLineCount) { - maxLineLength = _maxLineLength; - maxLineCount = _maxLineCount; + public String getText() { + return consoleTextPane.getText().trim(); } - /** buffer a string for insertion at the end of the DefaultStyledDocument */ - public synchronized void appendString(String text, AttributeSet a) { - changed = true; - char[] chars = text.toCharArray(); - int start = 0; - int stop = 0; - while (stop < chars.length) { - char c = chars[stop]; - stop++; - currentLineLength++; - if (c == '\n' || c == '\r' || currentLineLength > maxLineLength) { - elements.add(new ElementSpec(a, ElementSpec.ContentType, chars, start, - stop - start)); - elements.add(new ElementSpec(a, ElementSpec.EndTagType)); - elements.add(new ElementSpec(a, ElementSpec.StartTagType)); - currentLineLength = 0; - start = stop; - } - } - elements.add(new ElementSpec(a, ElementSpec.ContentType, chars, start, - stop - start)); - } - - /** insert the buffered strings */ - public synchronized void insertAll() { - try { - // Insert new elements at the bottom - ElementSpec[] elementArray = elements.toArray(new ElementSpec[0]); - insert(getLength(), elementArray); - - // check how many lines have been used - // if too many, shave off a few lines from the beginning - Element root = getDefaultRootElement(); - int lineCount = root.getElementCount(); - int overage = lineCount - maxLineCount; - if (overage > 0) { - // if 1200 lines, and 1000 lines is max, - // find the position of the end of the 200th line - Element lineElement = root.getElement(overage); - if (lineElement == null) - return; // do nuthin - - // remove to the end of the 200th line - int endOffset = lineElement.getEndOffset(); - remove(0, endOffset); - } - } catch (BadLocationException e) { - // ignore the error otherwise this will cause an infinite loop - // maybe not a good idea in the long run? - } - elements.clear(); - changed = false; - } - - public boolean isChanged() { - return changed; + public Document getDocument() { + return document; } } diff --git a/app/src/processing/app/EditorConsoleStream.java b/app/src/processing/app/EditorConsoleStream.java deleted file mode 100644 index 57d06cc91..000000000 --- a/app/src/processing/app/EditorConsoleStream.java +++ /dev/null @@ -1,65 +0,0 @@ -package processing.app; - -import java.io.OutputStream; -import java.io.PrintStream; - -class EditorConsoleStream extends OutputStream { - - private static EditorConsole currentConsole; - private static PrintStream systemErr; - private static PrintStream systemOut; - - public static void init() { - if (systemOut == null) { - systemOut = System.out; - systemErr = System.err; - - if (PreferencesData.getBoolean("console")) { - PrintStream consoleOut = new PrintStream(new EditorConsoleStream(false)); - PrintStream consoleErr = new PrintStream(new EditorConsoleStream(true)); - - System.setOut(consoleOut); - System.setErr(consoleErr); - } - } - } - - private final boolean isStdErr; // whether stderr or stdout - private final PrintStream system; - - private EditorConsoleStream(boolean isStdErr) { - this.isStdErr = isStdErr; - if (this.isStdErr) { - system = systemErr; - } else { - system = systemOut; - } - } - - public void close() { - } - - public void flush() { - } - - public void write(int b) { - write(new byte[]{(byte) b}); - } - - public void write(byte b[]) { // appears never to be used - write(b, 0, b.length); - } - - public void write(byte b[], int offset, int length) { - if (currentConsole != null) { - currentConsole.appendText(new String(b, offset, length), isStdErr); - } - - system.write(b, offset, length); - } - - public static void setCurrent(EditorConsole console) { - currentConsole = console; - } - -} diff --git a/app/src/processing/app/EditorStatus.java b/app/src/processing/app/EditorStatus.java index ce84c8c82..611457543 100644 --- a/app/src/processing/app/EditorStatus.java +++ b/app/src/processing/app/EditorStatus.java @@ -466,7 +466,7 @@ public class EditorStatus extends JPanel /*implements ActionListener*/ { String message = ""; message += _("Arduino: ") + BaseNoGui.VERSION_NAME_LONG + " (" + System.getProperty("os.name") + "), "; message += _("Board: ") + "\"" + BaseNoGui.getBoardPreferences().get("name") + "\"\n\n"; - message += editor.console.consoleTextPane.getText().trim(); + message += editor.console.getText(); if ((PreferencesData.getBoolean("build.verbose")) == false) { message += "\n\n"; message += " " + _("This report would have more information with") + "\n"; diff --git a/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java b/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java index b56559659..83897b196 100644 --- a/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java +++ b/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java @@ -54,6 +54,6 @@ public class HittingEscapeOnCloseConfirmationDialogTest extends AbstractGUITest EditorConsole console = (EditorConsole) window.scrollPane("console").component(); - assertEquals("", console.consoleDoc.getText(0, console.consoleDoc.getLength())); + assertEquals("", console.getText()); } } diff --git a/app/test/processing/app/ReduceIndentWith1CharOnLastLineTest.java b/app/test/processing/app/ReduceIndentWith1CharOnLastLineTest.java index f5cd01f89..81c467543 100644 --- a/app/test/processing/app/ReduceIndentWith1CharOnLastLineTest.java +++ b/app/test/processing/app/ReduceIndentWith1CharOnLastLineTest.java @@ -52,6 +52,6 @@ public class ReduceIndentWith1CharOnLastLineTest extends AbstractGUITest { EditorConsole console = (EditorConsole) window.scrollPane("console").component(); - assertEquals("", console.consoleDoc.getText(0, console.consoleDoc.getLength())); + assertEquals("", console.getText()); } } From 98874e4af29858136f0714c258c19d52499680fd Mon Sep 17 00:00:00 2001 From: Federico Fissore Date: Mon, 29 Jun 2015 15:04:30 +0200 Subject: [PATCH 2/2] ConsoleOutputStream: replacing \r to \n when printing on IDE console --- app/src/cc/arduino/ConsoleOutputStream.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/cc/arduino/ConsoleOutputStream.java b/app/src/cc/arduino/ConsoleOutputStream.java index a1c1e959c..b44603984 100644 --- a/app/src/cc/arduino/ConsoleOutputStream.java +++ b/app/src/cc/arduino/ConsoleOutputStream.java @@ -116,8 +116,9 @@ public class ConsoleOutputStream extends ByteArrayOutputStream { if (document != null) { SwingUtilities.invokeLater(() -> { try { + String lineWithoutSlashR = line.replace("\r\n", "\n").replace("\r", "\n"); int offset = document.getLength(); - document.insertString(offset, line, attributes); + document.insertString(offset, lineWithoutSlashR, attributes); } catch (BadLocationException ble) { //ignore }