/*
* JEditActionSet.java - A set of actions
* :tabSize=4:indentSize=4:noTabs=false:
* :folding=explicit:collapseFolds=1:
*
* Copyright (C) 2001, 2003 Slava Pestov
* Portions copyright (C) 2007 Matthieu Casanova
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.gjt.sp.jedit;
import java.io.*;
import java.net.URL;
import java.util.*;
import org.gjt.sp.jedit.input.AbstractInputHandler;
import org.gjt.sp.jedit.input.InputHandlerProvider;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.XMLUtilities;
/**
* A set of actions, either loaded from an XML file, or constructed at runtime
* by a plugin. <p>
*
* <h3>Action sets loaded from XML files</h3>
*
* Action sets are read from these files inside the plugin JAR:
* <ul>
* <li><code>actions.xml</code> - actions made available for use in jEdit views,
* including the view's <b>Plugins</b> menu, the tool bar, etc.</li>
* <li><code>browser.actions.xml</code> - actions for the file system browser's
* <b>Plugins</b> menu.</li>
* </ul>
*
* An action definition file has the following form:
*
* <pre><?xml version="1.0"?>
*<!DOCTYPE ACTIONS SYSTEM "actions.dtd">
*<ACTIONS>
* <ACTION NAME="some-action">
* <CODE>
* // BeanShell code evaluated when the action is invoked
* </CODE>
* </ACTION>
* <ACTION NAME="some-toggle-action">
* <CODE>
* // BeanShell code evaluated when the action is invoked
* </CODE>
* <IS_SELECTED>
* // BeanShell code that should evaluate to true or false
* </IS_SELECTED>
* </ACTION>
*</ACTIONS></pre>
*
* The following elements are valid:
*
* <ul>
* <li>
* <code>ACTIONS</code> is the top-level element and refers
* to the set of actions used by the plugin.
* </li>
* <li>
* An <code>ACTION</code> contains the data for a particular action.
* It has three attributes: a required <code>NAME</code>;
* an optional <code>NO_REPEAT</code>, which is a flag
* indicating whether the action should not be repeated with the
* <b>C+ENTER</b> command; and an optional
* <code>NO_RECORD</code> which is a a flag indicating whether the
* action should be recorded if it is invoked while the user is recording a
* macro. The two flag attributes
* can have two possible values, "TRUE" or
* "FALSE". In both cases, "FALSE" is the
* default if the attribute is not specified.
* </li>
* <li>
* An <code>ACTION</code> can have two child elements
* within it: a required <code>CODE</code> element which
* specifies the
* BeanShell code that will be executed when the action is invoked,
* and an optional <code>IS_SELECTED</code> element, used for
* checkbox
* menu items. The <code>IS_SELECTED</code> element contains
* BeanShell code that returns a boolean flag that will
* determine the state of the checkbox.
* </li>
* </ul>
*
* Each action must have a property <code><i>name</i>.label</code> containing
* the action's menu item label.
*
* <h3>View actions</h3>
*
* Actions defined in <code>actions.xml</code> can be added to the view's
* <b>Plugins</b> menu; see {@link EditPlugin}.
* The action code may use any standard predefined
* BeanShell variable; see {@link BeanShell}.
*
* <h3>File system browser actions</h3>
*
* Actions defined in <code>actions.xml</code> can be added to the file
* system browser's <b>Plugins</b> menu; see {@link EditPlugin}.
* The action code may use any standard predefined
* BeanShell variable, in addition to a variable <code>browser</code> which
* contains a reference to the current
* {@link org.gjt.sp.jedit.browser.VFSBrowser} instance.<p>
*
* File system browser actions should not define
* <code><IS_SELECTED></code> blocks.
*
* <h3>Custom action sets</h3>
*
* Call {@link jEdit#addActionSet(ActionSet)} to add a custom action set to
* jEdit's action context. You must also call {@link #initKeyBindings()} for new
* action sets. Don't forget to call {@link jEdit#removeActionSet(ActionSet)}
* before your plugin is unloaded, too.
*
* @see jEdit#getActionContext()
* @see org.gjt.sp.jedit.browser.VFSBrowser#getActionContext()
* @see ActionContext#getActionNames()
* @see ActionContext#getAction(String)
* @see jEdit#addActionSet(ActionSet)
* @see jEdit#removeActionSet(ActionSet)
* @see PluginJAR#getActionSet()
* @see BeanShell
* @see View
*
* @author Slava Pestov
* @author John Gellene (API documentation)
* @version $Id: ActionSet.java 9529 2007-05-12 15:06:52Z ezust $
* @since jEdit 4.3pre13
*/
public abstract class JEditActionSet<E extends JEditAbstractEditAction> implements InputHandlerProvider
{
//{{{ JEditActionSet constructor
/**
* Creates a new action set.
* @since jEdit 4.3pre13
*/
protected JEditActionSet()
{
actions = new Hashtable<String, Object>();
loaded = true;
} //}}}
//{{{ JEditActionSet constructor
/**
* Creates a new action set.
* @param cachedActionNames The list of cached action names
* @param uri The actions.xml URI
* @since jEdit 4.3pre13
*/
protected JEditActionSet(String[] cachedActionNames, URL uri)
{
this();
this.uri = uri;
if(cachedActionNames != null)
{
for (String cachedActionName : cachedActionNames)
actions.put(cachedActionName, placeholder);
}
loaded = false;
} //}}}
//{{{ addAction() method
/**
* Adds an action to the action set.
* @param action The action
* @since jEdit 4.0pre1
*/
public void addAction(E action)
{
actions.put(action.getName(),action);
if(context != null)
{
context.actionNames = null;
context.actionHash.put(action.getName(),this);
}
} //}}}
//{{{ removeAction() method
/**
* Removes an action from the action set.
* @param name The action name
* @since jEdit 4.0pre1
*/
public void removeAction(String name)
{
actions.remove(name);
if(context != null)
{
context.actionNames = null;
context.actionHash.remove(name);
}
} //}}}
//{{{ removeAllActions() method
/**
* Removes all actions from the action set.
* @since jEdit 4.0pre1
*/
public void removeAllActions()
{
if(context != null)
{
context.actionNames = null;
String[] actions = getActionNames();
for (String action : actions)
context.actionHash.remove(action);
}
actions.clear();
} //}}}
//{{{ getAction() method
/**
* Returns an action with the specified name.<p>
*
* <b>Deferred loading:</b> this will load the action set if necessary.
*
* @param name The action name
* @since jEdit 4.0pre1
*/
public E getAction(String name)
{
Object obj = actions.get(name);
if(obj == placeholder)
{
load();
obj = actions.get(name);
if(obj == placeholder)
{
Log.log(Log.WARNING,this,"Outdated cache");
obj = null;
}
}
return (E) obj;
} //}}}
//{{{ getActionCount() method
/**
* Returns the number of actions in the set.
* @since jEdit 4.0pre1
*/
public int getActionCount()
{
return actions.size();
} //}}}
//{{{ getActionNames() method
/**
* Returns an array of all action names in this action set.
* @since jEdit 4.2pre1
*/
public String[] getActionNames()
{
String[] retVal = new String[actions.size()];
Set<String> keys = actions.keySet();
int i = 0;
for (String key : keys)
{
retVal[i++] = key;
}
return retVal;
} //}}}
//{{{ getCacheableActionNames() method
/**
* Returns an array of all action names in this action set that should
* be cached; namely, <code>BeanShellAction</code>s.
* @since jEdit 4.2pre1
*/
public String[] getCacheableActionNames()
{
LinkedList<String> retVal = new LinkedList<String>();
for (Object obj : actions.values())
{
if (obj == placeholder)
{
// ??? this should only be called with
// fully loaded action set
Log.log(Log.WARNING, this, "Action set not up "
+ "to date");
}
else if (obj instanceof JEditBeanShellAction)
retVal.add(((JEditBeanShellAction) obj).getName());
}
return retVal.toArray(new String[retVal.size()]);
} //}}}
//{{{ getArray() method
/**
* Returns an empty array E[].
* I know it is bad, if you find a method to instantiate a generic Array,
* tell me
* @param size the size of the array
* @return the empty array
*/
protected abstract E[] getArray(int size);
//}}}
//{{{ getActions() method
/**
* Returns an array of all actions in this action set.<p>
*
* <b>Deferred loading:</b> this will load the action set if necessary.
*
* @since jEdit 4.0pre1
*/
public E[] getActions()
{
load();
E[] retVal = getArray(actions.size());
Collection<Object> values = actions.values();
int i = 0;
for (Object value : values)
{
retVal[i++] = (E) value;
}
return retVal;
} //}}}
//{{{ contains() method
/**
* Returns if this action set contains the specified action.
* @param action The action
* @since jEdit 4.2pre1
*/
public boolean contains(String action)
{
boolean retval = actions.containsKey(action);
return retval;
// return actions.containsKey(action);
} //}}}
//{{{ size() method
/**
* Returns the number of actions in this action set.
* @since jEdit 4.2pre2
*/
public int size()
{
return actions.size();
} //}}}
//{{{ load() method
/**
* Forces the action set to be loaded. Plugins and macros should not
* call this method.
* @since jEdit 4.2pre1
*/
public void load()
{
if(loaded)
return;
loaded = true;
//actions.clear();
if (uri == null)
return;
try
{
Log.log(Log.DEBUG,this,"Loading actions from " + uri);
ActionListHandler ah = new ActionListHandler(uri.toString(),this);
InputStream in;
try
{
in = uri.openStream();
}
catch(FileNotFoundException e)
{
in = null;
// this happened when calling generateCache() in the context of 'find orphan jars'
// in org.gjt.sp.jedit.pluginmgr.ManagePanel.FindOrphan.actionPerformed(ActionEvent)
// because for not loaded plugins, the plugin will not be added to the list of pluginJars
// so the org.gjt.sp.jedit.proto.jeditresource.PluginResURLConnection will not find the plugin
// to read the resource from.
// Better log a small error message than a big stack trace
Log.log(Log.WARNING, this, "Unable to open: " + uri);
}
if (in != null && XMLUtilities.parseXML(in, ah))
{
Log.log(Log.ERROR, this, "Unable to parse: " + uri);
}
}
catch(IOException e)
{
Log.log(Log.ERROR,this,uri,e);
}
} //}}}
//{{{ createBeanShellAction() method
/**
* This method should be implemented to return an action that will execute
* the given code
* @since 4.3pre13
*/
protected abstract JEditAbstractEditAction createBeanShellAction(String actionName,
String code,
String selected,
boolean noRepeat,
boolean noRecord,
boolean noRememberLast);
//}}}
//{{{ initKeyBindings() method
/**
* Initializes the action set's key bindings.
* jEdit calls this method for all registered action sets when the
* user changes key bindings in the <b>Global Options</b> dialog box.<p>
*
* Note if your plugin adds a custom action set to jEdit's collection,
* it must also call this method on the action set after adding it.
*
* @since jEdit 4.2pre1
*/
public void initKeyBindings()
{
AbstractInputHandler inputHandler = getInputHandler();
Set<Map.Entry<String, Object>> entries = actions.entrySet();
for (Map.Entry<String, Object> entry : entries)
{
String name = entry.getKey();
String shortcut1 = getProperty(name + ".shortcut");
if(shortcut1 != null)
inputHandler.addKeyBinding(shortcut1,name);
String shortcut2 = getProperty(name + ".shortcut2");
if(shortcut2 != null)
inputHandler.addKeyBinding(shortcut2,name);
}
} //}}}
//{{{ getProperty() method
/**
* Returns a property for the given name.
* In jEdit it will returns a jEdit.getProperty(name), but it can
* return something else for a standalone textarea.
* @param name the property name
* @return the property value
* @since 4.3pre13
*/
protected abstract String getProperty(String name);
//}}}
//{{{ Package-private members
JEditActionContext context;
//{{{ getActionNames() method
void getActionNames(List<String> list)
{
list.addAll(actions.keySet());
} //}}}
//}}}
//{{{ Private members
protected Hashtable<String,Object> actions;
protected URL uri;
protected boolean loaded;
protected static final Object placeholder = new Object();
//}}}
}