001 /*
002 * This file is part of muCommander, http://www.mucommander.com
003 * Copyright (C) 2002-2008 Maxence Bernard
004 *
005 * muCommander is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * muCommander is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018
019 package com.mucommander.ui.action;
020
021 import com.mucommander.Debug;
022 import com.mucommander.PlatformManager;
023 import com.mucommander.file.AbstractFile;
024 import com.mucommander.file.FileFactory;
025 import com.mucommander.file.util.ResourceLoader;
026 import com.mucommander.io.BackupInputStream;
027 import com.mucommander.io.StreamUtils;
028 import com.mucommander.ui.main.MainFrame;
029 import org.xml.sax.Attributes;
030 import org.xml.sax.SAXException;
031 import org.xml.sax.helpers.DefaultHandler;
032
033 import javax.swing.*;
034 import javax.xml.parsers.SAXParserFactory;
035 import java.io.*;
036 import java.util.Enumeration;
037 import java.util.HashSet;
038 import java.util.Hashtable;
039 import java.util.Vector;
040
041
042 /**
043 * This class manages keyboard associations with {@link MuAction} classes.
044 * Proper documentation and cleaning of this class is pending.
045 *
046 * @author Maxence Bernard
047 */
048 public class ActionKeymap extends DefaultHandler {
049
050 /** Maps action Class onto Keystroke instances*/
051 private static Hashtable primaryActionKeymap = new Hashtable();
052 /** Maps action Class instances onto Keystroke instances*/
053 private static Hashtable alternateActionKeymap = new Hashtable();
054
055 /** Maps Keystroke instances onto action Class */
056 private static Hashtable acceleratorMap = new Hashtable();
057
058 /** Default action keymap filename */
059 private final static String DEFAULT_ACTION_KEYMAP_FILE_NAME = "action_keymap.xml";
060 /** Path to the action keymap resource file within the application JAR file */
061 public final static String ACTION_KEYMAP_RESOURCE_PATH = "/" + DEFAULT_ACTION_KEYMAP_FILE_NAME;
062
063 /** Action keymap file used when calling {@link #loadActionKeyMap()} */
064 private static AbstractFile actionKeyMapFile;
065
066 /* Variables used for XML parsing */
067
068 private final static String ACTION_ELEMENT = "action";
069 private final static String CLASS_ATTRIBUTE = "class";
070 private final static String KEYSTROKE_ATTRIBUTE = "keystroke";
071 private final static String ALTERNATE_KEYSTROKE_ATTRIBUTE = "alt_keystroke";
072
073 /** True when default/JAR action keymap is being parsed */
074 private static boolean isParsingDefaultActionKeymap;
075
076 /** List of action Class which are defined in the user action keymap */
077 private static HashSet definedUserActionClasses = new HashSet();
078
079
080 /**
081 * Sets the path to the user action keymap file to be loaded when calling {@link #loadActionKeyMap()}.
082 * By default, this file is {@link #DEFAULT_ACTION_KEYMAP_FILE_NAME} within the preferences folder.
083 * <p>
084 * This is a convenience method and is strictly equivalent to calling <code>setActionKeyMapFile(FileFactory.getFile(file))</code>.
085 * </p>
086 * @param path path to the action keymap file
087 * @throws FileNotFoundException if <code>file</code> is not accessible.
088 */
089 public static void setActionKeyMapFile(String path) throws FileNotFoundException {
090 AbstractFile file;
091
092 if((file = FileFactory.getFile(path)) == null)
093 setActionKeyMapFile(new File(path));
094 else
095 setActionKeyMapFile(file);
096 }
097
098 /**
099 * Sets the path to the user action keymap file to be loaded when calling {@link #loadActionKeyMap()}.
100 * By default, this file is {@link #DEFAULT_ACTION_KEYMAP_FILE_NAME} within the preferences folder.
101 * <p>
102 * This is a convenience method and is strictly equivalent to calling <code>setActionKeyMapFile(FileFactory.getFile(file.getAbsolutePath()))</code>.
103 * </p>
104 * @param file path to the action keymap file
105 * @throws FileNotFoundException if <code>file</code> is not accessible.
106 */
107 public static void setActionKeyMapFile(File file) throws FileNotFoundException {setActionKeyMapFile(FileFactory.getFile(file.getAbsolutePath()));}
108
109 /**
110 * Sets the path to the user action keymap file to be loaded when calling {@link #loadActionKeyMap()}.
111 * By default, this file is {@link #DEFAULT_ACTION_KEYMAP_FILE_NAME} within the preferences folder.
112 * @param file path to the action keymap file
113 * @throws FileNotFoundException if <code>file</code> is not accessible.
114 */
115 public static void setActionKeyMapFile(AbstractFile file) throws FileNotFoundException {
116 if(file.isBrowsable())
117 throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath());
118
119 actionKeyMapFile = file;
120 }
121
122 /**
123 * Returns the path to the action keymap file.
124 * @return the path to the action keymap file.
125 * @throws IOException if an error occured while locating the default action keymap file.
126 */
127 public static AbstractFile getActionKeyMapFile() throws IOException {
128 if(actionKeyMapFile == null)
129 return PlatformManager.getPreferencesFolder().getChild(DEFAULT_ACTION_KEYMAP_FILE_NAME);
130 return actionKeyMapFile;
131 }
132
133
134 /**
135 * Loads the action keymap files: loads the one contained in the JAR file first, and then the user's one.
136 * This means any new action in the JAR action keymap (when a new version is released) will have the default
137 * keyboard mapping, but the keyboard mappings customized by the user in the user's action keymap will override
138 * the ones from the JAR action keymap.
139 *
140 * <p>This method must be called before requesting and registering any action.
141 */
142 public static void loadActionKeyMap() throws Exception {
143 new ActionKeymap();
144 }
145
146
147 public static KeyStroke getAccelerator(Class muActionClass) {
148 return (KeyStroke)primaryActionKeymap.get(muActionClass);
149 }
150
151 public static KeyStroke getAlternateAccelerator(Class muActionClass) {
152 return (KeyStroke)alternateActionKeymap.get(muActionClass);
153 }
154
155
156 public static boolean isKeyStrokeRegistered(KeyStroke ks) {
157 return getRegisteredActionClassForKeystroke(ks)!=null;
158 }
159
160 public static Class getRegisteredActionClassForKeystroke(KeyStroke ks) {
161 return (Class)acceleratorMap.get(ks);
162 }
163
164
165 public static void registerActions(MainFrame mainFrame) {
166 JComponent leftTable = mainFrame.getLeftPanel().getFileTable();
167 JComponent rightTable = mainFrame.getRightPanel().getFileTable();
168
169 Enumeration actionClasses = primaryActionKeymap.keys();
170 while(actionClasses.hasMoreElements()) {
171 MuAction action = ActionManager.getActionInstance((Class)actionClasses.nextElement(), mainFrame);
172 ActionKeymap.registerActionAccelerators(action, leftTable, JComponent.WHEN_FOCUSED);
173 ActionKeymap.registerActionAccelerators(action, rightTable, JComponent.WHEN_FOCUSED);
174 }
175
176 actionClasses = alternateActionKeymap.keys();
177 while(actionClasses.hasMoreElements()) {
178 MuAction action = ActionManager.getActionInstance((Class)actionClasses.nextElement(), mainFrame);
179 ActionKeymap.registerActionAccelerators(action, leftTable, JComponent.WHEN_FOCUSED);
180 ActionKeymap.registerActionAccelerators(action, rightTable, JComponent.WHEN_FOCUSED);
181 }
182 }
183
184
185 public static void registerAction(MainFrame mainFrame, MuAction action) {
186 registerActionAccelerators(action, mainFrame.getLeftPanel().getFileTable(), JComponent.WHEN_FOCUSED);
187 registerActionAccelerators(action, mainFrame.getRightPanel().getFileTable(), JComponent.WHEN_FOCUSED);
188 }
189
190 public static void unregisterAction(MainFrame mainFrame, MuAction action) {
191 unregisterActionAccelerators(action, mainFrame.getLeftPanel().getFileTable(), JComponent.WHEN_FOCUSED);
192 unregisterActionAccelerators(action, mainFrame.getRightPanel().getFileTable(), JComponent.WHEN_FOCUSED);
193 }
194
195
196 public static void registerActionAccelerator(MuAction action, KeyStroke accelerator, JComponent comp, int condition) {
197 if(accelerator==null)
198 return;
199 InputMap inputMap = comp.getInputMap(condition);
200 ActionMap actionMap = comp.getActionMap();
201 Class muActionClass = action.getClass();
202 inputMap.put(accelerator, muActionClass);
203 actionMap.put(muActionClass, action);
204 }
205
206 public static void unregisterActionAccelerator(MuAction action, KeyStroke accelerator, JComponent comp, int condition) {
207 if(accelerator==null)
208 return;
209 InputMap inputMap = comp.getInputMap(condition);
210 ActionMap actionMap = comp.getActionMap();
211 Class muActionClass = action.getClass();
212 inputMap.remove(accelerator);
213 actionMap.remove(muActionClass);
214 }
215
216
217 public static void registerActionAccelerators(MuAction action, JComponent comp, int condition) {
218 KeyStroke accelerator = action.getAccelerator();
219 if(accelerator!=null)
220 registerActionAccelerator(action, accelerator, comp, condition);
221
222 accelerator = action.getAlternateAccelerator();
223 if(accelerator!=null)
224 registerActionAccelerator(action, accelerator, comp, condition);
225 }
226
227 public static void unregisterActionAccelerators(MuAction action, JComponent comp, int condition) {
228 KeyStroke accelerator = action.getAccelerator();
229 if(accelerator!=null)
230 unregisterActionAccelerator(action, accelerator, comp, condition);
231
232 accelerator = action.getAlternateAccelerator();
233 if(accelerator!=null)
234 unregisterActionAccelerator(action, accelerator, comp, condition);
235 }
236
237
238 public static void changeActionAccelerators(Class muActionClass, KeyStroke accelerator, KeyStroke alternateAccelerator) {
239 // Remove old accelerators (primary and alternate) from accelerators map
240 KeyStroke oldAccelator = (KeyStroke)primaryActionKeymap.get(muActionClass);
241 if(oldAccelator!=null)
242 acceleratorMap.remove(oldAccelator);
243
244 oldAccelator = (KeyStroke)alternateActionKeymap.get(muActionClass);
245 if(oldAccelator!=null)
246 acceleratorMap.remove(oldAccelator);
247
248 // Register new accelerators
249 if(accelerator!=null) {
250 primaryActionKeymap.put(muActionClass, accelerator);
251 acceleratorMap.put(accelerator, muActionClass);
252 }
253
254 if(alternateAccelerator!=null) {
255 alternateActionKeymap.put(muActionClass, alternateAccelerator);
256 acceleratorMap.put(alternateAccelerator, muActionClass);
257 }
258
259 // Update each MainFrame's action instance and input map
260 Vector actionInstances = ActionManager.getActionInstances(muActionClass);
261 int nbActionInstances = actionInstances.size();
262 for(int i=0; i<nbActionInstances; i++) {
263 MuAction action = (MuAction)actionInstances.elementAt(i);
264 MainFrame mainFrame = action.getMainFrame();
265
266 // Remove action from MainFrame's action and input maps
267 unregisterAction(mainFrame, action);
268
269 // Change action's accelerators
270 action.setAccelerator(accelerator);
271 action.setAlternateAccelerator(alternateAccelerator);
272
273 // Add updated action to MainFrame's action and input maps
274 registerAction(mainFrame, action);
275 }
276 }
277
278
279 /**
280 * Loads the action keymap file: loads the one contained in the JAR file first, and then the user's one.
281 * This means any new action in the JAR action keymap (when a new version gets released) will have the default
282 * keyboard mapping, but the keyboard mappings customized by the user in the user's action keymap will override
283 * the ones from the JAR action keymap.
284 */
285 private ActionKeymap() throws Exception {
286 try {
287 AbstractFile file;
288
289 // If the user hasn't yet defined an action keymap, copies the default one.
290 file = getActionKeyMapFile();
291 if(!file.exists()) {
292 InputStream in = null;
293 OutputStream out = null;
294
295 if(Debug.ON) Debug.trace("Copying "+ACTION_KEYMAP_RESOURCE_PATH+" JAR resource to "+file);
296
297 try {
298 in = ResourceLoader.getResourceAsStream(ACTION_KEYMAP_RESOURCE_PATH);
299 out = file.getOutputStream(false);
300
301 StreamUtils.copyStream(in, out);
302 }
303 catch(IOException e) {
304 if(Debug.ON) Debug.trace("Error: unable to copy "+ACTION_KEYMAP_RESOURCE_PATH+" resource to "+actionKeyMapFile+": "+e);
305 }
306 finally {
307 if(in != null) {
308 try {in.close();}
309 catch(IOException e) {}
310 }
311
312 if(out != null) {
313 try {out.close();}
314 catch(IOException e) {}
315 }
316 }
317
318 // No need to load the user action keymap here as it is the same as the default keymap
319 }
320 else {
321 // Load the user's custom action keymap file.
322 if(Debug.ON) Debug.trace("Loading user action keymap at " + file.getAbsolutePath());
323 parseActionKeymapFile(new BackupInputStream(file));
324 }
325
326 isParsingDefaultActionKeymap = true;
327
328 // Loads the default action keymap.
329 if(Debug.ON) Debug.trace("Loading default JAR action keymap at "+ACTION_KEYMAP_RESOURCE_PATH);
330 parseActionKeymapFile(ResourceLoader.getResourceAsStream(ACTION_KEYMAP_RESOURCE_PATH));
331 }
332 finally {
333 definedUserActionClasses = null;
334 }
335 }
336
337
338 /**
339 * Starts parsing the XML action keymap file.
340 * @param in the file's input stream
341 * @throws Exception if an error was caught while parsing the file
342 */
343 private void parseActionKeymapFile(InputStream in) throws Exception {
344 // Parse action keymap file
345 try {SAXParserFactory.newInstance().newSAXParser().parse(in, this);}
346 finally {
347 if(in!=null) {
348 try { in.close(); }
349 catch(IOException e) {}
350 }
351 }
352 }
353
354
355 /**
356 * Parses the keystroke defined in the given attribute map (if any) and associates it with the given action class.
357 * The keystroke will not be associated in any of the following cases:
358 * <ul>
359 * <li>the keystroke attribute does not contain any value.</li>
360 * <li>the keystroke attribute has a value that does not represent a valid KeyStroke (syntax error).</li>
361 * <li>the keystroke is already associated with an action class. In this case, the existing association is preserved.</li>
362 * </ul>
363 *
364 * @param actionClass the action class to associate the keystroke with
365 * @param attributes the attributes map that holds the value
366 * @param alternate true to process the alternate keystroke attribute, false for the primary one
367 */
368 private void processKeystrokeAttribute(Class actionClass, Attributes attributes, boolean alternate) {
369 String keyStrokeString = attributes.getValue(alternate?ALTERNATE_KEYSTROKE_ATTRIBUTE:KEYSTROKE_ATTRIBUTE);
370 KeyStroke keyStroke = null;
371
372 // Parse the keystroke and retrieve the corresponding KeyStroke instance and return if the attribute's value
373 // is invalid.
374 if(keyStrokeString!=null) {
375 keyStroke = KeyStroke.getKeyStroke(keyStrokeString);
376 if(keyStroke==null)
377 System.out.println("Error: action keymap file contains a keystroke which could not be resolved: "+keyStrokeString);
378 }
379
380 // Return if keystroke attribute is not defined or KeyStroke instance could not be resolved
381 if(keyStroke==null)
382 return;
383
384 // Discard the mapping if the keystroke is already associated with an action
385 Class existingActionClass = (Class)acceleratorMap.get(keyStroke);
386 if(existingActionClass!=null) {
387 System.out.println("Warning: action keymap file contains multiple associations for keystroke: "+keyStrokeString+", preserving association with action: "+existingActionClass.getName());
388 return;
389 }
390
391 // Add the action/keystroke mapping
392 (alternate?alternateActionKeymap:primaryActionKeymap).put(actionClass, keyStroke);
393 acceleratorMap.put(keyStroke, actionClass);
394 }
395
396
397 ///////////////////////////////////
398 // ContentHandler implementation //
399 ///////////////////////////////////
400
401 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
402 if(qName.equals(ACTION_ELEMENT)) {
403 // Retrieve the action classname
404 String actionClassName = attributes.getValue(CLASS_ATTRIBUTE);
405 if(actionClassName==null) {
406 if(Debug.ON) Debug.trace("Error in action keymap file: no 'class' attribute specified in 'action' element");
407 return;
408 }
409
410 // Resolve the action Class
411 Class actionClass;
412 try {
413 actionClass = Class.forName(actionClassName);
414 }
415 catch(ClassNotFoundException e) {
416 if(Debug.ON) Debug.trace("Error in action keymap file: could not resolve class "+actionClassName);
417 return;
418 }
419
420 // When parsing the default/JAR action keymap:
421 // Skip action classes that have already been encountered in the user action keymap.
422 if(isParsingDefaultActionKeymap && definedUserActionClasses.contains(actionClass))
423 return;
424
425 // Load the action's primary accelator (if any)
426 processKeystrokeAttribute(actionClass, attributes, false);
427 // Load the action's secondary/alternate accelerator (if any)
428 processKeystrokeAttribute(actionClass, attributes, true);
429
430 // When parsing the user action keymap:
431 if(!isParsingDefaultActionKeymap) {
432 // Add the action Class to the list of actions that have already been encountered/defined in the user
433 // action keymap. Note that action elements that do not define any accelerator will still be added to
434 // this list ; this allows discarding accelerators defined in the default/JAR action keymap and having
435 // an action with no associated accelerator.
436 definedUserActionClasses.add(actionClass);
437 }
438 }
439 }
440 }