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