/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package edu.mit.csail.sdg.alloy4whole; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.border.EmptyBorder; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.BoxView; import javax.swing.text.Element; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import javax.swing.text.StyledEditorKit; import javax.swing.text.View; import javax.swing.text.ViewFactory; import edu.mit.csail.sdg.alloy4.OurAntiAlias; import edu.mit.csail.sdg.alloy4.OurUtil; /** This helper method is used by SimpleGUI; only the AWT Event Thread may call methods in this class. */ final class SwingLogPanel { /** Try to wrap the input to about 60 characters per line; however, if a token is too long, we won't break it. */ private static void linewrap(StringBuilder sb, String msg) { StringTokenizer tokenizer=new StringTokenizer(msg,"\r\n\t "); final int max=60; int now=0; while(tokenizer.hasMoreTokens()) { String x=tokenizer.nextToken(); if (now+1+x.length() > max) { if (now>0) { sb.append('\n'); } sb.append(x); now=x.length(); } else { if (now>0) { now++; sb.append(' '); } sb.append(x); now=now+x.length(); } } } /** This field buffers previous calls to log() so that we can write them out later in a single Swing call * (If there is nothing buffered, this field can be an empty list or even null). */ private final List<String> batch = new ArrayList<String>(); /** The newly created JTextPane object that will display the log; null if this log has been destroyed. */ private JTextPane log; /** The style to use when writing regular messages. */ private final Style styleRegular; /** The style to use when writing bold messages. */ private final Style styleBold; /** The style to use when writing red messages. */ private final Style styleRed; /** This stores the JLabels used for displaying hyperlinks. */ private final List<JLabel> links = new ArrayList<JLabel>(); /** When the window gains focus, we'll call handler.run(ev_logFocused); * When a hyperlink is clicked, we'll call handler.run(evs_visualize, linkURL). */ private final SimpleGUI handler; /** The current length of the log, not counting any "red" error message at the end of the log. */ private int lastSize = 0; /** The current font name. */ private String fontName; /** The current font size. */ private int fontSize; /** The color to use for hyperlinks when the mouse is not hovering over it. */ private static final Color linkColor = new Color(0.3f, 0.3f, 0.9f); /** The color to use for a hyperlink when the mouse is hovering over it. */ private static final Color hoverColor = new Color(0.9f, 0.3f, 0.3f); /** This stores a default ViewFactory that will handle the View requests that we don't care to override. */ private static final ViewFactory defaultFactory = (new StyledEditorKit()).getViewFactory(); /** Constructs a new JTextPane logger, and put it inside an existing JScrollPane. * @param parent - the existing JScrollPane to insert the JTextPane into * @param fontName - the font name to use * @param fontSize - the font size to use * @param background - the background color to use * @param regular - the color to use for regular messages * @param red - the color to use for red messages * @param handler - the SimpleGUI parent */ public SwingLogPanel( final JScrollPane parent, String fontName, int fontSize, final Color background, final Color regular, final Color red, final SimpleGUI handler) { this.handler = handler; this.fontName = fontName; this.fontSize = fontSize; this.log = OurUtil.make(OurAntiAlias.pane(), Color.BLACK, background, new EmptyBorder(1,1,1,1), new Font(fontName, Font.PLAIN, fontSize)); // This customized StyledEditorKit prevents line-wrapping up to 30000 pixels wide. // 30000 is a good number; value higher than about 32768 will cause errors. this.log.setEditorKit(new StyledEditorKit() { private static final long serialVersionUID = 0; @Override public final ViewFactory getViewFactory() { return new ViewFactory() { public final View create(Element x) { if (!AbstractDocument.SectionElementName.equals(x.getName())) return defaultFactory.create(x); return new BoxView(x, View.Y_AXIS) { @Override public final float getMinimumSpan(int axis) { return super.getPreferredSpan(axis); } @Override public final void layout(int width,int height) { super.layout(30000, height); } }; } }; } }); log.setEditable(false); log.addFocusListener(new FocusListener() { public final void focusGained(FocusEvent e) { if (handler!=null) handler.notifyFocusLost(); } public final void focusLost(FocusEvent e) { } }); StyledDocument doc = log.getStyledDocument(); styleRegular = doc.addStyle("regular", null); StyleConstants.setFontFamily(styleRegular, fontName); StyleConstants.setFontSize(styleRegular, fontSize); StyleConstants.setForeground(styleRegular, regular); styleBold = doc.addStyle("bold", styleRegular); StyleConstants.setBold(styleBold, true); styleRed = doc.addStyle("red", styleBold); StyleConstants.setForeground(styleRed, red); parent.setViewportView(log); parent.setBackground(background); } /** Write a horizontal separator into the log window. */ public void logDivider() { if (log==null) return; clearError(); StyledDocument doc = log.getStyledDocument(); Style dividerStyle = doc.addStyle("bar", styleRegular); JPanel jpanel = new JPanel(); jpanel.setBackground(Color.LIGHT_GRAY); jpanel.setPreferredSize(new Dimension(300,1)); // 300 is arbitrary, since it will auto-stretch StyleConstants.setComponent(dividerStyle, jpanel); reallyLog(".", dividerStyle); // Any character would do; "." will be replaced by the JPanel reallyLog("\n\n", styleRegular); log.setCaretPosition(doc.getLength()); lastSize = doc.getLength(); } /** Write a clickable link into the log window. */ public void logLink(final String link, final String linkDestination) { if (log==null || link.length()==0) return; if (linkDestination==null || linkDestination.length()==0) { log(link); return; } clearError(); StyledDocument doc = log.getStyledDocument(); Style linkStyle = doc.addStyle("link", styleRegular); final JLabel label = OurUtil.make(OurAntiAlias.label(link), new Font(fontName, Font.BOLD, fontSize), linkColor); label.setAlignmentY(0.8f); label.setMaximumSize(label.getPreferredSize()); label.addMouseListener(new MouseListener(){ public final void mousePressed(MouseEvent e) { if (handler!=null) handler.doVisualize(linkDestination); } public final void mouseClicked(MouseEvent e) { } public final void mouseReleased(MouseEvent e) { } public final void mouseEntered(MouseEvent e) { label.setForeground(hoverColor); } public final void mouseExited(MouseEvent e) { label.setForeground(linkColor); } }); StyleConstants.setComponent(linkStyle, label); links.add(label); reallyLog(".", linkStyle); // Any character would do; the "." will be replaced by the JLabel log.setCaretPosition(doc.getLength()); lastSize = doc.getLength(); } /** Write "msg" in regular style. */ public void log(String msg) { if (log!=null && msg.length()>0) batch.add(msg); } /** Write "msg" in bold style. */ public void logBold(String msg) { if (msg.length()>0) {clearError(); reallyLog(msg, styleBold);} } private void reallyLog(String text, Style style) { if (log==null || text.length()==0) return; int i=text.lastIndexOf('\n'), j=text.lastIndexOf('\r'); if (i>=0 && i<j) { i=j; } StyledDocument doc=log.getStyledDocument(); try { if (i<0) { doc.insertString(doc.getLength(), text, style); } else { // Performs intelligent caret positioning doc.insertString(doc.getLength(), text.substring(0,i+1), style); log.setCaretPosition(doc.getLength()); if (i<text.length()-1) { doc.insertString(doc.getLength(), text.substring(i+1), style); } } } catch (BadLocationException e) { // Harmless } if (style!=styleRed) { lastSize=doc.getLength(); } } /** Write "msg" in red style (with automatic line wrap). */ public void logRed (String msg) { if (log==null || msg==null || msg.length()==0) return; StringBuilder sb=new StringBuilder(); while (msg.length()>0) { int i = msg.indexOf('\n'); if (i>=0) { linewrap(sb, msg.substring(0,i)); sb.append('\n'); msg=msg.substring(i+1); } else { linewrap(sb, msg); break; } } clearError(); reallyLog(sb.toString(), styleRed); } /** Write "msg" in regular style (with automatic line wrap). */ public void logIndented (String msg) { if (log==null || msg.length()==0) return; StringBuilder sb=new StringBuilder(); while(msg.length()>0) { int i = msg.indexOf('\n'); if (i>=0) { linewrap(sb, msg.substring(0,i)); sb.append('\n'); msg=msg.substring(i+1); } else { linewrap(sb, msg); break; } } clearError(); reallyLog(sb.toString(), styleRegular); } /** Set the font name. */ public void setFontName(String fontName) { if (log==null) return; this.fontName = fontName; log.setFont(new Font(fontName, Font.PLAIN, fontSize)); StyleConstants.setFontFamily(styleRegular, fontName); StyleConstants.setFontFamily(styleBold, fontName); StyleConstants.setFontFamily(styleRed, fontName); StyleConstants.setFontSize(styleRegular, fontSize); StyleConstants.setFontSize(styleBold, fontSize); StyleConstants.setFontSize(styleRed, fontSize); // Changes all existing text StyledDocument doc=log.getStyledDocument(); Style temp=doc.addStyle("temp", null); StyleConstants.setFontFamily(temp, fontName); StyleConstants.setFontSize(temp, fontSize); doc.setCharacterAttributes(0, doc.getLength(), temp, false); // Changes all existing hyperlinks Font newFont = new Font(fontName, Font.BOLD, fontSize); for(JLabel link: links) { link.setFont(newFont); } } /** Set the font size. */ public void setFontSize(int fontSize) { if (log==null) return; this.fontSize = fontSize; setFontName(this.fontName); } /** Set the background color. */ public void setBackground(Color background) { if (log==null) return; log.setBackground(background); } /** Query the current length of the log. */ int getLength() { if (log==null) return 0; clearError(); return log.getStyledDocument().getLength(); } /** Truncate the log to the given length; if the log is shorter than the number given, then nothing happens. */ void setLength(int newLength) { if (log==null) return; clearError(); StyledDocument doc=log.getStyledDocument(); int n=doc.getLength(); if (n<=newLength) return; try { doc.remove(newLength, n-newLength); } catch (BadLocationException e) { // Harmless } if (lastSize>doc.getLength()) { lastSize=doc.getLength(); } } /** This method copies the currently selected text in the log (if any) into the clipboard. */ public void copy() { if (log==null) return; log.copy(); } /** Removes any messages writtin in "red" style. */ public void clearError() { if (log==null) return; // Since this class always removes "red" messages prior to writing anything, // that means if there are any red messages, they will always be at the end of the JTextPane. StyledDocument doc=log.getStyledDocument(); int n=doc.getLength(); if (n>lastSize) { try {doc.remove(lastSize, n-lastSize);} catch (BadLocationException e) {} } if (batch.size()>0) { for(String msg: batch) { reallyLog(msg, styleRegular); } batch.clear(); } } /** Commits all outstanding writes (if the messages are buffered). */ public void flush() { if (log==null) return; if (batch.size()>0) clearError(); } }