Source code: org/gjt/sp/jedit/ActionSet.java
1 /*
2 * ActionSet.java - A set of actions
3 * :tabSize=8:indentSize=8:noTabs=false:
4 * :folding=explicit:collapseFolds=1:
5 *
6 * Copyright (C) 2001, 2003 Slava Pestov
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21 */
22
23 package org.gjt.sp.jedit;
24
25 import com.microstar.xml.*;
26 import java.io.*;
27 import java.net.URL;
28 import java.util.*;
29 import org.gjt.sp.jedit.gui.InputHandler;
30 import org.gjt.sp.util.Log;
31
32 /**
33 * A set of actions, either loaded from an XML file, or constructed at runtime
34 * by a plugin.<p>
35 *
36 * <h3>Action sets loaded from XML files</h3>
37 *
38 * Action sets are read from these files inside the plugin JAR:
39 * <ul>
40 * <li><code>actions.xml</code> - actions made available for use in jEdit views,
41 * including the view's <b>Plugins</b> menu, the tool bar, etc.</li>
42 * <li><code>browser.actions.xml</code> - actions for the file system browser's
43 * <b>Plugins</b> menu.</li>
44 * </ul>
45 *
46 * An action definition file has the following form:
47 *
48 * <pre><?xml version="1.0"?>
49 *<!DOCTYPE ACTIONS SYSTEM "actions.dtd">
50 *<ACTIONS>
51 * <ACTION NAME="some-action">
52 * <CODE>
53 * // BeanShell code evaluated when the action is invoked
54 * </CODE>
55 * </ACTION>
56 * <ACTION NAME="some-toggle-action">
57 * <CODE>
58 * // BeanShell code evaluated when the action is invoked
59 * </CODE>
60 * <IS_SELECTED>
61 * // BeanShell code that should evaluate to true or false
62 * </IS_SELECTED>
63 * </ACTION>
64 *</ACTIONS></pre>
65 *
66 * The following elements are valid:
67 *
68 * <ul>
69 * <li>
70 * <code>ACTIONS</code> is the top-level element and refers
71 * to the set of actions used by the plugin.
72 * </li>
73 * <li>
74 * An <code>ACTION</code> contains the data for a particular action.
75 * It has three attributes: a required <code>NAME</code>;
76 * an optional <code>NO_REPEAT</code>, which is a flag
77 * indicating whether the action should not be repeated with the
78 * <b>C+ENTER</b> command; and an optional
79 * <code>NO_RECORD</code> which is a a flag indicating whether the
80 * action should be recorded if it is invoked while the user is recording a
81 * macro. The two flag attributes
82 * can have two possible values, "TRUE" or
83 * "FALSE". In both cases, "FALSE" is the
84 * default if the attribute is not specified.
85 * </li>
86 * <li>
87 * An <code>ACTION</code> can have two child elements
88 * within it: a required <code>CODE</code> element which
89 * specifies the
90 * BeanShell code that will be executed when the action is invoked,
91 * and an optional <code>IS_SELECTED</code> element, used for
92 * checkbox
93 * menu items. The <code>IS_SELECTED</code> element contains
94 * BeanShell code that returns a boolean flag that will
95 * determine the state of the checkbox.
96 * </li>
97 * </ul>
98 *
99 * Each action must have a property <code><i>name</i>.label</code> containing
100 * the action's menu item label.
101 *
102 * <h3>View actions</h3>
103 *
104 * Actions defined in <code>actions.xml</code> can be added to the view's
105 * <b>Plugins</b> menu; see {@link EditPlugin}.
106 * The action code may use any standard predefined
107 * BeanShell variable; see {@link BeanShell}.
108 *
109 * <h3>File system browser actions</h3>
110 *
111 * Actions defined in <code>actions.xml</code> can be added to the file
112 * system browser's <b>Plugins</b> menu; see {@link EditPlugin}.
113 * The action code may use any standard predefined
114 * BeanShell variable, in addition to a variable <code>browser</code> which
115 * contains a reference to the current
116 * {@link org.gjt.sp.jedit.browser.VFSBrowser} instance.<p>
117 *
118 * File system browser actions should not define
119 * <code><IS_SELECTED></code> blocks.
120 *
121 * <h3>Custom action sets</h3>
122 *
123 * Call {@link jEdit#addActionSet(ActionSet)} to add a custom action set to
124 * jEdit's action context. You must also call {@link #initKeyBindings()} for new
125 * action sets. Don't forget to call {@link jEdit#removeActionSet(ActionSet)}
126 * before your plugin is unloaded, too.
127 *
128 * @see jEdit#getActionContext()
129 * @see org.gjt.sp.jedit.browser.VFSBrowser#getActionContext()
130 * @see ActionContext#getActionNames()
131 * @see ActionContext#getAction(String)
132 * @see jEdit#addActionSet(ActionSet)
133 * @see jEdit#removeActionSet(ActionSet)
134 * @see PluginJAR#getActionSet()
135 * @see BeanShell
136 * @see View
137 *
138 * @author Slava Pestov
139 * @author John Gellene (API documentation)
140 * @version $Id: ActionSet.java,v 1.25 2003/11/16 00:04:07 spestov Exp $
141 * @since jEdit 4.0pre1
142 */
143 public class ActionSet
144 {
145 //{{{ ActionSet constructor
146 /**
147 * Creates a new action set.
148 * @param plugin The plugin
149 * @param cachedActionNames The list of cached action names
150 * @param cachedActionToggleFlags The list of cached action toggle flags
151 * @param uri The actions.xml URI
152 * @since jEdit 4.2pre2
153 */
154 public ActionSet(PluginJAR plugin, String[] cachedActionNames,
155 boolean[] cachedActionToggleFlags, URL uri)
156 {
157 this();
158 this.plugin = plugin;
159 this.uri = uri;
160 if(cachedActionNames != null)
161 {
162 for(int i = 0; i < cachedActionNames.length; i++)
163 {
164 actions.put(cachedActionNames[i],placeholder);
165 jEdit.setTemporaryProperty(cachedActionNames[i]
166 + ".toggle",cachedActionToggleFlags[i]
167 ? "true" : "false");
168 }
169 }
170 loaded = false;
171 } //}}}
172
173 //{{{ ActionSet constructor
174 /**
175 * Creates a new action set.
176 * @since jEdit 4.0pre1
177 */
178 public ActionSet()
179 {
180 actions = new Hashtable();
181 loaded = true;
182 } //}}}
183
184 //{{{ ActionSet constructor
185 /**
186 * Creates a new action set.
187 * @param label The label, shown in the shortcuts option pane
188 * @since jEdit 4.0pre1
189 */
190 public ActionSet(String label)
191 {
192 this();
193 this.label = label;
194 } //}}}
195
196 //{{{ getLabel() method
197 /**
198 * Return the action source label.
199 * @since jEdit 4.0pre1
200 */
201 public String getLabel()
202 {
203 return label;
204 } //}}}
205
206 //{{{ setLabel() method
207 /**
208 * Sets the action source label.
209 * @param label The label
210 * @since jEdit 4.0pre1
211 */
212 public void setLabel(String label)
213 {
214 this.label = label;
215 } //}}}
216
217 //{{{ addAction() method
218 /**
219 * Adds an action to the action set.
220 * @param action The action
221 * @since jEdit 4.0pre1
222 */
223 public void addAction(EditAction action)
224 {
225 actions.put(action.getName(),action);
226 if(context != null)
227 context.actionHash.put(action.getName(),this);
228 } //}}}
229
230 //{{{ removeAction() method
231 /**
232 * Removes an action from the action set.
233 * @param name The action name
234 * @since jEdit 4.0pre1
235 */
236 public void removeAction(String name)
237 {
238 actions.remove(name);
239 if(context != null)
240 context.actionHash.remove(name);
241 } //}}}
242
243 //{{{ removeAllActions() method
244 /**
245 * Removes all actions from the action set.
246 * @since jEdit 4.0pre1
247 */
248 public void removeAllActions()
249 {
250 if(context != null)
251 {
252 String[] actions = getActionNames();
253 for(int i = 0; i < actions.length; i++)
254 {
255 context.actionHash.remove(actions[i]);
256 }
257 }
258 this.actions.clear();
259 } //}}}
260
261 //{{{ getAction() method
262 /**
263 * Returns an action with the specified name.<p>
264 *
265 * <b>Deferred loading:</b> this will load the action set if necessary.
266 *
267 * @param name The action name
268 * @since jEdit 4.0pre1
269 */
270 public EditAction getAction(String name)
271 {
272 Object obj = actions.get(name);
273 if(obj == placeholder)
274 {
275 load();
276 obj = actions.get(name);
277 if(obj == placeholder)
278 {
279 Log.log(Log.WARNING,this,"Outdated cache");
280 obj = null;
281 }
282 }
283
284 return (EditAction)obj;
285 } //}}}
286
287 //{{{ getActionCount() method
288 /**
289 * Returns the number of actions in the set.
290 * @since jEdit 4.0pre1
291 */
292 public int getActionCount()
293 {
294 return actions.size();
295 } //}}}
296
297 //{{{ getActionNames() method
298 /**
299 * Returns an array of all action names in this action set.
300 * @since jEdit 4.2pre1
301 */
302 public String[] getActionNames()
303 {
304 String[] retVal = new String[actions.size()];
305 Enumeration enum = actions.keys();
306 int i = 0;
307 while(enum.hasMoreElements())
308 {
309 retVal[i++] = (String)enum.nextElement();
310 }
311 return retVal;
312 } //}}}
313
314 //{{{ getCacheableActionNames() method
315 /**
316 * Returns an array of all action names in this action set that should
317 * be cached; namely, <code>BeanShellAction</code>s.
318 * @since jEdit 4.2pre1
319 */
320 public String[] getCacheableActionNames()
321 {
322 LinkedList retVal = new LinkedList();
323 Enumeration enum = actions.elements();
324 int i = 0;
325 while(enum.hasMoreElements())
326 {
327 Object obj = enum.nextElement();
328 if(obj == placeholder)
329 {
330 // ??? this should only be called with
331 // fully loaded action set
332 Log.log(Log.WARNING,this,"Action set not up "
333 + "to date");
334 }
335 else if(obj instanceof BeanShellAction)
336 retVal.add(((BeanShellAction)obj).getName());
337 }
338 return (String[])retVal.toArray(new String[retVal.size()]);
339 } //}}}
340
341 //{{{ getActions() method
342 /**
343 * Returns an array of all actions in this action set.<p>
344 *
345 * <b>Deferred loading:</b> this will load the action set if necessary.
346 *
347 * @since jEdit 4.0pre1
348 */
349 public EditAction[] getActions()
350 {
351 load();
352
353 EditAction[] retVal = new EditAction[actions.size()];
354 Enumeration enum = actions.elements();
355 int i = 0;
356 while(enum.hasMoreElements())
357 {
358 retVal[i++] = (EditAction)enum.nextElement();
359 }
360 return retVal;
361 } //}}}
362
363 //{{{ contains() method
364 /**
365 * Returns if this action set contains the specified action.
366 * @param action The action
367 * @since jEdit 4.2pre1
368 */
369 public boolean contains(String action)
370 {
371 return actions.containsKey(action);
372 } //}}}
373
374 //{{{ size() method
375 /**
376 * Returns the number of actions in this action set.
377 * @since jEdit 4.2pre2
378 */
379 public int size()
380 {
381 return actions.size();
382 } //}}}
383
384 //{{{ toString() method
385 public String toString()
386 {
387 return label;
388 } //}}}
389
390 //{{{ initKeyBindings() method
391 /**
392 * Initializes the action set's key bindings.
393 * jEdit calls this method for all registered action sets when the
394 * user changes key bindings in the <b>Global Options</b> dialog box.<p>
395 *
396 * Note if your plugin adds a custom action set to jEdit's collection,
397 * it must also call this method on the action set after adding it.
398 *
399 * @since jEdit 4.2pre1
400 */
401 public void initKeyBindings()
402 {
403 InputHandler inputHandler = jEdit.getInputHandler();
404
405 Iterator iter = actions.entrySet().iterator();
406 while(iter.hasNext())
407 {
408 Map.Entry entry = (Map.Entry)iter.next();
409 String name = (String)entry.getKey();
410
411 String shortcut1 = jEdit.getProperty(name + ".shortcut");
412 if(shortcut1 != null)
413 inputHandler.addKeyBinding(shortcut1,name);
414
415 String shortcut2 = jEdit.getProperty(name + ".shortcut2");
416 if(shortcut2 != null)
417 inputHandler.addKeyBinding(shortcut2,name);
418 }
419 } //}}}
420
421 //{{{ load() method
422 /**
423 * Forces the action set to be loaded. Plugins and macros should not
424 * call this method.
425 * @since jEdit 4.2pre1
426 */
427 public void load()
428 {
429 if(loaded)
430 return;
431
432 loaded = true;
433 //actions.clear();
434
435 Reader stream = null;
436
437 try
438 {
439 Log.log(Log.DEBUG,this,"Loading actions from " + uri);
440
441 ActionListHandler ah = new ActionListHandler(uri.toString(),this);
442 stream = new BufferedReader(new InputStreamReader(
443 uri.openStream()));
444 XmlParser parser = new XmlParser();
445 parser.setHandler(ah);
446 parser.parse(null, null, stream);
447 }
448 catch(XmlException xe)
449 {
450 int line = xe.getLine();
451 String message = xe.getMessage();
452 Log.log(Log.ERROR,this,uri + ":" + line
453 + ": " + message);
454 }
455 catch(Exception e)
456 {
457 Log.log(Log.ERROR,uri,e);
458 }
459 finally
460 {
461 try
462 {
463 if(stream != null)
464 stream.close();
465 }
466 catch(IOException io)
467 {
468 Log.log(Log.ERROR,this,io);
469 }
470 }
471 } //}}}
472
473 //{{{ Package-private members
474 ActionContext context;
475
476 //{{{ getActionNames() method
477 void getActionNames(ArrayList vec)
478 {
479 Enumeration enum = actions.keys();
480 while(enum.hasMoreElements())
481 vec.add(enum.nextElement());
482 } //}}}
483
484 //}}}
485
486 //{{{ Private members
487 private String label;
488 private Hashtable actions;
489 private PluginJAR plugin;
490 private URL uri;
491 private boolean loaded;
492
493 private static final Object placeholder = new Object();
494
495 //}}}
496 }