mirror of
https://github.com/arduino/Arduino.git
synced 2025-01-18 07:52:14 +01:00
New console: replaces previous EditorConsoleStream with one that's faster and doesn't discard end chars.
See #2798
This commit is contained in:
parent
16c852ada5
commit
e15ba64ee2
131
app/src/cc/arduino/ConsoleOutputStream.java
Normal file
131
app/src/cc/arduino/ConsoleOutputStream.java
Normal file
@ -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
|
||||
* <p>
|
||||
* https://tips4java.wordpress.com/2008/11/08/message-console/
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
* <P>
|
||||
* 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.
|
||||
* <P>
|
||||
* 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.
|
||||
* <P>
|
||||
* 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<ElementSpec> elements = new ArrayList<ElementSpec>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user