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; 014 015import javax.swing.AbstractAction; 016import javax.swing.JPanel; 017 018import org.openstreetmap.gui.jmapviewer.JMapViewer; 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.actions.mapmode.SelectAction; 021import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 022import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 023import org.openstreetmap.josm.data.coor.EastNorth; 024import org.openstreetmap.josm.data.preferences.BooleanProperty; 025import org.openstreetmap.josm.tools.Destroyable; 026import org.openstreetmap.josm.tools.Pair; 027import org.openstreetmap.josm.tools.Shortcut; 028 029/** 030 * Enables moving of the map by holding down the right mouse button and drag 031 * the mouse. Also, enables zooming by the mouse wheel. 032 * 033 * @author imi 034 */ 035public class MapMover extends MouseAdapter implements Destroyable { 036 037 public static final BooleanProperty PROP_ZOOM_REVERSE_WHEEL = new BooleanProperty("zoom.reverse-wheel", false); 038 039 private static final JMapViewerUpdater jMapViewerUpdater = new JMapViewerUpdater(); 040 041 private static class JMapViewerUpdater implements PreferenceChangedListener { 042 043 JMapViewerUpdater() { 044 Main.pref.addPreferenceChangeListener(this); 045 updateJMapViewer(); 046 } 047 048 @Override 049 public void preferenceChanged(PreferenceChangeEvent e) { 050 if (MapMover.PROP_ZOOM_REVERSE_WHEEL.getKey().equals(e.getKey())) { 051 updateJMapViewer(); 052 } 053 } 054 055 private static void updateJMapViewer() { 056 JMapViewer.zoomReverseWheel = MapMover.PROP_ZOOM_REVERSE_WHEEL.get(); 057 } 058 } 059 060 private final class ZoomerAction extends AbstractAction { 061 private final String action; 062 063 ZoomerAction(String action) { 064 this(action, "MapMover.Zoomer." + action); 065 } 066 067 ZoomerAction(String action, String name) { 068 this.action = action; 069 putValue(NAME, name); 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 default: // Do nothing 098 } 099 } 100 } 101 } 102 103 /** 104 * The point in the map that was the under the mouse point 105 * when moving around started. 106 */ 107 private EastNorth mousePosMove; 108 /** 109 * The map to move around. 110 */ 111 private final NavigatableComponent nc; 112 113 private boolean movementInPlace; 114 115 private final ArrayList<Pair<ZoomerAction, Shortcut>> registeredShortcuts = new ArrayList<>(); 116 117 /** 118 * Constructs a new {@code MapMover}. 119 * @param navComp the navigatable component 120 * @param contentPane Ignored. The main action map is used. 121 */ 122 public MapMover(NavigatableComponent navComp, JPanel contentPane) { 123 this.nc = navComp; 124 nc.addMouseListener(this); 125 nc.addMouseMotionListener(this); 126 nc.addMouseWheelListener(this); 127 128 registerActionShortcut(new ZoomerAction("right"), 129 Shortcut.registerShortcut("system:movefocusright", tr("Map: {0}", tr("Move right")), KeyEvent.VK_RIGHT, Shortcut.CTRL)); 130 131 registerActionShortcut(new ZoomerAction("left"), 132 Shortcut.registerShortcut("system:movefocusleft", tr("Map: {0}", tr("Move left")), KeyEvent.VK_LEFT, Shortcut.CTRL)); 133 134 registerActionShortcut(new ZoomerAction("up"), 135 Shortcut.registerShortcut("system:movefocusup", tr("Map: {0}", tr("Move up")), KeyEvent.VK_UP, Shortcut.CTRL)); 136 registerActionShortcut(new ZoomerAction("down"), 137 Shortcut.registerShortcut("system:movefocusdown", tr("Map: {0}", tr("Move down")), KeyEvent.VK_DOWN, Shortcut.CTRL)); 138 139 // see #10592 - Disable these alternate shortcuts on OS X because of conflict with system shortcut 140 if (!Main.isPlatformOsx()) { 141 registerActionShortcut(new ZoomerAction(",", "MapMover.Zoomer.in"), 142 Shortcut.registerShortcut("view:zoominalternate", tr("Map: {0}", tr("Zoom in")), KeyEvent.VK_COMMA, Shortcut.CTRL)); 143 144 registerActionShortcut(new ZoomerAction(".", "MapMover.Zoomer.out"), 145 Shortcut.registerShortcut("view:zoomoutalternate", tr("Map: {0}", tr("Zoom out")), KeyEvent.VK_PERIOD, Shortcut.CTRL)); 146 } 147 } 148 149 private void registerActionShortcut(ZoomerAction action, Shortcut shortcut) { 150 Main.registerActionShortcut(action, shortcut); 151 registeredShortcuts.add(new Pair<>(action, shortcut)); 152 } 153 154 /** 155 * If the right (and only the right) mouse button is pressed, move the map. 156 */ 157 @Override 158 public void mouseDragged(MouseEvent e) { 159 int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK; 160 int macMouseMask = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; 161 boolean stdMovement = (e.getModifiersEx() & (MouseEvent.BUTTON3_DOWN_MASK | offMask)) == MouseEvent.BUTTON3_DOWN_MASK; 162 boolean macMovement = Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask; 163 boolean allowedMode = !Main.map.mapModeSelect.equals(Main.map.mapMode) 164 || SelectAction.Mode.SELECT.equals(Main.map.mapModeSelect.getMode()); 165 if (stdMovement || (macMovement && allowedMode)) { 166 if (mousePosMove == null) 167 startMovement(e); 168 EastNorth center = nc.getCenter(); 169 EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY()); 170 nc.zoomTo(new EastNorth( 171 mousePosMove.east() + center.east() - mouseCenter.east(), 172 mousePosMove.north() + center.north() - mouseCenter.north())); 173 } else { 174 endMovement(); 175 } 176 } 177 178 /** 179 * Start the movement, if it was the 3rd button (right button). 180 */ 181 @Override 182 public void mousePressed(MouseEvent e) { 183 int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK; 184 int macMouseMask = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; 185 if ((e.getButton() == MouseEvent.BUTTON3 && (e.getModifiersEx() & offMask) == 0) || 186 (Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask)) { 187 startMovement(e); 188 } 189 } 190 191 /** 192 * Change the cursor back to it's pre-move cursor. 193 */ 194 @Override 195 public void mouseReleased(MouseEvent e) { 196 if (e.getButton() == MouseEvent.BUTTON3 || (Main.isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1)) { 197 endMovement(); 198 } 199 } 200 201 /** 202 * Start movement by setting a new cursor and remember the current mouse 203 * position. 204 * @param e The mouse event that leat to the movement from. 205 */ 206 private void startMovement(MouseEvent e) { 207 if (movementInPlace) 208 return; 209 movementInPlace = true; 210 mousePosMove = nc.getEastNorth(e.getX(), e.getY()); 211 nc.setNewCursor(Cursor.MOVE_CURSOR, this); 212 } 213 214 /** 215 * End the movement. Setting back the cursor and clear the movement variables 216 */ 217 private void endMovement() { 218 if (!movementInPlace) 219 return; 220 movementInPlace = false; 221 nc.resetCursor(this); 222 mousePosMove = null; 223 } 224 225 /** 226 * Zoom the map by 1/5th of current zoom per wheel-delta. 227 * @param e The wheel event. 228 */ 229 @Override 230 public void mouseWheelMoved(MouseWheelEvent e) { 231 int rotation = PROP_ZOOM_REVERSE_WHEEL.get() ? -e.getWheelRotation() : e.getWheelRotation(); 232 nc.zoomManyTimes(e.getX(), e.getY(), rotation); 233 } 234 235 /** 236 * Emulates dragging on Mac OSX. 237 */ 238 @Override 239 public void mouseMoved(MouseEvent e) { 240 if (!movementInPlace) 241 return; 242 // Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired. 243 // Is only the selected mouse button pressed? 244 if (Main.isPlatformOsx()) { 245 if (e.getModifiersEx() == MouseEvent.CTRL_DOWN_MASK) { 246 if (mousePosMove == null) { 247 startMovement(e); 248 } 249 EastNorth center = nc.getCenter(); 250 EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY()); 251 nc.zoomTo(new EastNorth(mousePosMove.east() + center.east() - mouseCenter.east(), mousePosMove.north() 252 + center.north() - mouseCenter.north())); 253 } else { 254 endMovement(); 255 } 256 } 257 } 258 259 @Override 260 public void destroy() { 261 for (Pair<ZoomerAction, Shortcut> shortcut : registeredShortcuts) { 262 Main.unregisterActionShortcut(shortcut.a, shortcut.b); 263 } 264 } 265}