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}