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