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