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: DockableWindowManager.java 23222 2013-09-29 20:43:34Z shlomy $
* @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;
}
} // }}}
//}}}
} // }}}