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