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