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