001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Cursor; 007import java.awt.Point; 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.awt.event.MouseAdapter; 011import java.awt.event.MouseEvent; 012import java.awt.event.MouseWheelEvent; 013 014import javax.swing.AbstractAction; 015import javax.swing.ActionMap; 016import javax.swing.InputMap; 017import javax.swing.JComponent; 018import javax.swing.JPanel; 019import javax.swing.KeyStroke; 020 021import org.openstreetmap.gui.jmapviewer.JMapViewer; 022import org.openstreetmap.josm.Main; 023import org.openstreetmap.josm.actions.mapmode.SelectAction; 024import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 025import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 026import org.openstreetmap.josm.data.coor.EastNorth; 027import org.openstreetmap.josm.data.preferences.BooleanProperty; 028import org.openstreetmap.josm.tools.Destroyable; 029import org.openstreetmap.josm.tools.Shortcut; 030 031/** 032 * Enables moving of the map by holding down the right mouse button and drag 033 * the mouse. Also, enables zooming by the mouse wheel. 034 * 035 * @author imi 036 */ 037public class MapMover extends MouseAdapter implements Destroyable { 038 039 public static final BooleanProperty PROP_ZOOM_REVERSE_WHEEL = new BooleanProperty("zoom.reverse-wheel", false); 040 041 private static final JMapViewerUpdater jMapViewerUpdater = new JMapViewerUpdater(); 042 043 private static class JMapViewerUpdater implements PreferenceChangedListener { 044 045 JMapViewerUpdater() { 046 Main.pref.addPreferenceChangeListener(this); 047 updateJMapViewer(); 048 } 049 050 @Override 051 public void preferenceChanged(PreferenceChangeEvent e) { 052 if (MapMover.PROP_ZOOM_REVERSE_WHEEL.getKey().equals(e.getKey())) { 053 updateJMapViewer(); 054 } 055 } 056 057 private static void updateJMapViewer() { 058 JMapViewer.zoomReverseWheel = MapMover.PROP_ZOOM_REVERSE_WHEEL.get(); 059 } 060 } 061 062 private final class ZoomerAction extends AbstractAction { 063 private final String action; 064 065 ZoomerAction(String action) { 066 this.action = action; 067 } 068 069 @Override 070 public void actionPerformed(ActionEvent e) { 071 if (".".equals(action) || ",".equals(action)) { 072 Point mouse = nc.getMousePosition(); 073 if (mouse == null) 074 mouse = new Point((int) nc.getBounds().getCenterX(), (int) nc.getBounds().getCenterY()); 075 MouseWheelEvent we = new MouseWheelEvent(nc, e.getID(), e.getWhen(), e.getModifiers(), mouse.x, mouse.y, 0, false, 076 MouseWheelEvent.WHEEL_UNIT_SCROLL, 1, ",".equals(action) ? -1 : 1); 077 mouseWheelMoved(we); 078 } else { 079 EastNorth center = nc.getCenter(); 080 EastNorth newcenter = nc.getEastNorth(nc.getWidth()/2+nc.getWidth()/5, nc.getHeight()/2+nc.getHeight()/5); 081 switch(action) { 082 case "left": 083 nc.zoomTo(new EastNorth(2*center.east()-newcenter.east(), center.north())); 084 break; 085 case "right": 086 nc.zoomTo(new EastNorth(newcenter.east(), center.north())); 087 break; 088 case "up": 089 nc.zoomTo(new EastNorth(center.east(), 2*center.north()-newcenter.north())); 090 break; 091 case "down": 092 nc.zoomTo(new EastNorth(center.east(), newcenter.north())); 093 break; 094 default: // Do nothing 095 } 096 } 097 } 098 } 099 100 /** 101 * The point in the map that was the under the mouse point 102 * when moving around started. 103 */ 104 private EastNorth mousePosMove; 105 /** 106 * The map to move around. 107 */ 108 private final NavigatableComponent nc; 109 private final JPanel contentPane; 110 111 private boolean movementInPlace; 112 113 /** 114 * Constructs a new {@code MapMover}. 115 * @param navComp the navigatable component 116 * @param contentPane the content pane 117 */ 118 public MapMover(NavigatableComponent navComp, JPanel contentPane) { 119 this.nc = navComp; 120 this.contentPane = contentPane; 121 nc.addMouseListener(this); 122 nc.addMouseMotionListener(this); 123 nc.addMouseWheelListener(this); 124 125 if (contentPane != null) { 126 // CHECKSTYLE.OFF: LineLength 127 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 128 Shortcut.registerShortcut("system:movefocusright", tr("Map: {0}", tr("Move right")), KeyEvent.VK_RIGHT, Shortcut.CTRL).getKeyStroke(), 129 "MapMover.Zoomer.right"); 130 contentPane.getActionMap().put("MapMover.Zoomer.right", new ZoomerAction("right")); 131 132 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 133 Shortcut.registerShortcut("system:movefocusleft", tr("Map: {0}", tr("Move left")), KeyEvent.VK_LEFT, Shortcut.CTRL).getKeyStroke(), 134 "MapMover.Zoomer.left"); 135 contentPane.getActionMap().put("MapMover.Zoomer.left", new ZoomerAction("left")); 136 137 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 138 Shortcut.registerShortcut("system:movefocusup", tr("Map: {0}", tr("Move up")), KeyEvent.VK_UP, Shortcut.CTRL).getKeyStroke(), 139 "MapMover.Zoomer.up"); 140 contentPane.getActionMap().put("MapMover.Zoomer.up", new ZoomerAction("up")); 141 142 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 143 Shortcut.registerShortcut("system:movefocusdown", tr("Map: {0}", tr("Move down")), KeyEvent.VK_DOWN, Shortcut.CTRL).getKeyStroke(), 144 "MapMover.Zoomer.down"); 145 contentPane.getActionMap().put("MapMover.Zoomer.down", new ZoomerAction("down")); 146 // CHECKSTYLE.ON: LineLength 147 148 // see #10592 - Disable these alternate shortcuts on OS X because of conflict with system shortcut 149 if (!Main.isPlatformOsx()) { 150 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 151 Shortcut.registerShortcut("view:zoominalternate", 152 tr("Map: {0}", tr("Zoom in")), KeyEvent.VK_COMMA, Shortcut.CTRL).getKeyStroke(), 153 "MapMover.Zoomer.in"); 154 contentPane.getActionMap().put("MapMover.Zoomer.in", new ZoomerAction(",")); 155 156 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 157 Shortcut.registerShortcut("view:zoomoutalternate", 158 tr("Map: {0}", tr("Zoom out")), KeyEvent.VK_PERIOD, Shortcut.CTRL).getKeyStroke(), 159 "MapMover.Zoomer.out"); 160 contentPane.getActionMap().put("MapMover.Zoomer.out", new ZoomerAction(".")); 161 } 162 } 163 } 164 165 /** 166 * If the right (and only the right) mouse button is pressed, move the map. 167 */ 168 @Override 169 public void mouseDragged(MouseEvent e) { 170 int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK; 171 int macMouseMask = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; 172 boolean stdMovement = (e.getModifiersEx() & (MouseEvent.BUTTON3_DOWN_MASK | offMask)) == MouseEvent.BUTTON3_DOWN_MASK; 173 boolean macMovement = Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask; 174 boolean allowedMode = !Main.map.mapModeSelect.equals(Main.map.mapMode) 175 || SelectAction.Mode.SELECT.equals(Main.map.mapModeSelect.getMode()); 176 if (stdMovement || (macMovement && allowedMode)) { 177 if (mousePosMove == null) 178 startMovement(e); 179 EastNorth center = nc.getCenter(); 180 EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY()); 181 nc.zoomTo(new EastNorth( 182 mousePosMove.east() + center.east() - mouseCenter.east(), 183 mousePosMove.north() + center.north() - mouseCenter.north())); 184 } else { 185 endMovement(); 186 } 187 } 188 189 /** 190 * Start the movement, if it was the 3rd button (right button). 191 */ 192 @Override 193 public void mousePressed(MouseEvent e) { 194 int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK; 195 int macMouseMask = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; 196 if (e.getButton() == MouseEvent.BUTTON3 && (e.getModifiersEx() & offMask) == 0 || 197 Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask) { 198 startMovement(e); 199 } 200 } 201 202 /** 203 * Change the cursor back to it's pre-move cursor. 204 */ 205 @Override 206 public void mouseReleased(MouseEvent e) { 207 if (e.getButton() == MouseEvent.BUTTON3 || Main.isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1) { 208 endMovement(); 209 } 210 } 211 212 /** 213 * Start movement by setting a new cursor and remember the current mouse 214 * position. 215 * @param e The mouse event that leat to the movement from. 216 */ 217 private void startMovement(MouseEvent e) { 218 if (movementInPlace) 219 return; 220 movementInPlace = true; 221 mousePosMove = nc.getEastNorth(e.getX(), e.getY()); 222 nc.setNewCursor(Cursor.MOVE_CURSOR, this); 223 } 224 225 /** 226 * End the movement. Setting back the cursor and clear the movement variables 227 */ 228 private void endMovement() { 229 if (!movementInPlace) 230 return; 231 movementInPlace = false; 232 nc.resetCursor(this); 233 mousePosMove = null; 234 } 235 236 /** 237 * Zoom the map by 1/5th of current zoom per wheel-delta. 238 * @param e The wheel event. 239 */ 240 @Override 241 public void mouseWheelMoved(MouseWheelEvent e) { 242 int rotation = PROP_ZOOM_REVERSE_WHEEL.get() ? -e.getWheelRotation() : e.getWheelRotation(); 243 nc.zoomManyTimes(e.getX(), e.getY(), rotation); 244 } 245 246 /** 247 * Emulates dragging on Mac OSX. 248 */ 249 @Override 250 public void mouseMoved(MouseEvent e) { 251 if (!movementInPlace) 252 return; 253 // Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired. 254 // Is only the selected mouse button pressed? 255 if (Main.isPlatformOsx()) { 256 if (e.getModifiersEx() == MouseEvent.CTRL_DOWN_MASK) { 257 if (mousePosMove == null) { 258 startMovement(e); 259 } 260 EastNorth center = nc.getCenter(); 261 EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY()); 262 nc.zoomTo(new EastNorth(mousePosMove.east() + center.east() - mouseCenter.east(), mousePosMove.north() 263 + center.north() - mouseCenter.north())); 264 } else { 265 endMovement(); 266 } 267 } 268 } 269 270 @Override 271 public void destroy() { 272 if (this.contentPane != null) { 273 InputMap inputMap = contentPane.getInputMap(); 274 KeyStroke[] inputKeys = inputMap.keys(); 275 if (inputKeys != null) { 276 for (KeyStroke key : inputKeys) { 277 Object binding = inputMap.get(key); 278 if (binding instanceof String && ((String) binding).startsWith("MapMover.")) { 279 inputMap.remove(key); 280 } 281 } 282 } 283 ActionMap actionMap = contentPane.getActionMap(); 284 Object[] actionsKeys = actionMap.keys(); 285 if (actionsKeys != null) { 286 for (Object key : actionsKeys) { 287 if (key instanceof String && ((String) key).startsWith("MapMover.")) { 288 actionMap.remove(key); 289 } 290 } 291 } 292 } 293 } 294}