/* -*- mode: jde; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2004-05 Ben Fry and Casey Reas Copyright (c) 2001-04 Massachusetts Institute of Technology This program 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package processing.app; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.text.*; import java.util.*; /** * 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. */ public class EditorConsole extends JScrollPane { Editor editor; JTextPane consoleTextPane; BufferedStyledDocument consoleDoc; MutableAttributeSet stdStyle; MutableAttributeSet errStyle; boolean cerror; //int maxCharCount; int maxLineCount; static File errFile; static File outFile; static File tempFolder; static PrintStream systemOut; static PrintStream systemErr; static PrintStream consoleOut; static PrintStream consoleErr; static OutputStream stdoutFile; static OutputStream stderrFile; public EditorConsole(Editor editor) { this.editor = editor; maxLineCount = Preferences.getInteger("console.length"); consoleDoc = new BufferedStyledDocument(10000, maxLineCount); consoleTextPane = new JTextPane(consoleDoc); consoleTextPane.setEditable(false); // necessary? MutableAttributeSet standard = new SimpleAttributeSet(); StyleConstants.setAlignment(standard, StyleConstants.ALIGN_LEFT); consoleDoc.setParagraphAttributes(0, 0, standard, true); // build styles for different types of console output Color bgColor = Preferences.getColor("console.color"); Color fgColorOut = Preferences.getColor("console.output.color"); Color fgColorErr = Preferences.getColor("console.error.color"); Font font = Preferences.getFont("console.font"); 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 this.setViewportView(consoleTextPane); // calculate height of a line of text in pixels // and size window accordingly FontMetrics metrics = this.getFontMetrics(font); int height = metrics.getAscent() + metrics.getDescent(); int lines = Preferences.getInteger("console.lines"); //, 4); 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)); if (systemOut == null) { systemOut = System.out; systemErr = System.err; tempFolder = Base.createTempFolder("console"); try { String outFileName = Preferences.get("console.output.file"); if (outFileName != null) { outFile = new File(tempFolder, outFileName); stdoutFile = new FileOutputStream(outFile); //outFile.deleteOnExit(); } String errFileName = Preferences.get("console.error.file"); if (errFileName != null) { errFile = new File(tempFolder, errFileName); stderrFile = new FileOutputStream(errFile); //errFile.deleteOnExit(); } } catch (IOException e) { Base.showWarning("Console Error", "A problem occurred while trying to open the\n" + "files used to store the console output.", e); } consoleOut = new PrintStream(new EditorConsoleStream(this, false, stdoutFile)); consoleErr = new PrintStream(new EditorConsoleStream(this, true, stderrFile)); if (Preferences.getBoolean("console")) { try { System.setOut(consoleOut); System.setErr(consoleErr); } catch (Exception e) { e.printStackTrace(systemOut); } } } // to fix ugliness.. normally macosx java 1.3 puts an // ugly white border around this object, so turn it off. if (Base.isMacOS()) { setBorder(null); } // periodically post buffered messages to the console // should the interval come from the preferences file? new javax.swing.Timer(250, new ActionListener() { public void actionPerformed(ActionEvent evt) { // only if new text has been added if (consoleDoc.hasAppendage) { // 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(); } /** * Close the streams so that the temporary files can be deleted. *
* File.deleteOnExit() cannot be used because the stdout and stderr * files are inside a folder, and have to be deleted before the * folder itself is deleted, which can't be guaranteed when using * the deleteOnExit() method. */ public void handleQuit() { // replace original streams to remove references to console's streams System.setOut(systemOut); System.setErr(systemErr); // close the PrintStream consoleOut.close(); consoleErr.close(); // also have to close the original FileOutputStream // otherwise it won't be shut down completely try { stdoutFile.close(); stderrFile.close(); } catch (IOException e) { e.printStackTrace(systemOut); } outFile.delete(); errFile.delete(); tempFolder.delete(); } public void write(byte b[], int offset, int length, boolean err) { if (err != cerror) { // advance the line because switching between err/out streams // potentially, could check whether we're already on a new line message("", cerror, true); } // we could do some cross platform CR/LF mangling here before outputting // add text to output document message(new String(b, offset, length), err, false); // set last error state cerror = err; } // added sync for 0091.. not sure if it helps or hinders synchronized public void message(String what, boolean err, boolean advance) { if (err) { systemErr.print(what); } else { systemOut.print(what); } if (advance) { appendText("\n", err); if (err) { systemErr.println(); } else { systemOut.println(); } } // to console display appendText(what, err); // moved down here since something is punting } /** * 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 private 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? } } } class EditorConsoleStream extends OutputStream { EditorConsole parent; boolean err; // whether stderr or stdout byte single[] = new byte[1]; OutputStream echo; public EditorConsoleStream(EditorConsole parent, boolean err, OutputStream echo) { this.parent = parent; this.err = err; this.echo = echo; } public void close() { } public void flush() { } public void write(byte b[]) { // appears never to be used parent.write(b, 0, b.length, err); if (echo != null) { try { echo.write(b); //, 0, b.length); echo.flush(); } catch (IOException e) { e.printStackTrace(); echo = null; } } } public void write(byte b[], int offset, int length) { parent.write(b, offset, length, err); if (echo != null) { try { echo.write(b, offset, length); echo.flush(); } catch (IOException e) { e.printStackTrace(); echo = null; } } } public void write(int b) { single[0] = (byte)b; parent.write(single, 0, 1, err); if (echo != null) { try { echo.write(b); echo.flush(); } catch (IOException e) { e.printStackTrace(); echo = null; } } } } /** * 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 */ class BufferedStyledDocument extends DefaultStyledDocument { ArrayList elements = new ArrayList(); int maxLineLength, maxLineCount; int currentLineLength = 0; boolean needLineBreak = false; boolean hasAppendage = false; public BufferedStyledDocument(int maxLineLength, int maxLineCount) { this.maxLineLength = maxLineLength; this.maxLineCount = maxLineCount; } /** buffer a string for insertion at the end of the DefaultStyledDocument */ public synchronized void appendString(String str, AttributeSet a) { // do this so that it's only updated when needed (otherwise console // updates every 250 ms when an app isn't even running.. see bug 180) hasAppendage = true; // process each line of the string while (str.length() > 0) { // newlines within an element have (almost) no effect, so we need to // replace them with proper paragraph breaks (start and end tags) if (needLineBreak || currentLineLength > maxLineLength) { elements.add(new ElementSpec(a, ElementSpec.EndTagType)); elements.add(new ElementSpec(a, ElementSpec.StartTagType)); currentLineLength = 0; } if (str.indexOf('\n') == -1) { elements.add(new ElementSpec(a, ElementSpec.ContentType, str.toCharArray(), 0, str.length())); currentLineLength += str.length(); needLineBreak = false; str = str.substring(str.length()); // eat the string } else { elements.add(new ElementSpec(a, ElementSpec.ContentType, str.toCharArray(), 0, str.indexOf('\n') + 1)); needLineBreak = true; str = str.substring(str.indexOf('\n') + 1); // eat the line } } } /** insert the buffered strings */ public synchronized void insertAll() { ElementSpec[] elementArray = new ElementSpec[elements.size()]; elements.toArray(elementArray); try { // check how many lines have been used so far // if too many, shave off a few lines from the beginning Element element = super.getDefaultRootElement(); int lineCount = element.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 //systemOut.println("overage is " + overage); Element lineElement = element.getElement(overage); if (lineElement == null) return; // do nuthin int endOffset = lineElement.getEndOffset(); // remove to the end of the 200th line super.remove(0, endOffset); } super.insert(super.getLength(), elementArray); } catch (BadLocationException e) { // ignore the error otherwise this will cause an infinite loop // maybe not a good idea in the long run? } elements.clear(); hasAppendage = false; } }