zoombox: fix for small mode
[fanfix.git] / ui / ZoomBox.java
1 package be.nikiroo.utils.ui;
2
3 import java.awt.event.ActionEvent;
4 import java.awt.event.ActionListener;
5
6 import javax.swing.BoxLayout;
7 import javax.swing.DefaultComboBoxModel;
8 import javax.swing.Icon;
9 import javax.swing.JButton;
10 import javax.swing.JComboBox;
11 import javax.swing.JLabel;
12
13 /**
14 * A small panel that let you choose a zoom level or an actual zoom value (when
15 * there is enough space to allow that).
16 *
17 * @author niki
18 */
19 public class ZoomBox extends ListenerPanel {
20 private static final long serialVersionUID = 1L;
21
22 /** The event that is fired on zoom change. */
23 public static final String ZOOM_CHANGED = "zoom_changed";
24
25 private enum ZoomLevel {
26 FIT_TO_WIDTH(0, true), //
27 FIT_TO_HEIGHT(0, false), //
28 ACTUAL_SIZE(1, null), //
29 HALF_SIZE(0.5, null), //
30 DOUBLE_SIZE(2, null),//
31 ;
32
33 private final double zoom;
34 private final Boolean snapMode;
35
36 private ZoomLevel(double zoom, Boolean snapMode) {
37 this.zoom = zoom;
38 this.snapMode = snapMode;
39 }
40
41 public double getZoom() {
42 return zoom;
43 }
44
45 public Boolean getSnapToWidth() {
46 return snapMode;
47 }
48
49 /**
50 * Use default values that can be understood by a human.
51 */
52 @Override
53 public String toString() {
54 switch (this) {
55 case FIT_TO_WIDTH:
56 return "Fit to width";
57 case FIT_TO_HEIGHT:
58 return "Fit to height";
59 case ACTUAL_SIZE:
60 return "Actual size";
61 case HALF_SIZE:
62 return "Half size";
63 case DOUBLE_SIZE:
64 return "Double size";
65 }
66 return super.toString();
67 }
68
69 static ZoomLevel[] values(boolean orderedSelection) {
70 if (orderedSelection) {
71 return new ZoomLevel[] { //
72 FIT_TO_WIDTH, //
73 FIT_TO_HEIGHT, //
74 ACTUAL_SIZE, //
75 HALF_SIZE, //
76 DOUBLE_SIZE,//
77 };
78 }
79
80 return values();
81 }
82 }
83
84 private boolean vertical;
85 private boolean small;
86
87 private JButton zoomIn;
88 private JButton zoomOut;
89 private JButton snapWidth;
90 private JButton snapHeight;
91 private JLabel zoomLabel;
92
93 @SuppressWarnings("rawtypes") // JComboBox<?> is not java 1.6 compatible
94 private JComboBox zoombox;
95
96 private double zoom = 1;
97 private Boolean snapMode = true;
98
99 @SuppressWarnings("rawtypes") // JComboBox<?> not compatible java 1.6
100 private DefaultComboBoxModel zoomBoxModel;
101
102 /**
103 * Create a new {@link ZoomBox}.
104 */
105 @SuppressWarnings({ "unchecked", "rawtypes" }) // JComboBox<?> not
106 // compatible java 1.6
107 public ZoomBox() {
108 zoomIn = new JButton();
109 zoomIn.addActionListener(new ActionListener() {
110 @Override
111 public void actionPerformed(ActionEvent e) {
112 zoomIn(1);
113 }
114 });
115
116 zoomBoxModel = new DefaultComboBoxModel(ZoomLevel.values(true));
117 zoombox = new JComboBox(zoomBoxModel);
118 zoombox.setEditable(true);
119 zoombox.addActionListener(new ActionListener() {
120 @Override
121 public void actionPerformed(ActionEvent e) {
122 Object selected = zoomBoxModel.getSelectedItem();
123
124 if (selected == null) {
125 return;
126 }
127
128 if (selected instanceof ZoomLevel) {
129 ZoomLevel selectedZoomLevel = (ZoomLevel) selected;
130 setZoomSnapMode(selectedZoomLevel.getZoom(),
131 selectedZoomLevel.getSnapToWidth());
132 } else {
133 String selectedString = selected.toString();
134 selectedString = selectedString.trim();
135 if (selectedString.endsWith("%")) {
136 selectedString = selectedString
137 .substring(0, selectedString.length() - 1)
138 .trim();
139 }
140
141 try {
142 int pc = Integer.parseInt(selectedString);
143 if (pc <= 0) {
144 throw new NumberFormatException("invalid");
145 }
146
147 setZoomSnapMode(pc / 100.0, null);
148 } catch (NumberFormatException nfe) {
149 }
150 }
151
152 fireActionPerformed(ZOOM_CHANGED);
153 }
154 });
155
156 zoomOut = new JButton();
157 zoomOut.addActionListener(new ActionListener() {
158 @Override
159 public void actionPerformed(ActionEvent e) {
160 zoomOut(1);
161 }
162 });
163
164 snapWidth = new JButton();
165 snapWidth.addActionListener(new ActionListener() {
166 @Override
167 public void actionPerformed(ActionEvent e) {
168 setSnapMode(true);
169 }
170 });
171
172 snapHeight = new JButton();
173 snapHeight.addActionListener(new ActionListener() {
174 @Override
175 public void actionPerformed(ActionEvent e) {
176 setSnapMode(false);
177 }
178 });
179
180 zoomLabel = new JLabel();
181
182 setIcons(null, null, null, null);
183 setOrientation(vertical);
184 }
185
186 /**
187 * The zoom level.
188 * <p>
189 * It usually returns 1 (default value), the value you passed yourself or 1
190 * (a snap to width or snap to height was asked by the user).
191 * <p>
192 * Will cause a fire event if needed.
193 *
194 * @param zoom
195 * the zoom level
196 */
197 public void setZoom(double zoom) {
198 if (this.zoom != zoom) {
199 doSetZoom(zoom);
200 fireActionPerformed(ZOOM_CHANGED);
201 }
202 }
203
204 /**
205 * The snap mode (NULL means no snap mode, TRUE for snap to width, FALSE for
206 * snap to height).
207 * <p>
208 * Will cause a fire event if needed.
209 *
210 * @param snapToWidth
211 * the snap mode
212 */
213 public void setSnapMode(Boolean snapToWidth) {
214 if (this.snapMode != snapToWidth) {
215 doSetSnapMode(snapToWidth);
216 fireActionPerformed(ZOOM_CHANGED);
217 }
218 }
219
220 /**
221 * Set both {@link ZoomBox#setZoom(double)} and
222 * {@link ZoomBox#setSnapMode(Boolean)} but fire only one change event.
223 * <p>
224 * Will cause a fire event if needed.
225 *
226 * @param zoom
227 * the zoom level
228 * @param snapMode
229 * the snap mode
230 */
231 public void setZoomSnapMode(double zoom, Boolean snapMode) {
232 if (this.zoom != zoom || this.snapMode != snapMode) {
233 doSetZoom(zoom);
234 doSetSnapMode(snapMode);
235 fireActionPerformed(ZOOM_CHANGED);
236 }
237 }
238
239 /**
240 * The zoom level.
241 * <p>
242 * It usually returns 1 (default value), the value you passed yourself or 0
243 * (a snap to width or snap to height was asked by the user).
244 *
245 * @return the zoom level
246 */
247 public double getZoom() {
248 return zoom;
249 }
250
251 /**
252 * The snap mode (NULL means no snap mode, TRUE for snap to width, FALSE for
253 * snap to height).
254 *
255 * @return the snap mode
256 */
257 public Boolean getSnapMode() {
258 return snapMode;
259 }
260
261 /**
262 * Zoom in, by a certain amount in "steps".
263 * <p>
264 * Note that zoomIn(-1) is the same as zoomOut(1).
265 *
266 * @param steps
267 * the number of zoom steps to make, can be negative
268 */
269 public void zoomIn(int steps) {
270 // TODO: redo zoomIn/zoomOut correctly
271 if (steps < 0) {
272 zoomOut(-steps);
273 return;
274 }
275
276 double newZoom = zoom;
277 for (int i = 0; i < steps; i++) {
278 newZoom = newZoom + (newZoom < 0.1 ? 0.01 : 0.1);
279 if (newZoom > 0.1) {
280 newZoom = Math.round(newZoom * 10.0) / 10.0; // snap to 10%
281 } else {
282 newZoom = Math.round(newZoom * 100.0) / 100.0; // snap to 1%
283 }
284 }
285
286 setZoomSnapMode(newZoom, null);
287 fireActionPerformed(ZOOM_CHANGED);
288 }
289
290 /**
291 * Zoom out, by a certain amount in "steps".
292 * <p>
293 * Note that zoomOut(-1) is the same as zoomIn(1).
294 *
295 * @param steps
296 * the number of zoom steps to make, can be negative
297 */
298 public void zoomOut(int steps) {
299 if (steps < 0) {
300 zoomIn(-steps);
301 return;
302 }
303
304 double newZoom = zoom;
305 for (int i = 0; i < steps; i++) {
306 newZoom = newZoom - (newZoom > 0.19 ? 0.1 : 0.01);
307 if (newZoom < 0.01) {
308 newZoom = 0.01;
309 break;
310 }
311
312 if (newZoom > 0.1) {
313 newZoom = Math.round(newZoom * 10.0) / 10.0; // snap to 10%
314 } else {
315 newZoom = Math.round(newZoom * 100.0) / 100.0; // snap to 1%
316 }
317 }
318
319 setZoomSnapMode(newZoom, null);
320 fireActionPerformed(ZOOM_CHANGED);
321 }
322
323 /**
324 * Set icons for the buttons instead of square brackets.
325 * <p>
326 * Any NULL value will make the button use square brackets again.
327 *
328 * @param zoomIn
329 * the icon of the button "go to first page"
330 * @param zoomOut
331 * the icon of the button "go to previous page"
332 * @param snapWidth
333 * the icon of the button "go to next page"
334 * @param snapHeight
335 * the icon of the button "go to last page"
336 */
337 public void setIcons(Icon zoomIn, Icon zoomOut, Icon snapWidth,
338 Icon snapHeight) {
339 this.zoomIn.setIcon(zoomIn);
340 this.zoomIn.setText(zoomIn == null ? "+" : "");
341 this.zoomOut.setIcon(zoomOut);
342 this.zoomOut.setText(zoomOut == null ? "-" : "");
343 this.snapWidth.setIcon(snapWidth);
344 this.snapWidth.setText(snapWidth == null ? "W" : "");
345 this.snapHeight.setIcon(snapHeight);
346 this.snapHeight.setText(snapHeight == null ? "H" : "");
347 }
348
349 /**
350 * A smaller {@link ZoomBox} that uses buttons instead of a big combo box
351 * for the zoom modes.
352 * <p>
353 * Always small in vertical orientation.
354 *
355 * @return TRUE if it is small
356 */
357 public boolean getSmall() {
358 return small;
359 }
360
361 /**
362 * A smaller {@link ZoomBox} that uses buttons instead of a big combo box
363 * for the zoom modes.
364 * <p>
365 * Always small in vertical orientation.
366 *
367 * @param small
368 * TRUE to set it small
369 *
370 * @return TRUE if it changed something
371 */
372 public boolean setSmall(boolean small) {
373 return setUi(small, vertical);
374 }
375
376 /**
377 * The general orientation of the component.
378 *
379 * @return TRUE for vertical orientation, FALSE for horisontal orientation
380 */
381 public boolean getOrientation() {
382 return vertical;
383 }
384
385 /**
386 * The general orientation of the component.
387 *
388 * @param vertical
389 * TRUE for vertical orientation, FALSE for horisontal
390 * orientation
391 *
392 * @return TRUE if it changed something
393 */
394 public boolean setOrientation(boolean vertical) {
395 return setUi(small, vertical);
396 }
397
398 /**
399 * Set the zoom level, no fire event.
400 * <p>
401 * It usually returns 1 (default value), the value you passed yourself or 0
402 * (a snap to width or snap to height was asked by the user).
403 *
404 * @param zoom
405 * the zoom level
406 */
407 private void doSetZoom(double zoom) {
408 if (zoom > 0) {
409 String zoomStr = Integer.toString((int) Math.round(zoom * 100))
410 + " %";
411 zoomLabel.setText(zoomStr);
412 if (snapMode == null) {
413 zoomBoxModel.setSelectedItem(zoomStr);
414 }
415 }
416
417 this.zoom = zoom;
418 }
419
420 /**
421 * Set the snap mode, no fire event.
422 *
423 * @param snapToWidth
424 * the snap mode
425 */
426 private void doSetSnapMode(Boolean snapToWidth) {
427 if (snapToWidth == null) {
428 String zoomStr = Integer.toString((int) Math.round(zoom * 100))
429 + " %";
430 if (zoom > 0) {
431 zoomBoxModel.setSelectedItem(zoomStr);
432 }
433 } else {
434 for (ZoomLevel level : ZoomLevel.values()) {
435 if (level.getSnapToWidth() == snapToWidth) {
436 zoomBoxModel.setSelectedItem(level);
437 }
438 }
439 }
440
441 this.snapMode = snapToWidth;
442 }
443
444 private boolean setUi(boolean small, boolean vertical) {
445 if (getWidth() == 0 || this.small != small
446 || this.vertical != vertical) {
447 this.small = small;
448 this.vertical = vertical;
449
450 BoxLayout layout = new BoxLayout(this,
451 vertical ? BoxLayout.Y_AXIS : BoxLayout.X_AXIS);
452 this.removeAll();
453 setLayout(layout);
454
455 if (vertical || small) {
456 this.add(zoomIn);
457 this.add(snapWidth);
458 this.add(snapHeight);
459 this.add(zoomOut);
460 this.add(zoomLabel);
461 } else {
462 this.add(zoomIn);
463 this.add(zoombox);
464 this.add(zoomOut);
465 }
466
467 this.revalidate();
468 this.repaint();
469
470 return true;
471 }
472
473 return false;
474 }
475 }