001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.KeyEventDispatcher; 007import java.awt.KeyboardFocusManager; 008import java.awt.event.ActionEvent; 009import java.awt.event.ActionListener; 010import java.awt.event.KeyEvent; 011import java.util.HashMap; 012import java.util.Map; 013import java.util.Timer; 014import java.util.TimerTask; 015 016import javax.swing.AbstractAction; 017import javax.swing.Action; 018import javax.swing.JMenuItem; 019import javax.swing.JPanel; 020import javax.swing.JPopupMenu; 021import javax.swing.KeyStroke; 022import javax.swing.SwingUtilities; 023import javax.swing.event.PopupMenuEvent; 024import javax.swing.event.PopupMenuListener; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.tools.MultikeyShortcutAction.MultikeyInfo; 028 029public final class MultikeyActionsHandler { 030 031 private static final long DIALOG_DELAY = 1000; 032 private static final String STATUS_BAR_ID = "multikeyShortcut"; 033 034 private Map<MultikeyShortcutAction, MyAction> myActions = new HashMap<MultikeyShortcutAction,MyAction>(); 035 036 private class MyKeyEventDispatcher implements KeyEventDispatcher { 037 @Override 038 public boolean dispatchKeyEvent(KeyEvent e) { 039 040 if (e.getWhen() == lastTimestamp) 041 return false; 042 043 if (lastAction != null && e.getID() == KeyEvent.KEY_PRESSED) { 044 int index = getIndex(e.getKeyCode()); 045 if (index >= 0) { 046 lastAction.action.executeMultikeyAction(index, e.getKeyCode() == lastAction.shortcut.getKeyStroke().getKeyCode()); 047 } 048 lastAction = null; 049 Main.map.statusLine.resetHelpText(STATUS_BAR_ID); 050 return true; 051 } 052 return false; 053 } 054 055 private int getIndex(int lastKey) { 056 if (lastKey >= KeyEvent.VK_1 && lastKey <= KeyEvent.VK_9) 057 return lastKey - KeyEvent.VK_1; 058 else if (lastKey == KeyEvent.VK_0) 059 return 9; 060 else if (lastKey >= KeyEvent.VK_A && lastKey <= KeyEvent.VK_Z) 061 return lastKey - KeyEvent.VK_A + 10; 062 else 063 return -1; 064 } 065 } 066 067 private class MyAction extends AbstractAction { 068 069 final MultikeyShortcutAction action; 070 final Shortcut shortcut; 071 072 MyAction(MultikeyShortcutAction action) { 073 this.action = action; 074 this.shortcut = action.getMultikeyShortcut(); 075 } 076 077 @Override 078 public void actionPerformed(ActionEvent e) { 079 lastTimestamp = e.getWhen(); 080 lastAction = this; 081 timer.schedule(new MyTimerTask(lastTimestamp, lastAction), DIALOG_DELAY); 082 Main.map.statusLine.setHelpText(STATUS_BAR_ID, tr("{0}... [please type its number]", (String) action.getValue(SHORT_DESCRIPTION))); 083 } 084 085 @Override 086 public String toString() { 087 return "MultikeyAction" + action.toString(); 088 } 089 } 090 091 private class MyTimerTask extends TimerTask { 092 private final long lastTimestamp; 093 private final MyAction lastAction; 094 095 MyTimerTask(long lastTimestamp, MyAction lastAction) { 096 this.lastTimestamp = lastTimestamp; 097 this.lastAction = lastAction; 098 } 099 100 @Override 101 public void run() { 102 if (lastTimestamp == MultikeyActionsHandler.this.lastTimestamp && 103 lastAction == MultikeyActionsHandler.this.lastAction) { 104 showLayersPopup(lastAction); 105 MultikeyActionsHandler.this.lastAction = null; 106 } 107 } 108 } 109 110 private long lastTimestamp; 111 private MyAction lastAction; 112 private Timer timer; 113 114 115 private MultikeyActionsHandler() { 116 KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new MyKeyEventDispatcher()); 117 timer =new Timer(); 118 } 119 120 private static MultikeyActionsHandler instance; 121 122 /** 123 * Replies the unique instance of this class. 124 * @return The unique instance of this class 125 */ 126 public static MultikeyActionsHandler getInstance() { 127 if (instance == null) { 128 instance = new MultikeyActionsHandler(); 129 } 130 return instance; 131 } 132 133 private String formatMenuText(KeyStroke keyStroke, String index, String description) { 134 String shortcutText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers()) + "+" + KeyEvent.getKeyText(keyStroke.getKeyCode()) + "," + index; 135 136 return "<html><i>" + shortcutText + "</i> " + description; 137 138 } 139 140 private void showLayersPopup(final MyAction action) { 141 SwingUtilities.invokeLater(new Runnable() { 142 @Override 143 public void run() { 144 JPopupMenu layers = new JPopupMenu(); 145 146 JMenuItem lbTitle = new JMenuItem((String) action.action.getValue(Action.SHORT_DESCRIPTION)); 147 lbTitle.setEnabled(false); 148 JPanel pnTitle = new JPanel(); 149 pnTitle.add(lbTitle); 150 layers.add(pnTitle); 151 152 char repeatKey = (char) action.shortcut.getKeyStroke().getKeyCode(); 153 boolean repeatKeyUsed = false; 154 155 156 for (final MultikeyInfo info: action.action.getMultikeyCombinations()) { 157 158 if (info.getShortcut() == repeatKey) { 159 repeatKeyUsed = true; 160 } 161 162 JMenuItem item = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(), String.valueOf(info.getShortcut()), info.getDescription())); 163 item.setMnemonic(info.getShortcut()); 164 item.addActionListener(new ActionListener() { 165 @Override 166 public void actionPerformed(ActionEvent e) { 167 action.action.executeMultikeyAction(info.getIndex(), false); 168 } 169 }); 170 layers.add(item); 171 } 172 173 if (!repeatKeyUsed) { 174 MultikeyInfo lastLayer = action.action.getLastMultikeyAction(); 175 if (lastLayer != null) { 176 JMenuItem repeateItem = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(), 177 KeyEvent.getKeyText(action.shortcut.getKeyStroke().getKeyCode()), 178 "Repeat " + lastLayer.getDescription())); 179 repeateItem.setMnemonic(action.shortcut.getKeyStroke().getKeyCode()); 180 repeateItem.addActionListener(new ActionListener() { 181 @Override 182 public void actionPerformed(ActionEvent e) { 183 action.action.executeMultikeyAction(-1, true); 184 } 185 }); 186 layers.add(repeateItem); 187 } 188 } 189 layers.addPopupMenuListener(new PopupMenuListener() { 190 191 @Override 192 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} 193 194 @Override 195 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 196 Main.map.statusLine.resetHelpText(STATUS_BAR_ID); 197 } 198 199 @Override 200 public void popupMenuCanceled(PopupMenuEvent e) {} 201 }); 202 203 layers.show(Main.parent, Integer.MAX_VALUE, Integer.MAX_VALUE); 204 layers.setLocation(Main.parent.getX() + Main.parent.getWidth() - layers.getWidth(), Main.parent.getY() + Main.parent.getHeight() - layers.getHeight()); 205 } 206 }); 207 } 208 209 /** 210 * Registers an action and its shortcut 211 * @param action The action to add 212 */ 213 public void addAction(MultikeyShortcutAction action) { 214 if (action.getMultikeyShortcut() != null) { 215 MyAction myAction = new MyAction(action); 216 myActions.put(action, myAction); 217 Main.registerActionShortcut(myAction, myAction.shortcut); 218 } 219 } 220 221 /** 222 * Unregisters an action and its shortcut completely 223 * @param action The action to remove 224 */ 225 public void removeAction(MultikeyShortcutAction action) { 226 MyAction a = myActions.get(action); 227 if (a!=null) { 228 Main.unregisterActionShortcut(a, a.shortcut); 229 myActions.remove(action); 230 } 231 } 232}