package org.freeplane.core.ui.ribbon;
import java.awt.Event;
import java.awt.event.KeyEvent;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import org.freeplane.core.resources.ResourceController;
import org.freeplane.core.resources.components.GrabKeyDialog;
import org.freeplane.core.resources.components.IKeystrokeValidator;
import org.freeplane.core.ui.AFreeplaneAction;
import org.freeplane.core.ui.IAcceleratorChangeListener;
import org.freeplane.core.ui.IEditHandler.FirstAction;
import org.freeplane.core.ui.IKeyStrokeProcessor;
import org.freeplane.core.ui.components.FreeplaneMenuBar;
import org.freeplane.core.ui.components.UITools;
import org.freeplane.core.util.Compat;
import org.freeplane.core.util.LogUtils;
import org.freeplane.core.util.TextUtils;
import org.freeplane.features.mode.Controller;
import org.freeplane.features.mode.ModeController;
public class RibbonAcceleratorManager implements IKeyStrokeProcessor, IAcceleratorChangeListener {
private static final String SHORTCUT_PROPERTY_PREFIX = "ribbon.acceleratorFor.";
private final Map<KeyStroke, AFreeplaneAction> accelerators = new HashMap<KeyStroke, AFreeplaneAction>();
private final Map<String, KeyStroke> actionMap = new HashMap<String, KeyStroke>();
private final List<IAcceleratorChangeListener> changeListeners = new ArrayList<IAcceleratorChangeListener>();
private final RibbonBuilder builder;
private IAcceleratorChangeListener acceleratorChangeListener;
private final Properties keysetProps = new Properties();
private final Properties defaultProps = new Properties();
/***********************************************************************************
* CONSTRUCTORS
**********************************************************************************/
public RibbonAcceleratorManager(RibbonBuilder ribbonBuilder) {
this.builder = ribbonBuilder;
}
/***********************************************************************************
* METHODS
**********************************************************************************/
public void setAccelerator(final AFreeplaneAction action, final KeyStroke keyStroke) {
if(action == null) {
return;
}
if(keyStroke != null) {
final AFreeplaneAction oldAction = accelerators.put(keyStroke, action);
if(action == oldAction) {
return;
}
if (keyStroke != null && oldAction != null) {
UITools.errorMessage(TextUtils.removeTranslateComment(TextUtils.format("action_keystroke_in_use_error", keyStroke, getActionTitle(action.getKey()), getActionTitle(oldAction.getKey()))));
accelerators.put(keyStroke, oldAction);
final String shortcutKey = getPropertyKey(action.getKey());
keysetProps.setProperty(shortcutKey, "");
return;
}
}
final KeyStroke removedAccelerator = removeAccelerator(action);
if(keyStroke != null) {
actionMap.put(action.getKey(), keyStroke);
}
else {
actionMap.remove(action.getKey());
}
if (acceleratorChangeListener != null && (removedAccelerator != null || keyStroke != null)) {
acceleratorChangeListener.acceleratorChanged(action, removedAccelerator, keyStroke);
}
fireAcceleratorChanged(action, removedAccelerator, keyStroke);
}
private String getActionTitle(String key) {
String title = TextUtils.getText(key+".text");
if(title == null || title.isEmpty()) {
title = key;
}
return TextUtils.removeTranslateComment(title);
}
public void setDefaultAccelerator(final String itemKey, final String accelerator) {
final String shortcutKey = getPropertyKey(itemKey);
if (null == getProperty(shortcutKey)) {
defaultProps.setProperty(shortcutKey, accelerator);
}
KeyStroke ks = KeyStroke.getKeyStroke(accelerator);
AFreeplaneAction action = builder.getMode().getAction(itemKey);
setAccelerator(action, ks);
}
public KeyStroke removeAccelerator(final AFreeplaneAction action) throws AssertionError {
if(action == null) {
return null;
}
final KeyStroke oldAccelerator = actionMap.get(action.getKey());
if (oldAccelerator != null) {
final AFreeplaneAction oldAction = accelerators.remove(oldAccelerator);
if (!action.equals(oldAction)) {
throw new AssertionError("unexpected action " + "for accelerator " + oldAccelerator);
}
}
return oldAccelerator;
}
public void setAcceleratorChangeListener(final IAcceleratorChangeListener acceleratorChangeListener) {
this.acceleratorChangeListener = acceleratorChangeListener;
}
public String getPropertyKey(final String key) {
return SHORTCUT_PROPERTY_PREFIX + builder.getMode().getModeName() + "/" + key;
}
public KeyStroke getAccelerator(String actionKey) {
KeyStroke ks = actionMap.get(actionKey);
return ks;
}
public void addAcceleratorChangeListener(IAcceleratorChangeListener changeListener) {
synchronized (changeListeners) {
if(!changeListeners.contains(changeListener)) {
changeListeners.add(changeListener);
}
}
}
protected void fireAcceleratorChanged(AFreeplaneAction action, KeyStroke oldStroke, KeyStroke newStroke) {
synchronized (changeListeners) {
for (IAcceleratorChangeListener listener : changeListeners) {
listener.acceleratorChanged(action, oldStroke, newStroke);
}
}
}
private String getProperty(String key) {
return keysetProps.getProperty(key, defaultProps.getProperty(key, null));
}
public void newAccelerator(final AFreeplaneAction action, final KeyStroke newAccelerator) {
final String shortcutKey = getPropertyKey(action.getKey());
final String oldShortcut = getProperty(shortcutKey);
if (newAccelerator == null || !new KeystrokeValidator(action).isValid(newAccelerator, newAccelerator.getKeyChar())) {
final GrabKeyDialog grabKeyDialog = new GrabKeyDialog(oldShortcut);
final IKeystrokeValidator validator = new KeystrokeValidator(action);
grabKeyDialog.setValidator(validator);
grabKeyDialog.setVisible(true);
if (grabKeyDialog.isOK()) {
final String shortcut = grabKeyDialog.getShortcut();
final KeyStroke accelerator = UITools.getKeyStroke(shortcut);
setAccelerator(action, accelerator);
keysetProps.setProperty(shortcutKey, shortcut);
LogUtils.info("created shortcut '" + shortcut + "' for action '" + action.getKey() + "', shortcutKey '"
+ shortcutKey + "' (" + RibbonActionContributorFactory.getActionTitle(action) + ")");
}
}
else{
if(oldShortcut != null){
final int replace = JOptionPane.showConfirmDialog(UITools.getFrame(), oldShortcut, TextUtils.removeTranslateComment(TextUtils.getText("remove_shortcut_question")), JOptionPane.YES_NO_OPTION);
if (replace != JOptionPane.YES_OPTION) {
return;
}
}
setAccelerator(action, newAccelerator);
keysetProps.setProperty(shortcutKey, toString(newAccelerator));
LogUtils.info("created shortcut '" + toString(newAccelerator) + "' for action '" + action+ "', shortcutKey '" + shortcutKey + "' (" + RibbonActionContributorFactory.getActionTitle(action) + ")");
}
try {
if(!getPresetsFile().exists()) {
getPresetsFile().createNewFile();
}
storeAcceleratorPreset(new FileOutputStream(getPresetsFile()));
} catch (IOException e) {
e.printStackTrace();
}
}
public File getPresetsFile() {
File ribbonsDir = new File(ResourceController.getResourceController().getFreeplaneUserDirectory(), "ribbons");
if(!ribbonsDir.exists()) {
ribbonsDir.mkdirs();
}
return new File(ribbonsDir, "accelerator.properties");
}
public void loadAcceleratorPresets(final InputStream in) {
final Properties prop = new Properties();
try {
prop.load(in);
for (final Entry<Object, Object> property : prop.entrySet()) {
final String shortcutKey = (String) property.getKey();
final String keystrokeString = (String) property.getValue();
if (!shortcutKey.startsWith(SHORTCUT_PROPERTY_PREFIX)) {
LogUtils.warn("wrong property key " + shortcutKey);
continue;
}
final int pos = shortcutKey.indexOf("/", SHORTCUT_PROPERTY_PREFIX.length());
if (pos <= 0) {
LogUtils.warn("wrong property key " + shortcutKey);
continue;
}
final String modeName = shortcutKey.substring(SHORTCUT_PROPERTY_PREFIX.length(), pos);
final String itemKey = shortcutKey.substring(pos + 1);
Controller controller = Controller.getCurrentController();
final ModeController modeController = controller.getModeController(modeName);
if (modeController == null) {
LogUtils.warn("unknown mode name in " + shortcutKey);
continue;
}
final AFreeplaneAction action = modeController.getAction(itemKey);
if (action == null) {
LogUtils.warn("wrong key in " + shortcutKey);
continue;
}
final KeyStroke keyStroke;
if (!keystrokeString.equals("")) {
keyStroke = UITools.getKeyStroke(parseKeyStroke(keystrokeString).toString());
final AFreeplaneAction oldAction = accelerators.get(keyStroke);
if (oldAction != null) {
setAccelerator(oldAction, null);
final Object key = oldAction.getKey();
final String oldShortcutKey = getPropertyKey(key.toString());
keysetProps.setProperty(oldShortcutKey, "");
}
}
else {
keyStroke = null;
}
setAccelerator(action, keyStroke);
keysetProps.setProperty(shortcutKey, keystrokeString);
}
}
catch (final IOException e) {
LogUtils.warn("shortcut presets not stored: "+e.getMessage());
}
}
public void storeAcceleratorPreset(OutputStream out) {
try {
final OutputStream output = new BufferedOutputStream(out);
keysetProps.store(output, "");
output.close();
}
catch (final IOException e1) {
UITools.errorMessage(TextUtils.removeTranslateComment(TextUtils.getText("can_not_save_key_set")));
}
}
private static String toString(final KeyStroke newAccelerator) {
return newAccelerator.toString().replaceFirst("pressed ", "");
}
private static boolean askForReplaceShortcutViaDialog(String oldMenuItemTitle) {
final int replace = JOptionPane.showConfirmDialog(UITools.getFrame(),
TextUtils.removeTranslateComment(TextUtils.format("replace_shortcut_question", oldMenuItemTitle)),
TextUtils.removeTranslateComment(TextUtils.format("replace_shortcut_title")), JOptionPane.YES_NO_OPTION);
return replace == JOptionPane.YES_OPTION;
}
/***********************************************************************************
* REQUIRED METHODS FOR INTERFACES
**********************************************************************************/
public boolean processKeyBinding(KeyStroke ks, KeyEvent event, int condition, boolean pressed, boolean consumed) {
if (!consumed && condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) {
AFreeplaneAction action = accelerators.get(ks);
if(action == null) {
action = accelerators.get(FreeplaneMenuBar.derive(ks, event.getKeyChar()));
}
if(action != null && action.isEnabled()) {
if(action != null && SwingUtilities.notifyAction(action, ks, event, event.getComponent(), event.getModifiers())) {
return true;
}
}
}
return false;
}
public void acceleratorChanged(JMenuItem action, KeyStroke oldStroke, KeyStroke newStroke) {
// TODO Auto-generated method stub
}
public void acceleratorChanged(AFreeplaneAction action, KeyStroke oldStroke, KeyStroke newStroke) {
KeyStroke ks = actionMap.put(action.getKey(), newStroke);
if(ks != null) {
accelerators.remove(ks);
}
accelerators.put(newStroke, action);
}
/***********************************************************************************
* NESTED TYPE DECLARATIONS
**********************************************************************************/
private class KeystrokeValidator implements IKeystrokeValidator {
private final AFreeplaneAction action;
private KeystrokeValidator(AFreeplaneAction action) {
this.action = action;
}
private boolean checkForOverwriteShortcut(final KeyStroke keystroke) {
final AFreeplaneAction priorAssigned = accelerators.get(keystroke);
if (priorAssigned == null || action.getKey().equals(priorAssigned.getKey())) {
return true;
}
return replaceOrCancel(priorAssigned, RibbonActionContributorFactory.getActionTitle(priorAssigned));
}
private boolean replaceOrCancel(AFreeplaneAction action, String oldMenuItemTitle) {
if (askForReplaceShortcutViaDialog(oldMenuItemTitle)) {
setAccelerator(action, null);
final String shortcutKey = getPropertyKey(action.getKey());
keysetProps.setProperty(shortcutKey, "");
return true;
} else {
return false;
}
}
public boolean isValid(final KeyStroke keystroke, final Character keyChar) {
if (keystroke == null) {
return true;
}
if (actionMap.containsKey(action.getKey())) {
return true;
}
if (keyChar != KeyEvent.CHAR_UNDEFINED && (keystroke.getModifiers() & (Event.ALT_MASK | Event.CTRL_MASK | Event.META_MASK)) == 0) {
final String keyTypeActionString = ResourceController.getResourceController().getProperty("key_type_action",
FirstAction.EDIT_CURRENT.toString());
FirstAction keyTypeAction = FirstAction.valueOf(keyTypeActionString);
return FirstAction.IGNORE.equals(keyTypeAction);
}
if (!checkForOverwriteShortcut(keystroke)) {
return false;
}
final KeyStroke derivedKS = FreeplaneMenuBar.derive(keystroke, keyChar);
if (derivedKS == keystroke) {
return true;
}
return checkForOverwriteShortcut(derivedKS);
}
}
public static KeyStroke parseKeyStroke(String accelerator) {
if (accelerator != null) {
if (Compat.isMacOsX()) {
accelerator = accelerator.replaceFirst("CONTROL", "META").replaceFirst("control", "meta");
}
else {
accelerator = accelerator.replaceFirst("META", "CONTROL").replaceFirst("meta", "control");
}
return KeyStroke.getKeyStroke(accelerator);
}
return null;
}
}