/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2012 Maxence Bernard
*
* muCommander 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 3 of the License, or
* (at your option) any later version.
*
* muCommander 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, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.action;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import javax.swing.KeyStroke;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import com.mucommander.RuntimeConstants;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.io.backup.BackupInputStream;
/**
* This class is responsible for reading the actions.
* it read and parse the two action files - the default one
* (placed in the jar file) and the user's one.
*
* @author Maxence Bernard, Arik Hadas
*/
class ActionKeymapReader extends ActionKeymapIO {
private static final Logger LOGGER = LoggerFactory.getLogger(ActionKeymapReader.class);
/** Maps action Class onto Keystroke instances*/
private HashMap<String, KeyStroke> primaryActionsReadKeymap;
/** Maps action Class instances onto Keystroke instances*/
private HashMap<String, KeyStroke> alternateActionsReadKeymap;
/** Parsed file */
private AbstractFile file;
/**
* Loads the action file: loads the one contained in the JAR file first, and then the user's one.
* This means any new action in the JAR action keymap (when a new version gets released) will have the default
* keyboard mapping, but the keyboard mappings customized by the user in the user's action keymap will override
* the ones from the JAR action keymap.
*
* Starts parsing the XML actions file.
*
* @throws ParserConfigurationException
* @throws IOException
* @throws SAXException
*/
ActionKeymapReader(AbstractFile file) throws SAXException, IOException, ParserConfigurationException {
this.file = file;
InputStream in = null;
try {SAXParserFactory.newInstance().newSAXParser().parse(in = new BackupInputStream(file), this);}
finally {
if(in!=null) {
try { in.close(); }
catch(IOException e) {}
}
}
}
/**
* Parses the keystrokes defined in the given attribute map (if any) and associates them with the given action id.
* The keystroke will not be associated in any of the following cases:
* <ul>
* <li>the keystrokes attributes do not contain any value.</li>
* <li>the keystrokes attributes have values that do not represent a valid KeyStroke (syntax error).</li>
* </ul>
* If a given keystroke is already associated to an action, the existing association is replaced.
* If there is a valid alternative keystroke defined but there is no valid primary keystroke defined, the primary keystroke
* is replaced by the alternative keystroke.
*
* @param actionId the action id to associate the keystroke with
* @param attributes the attributes map that holds the value
*/
private void processKeystrokeAttribute(String actionId, Attributes attributes) {
String keyStrokeString;
KeyStroke alternateKeyStroke = null;
KeyStroke primaryKeyStroke = null;
// Parse the primary keystroke and retrieve the corresponding KeyStroke instance
keyStrokeString = attributes.getValue(PRIMARY_KEYSTROKE_ATTRIBUTE);
if (keyStrokeString != null) {
primaryKeyStroke = KeyStroke.getKeyStroke(keyStrokeString);
if (primaryKeyStroke == null)
LOGGER.info("Action keymap file contains a keystroke which could not be resolved: " + keyStrokeString);
else {
String prevAssignedActionId = ActionKeymap.getRegisteredActionIdForKeystroke(primaryKeyStroke);
if (prevAssignedActionId != null && !prevAssignedActionId.equals(actionId))
LOGGER.debug("Canceling previous association of keystroke " + keyStrokeString + ", reassign it to action: " + actionId);
}
}
// Parse the alternate keystroke and retrieve the corresponding KeyStroke instance
keyStrokeString = attributes.getValue(ALTERNATE_KEYSTROKE_ATTRIBUTE);
if (keyStrokeString != null) {
alternateKeyStroke = KeyStroke.getKeyStroke(keyStrokeString);
if (alternateKeyStroke == null)
LOGGER.info("Action keymap file contains a keystroke which could not be resolved: " + keyStrokeString);
else {
String prevAssignedActionId = ActionKeymap.getRegisteredActionIdForKeystroke(alternateKeyStroke);
if (prevAssignedActionId != null && !prevAssignedActionId.equals(actionId))
LOGGER.debug("Canceling previous association of keystroke " + keyStrokeString + ", reassign it to action: " + actionId);
}
}
// If there is no primary shortcut defined for the action but there is an alternative shortcut defined,
// turn the alternative shortcut to the action's primary shortcut
if (primaryKeyStroke == null) {
LOGGER.debug("Action \"" + actionId +"\" has an alternative shortcut with no primary shortcut, so the alternative shortcut become primary");
primaryActionsReadKeymap.put(actionId, alternateKeyStroke);
alternateActionsReadKeymap.put(actionId, null);
// Mark that the actions keymap file should be updated
setModified();
}
else {
primaryActionsReadKeymap.put(actionId, primaryKeyStroke);
alternateActionsReadKeymap.put(actionId, alternateKeyStroke);
}
}
///////////////////
///// getters /////
///////////////////
public HashMap<String, KeyStroke> getPrimaryActionsKeymap() {return primaryActionsReadKeymap;}
public HashMap<String, KeyStroke> getAlternateActionsKeymap() {return alternateActionsReadKeymap;}
///////////////////////////////////
// ContentHandler implementation //
///////////////////////////////////
@Override
public void startDocument() {
LOGGER.trace(file.getAbsolutePath()+" parsing started");
primaryActionsReadKeymap = new HashMap<String, KeyStroke>();
alternateActionsReadKeymap = new HashMap<String, KeyStroke>();
}
@Override
public void endDocument() {
LOGGER.trace(file.getAbsolutePath()+" parsing finished");
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equals(ACTION_ELEMENT)) {
// Retrieve the action id
String actionId = attributes.getValue(ID_ATTRIBUTE);
// if id attribute not exits, read class attribute
if (actionId == null) {
String actionClassPath = attributes.getValue(CLASS_ATTRIBUTE);
if(actionClassPath==null) {
LOGGER.warn("Error in action keymap file: no 'class' or 'id' attribute specified in 'action' element");
return;
}
// extrapolate the action id from its class path
actionId = ActionManager.extrapolateId(actionClassPath);
}
if (!ActionManager.isActionExist(actionId)) {
LOGGER.warn("Error in action keymap file: could not resolve action "+actionId);
return;
}
// Load the action's accelerators (if any)
processKeystrokeAttribute(actionId, attributes);
}
else if (qName.equals(ROOT_ELEMENT)) {
// Note: early 0.8 beta3 nightly builds did not have version attribute, so the attribute may be null
String fileVersion = attributes.getValue(VERSION_ATTRIBUTE);
// if the file's version is not up-to-date, update the file to the current version at quitting.
if (!RuntimeConstants.VERSION.equals(fileVersion))
setModified();
}
}
}