/* 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.alloy4; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; import java.awt.Rectangle; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.LinkedHashMap; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import edu.mit.csail.sdg.alloy4.Listener.Event; /** Graphical multi-tabbed syntax-highlighting editor. * * <p><b>Thread Safety:</b> Can be called only by the AWT event thread. * * <p><b>Invariant</b>: each tab has distinct file name. */ public final class OurTabbedSyntaxWidget { /** The current list of listeners; possible events are { STATUS_CHANGE, FOCUSED, CARET_MOVED }. */ public final Listeners listeners = new Listeners(); /** The JScrollPane containing everything. */ private final JPanel component = OurUtil.make(new JPanel()); /** Background color for the list of tabs. */ private static final Color GRAY = new Color(0.9f, 0.9f, 0.9f); /** Background color for an inactive tab. */ private static final Color INACTIVE = new Color(0.8f, 0.8f, 0.8f); /** Background color for a inactive and highlighted tab. */ private static final Color INACTIVE_HIGHLIGHTED = new Color(0.7f, 0.5f, 0.5f); /** Foreground color for a active and highlighted tab. */ private static final Color ACTIVE_HIGHLIGHTED = new Color(0.5f, 0.2f, 0.2f); /** Default border color for each tab. */ private static final Color BORDER = Color.LIGHT_GRAY; /** The font name to use in the JTextArea */ private String fontName = "Monospaced"; /** The font size to use in the JTextArea */ private int fontSize = 14; /** The tabsize to use in the JTextArea */ private int tabSize = 4; /** Whether syntax highlighting is current enabled or not. */ private boolean syntaxHighlighting; /** The list of clickable tabs. */ private final JPanel tabBar; /** The scrollPane that wraps around this.tabbar */ private final JScrollPane tabBarScroller; /** The list of tabs. */ private final List<OurSyntaxWidget> tabs = new ArrayList<OurSyntaxWidget>(); /** The currently selected tab from 0 to list.size()-1 (This value must be 0 if there are no tabs) */ private int me = 0; /** This object receives messages from sub-JTextPane objects. */ private final Listener listener = new Listener() { public Object do_action(Object sender, Event e) { final OurTabbedSyntaxWidget me = OurTabbedSyntaxWidget.this; if (sender instanceof OurSyntaxWidget) switch(e) { case FOCUSED: listeners.fire(me, e); break; case CARET_MOVED: listeners.fire(me, Event.STATUS_CHANGE); break; case CTRL_PAGE_UP: prev(); break; case CTRL_PAGE_DOWN: next(); break; case STATUS_CHANGE: clearShade(); OurSyntaxWidget t = (OurSyntaxWidget)sender; String n = t.getFilename(); t.obj1.setToolTipText(n); int i = n.lastIndexOf('/'); if (i >= 0) n = n.substring(i + 1); i = n.lastIndexOf('\\'); if (i >= 0) n = n.substring(i + 1); i = n.lastIndexOf('.'); if (i >= 0) n = n.substring(0, i); if (n.length() > 14) { n = n.substring(0, 14) + "..."; } if (t.obj1 instanceof JLabel) { ((JLabel)(t.obj1)).setText(" " + n + (t.modified() ? " * " : " ")); } listeners.fire(me, Event.STATUS_CHANGE); break; } return true; } public Object do_action(Object sender, Event e, Object arg) { return true; } }; /** Constructs a tabbed editor pane. */ public OurTabbedSyntaxWidget(String fontName, int fontSize, int tabSize) { component.setBorder(null); component.setLayout(new BorderLayout()); JPanel glue = OurUtil.makeHB(new Object[]{null}); glue.setBorder(new OurBorder(null, null, BORDER, null)); tabBar = OurUtil.makeHB(glue); if (!Util.onMac()) { tabBar.setOpaque(true); tabBar.setBackground(GRAY); } tabBarScroller = new JScrollPane(tabBar, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); tabBarScroller.setFocusable(false); tabBarScroller.setBorder(null); setFont(fontName, fontSize, tabSize); newtab(null); tabBarScroller.addComponentListener(new ComponentListener() { public final void componentResized(ComponentEvent e) { select(me); } public final void componentMoved(ComponentEvent e) { select(me); } public final void componentShown(ComponentEvent e) { select(me); } public final void componentHidden(ComponentEvent e) { } }); } /** Add this object into the given container. */ public void addTo(JComponent newParent, Object constraint) { newParent.add(component, constraint); } /** Adjusts the background and foreground of all labels. */ private void adjustLabelColor() { for(int i=0; i<tabs.size(); i++) { OurSyntaxWidget tab = tabs.get(i); boolean hl = tab.shaded(); tab.obj1.setBorder(new OurBorder(BORDER, BORDER, (i!=me ? BORDER : Color.WHITE), BORDER)); tab.obj1.setBackground(i!=me ? (hl ? INACTIVE_HIGHLIGHTED : INACTIVE) : Color.WHITE); tab.obj1.setForeground(hl ? (i!=me ? Color.BLACK : ACTIVE_HIGHLIGHTED) : Color.BLACK); } } /** Removes all highlights from every text buffer, then adjust the TAB LABEL COLOR if necessary. */ public void clearShade() { for(int i=0; ;i++) if (i < tabs.size()) tabs.get(i).clearShade(); else { adjustLabelColor(); break; } } /** Switch to the i-th tab (Note: if successful, it will always send STATUS_CHANGE to the registered listeners. */ private void select(int i) { if (i < 0 || i >= tabs.size()) return; else { me=i; component.revalidate(); adjustLabelColor(); component.removeAll(); } if (tabs.size() > 1) component.add(tabBarScroller, BorderLayout.NORTH); tabs.get(me).addTo(component, BorderLayout.CENTER); component.repaint(); tabs.get(me).requestFocusInWindow(); tabBar.scrollRectToVisible(new Rectangle(0,0,0,0)); // Forces recalculation tabBar.scrollRectToVisible(new Rectangle(tabs.get(me).obj2.getX(), 0, tabs.get(me).obj2.getWidth()+200, 1)); listeners.fire(this, Event.STATUS_CHANGE); } /** Refresh all tabs. */ public void reloadAll() { for(OurSyntaxWidget t: tabs) t.reload(); } /** Return the list of all filenames except the filename in the i-th tab. */ private List<String> getAllNamesExcept(int i) { ArrayList<String> ans = new ArrayList<String>(); for(int x=0; ;x++) if (x == tabs.size()) return ans; else if (x != i) ans.add(tabs.get(x).getFilename()); } /** Save the current tab content to the file system. * @param alwaysPickNewName - if true, it will always pop up a File Selection dialog box to ask for the filename * @return null if an error occurred (otherwise, return the filename) */ public String save(boolean alwaysPickNewName) { if (me < 0 || me >= tabs.size() || !tabs.get(me).save(alwaysPickNewName, getAllNamesExcept(me))) return null; return tabs.get(me).getFilename(); } /** Close the i-th tab (if there are no more tabs afterwards, we'll create a new empty tab). * * <p> If the text editor content is not modified since the last save, then return true; otherwise, ask the user. * <p> If the user says to save it, we will attempt to save it, then return true iff the save succeeded. * <p> If the user says to discard it, this method returns true. * <p> If the user says to cancel it, this method returns false (and the original tab and its contents will not be harmed). */ private boolean close(int i) { clearShade(); if (i<0 || i>=tabs.size()) return true; else if (!tabs.get(i).discard(true, getAllNamesExcept(i))) return false; if (tabs.size() > 1) { tabBar.remove(i); tabs.remove(i); if (me >= tabs.size()) me = tabs.size() - 1; } select(me); return true; } /** Close the current tab (then create a new empty tab if there were no tabs remaining) */ public void close() { close(me); } /** Close every tab then create a new empty tab; returns true iff success. */ public boolean closeAll() { for(int i=tabs.size()-1; i>=0; i--) if (tabs.get(i).modified()==false) close(i); // first close all the unmodified files for(int i=tabs.size()-1; i>=0; i--) if (close(i)==false) return false; // then attempt to close modified files one-by-one return true; } /** Returns the number of tabs. */ public int count() { return tabs.size(); } /** Return a modifiable map from each filename to its text content (Note: changes to the map does NOT affect this object) */ public Map<String,String> takeSnapshot() { Map<String,String> map = new LinkedHashMap<String,String>(); for(OurSyntaxWidget t: tabs) { map.put(t.getFilename(), t.getText()); } return map; } /** Returns the list of filenames corresponding to each text buffer. */ public List<String> getFilenames() { return getAllNamesExcept(-1); } /** Changes the font name, font size, and tabsize of every text buffer. */ public void setFont(String name, int size, int tabSize) { fontName=name; fontSize=size; this.tabSize=tabSize; for(OurSyntaxWidget t: tabs) t.setFont(name, size, tabSize); } /** Enables or disables syntax highlighting. */ public void enableSyntax(boolean flag) { syntaxHighlighting=flag; for(OurSyntaxWidget t: tabs) t.enableSyntax(flag); } /** Returns the JTextArea of the current text buffer. */ public OurSyntaxWidget get() { return (me>=0 && me<tabs.size()) ? tabs.get(me) : new OurSyntaxWidget(); } /** True if the i-th text buffer has been modified since it was last loaded/saved */ public boolean modified(int i) { return (i>=0 && i<tabs.size()) ? tabs.get(i).modified() : false; } /** Switches to the previous tab. */ public void prev() { if (tabs.size()>=2) select(me==0 ? tabs.size()-1 : (me-1)); } /** Switches to the next tab. */ public void next() { if (tabs.size()>=2) select(me==tabs.size()-1 ? 0 : (me+1)); } /** Create a new tab with the given filename (if filename==null, we'll create a blank tab instead) * <p> If a text buffer with that filename already exists, we will just switch to it; else we'll read that file into a new tab. * @return false iff an error occurred */ public boolean newtab(String filename) { if (filename!=null) { filename = Util.canon(filename); for(int i=0; i<tabs.size(); i++) if (tabs.get(i).getFilename().equals(filename)) { if (i!=me) select(i); return true; } } final JLabel lb = OurUtil.label("", OurUtil.getVizFont().deriveFont(Font.BOLD), Color.BLACK, Color.WHITE); lb.setBorder(new OurBorder(BORDER, BORDER, Color.WHITE, BORDER)); lb.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { for(int i=0; i<tabs.size(); i++) if (tabs.get(i).obj1 == lb) select(i); } }); JPanel h1 = OurUtil.makeH(4); h1.setBorder(new OurBorder(null, null, BORDER, null)); JPanel h2 = OurUtil.makeH(3); h2.setBorder(new OurBorder(null, null, BORDER, null)); JPanel pan = Util.onMac() ? OurUtil.makeVL(null, 2, OurUtil.makeHB(h1, lb, h2)) : OurUtil.makeVL(null, 2, OurUtil.makeHB(h1, lb, h2, GRAY), GRAY); pan.setAlignmentX(0.0f); pan.setAlignmentY(1.0f); OurSyntaxWidget text = new OurSyntaxWidget(syntaxHighlighting, "", fontName, fontSize, tabSize, lb, pan); tabBar.add(pan, tabs.size()); tabs.add(text); text.listeners.add(listener); // add listener AFTER we've updated this.tabs and this.tabBar if (filename==null) { text.discard(false, getFilenames()); // forces the tab to re-derive a suitable fresh name } else { if (!text.load(filename)) return false; for(int i=tabs.size()-1; i>=0; i--) if (!tabs.get(i).isFile() && tabs.get(i).getText().length()==0) { tabs.get(i).discard(false, getFilenames()); close(i); break; // Remove the rightmost untitled empty tab } } select(tabs.size() - 1); // Must call this to switch to the new tab; and it will fire STATUS_CHANGE message which is important return true; } /** Highlights the text editor, based on the location information in the set of Pos objects. */ public void shade(Iterable<Pos> set, Color color, boolean clearOldHighlightsFirst) { if (clearOldHighlightsFirst) clearShade(); OurSyntaxWidget text = null; int c = 0, d; for(Pos p: set) if (p!=null && p.filename.length()>0 && p.y>0 && p.x>0 && newtab(p.filename)) { text = get(); c = text.getLineStartOffset(p.y-1) + p.x - 1; d = text.getLineStartOffset(p.y2-1) + p.x2 - 1; text.shade(color, c, d+1); } if (text!=null) { text.moveCaret(0, 0); text.moveCaret(c, c); } // Move to 0 ensures we'll scroll to the highlighted section get().requestFocusInWindow(); adjustLabelColor(); listeners.fire(this, Event.STATUS_CHANGE); } /** Highlights the text editor, based on the location information in the Pos object. */ public void shade(Pos pos) { shade(Util.asList(pos), new Color(0.9f, 0.4f, 0.4f), true); } }