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