001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import java.awt.BorderLayout;
005import java.awt.Dimension;
006import java.awt.Point;
007import java.awt.Rectangle;
008import java.awt.event.ComponentAdapter;
009import java.awt.event.ComponentEvent;
010import java.awt.event.MouseAdapter;
011import java.awt.event.MouseEvent;
012import java.util.ArrayList;
013import java.util.List;
014
015import javax.swing.JButton;
016import javax.swing.JComponent;
017import javax.swing.JPanel;
018import javax.swing.JViewport;
019import javax.swing.Timer;
020
021import org.openstreetmap.josm.tools.ImageProvider;
022
023/**
024 * A viewport with UP and DOWN arrow buttons, so that the user can make the
025 * content scroll.
026 *
027 * This should be used for long, vertical toolbars.
028 */
029public class ScrollViewport extends JPanel {
030
031    private static final int NO_SCROLL = 0;
032
033    /**
034     * Direction flag for upwards
035     */
036    public static final int UP_DIRECTION = 1;
037    /**
038     * Direction flag for downwards
039     */
040    public static final int DOWN_DIRECTION = 2;
041    /**
042     * Direction flag for left
043     */
044    public static final int LEFT_DIRECTION = 4;
045    /**
046     * Direction flag for right
047     */
048    public static final int RIGHT_DIRECTION = 8;
049    /**
050     * Allow vertical scrolling
051     */
052    public static final int VERTICAL_DIRECTION = UP_DIRECTION | DOWN_DIRECTION;
053
054    /**
055     * Allow horizontal scrolling
056     */
057    public static final int HORIZONTAL_DIRECTION = LEFT_DIRECTION | RIGHT_DIRECTION;
058
059    /**
060     * Allow scrolling in both directions
061     */
062    public static final int ALL_DIRECTION = HORIZONTAL_DIRECTION | VERTICAL_DIRECTION;
063
064    private class ScrollViewPortMouseListener extends MouseAdapter {
065        private final int direction;
066
067        ScrollViewPortMouseListener(int direction) {
068            this.direction = direction;
069        }
070
071        @Override
072        public void mouseExited(MouseEvent e) {
073            mouseReleased(e);
074        }
075
076        @Override
077        public void mouseReleased(MouseEvent e) {
078            ScrollViewport.this.scrollDirection = NO_SCROLL;
079            timer.stop();
080        }
081
082        @Override
083        public void mousePressed(MouseEvent e) {
084            ScrollViewport.this.scrollDirection = direction;
085            scroll();
086            timer.restart();
087        }
088    }
089
090    private final JViewport vp = new JViewport();
091    private JComponent component;
092
093    private final List<JButton> buttons = new ArrayList<>();
094
095    private final Timer timer = new Timer(100, evt -> scroll());
096
097    private int scrollDirection = NO_SCROLL;
098
099    private final int allowedScrollDirections;
100
101    private final transient ComponentAdapter refreshButtonsOnResize = new ComponentAdapter() {
102        @Override
103        public void componentResized(ComponentEvent e) {
104            showOrHideButtons();
105        }
106    };
107
108    /**
109     * Create a new scroll viewport
110     * @param c The component to display as content.
111     * @param direction The direction to scroll.
112     *        Should be one of {@link #VERTICAL_DIRECTION}, {@link #HORIZONTAL_DIRECTION}, {@link #ALL_DIRECTION}
113     */
114    public ScrollViewport(JComponent c, int direction) {
115        this(direction);
116        add(c);
117    }
118
119    /**
120     * Create a new scroll viewport
121     * @param direction The direction to scroll.
122     *        Should be one of {@link #VERTICAL_DIRECTION}, {@link #HORIZONTAL_DIRECTION}, {@link #ALL_DIRECTION}
123     */
124    public ScrollViewport(int direction) {
125        super(new BorderLayout());
126        this.allowedScrollDirections = direction;
127
128        // UP
129        if ((direction & UP_DIRECTION) != 0) {
130            addScrollButton(UP_DIRECTION, /* ICON */ "svpUp", BorderLayout.NORTH);
131        }
132
133        // DOWN
134        if ((direction & DOWN_DIRECTION) != 0) {
135            addScrollButton(DOWN_DIRECTION, /* ICON */ "svpDown", BorderLayout.SOUTH);
136        }
137
138        // LEFT
139        if ((direction & LEFT_DIRECTION) != 0) {
140            addScrollButton(LEFT_DIRECTION, /* ICON */ "svpLeft", BorderLayout.WEST);
141        }
142
143        // RIGHT
144        if ((direction & RIGHT_DIRECTION) != 0) {
145            addScrollButton(RIGHT_DIRECTION, /* ICON */ "svpRight", BorderLayout.EAST);
146        }
147
148        add(vp, BorderLayout.CENTER);
149
150        this.addComponentListener(refreshButtonsOnResize);
151
152        showOrHideButtons();
153
154        if ((direction & VERTICAL_DIRECTION) != 0) {
155            addMouseWheelListener(e -> scroll(0, e.getUnitsToScroll() * 5));
156        } else if ((direction & HORIZONTAL_DIRECTION) != 0) {
157            addMouseWheelListener(e -> scroll(e.getUnitsToScroll() * 5, 0));
158        }
159
160        timer.setRepeats(true);
161        timer.setInitialDelay(400);
162    }
163
164    private void addScrollButton(int direction, String icon, String borderLayoutPosition) {
165        JButton button = new JButton();
166        button.addMouseListener(new ScrollViewPortMouseListener(direction));
167        button.setPreferredSize(new Dimension(10, 10));
168        button.setIcon(ImageProvider.get(icon));
169        add(button, borderLayoutPosition);
170        buttons.add(button);
171    }
172
173    /**
174     * Scrolls in the currently selected scroll direction.
175     */
176    public synchronized void scroll() {
177        int direction = scrollDirection;
178
179        if (component == null || direction == NO_SCROLL)
180            return;
181
182        Rectangle viewRect = vp.getViewRect();
183
184        int deltaX = 0;
185        int deltaY = 0;
186
187        if (direction < LEFT_DIRECTION) {
188            deltaY = viewRect.height * 2 / 7;
189        } else {
190            deltaX = viewRect.width * 2 / 7;
191        }
192
193        switch (direction) {
194        case UP_DIRECTION :
195            deltaY *= -1;
196            break;
197        case LEFT_DIRECTION :
198            deltaX *= -1;
199            break;
200        default: // Do nothing
201        }
202
203        scroll(deltaX, deltaY);
204    }
205
206    /**
207     * Scrolls by the given offset
208     * @param deltaX offset x
209     * @param deltaY offset y
210     */
211    public synchronized void scroll(int deltaX, int deltaY) {
212        if (component == null)
213            return;
214        Dimension compSize = component.getSize();
215        Rectangle viewRect = vp.getViewRect();
216
217        int newX = viewRect.x + deltaX;
218        int newY = viewRect.y + deltaY;
219
220        if (newY < 0) {
221            newY = 0;
222        }
223        if (newY > compSize.height - viewRect.height) {
224            newY = compSize.height - viewRect.height;
225        }
226        if (newX < 0) {
227            newX = 0;
228        }
229        if (newX > compSize.width - viewRect.width) {
230            newX = compSize.width - viewRect.width;
231        }
232
233        vp.setViewPosition(new Point(newX, newY));
234    }
235
236    /**
237     * Update the visibility of the buttons
238     * Only show them if the Viewport is too small for the content.
239     */
240    public void showOrHideButtons() {
241        boolean needButtons = false;
242        if ((allowedScrollDirections & VERTICAL_DIRECTION) != 0) {
243            needButtons |= getViewSize().height > getViewRect().height;
244        }
245        if ((allowedScrollDirections & HORIZONTAL_DIRECTION) != 0) {
246            needButtons |= getViewSize().width > getViewRect().width;
247        }
248        for (JButton b : buttons) {
249            b.setVisible(needButtons);
250        }
251    }
252
253    /**
254     * Gets the current visible part of the view
255     * @return The current view rect
256     */
257    public Rectangle getViewRect() {
258        return vp.getViewRect();
259    }
260
261    /**
262     * Gets the size of the view
263     * @return The size
264     */
265    public Dimension getViewSize() {
266        return vp.getViewSize();
267    }
268
269    /**
270     * Gets the position (offset) of the view area
271     * @return The offset
272     */
273    public Point getViewPosition() {
274        return vp.getViewPosition();
275    }
276
277    @Override
278    public Dimension getPreferredSize() {
279        if (component == null) {
280            return vp.getPreferredSize();
281        } else {
282            return component.getPreferredSize();
283        }
284    }
285
286    @Override
287    public Dimension getMinimumSize() {
288        if (component == null) {
289            return vp.getMinimumSize();
290        } else {
291            Dimension minSize = component.getMinimumSize();
292            if ((allowedScrollDirections & HORIZONTAL_DIRECTION) != 0) {
293                minSize = new Dimension(20, minSize.height);
294            }
295            if ((allowedScrollDirections & VERTICAL_DIRECTION) != 0) {
296                minSize = new Dimension(minSize.width, 20);
297            }
298            return minSize;
299        }
300    }
301
302    /**
303     * Sets the component to be used as content.
304     * @param c The component
305     */
306    public void add(JComponent c) {
307        vp.removeAll();
308        if (this.component != null) {
309            this.component.removeComponentListener(refreshButtonsOnResize);
310        }
311        this.component = c;
312        c.addComponentListener(refreshButtonsOnResize);
313        vp.add(c);
314    }
315}