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    }