/** Message.java * * @author zweibieren * @date Mar 10, 2012 11:17:57 AM * */ package com.physpics.tools; import java.awt.*; import javax.swing.*; import java.awt.event.*; import java.io.*; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultHighlighter; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.Document; import javax.swing.text.Highlighter; /** * Static methods for reporting errors. * The client may display the message area on screen or * in combination with some other element. * Otherwise messages are displayed in a standalone scrolled window. */ public class Message { private static final boolean DEBUG = false; /** * 0 = stdout is separate * 1 = Message is sending messages to stdout * 2 = stdout is being redirected to Messages */ private static int stdoutState = 0; private static final PrintStream stdout = System.out; private static final PrintStream stderr = System.err; public static final int MESSAGE_LINE_HEIGHT = 20; static Dimension fourLines = new Dimension(999, 4*MESSAGE_LINE_HEIGHT); static final JTextPane msgArea = new JTextPane(new DefaultStyledDocument()); static JScrollPane scrollMessage = null; static JFrame msgWindow = null; static JSplitPane splitter = null; // the message area has background msgBkgd for BACK_GROUND_TIME seconds // after first displaying a message static Color normalBkgd = Color.WHITE; static Color msgBkgd = new Color(255,238,119); static final int BACKGROUND_TIME = 4; static final DefaultHighlighter bodyHighs = new DefaultHighlighter(); // for body.doc static final Color HILIT_PINK = new Color(255, 170, 187); public static final Highlighter.HighlightPainter pinkPainter = new DefaultHighlighter.DefaultHighlightPainter(HILIT_PINK); static final Color HILIT_GREEN = new Color(187, 255, 170); public static final Highlighter.HighlightPainter greenPainter = new DefaultHighlighter.DefaultHighlightPainter(HILIT_GREEN); static final Color HILIT_BLUE = new Color(204, 238, 255); public static final Highlighter.HighlightPainter bluePainter = new DefaultHighlighter.DefaultHighlightPainter(HILIT_BLUE); static int promptStart = -1; // >= 0 iff points to location of last message static boolean needNewline = false; // T iff no need to add newline // after ten seconds, the background reverts to white static final javax.swing.Timer msgTimer = new javax.swing.Timer(BACKGROUND_TIME*1000, new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { appendNewline(); needNewline = false; msgArea.setBackground(normalBkgd); msgTimer.stop(); } }); static { msgTimer.setRepeats(false); } /** Cannot construct a Message. There is no such object */ private Message() { } /** * Create a scrolled message area. Suitable * for display in a splitpane or a window. * Result is in variable 'scrollMessage'. */ static void createMessageScroll() { msgArea.setEditable(false); bodyHighs.setDrawsLayeredHighlights(false); msgArea.setHighlighter(bodyHighs); scrollMessage = new JScrollPane(msgArea); scrollMessage.setVerticalScrollBarPolicy( ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); scrollMessage.setBorder(BorderFactory .createEmptyBorder(2, 3, 2, 3)); } private static void closing() { try { Thread.sleep(4000); } catch (InterruptedException ex) { } msgWindow = null; } static synchronized void showScrollInFrame() { if (msgWindow != null) return; if (scrollMessage == null) createMessageScroll(); msgWindow = new JFrame("M E S S A G E S"); msgWindow.add(scrollMessage); msgWindow.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { closing(); } @Override public void windowClosed(WindowEvent e) { closing(); } }); msgWindow.pack(); msgWindow.setSize(400, 200); msgWindow.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); javax.swing.SwingUtilities.invokeLater(() -> { if (msgWindow != null) msgWindow.setVisible(true); }); } /** * Report the message window. The message window appears for messages when * createSplitPane has not been called. Client may set icons on the message * window. This method has the side-effect of creating the message window * if it did not exist. * * Closing the message window disposes its resources, but does not * terminate the application. Presumably Message is invoked from an * application and that application will terminate execution. * * @return The JFrame for the message window. */ public static JFrame getMsgWindow() { if (msgWindow == null) showScrollInFrame(); // create the message window return msgWindow; } public static JTextPane getMsgArea() { return msgArea; } /** Create the msgarea and its scroller as soon as a message arrives * Initially put it in a Frame. * When createSplitPane is called, close the frame and move * the scroller to the SpitPane. * @param top What component to display above the message area * @return a JSplitPane wrapping top and the message line */ public static JSplitPane createSplitPane (JComponent top) { if (msgArea == null) createMessageScroll(); else if (msgWindow != null) { msgWindow.dispose(); // dispose the Frame msgWindow = null; } splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT, top, scrollMessage){ /** enforce a minimum height of one line */ @Override public void setDividerLocation(int v) { int totalH = splitter.getHeight(); int min = MESSAGE_LINE_HEIGHT + splitter.getDividerSize() + 1; int div = Math.min(v, totalH-min); // System.out.println("set divider v " + v + " totH " + totalH // + " min " + min + " => " + div); super.setDividerLocation(div); } }; splitter.setResizeWeight(1.0); splitter.setDividerSize(4); splitter.setOneTouchExpandable(false); return splitter; } /** * @return the stdoutState */ public static int getStdoutState() { return stdoutState; } /** Set the System.out and System.err destinations. * 0 = stdout is separate * 1 = Message is sending messages to stdout * 2 = stdout is being redirected to Messages * @param newState the stdoutState to set */ public static void setStdoutState(int newState) { if (stdoutState == newState) return; if (stdoutState == 2) { System.out.flush(); // flush to the Message window System.setOut(stdout); // revert to system stdout System.err.flush(); // flush to the Message window System.setErr(stderr); // revert to system stderr } stdoutState = newState; // (states 0 and 1 are tested when needed) if (stdoutState == 2) { // initiate capturing stdout // establish a PrintStream that flushes lines to the Message window // (we continue using the buffers of stdout until we get newline) System.setOut(new PrintStream(new ShowStream(bluePainter))); System.setErr(new PrintStream(new ShowStream(pinkPainter))); } } // C L I E N T M E S S A G E D I S P L A Y /** * Make sure the text in the message line ends with a newline. * Also, if a prompt has been displayed, remove it. */ static void appendNewline() { Document doc = msgArea.getDocument(); int doclen = doc.getLength(); try { if (promptStart >= 0) { msgArea.getDocument().remove(promptStart, doclen-promptStart); promptStart = -1; } else msgArea.getDocument().insertString(doclen, "\n", null); msgArea.setCaretPosition(msgArea.getDocument().getLength()); } catch (BadLocationException ex) { System.err.println("Message.appendNewLine() failed in removing prompt"); } } public static void show(String msg) { show(msg, null); } public static void show(final String msg, final int option) { if (option == JOptionPane.ERROR_MESSAGE) show(msg, pinkPainter); else show(msg, null); } /** * Display a message to output, a window, or a message line. * Is synchronized so it can be called from multiple threads. * @param msg The text of the message. * @param color Color of message as a Highlighter.HighlightPainter. * Should usually be greenPainter or pinkPainter. */ public static synchronized void show(final String msg, final Highlighter.HighlightPainter color) { if (stdoutState == 1) { System.out.println(":::" + msg); return; } if (scrollMessage == null) { // create SplitPane has not been called; there is no message line // open a window for messages createMessageScroll(); showScrollInFrame(); } if (msgTimer.isRunning()) { msgTimer.stop(); // abort any possible pending timer-fire // inserting message before timeout // grow the message area to show both // maybeGrowMsgArea(); } // ensure msgArea text never ends with extra \n String msgNoNL; if (msg.endsWith("\n")) msgNoNL = msg.substring(0, msg.length()-1); else msgNoNL = msg; msgNoNL += " "; // add space to prevent color leaking to next line appendNewline(); int textLen = msgArea.getDocument().getLength(); try { msgArea.getDocument().insertString(textLen, msgNoNL, null); // if msg takes more than one line AND if splitpane, adjust split upward } catch (BadLocationException ex) { System.err.println("bad doc in msgArea"); } if (color != null) { try { // the "-1" adjusts highlight to precede the trailing space bodyHighs.addHighlight(textLen, textLen+msgNoNL.length()-1, color); } catch (BadLocationException ex) { System.err.println("Message: bad loc for coloring"); } } needNewline = true; // timer adds newline to hide message msgArea.setBackground(msgBkgd); msgTimer.start(); msgArea.setCaretPosition(msgArea.getDocument().getLength()-2); } /** * Show a message for a short time or until another message replaces it. * The message does NOT remain in the log. * @param msg The message to display. */ public static void prompt(String msg) { appendNewline(); int tps = msgArea.getDocument().getLength(); show(msg); promptStart = tps; } /** Ring the bell at the terminal. */ public static void beep() { Toolkit.getDefaultToolkit().beep(); } /** Ring the bell and display an error message. * It will be displayed with an error color background. * @param msg The message to display in the log. */ public static void beep(String msg) { beep(); show(msg, JOptionPane.ERROR_MESSAGE); int breakpointable; breakpointable = 1; // System.out.println("BEEP: " + msg); } /** Ring the bell and display an error message * prefixed with "Programmer error". This tells the user * that it not hisr fault. The message is * displayed with an error color background. * @param msg The message to display in the log. */ public static void programmerError(String msg) { beep("Programmer error: " + msg); } /** Ring the bell and display an exception message * prefixed with "EXCEPTION". This suggests to the user * that it not hisr fault. The message is * displayed with an error color background. * @param msg The message to display in the log. * @param ex The exception. */ public static void exception(String msg, Exception ex) { beep("EXCEPTION: " + msg + " " + " (" + ex.getLocalizedMessage() + ")"); } /** Identical to the exception method * @param msg message to identify source of error * @param ex should be an IOException*/ public static void fileException(String msg, IOException ex) { beep("EXCEPTION: " + msg + " " + " (" + ex.getLocalizedMessage() + ")"); } /** Tell the user there was a file error, but execution is continuing. * @param msg printed in parentheses after the file name. * @param fileName name of the file in error */ public static void ignoreFileException(String msg, String fileName) { beep("Ignoring IO error for " + fileName +(msg==null ? "" : ( ": " + msg))); } /** * An exception has occurred and is being ignored. * By turning on DEBUG, the programmer will see these messages. * @param ex the exception being ignored. */ public static void ignoreException(Exception ex) { // this is here so we can catch these Exceptions during debuging if (DEBUG) exception("ignored Exception occurred", ex); int breakpointable; breakpointable = 1; } /** * Print a message on System.out. This method should be called at * all program points that supposedly cannot be reached. * It can be useful to set a breakpoint here. * @param msg */ public static void impossible(String msg) { System.out.println("The impossible has happened: " + msg); int breakpointable; breakpointable= 1; } /** Text written to a ShowStream is passed to the show method * when a newline is encountered or a flush occurs. * CR characters are ignored. */ public static class ShowStream extends OutputStream{ Highlighter.HighlightPainter painter; ByteArrayOutputStream buf = new ByteArrayOutputStream(200); public ShowStream(Highlighter.HighlightPainter color) { painter = color; } @Override public void close() { flush(); } /** show() the buffer contents, if any */ @Override public void flush() { String s = buf.toString(); buf.reset(); show(s, painter); } /** Put characters in a buffer until newline arrives; * then show() the whole * @param b Incoming character */ @Override public void write(int b) { if (b != '\r') // ignore CR buf.write(b); if (b == '\n') // flush after newline flush(); } } } // end class Message