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