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