package org.gjt.sp.jedit.gui; // {{{ imports import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.io.File; import java.io.FilenameFilter; import java.util.*; import java.util.Map.Entry; import javax.swing.JComponent; import javax.swing.JPanel; import org.gjt.sp.jedit.EditBus; import org.gjt.sp.jedit.PluginJAR; import org.gjt.sp.jedit.View; import org.gjt.sp.jedit.jEdit; import org.gjt.sp.jedit.EditBus.EBHandler; import org.gjt.sp.jedit.View.ViewConfig; import org.gjt.sp.jedit.gui.KeyEventTranslator.Key; import org.jedit.keymap.Keymap; import org.jedit.keymap.KeymapManager; import org.gjt.sp.jedit.msg.DockableWindowUpdate; import org.gjt.sp.jedit.msg.PluginUpdate; import org.gjt.sp.jedit.msg.PropertiesChanged; import org.gjt.sp.util.Log; // }}} // {{{ abstract class DockableWindowManager /** <p> Keeps track of all dockable windows for a single View, and provides * an API for getting/showing/hiding them. </p> * * <p>Each {@link org.gjt.sp.jedit.View} has an instance of this class.</p> * * <p><b>dockables.xml:</b></p> * * <p>Dockable window definitions are read from <code>dockables.xml</code> files * contained inside plugin JARs. A dockable definition file has the following * form: </p> * * <pre><?xml version="1.0"?> *<!DOCTYPE DOCKABLES SYSTEM "dockables.dtd"> *<DOCKABLES> * <DOCKABLE NAME="<i>dockableName</i>" MOVABLE="TRUE|FALSE"> * // Code to create the dockable * </DOCKABLE> *</DOCKABLES></pre> * * <p>The MOVABLE attribute specifies the behavior when the docking position of * the dockable window is changed. If MOVABLE is TRUE, the existing instance of * the dockable window is moved to the new docking position, and if the dockable * window implements the DockableWindow interface (see {@link DockableWindow}), * it is also notified about the change in docking position before it is moved. * If MOVABLE is FALSE, the BeanShell code is invoked to get the instance of * the dockable window to put in the new docking position. Typically, the * BeanShell code returns a new instance of the dockable window, and the state * of the existing instance is not preserved after the change. It is therefore * recommended to set MOVABLE to TRUE for all dockables in order to make them * preserve their state when they are moved. For backward compatibility reasons, * this attribute is set to FALSE by default.</p> * <p>More than one <code><DOCKABLE></code> tag may be present. The code that * creates the dockable can reference any BeanShell built-in variable * (see {@link org.gjt.sp.jedit.BeanShell}), along with a variable * <code>position</code> whose value is one of * {@link #FLOATING}, {@link #TOP}, {@link #LEFT}, {@link #BOTTOM}, * and {@link #RIGHT}. </p> * * <p>The following properties must be defined for each dockable window: </p> * * <ul> * <li><code><i>dockableName</i>.title</code> - the string to show on the dockable * button. </li> * <li><code><i>dockableName</i>.label</code> - The string to use for generating * menu items and action names. </li> * <li><code><i>dockableName</i>.longtitle</code> - (optional) the string to use * in the dockable's floating window title (when it is floating). * If not specified, the <code><i>dockableName</i>.title</code> property is used. </li> * </ul> * * A number of actions are automatically created for each dockable window: * * <ul> * <li><code><i>dockableName</i></code> - opens the dockable window.</li> * <li><code><i>dockableName</i>-toggle</code> - toggles the dockable window's visibility.</li> * <li><code><i>dockableName</i>-float</code> - opens the dockable window in a new * floating window.</li> * </ul> * * Note that only the first action needs a <code>label</code> property, the * rest have automatically-generated labels. * * <p> <b>Implementation details:</b></p> * * <p> When an instance of this class is initialized by the {@link org.gjt.sp.jedit.View} * class, it * iterates through the list of registered dockable windows (from jEdit itself, * and any loaded plugins) and * examines options supplied by the user in the <b>Global * Options</b> dialog box. Any plugins designated for one of the * four docking positions are displayed.</p> * * <p> To create an instance of a dockable window, the <code>DockableWindowManager</code> * finds and executes the BeanShell code extracted from the appropriate * <code>dockables.xml</code> file. This code will typically consist of a call * to the constructor of the dockable window component. The result of the * BeanShell expression, typically a newly constructed component, is placed * in a window managed by this class. </p> * * @see org.gjt.sp.jedit.View#getDockableWindowManager() * * @author Slava Pestov * @author John Gellene (API documentation) * @author Shlomy Reinstein (refactoring into a base and an impl) * @version $Id$ * @since jEdit 2.6pre3 * */ @SuppressWarnings("serial") public abstract class DockableWindowManager extends JPanel { //{{{ Constants /** * Floating position. * @since jEdit 2.6pre3 */ public static final String FLOATING = "floating"; /** * Top position. * @since jEdit 2.6pre3 */ public static final String TOP = "top"; /** * Left position. * @since jEdit 2.6pre3 */ public static final String LEFT = "left"; /** * Bottom position. * @since jEdit 2.6pre3 */ public static final String BOTTOM = "bottom"; /** * Right position. * @since jEdit 2.6pre3 */ public static final String RIGHT = "right"; //}}} // {{{ data members private final Map<PluginJAR, Set<String>> plugins = new HashMap<PluginJAR, Set<String>>(); private final Map<String, String> positions = new HashMap<String, String>(); protected View view; protected DockableWindowFactory factory; protected Map<String, JComponent> windows = new HashMap<String, JComponent>(); // variables for toggling all dock areas private boolean tBottom, tTop, tLeft, tRight; private boolean closeToggle = true; private static final String ALTERNATE_LAYOUT_PROP = "view.docking.alternateLayout"; private boolean alternateLayout; // }}} // {{{ DockableWindowManager constructor public DockableWindowManager(View view, DockableWindowFactory instance, ViewConfig config) { this.view = view; this.factory = instance; alternateLayout = jEdit.getBooleanProperty(ALTERNATE_LAYOUT_PROP); } // }}} // {{{ Abstract methods public abstract void setMainPanel(JPanel panel); public abstract void showDockableWindow(String name); public abstract void hideDockableWindow(String name); /** Completely dispose of a dockable - called when a plugin is unloaded, to remove all references to the its dockables. */ public abstract void disposeDockableWindow(String name); public abstract JComponent floatDockableWindow(String name); public abstract boolean isDockableWindowDocked(String name); public abstract boolean isDockableWindowVisible(String name); public abstract void closeCurrentArea(); public abstract DockingLayout getDockingLayout(ViewConfig config); public abstract DockingArea getLeftDockingArea(); public abstract DockingArea getRightDockingArea(); public abstract DockingArea getTopDockingArea(); public abstract DockingArea getBottomDockingArea(); // }}} // {{{ public methods // {{{ init() public void init() { EditBus.addToBus(this); Iterator<DockableWindowFactory.Window> entries = factory.getDockableWindowIterator(); while(entries.hasNext()) { DockableWindowFactory.Window window = entries.next(); String dockable = window.name; positions.put(dockable, getDockablePosition(dockable)); addPluginDockable(window.plugin, dockable); } } // }}} // {{{ close() public void close() { EditBus.removeFromBus(this); } // }}} // {{{ applyDockingLayout public void applyDockingLayout(DockingLayout docking) { // By default, use the docking positions specified by the jEdit properties for (Entry<String, String> entry : positions.entrySet()) { String dockable = entry.getKey(); String position = entry.getValue(); if (!position.equals(FLOATING)) showDockableWindow(dockable); } } //}}} //{{{ addDockableWindow() method /** * Opens the specified dockable window. As of jEdit 4.0pre1, has the * same effect as calling showDockableWindow(). * @param name The dockable window name * @since jEdit 2.6pre3 */ public void addDockableWindow(String name) { showDockableWindow(name); } //}}} //{{{ removeDockableWindow() method /** * Hides the specified dockable window. As of jEdit 4.2pre1, has the * same effect as calling hideDockableWindow(). * @param name The dockable window name * @since jEdit 4.2pre1 */ public void removeDockableWindow(String name) { hideDockableWindow(name); } //}}} //{{{ toggleDockableWindow() method /** * Toggles the visibility of the specified dockable window. * @param name The dockable window name */ public void toggleDockableWindow(String name) { if(isDockableWindowVisible(name)) removeDockableWindow(name); else addDockableWindow(name); } //}}} //{{{ getDockableWindow() method /** * Returns the specified dockable window. * * Note that this method * will return null if the dockable has not been added yet. * Make sure you call {@link #addDockableWindow(String)} first. * * @param name The name of the dockable window * @since jEdit 4.1pre2 */ public JComponent getDockableWindow(String name) { return getDockable(name); } //}}} // {{{ toggleDockAreas() /** * Hides all visible dock areas, or shows them again, * if the last time it was a hide. * @since jEdit 4.3pre16 * */ public void toggleDockAreas() { if (closeToggle) { tTop = getTopDockingArea().getCurrent() != null; tLeft = getLeftDockingArea().getCurrent() != null; tRight = getRightDockingArea().getCurrent() != null; tBottom = getBottomDockingArea().getCurrent() != null; getBottomDockingArea().show(null); getTopDockingArea().show(null); getRightDockingArea().show(null); getLeftDockingArea().show(null); } else { if (tBottom) getBottomDockingArea().showMostRecent(); if (tLeft) getLeftDockingArea().showMostRecent(); if (tRight) getRightDockingArea().showMostRecent(); if (tTop) getTopDockingArea().showMostRecent(); } view.closeAllMenus(); closeToggle = !closeToggle; view.getTextArea().requestFocus(); } // }}} /** @return true if the next invocation of "toggle docked areas" will hide the dockables. false otherwise. */ public boolean willToggleHide() { return closeToggle; } // {{{ dockableTitleChanged public void dockableTitleChanged(String dockable, String newTitle) { } // }}} // {{{ closeListener() method /** * The actionEvent "close-docking-area" by default only works on * dockable windows that have no special keyboard handling. * If you have dockable widgets with input widgets and/or other fancy * keyboard handling, those components may not respond to close docking area. * You can add key listeners to each keyboard-handling component * in your dockable that usually has keyboard focus. * * This function creates and returns a key listener which does exactly that. * It is also used by FloatingWindowContainer when creating new floating windows. * * @param dockableName the name of your dockable * @return a KeyListener you can add to that plugin's component. * @since jEdit 4.3pre6 * */ public KeyListener closeListener(String dockableName) { return new KeyHandler(dockableName); } // }}} //{{{ getView() method /** * Returns this dockable window manager's view. * @since jEdit 4.0pre2 */ public View getView() { return view; } //}}} //{{{ getDockable method /** * @since jEdit 4.3pre2 */ public JComponent getDockable(String name) { return windows.get(name); } // }}} //{{{ getDockableTitle() method /** * Returns the title of the specified dockable window. * @param name The name of the dockable window. * @since jEdit 4.1pre5 */ public String getDockableTitle(String name) { return longTitle(name); }//}}} //{{{ setDockableTitle() method /** * Changes the .longtitle property of a dockable window, which corresponds to the * title shown when it is floating (not docked). Fires a change event that makes sure * all floating dockables change their title. * * @param dockable the name of the dockable, as specified in the dockables.xml * @param title the new .longtitle you want to see above it. * @since 4.3pre5 * */ public void setDockableTitle(String dockable, String title) { String propName = getLongTitlePropertyName(dockable); String oldTitle = jEdit.getProperty(propName); jEdit.setProperty(propName, title); firePropertyChange(propName, oldTitle, title); dockableTitleChanged(dockable, title); } // }}} //{{{ getRegisteredDockableWindows() method public static String[] getRegisteredDockableWindows() { return DockableWindowFactory.getInstance() .getRegisteredDockableWindows(); } //}}} //{{{ getDockableWindowPluginClassName() method public static String getDockableWindowPluginName(String name) { String pluginClass = DockableWindowFactory.getInstance().getDockableWindowPluginClass(name); if (pluginClass == null) return null; return jEdit.getProperty("plugin." + pluginClass + ".name"); } //}}} // {{{ setDockingLayout method public void setDockingLayout(DockingLayout docking) { applyDockingLayout(docking); applyAlternateLayout(alternateLayout); } // }}} // {{{ addPluginDockable private void addPluginDockable(PluginJAR plugin, String name) { Set<String> dockables = plugins.get(plugin); if (dockables == null) { dockables = new HashSet<String>(); plugins.put(plugin, dockables); } dockables.add(name); } // }}} // {{{ handleDockableWindowUpdate() method @EBHandler public void handleDockableWindowUpdate(DockableWindowUpdate msg) { if(msg.getWhat() == DockableWindowUpdate.PROPERTIES_CHANGED) propertiesChanged(); } // }}} // {{{ handlePropertiesChanged() method @EBHandler public void handlePropertiesChanged(PropertiesChanged msg) { propertiesChanged(); } // }}} // {{{ handlePluginUpdate() method @EBHandler public void handlePluginUpdate(PluginUpdate pmsg) { if (pmsg.getWhat() == PluginUpdate.LOADED) { Iterator<DockableWindowFactory.Window> iter = factory.getDockableWindowIterator(); while (iter.hasNext()) { DockableWindowFactory.Window w = iter.next(); if (w.plugin == pmsg.getPluginJAR()) { String position = getDockablePosition(w.name); positions.put(w.name, position); addPluginDockable(w.plugin, w.name); dockableLoaded(w.name, position); } } propertiesChanged(); } else if(pmsg.isExiting()) { // we don't care } else if(pmsg.getWhat() == PluginUpdate.DEACTIVATED || pmsg.getWhat() == PluginUpdate.UNLOADED) { Set<String> dockables = plugins.remove(pmsg.getPluginJAR()); if (dockables != null) { for (String dockable: dockables) { disposeDockableWindow(dockable); windows.remove(dockable); } } } } // }}} // {{{ longTitle() method public String longTitle(String name) { String title = jEdit.getProperty(getLongTitlePropertyName(name)); if (title == null) return shortTitle(name); return title; } // }}} // {{{ shortTitle() method public String shortTitle(String name) { String title = jEdit.getProperty(name + ".title"); if(title == null) return "NO TITLE PROPERTY: " + name; return title; } // }}} // }}} // {{{ protected methods // {{{ applyAlternateLayout protected void applyAlternateLayout(boolean alternateLayout) { } //}}} // {{{ protected void dockableLoaded(String dockableName, String position) { } // }}} // {{{ protected void dockingPositionChanged(String dockableName, String oldPosition, String newPosition) { } //}}} // {{{ getAlternateLayoutProp() protected boolean getAlternateLayoutProp() { return alternateLayout; } // }}} // {{{ propertiesChanged protected void propertiesChanged() { if(view.isPlainView()) return; boolean newAlternateLayout = jEdit.getBooleanProperty(ALTERNATE_LAYOUT_PROP); if (newAlternateLayout != alternateLayout) { alternateLayout = newAlternateLayout; applyAlternateLayout(newAlternateLayout); } String[] dockables = factory.getRegisteredDockableWindows(); for (String dockable : dockables) { String oldPosition = positions.get(dockable); String newPosition = getDockablePosition(dockable); if (oldPosition == null || !newPosition.equals(oldPosition)) { positions.put(dockable, newPosition); dockingPositionChanged(dockable, oldPosition, newPosition); } } } // }}} // {{{ createDockable() protected JComponent createDockable(String name) { DockableWindowFactory.Window wf = factory.getDockableWindowFactory(name); if (wf == null) { Log.log(Log.ERROR,this,"Unknown dockable window: " + name); return null; } String position = getDockablePosition(name); JComponent window = wf.createDockableWindow(view, position); if (window != null) windows.put(name, window); return window; } // }}} // {{{ getDockablePosition() protected String getDockablePosition(String name) { return jEdit.getProperty(name + ".dock-position", FLOATING); } // }}} // {{{ focusDockable protected void focusDockable(String name) { JComponent c = getDockable(name); if (c == null) return; if (c instanceof DefaultFocusComponent) ((DefaultFocusComponent)c).focusOnDefaultComponent(); else c.requestFocus(); } // }}} // {{{ getLongTitlePropertyName() protected String getLongTitlePropertyName(String dockableName) { return dockableName + ".longtitle"; } //}}} // }}} // {{{ Inner classes // {{{ DockingArea interface public interface DockingArea { void showMostRecent(); String getCurrent(); void show(String name); String [] getDockables(); } // }}} //{{{ KeyHandler class /** * This keyhandler responds to only two key events - those corresponding to * the close-docking-area action event. * * @author ezust */ class KeyHandler extends KeyAdapter { static final String action = "close-docking-area"; private List<Key> b1; private List<Key> b2; private final String name; private int match1; private int match2; KeyHandler(String dockableName) { KeymapManager keymapManager = jEdit.getKeymapManager(); Keymap keymap = keymapManager.getKeymap(); String shortcut1 = keymap.getShortcut(action + ".shortcut"); String shortcut2 = keymap.getShortcut(action + ".shortcut2"); if (shortcut1 != null) b1 = parseShortcut(shortcut1); if (shortcut2 != null) b2 = parseShortcut(shortcut2); name = dockableName; match1 = match2 = 0; } @Override public void keyTyped(KeyEvent e) { if (b1 != null) match1 = match(e, b1, match1); if (b2 != null) match2 = match(e, b2, match2); if ((match1 > 0 && b1 != null && match1 == b1.size()) || (match2 > 0 && b2 != null && match2 == b2.size())) { hideDockableWindow(name); match1 = match2 = 0; } } private int match(KeyEvent e, List<Key> shortcut, int index) { char c = e.getKeyChar(); if (shortcut != null && c == shortcut.get(index).key) return index + 1; return 0; } private List<Key> parseShortcut(String shortcut) { String [] parts = shortcut.split("\\s+"); List<Key> keys = new ArrayList<Key>(parts.length); for (String part: parts) { if (part.length() > 0) keys.add(KeyEventTranslator.parseKey(part)); } return keys; } } //}}} // {{{ DockingLayout class /** * Objects of DockingLayout class describe which dockables are docked where, * which ones are floating, and their sizes/positions for saving/loading perspectives. */ public abstract static class DockingLayout { public static final int NO_VIEW_INDEX = -1; public abstract boolean loadLayout(String baseName, int viewIndex); public abstract boolean saveLayout(String baseName, int viewIndex); public abstract String getName(); public void setPlainView(boolean plain) { } public String [] getSavedLayouts() { String layoutDir = getLayoutDirectory(); if (layoutDir == null) return null; File dir = new File(layoutDir); File[] files = dir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".xml"); } }); String[] layouts = new String[files.length]; for (int i = 0; i < files.length; i++) layouts[i] = fileToLayout(files[i].getName()); return layouts; } private static String fileToLayout(String filename) { return filename.replaceFirst(".xml", ""); } private static String layoutToFile(String baseName, int viewIndex) { StringBuilder name = new StringBuilder(baseName); if (viewIndex != NO_VIEW_INDEX) name.append("-view").append(viewIndex); name.append(".xml"); return name.toString(); } public String getLayoutFilename(String baseName, int viewIndex) { String dir = getLayoutDirectory(); if (dir == null) return null; return dir + File.separator + layoutToFile(baseName, viewIndex); } private String getLayoutDirectory() { String name = getName(); if (name == null) return null; String dir = jEdit.getSettingsDirectory(); if (dir == null) return null; dir = dir + File.separator + name; File d = new File(dir); if (!d.exists()) d.mkdir(); return dir; } } // }}} //}}} } // }}}