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