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.gui.MainApplication; 026import org.openstreetmap.josm.gui.MainFrame; 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 MainFrame mainFrame = MainApplication.getMainFrame(); 105 if (mainFrame.isShowing()) { 106 layers.show(mainFrame, Integer.MAX_VALUE, Integer.MAX_VALUE); 107 layers.setLocation(mainFrame.getX() + mainFrame.getWidth() - layers.getWidth(), 108 mainFrame.getY() + mainFrame.getHeight() - layers.getHeight()); 109 } 110 } 111 } 112 113 private class MyKeyEventDispatcher implements KeyEventDispatcher { 114 @Override 115 public boolean dispatchKeyEvent(KeyEvent e) { 116 117 if (e.getWhen() == lastTimestamp) 118 return false; 119 120 if (lastAction != null && e.getID() == KeyEvent.KEY_PRESSED) { 121 int index = getIndex(e.getKeyCode()); 122 if (index >= 0) { 123 lastAction.action.executeMultikeyAction(index, e.getKeyCode() == lastAction.shortcut.getKeyStroke().getKeyCode()); 124 } 125 lastAction = null; 126 MainApplication.getMap().statusLine.resetHelpText(STATUS_BAR_ID); 127 return true; 128 } 129 return false; 130 } 131 132 private int getIndex(int lastKey) { 133 if (lastKey >= KeyEvent.VK_1 && lastKey <= KeyEvent.VK_9) 134 return lastKey - KeyEvent.VK_1; 135 else if (lastKey == KeyEvent.VK_0) 136 return 9; 137 else if (lastKey >= KeyEvent.VK_A && lastKey <= KeyEvent.VK_Z) 138 return lastKey - KeyEvent.VK_A + 10; 139 else 140 return -1; 141 } 142 } 143 144 private class MyAction extends AbstractAction { 145 146 private final transient MultikeyShortcutAction action; 147 private final transient Shortcut shortcut; 148 149 MyAction(MultikeyShortcutAction action) { 150 this.action = action; 151 this.shortcut = action.getMultikeyShortcut(); 152 } 153 154 @Override 155 public void actionPerformed(ActionEvent e) { 156 lastTimestamp = e.getWhen(); 157 lastAction = this; 158 timer.schedule(new MyTimerTask(lastTimestamp, lastAction), DIALOG_DELAY); 159 MainApplication.getMap().statusLine.setHelpText(STATUS_BAR_ID, 160 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 final 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 = Shortcut.getKeyText(keyStroke) + ',' + index; 212 213 return "<html><i>" + shortcutText + "</i> " + description; 214 } 215 216 /** 217 * Registers an action and its shortcut 218 * @param action The action to add 219 */ 220 public void addAction(MultikeyShortcutAction action) { 221 if (action.getMultikeyShortcut() != null) { 222 MyAction myAction = new MyAction(action); 223 myActions.put(action, myAction); 224 MainApplication.registerActionShortcut(myAction, myAction.shortcut); 225 } 226 } 227 228 /** 229 * Unregisters an action and its shortcut completely 230 * @param action The action to remove 231 */ 232 public void removeAction(MultikeyShortcutAction action) { 233 MyAction a = myActions.get(action); 234 if (a != null) { 235 MainApplication.unregisterActionShortcut(a, a.shortcut); 236 myActions.remove(action); 237 } 238 } 239}