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