// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Cursor; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.util.ArrayList; import java.util.Optional; import javax.swing.AbstractAction; import org.openstreetmap.gui.jmapviewer.JMapViewer; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.mapmode.SelectAction; import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.preferences.BooleanProperty; import org.openstreetmap.josm.tools.Destroyable; import org.openstreetmap.josm.tools.Pair; import org.openstreetmap.josm.tools.Shortcut; /** * Enables moving of the map by holding down the right mouse button and drag * the mouse. Also, enables zooming by the mouse wheel. * * @author imi */ public class MapMover extends MouseAdapter implements Destroyable { public static final BooleanProperty PROP_ZOOM_REVERSE_WHEEL = new BooleanProperty("zoom.reverse-wheel", false); static { new JMapViewerUpdater(); } private static class JMapViewerUpdater implements PreferenceChangedListener { JMapViewerUpdater() { Main.pref.addPreferenceChangeListener(this); updateJMapViewer(); } @Override public void preferenceChanged(PreferenceChangeEvent e) { if (MapMover.PROP_ZOOM_REVERSE_WHEEL.getKey().equals(e.getKey())) { updateJMapViewer(); } } private static void updateJMapViewer() { JMapViewer.zoomReverseWheel = MapMover.PROP_ZOOM_REVERSE_WHEEL.get(); } } private final class ZoomerAction extends AbstractAction { private final String action; ZoomerAction(String action) { this(action, "MapMover.Zoomer." + action); } ZoomerAction(String action, String name) { this.action = action; putValue(NAME, name); } @Override public void actionPerformed(ActionEvent e) { if (".".equals(action) || ",".equals(action)) { Point mouse = Optional.ofNullable(nc.getMousePosition()).orElseGet( () -> new Point((int) nc.getBounds().getCenterX(), (int) nc.getBounds().getCenterY())); mouseWheelMoved(new MouseWheelEvent(nc, e.getID(), e.getWhen(), e.getModifiers(), mouse.x, mouse.y, 0, false, MouseWheelEvent.WHEEL_UNIT_SCROLL, 1, ",".equals(action) ? -1 : 1)); } else { EastNorth center = nc.getCenter(); EastNorth newcenter = nc.getEastNorth(nc.getWidth()/2+nc.getWidth()/5, nc.getHeight()/2+nc.getHeight()/5); switch(action) { case "left": nc.zoomTo(new EastNorth(2*center.east()-newcenter.east(), center.north())); break; case "right": nc.zoomTo(new EastNorth(newcenter.east(), center.north())); break; case "up": nc.zoomTo(new EastNorth(center.east(), 2*center.north()-newcenter.north())); break; case "down": nc.zoomTo(new EastNorth(center.east(), newcenter.north())); break; default: // Do nothing } } } } /** * The point in the map that was the under the mouse point * when moving around started. */ private EastNorth mousePosMove; /** * The map to move around. */ private final NavigatableComponent nc; private boolean movementInPlace; private final ArrayList> registeredShortcuts = new ArrayList<>(); /** * Constructs a new {@code MapMover}. * @param navComp the navigatable component * @since 11713 */ public MapMover(NavigatableComponent navComp) { this.nc = navComp; nc.addMouseListener(this); nc.addMouseMotionListener(this); nc.addMouseWheelListener(this); registerActionShortcut(new ZoomerAction("right"), Shortcut.registerShortcut("system:movefocusright", tr("Map: {0}", tr("Move right")), KeyEvent.VK_RIGHT, Shortcut.CTRL)); registerActionShortcut(new ZoomerAction("left"), Shortcut.registerShortcut("system:movefocusleft", tr("Map: {0}", tr("Move left")), KeyEvent.VK_LEFT, Shortcut.CTRL)); registerActionShortcut(new ZoomerAction("up"), Shortcut.registerShortcut("system:movefocusup", tr("Map: {0}", tr("Move up")), KeyEvent.VK_UP, Shortcut.CTRL)); registerActionShortcut(new ZoomerAction("down"), Shortcut.registerShortcut("system:movefocusdown", tr("Map: {0}", tr("Move down")), KeyEvent.VK_DOWN, Shortcut.CTRL)); // see #10592 - Disable these alternate shortcuts on OS X because of conflict with system shortcut if (!Main.isPlatformOsx()) { registerActionShortcut(new ZoomerAction(",", "MapMover.Zoomer.in"), Shortcut.registerShortcut("view:zoominalternate", tr("Map: {0}", tr("Zoom in")), KeyEvent.VK_COMMA, Shortcut.CTRL)); registerActionShortcut(new ZoomerAction(".", "MapMover.Zoomer.out"), Shortcut.registerShortcut("view:zoomoutalternate", tr("Map: {0}", tr("Zoom out")), KeyEvent.VK_PERIOD, Shortcut.CTRL)); } } private void registerActionShortcut(ZoomerAction action, Shortcut shortcut) { Main.registerActionShortcut(action, shortcut); registeredShortcuts.add(new Pair<>(action, shortcut)); } /** * If the right (and only the right) mouse button is pressed, move the map. */ @Override public void mouseDragged(MouseEvent e) { int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK; int macMouseMask = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; boolean stdMovement = (e.getModifiersEx() & (MouseEvent.BUTTON3_DOWN_MASK | offMask)) == MouseEvent.BUTTON3_DOWN_MASK; boolean macMovement = Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask; boolean allowedMode = !Main.map.mapModeSelect.equals(Main.map.mapMode) || SelectAction.Mode.SELECT.equals(Main.map.mapModeSelect.getMode()); if (stdMovement || (macMovement && allowedMode)) { if (mousePosMove == null) startMovement(e); EastNorth center = nc.getCenter(); EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY()); nc.zoomTo(new EastNorth( mousePosMove.east() + center.east() - mouseCenter.east(), mousePosMove.north() + center.north() - mouseCenter.north())); } else { endMovement(); } } /** * Start the movement, if it was the 3rd button (right button). */ @Override public void mousePressed(MouseEvent e) { int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK; int macMouseMask = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; if ((e.getButton() == MouseEvent.BUTTON3 && (e.getModifiersEx() & offMask) == 0) || (Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask)) { startMovement(e); } } /** * Change the cursor back to it's pre-move cursor. */ @Override public void mouseReleased(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON3 || (Main.isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1)) { endMovement(); } } /** * Start movement by setting a new cursor and remember the current mouse * position. * @param e The mouse event that leat to the movement from. */ private void startMovement(MouseEvent e) { if (movementInPlace) return; movementInPlace = true; mousePosMove = nc.getEastNorth(e.getX(), e.getY()); nc.setNewCursor(Cursor.MOVE_CURSOR, this); } /** * End the movement. Setting back the cursor and clear the movement variables */ private void endMovement() { if (!movementInPlace) return; movementInPlace = false; nc.resetCursor(this); mousePosMove = null; } /** * Zoom the map by 1/5th of current zoom per wheel-delta. * @param e The wheel event. */ @Override public void mouseWheelMoved(MouseWheelEvent e) { int rotation = PROP_ZOOM_REVERSE_WHEEL.get() ? -e.getWheelRotation() : e.getWheelRotation(); nc.zoomManyTimes(e.getX(), e.getY(), rotation); } /** * Emulates dragging on Mac OSX. */ @Override public void mouseMoved(MouseEvent e) { if (!movementInPlace) return; // Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired. // Is only the selected mouse button pressed? if (Main.isPlatformOsx()) { if (e.getModifiersEx() == MouseEvent.CTRL_DOWN_MASK) { if (mousePosMove == null) { startMovement(e); } EastNorth center = nc.getCenter(); EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY()); nc.zoomTo(new EastNorth(mousePosMove.east() + center.east() - mouseCenter.east(), mousePosMove.north() + center.north() - mouseCenter.north())); } else { endMovement(); } } } @Override public void destroy() { for (Pair shortcut : registeredShortcuts) { Main.unregisterActionShortcut(shortcut.a, shortcut.b); } } }