ConfigItem add icon for "+"
[nikiroo-utils.git] / src / be / nikiroo / utils / ui / ConfigItem.java
1 package be.nikiroo.utils.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Cursor;
5 import java.awt.Dimension;
6 import java.awt.event.ActionEvent;
7 import java.awt.event.ActionListener;
8 import java.awt.event.MouseAdapter;
9 import java.awt.event.MouseEvent;
10 import java.awt.image.BufferedImage;
11 import java.io.IOException;
12 import java.util.ArrayList;
13 import java.util.List;
14
15 import javax.swing.BoxLayout;
16 import javax.swing.ImageIcon;
17 import javax.swing.JButton;
18 import javax.swing.JComponent;
19 import javax.swing.JLabel;
20 import javax.swing.JOptionPane;
21 import javax.swing.JPanel;
22 import javax.swing.JTextField;
23
24 import be.nikiroo.utils.Image;
25 import be.nikiroo.utils.StringUtils;
26 import be.nikiroo.utils.StringUtils.Alignment;
27 import be.nikiroo.utils.resources.Bundle;
28 import be.nikiroo.utils.resources.MetaInfo;
29
30 /**
31 * A graphical item that reflect a configuration option from the given
32 * {@link Bundle}.
33 * <p>
34 * This graphical item can be edited, and the result will be saved back into the
35 * linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should
36 * you wish to, of course.
37 *
38 * @author niki
39 *
40 * @param <E>
41 * the type of {@link Bundle} to edit
42 */
43 public class ConfigItem<E extends Enum<E>> extends JPanel {
44 private static final long serialVersionUID = 1L;
45
46 private static int minimumHeight = -1;
47
48 /** A small 16x16 "?" blue in PNG, base64 encoded. */
49 private static String infoImage64 = //
50 ""
51 + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
52 + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wURFRg6IrtcdgAAATdJREFUOMvtkj8sQ1EUxr9z/71G"
53 + "m1RDogYxq7WDDYMYTSajSG4n6YRYzSaSLibWbiaDIGwdiLIYDFKDNJEgKu969xi8UNHy7H7LPcN3"
54 + "v/Odcy+hG9oOIeIcBCJS9MAvlZtOMtHxsrFrJHGqe0RVGnHAHpcIbPlng8BS3HmKBJYzabGUzcrJ"
55 + "XK+ckIrqANYR2JEv2nYDEVck0WKGfHzyq82Go+btxoX3XAcAIqTj8wPqOH6mtMeM4bGCLhyfhTMA"
56 + "qlLhKHqujCfaweCAmV0p50dPzsNpEKpK01V/n55HIvTnfDC2odKlfeYadZN/T+AqDACUsnkhqaU1"
57 + "LRIVuX1x7ciuSWQxVIrunONrfq3dI6oh+T94Z8453vEem/HTqT8ZpFJ0qDXtGkPbAGAMeSRngQCA"
58 + "eUvgn195AwlZWyvjtQdhAAAAAElFTkSuQmCC";
59
60 // A small 16x16 "+" image with colours
61 private static String addImage64 = //
62 ""
63 + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
64 + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeES0QBFvvnAAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
65 + "YXRlZCB3aXRoIEdJTVBkLmUHAAACH0lEQVQ4y42Tz0sVURTHP+fMmC7CQMpH1EjgIimCsEVBEIg/"
66 + "qIbcBAW2Uai1m/oH2rlJXLQpeRJt2gQhTO0iTTKC1I2JBf5gKCJCRPvhPOed22LmvV70Fn7hwr3c"
67 + "+z3ne+73HCFHEClxaASRHgduA91AW369BkwDI3Foy0GkEofmACQnSxyaCyItAkMClMzYdeCAJgVP"
68 + "tJJrPA7tVoUjNZlngXMAiRmXClfoK/Tjq09x7T6LW+8RxOVJ5+LQzgSRojm5WCEDlMrQVbjIQNtN"
69 + "rh0d5FTzaTLBmWKgM4h0Ig4NzWseohYCJUuqx123Sx0MBpF2+MAdyWUnlqX4lf4bIDHjR+rwJJPR"
70 + "qNCgCjDsA10lM/oKIRcO9lByCYklnG/pqQa4euQ6J5tPoKI0yD6ef33Ku40Z80R7CSJNWyZxT+Ki"
71 + "2ytGP911hyZxQaRp1RtPPPYKD4+sGJwPrDUp7Q9Xxnj9fYrUUnaszEAwQHfrZQAerT/g7cYMiuCp"
72 + "z8LmLI0qBqz6wLQn2v5he57FrXkAtlPH2ZZOuskCzG2+4dnnx3iSuSgCKqLAlAIjmXPiVIRsgYjU"
73 + "usrfO0Gq7cA9jUNbBsZrmiQnac1e6n3FeBzakpf39OSBG9IPHAZwzlFoagVg5edHXn57wZed9dpA"
74 + "C3FoYRDpf8M0AQwKwu9yubxjeA7Y72ENqlp3mOqMcwcwDPQCx8gGchV4BYzGoS1V3gL8AVA5C5/0"
75 + "oRFoAAAAAElFTkSuQmCC";
76
77 // A small 32x32 "-" image with colours
78 private static String removeImage64 = //
79 ""
80 + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
81 + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeESw5X/JGsQAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
82 + "YXRlZCB3aXRoIEdJTVBkLmUHAAACKUlEQVQ4y5WTO2iTYRSG3+//v/+SJrG5SSABh1JQBHFJNUNR"
83 + "YodCLoMoTkK0YKhQtBmsl01wKVZRBwcrgosg3SwFW9Cippe0VmlpB6uYqYIaNSZtbv/lOKRx0iR9"
84 + "4YOzvOc8vOd8wLbG4nYGAKP9tshKr3Pq0zFXORt0UzbopvUeZ2ml1/niUcIWAYBzwwqr+xgAjCSt"
85 + "wpXjWzx105Ha+1XsMgT8U6IJfPAacyfO50OXJi3VwbtbxMbidtZ3tiClbzi/eAuCmxgai4AfNvNn"
86 + "KJn3X5xWKgwA0lHHYud3MdDUXMcmIOMx0oGJXJCN9tuiJ98p4//DbtTk2cFKhB/OSBcMgQHVMkir"
87 + "AqwJBhGYrIIkCQc2eJK3aewI9Crko2FIh0K1Jo0mcwmV6XFUlmfRXhK7eXuRKaRVIYdiUGKnW8Kn"
88 + "0ia0t6/hKHJVqCcLzncQgLhtIvBfbWbZZahq+cl96AuvQLre2Mw59NUlkCwjZ6USL0uYgSj26B/X"
89 + "oK+vtkYgMAhMRF4x5oWlPdod0UQtfUFo7YEBBKz59BEGAAtRx1xHVgzu5AYyHmMmMJHrZolhhU3t"
90 + "05XJe7s2PJuCq9k1MgKyNjOXiBf8kWW5JDy4XKHBl2ql6+pvX8ZjzDOqrcWsFQAAE/T3H3z2GG/6"
91 + "zhT8sfdKeehWkUQAeJ7WcH23xTz1uPBwf1hclA3mBZjPojFOIOSsVPpmN1OznfpA+Gn+2kCHqg/d"
92 + "LhIA/AFU5d0V6gTjtQAAAABJRU5ErkJggg==";
93
94 /** The original value before current changes. */
95 private Object orig;
96 private List<Integer> dirtyBits;
97
98 private JComponent field;
99 private List<JComponent> fields = new ArrayList<JComponent>();
100
101 /** The {@link MetaInfo} linked to the field. */
102 protected MetaInfo<E> info;
103
104 /**
105 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
106 *
107 * @param info
108 * the {@link MetaInfo}
109 * @param nhgap
110 * negative horisontal gap in pixel to use for the label, i.e.,
111 * the step lock sized labels will start smaller by that amount
112 * (the use case would be to align controls that start at a
113 * different horisontal position)
114 */
115 public ConfigItem(MetaInfo<E> info, int nhgap) {
116 this(info, true);
117
118 ConfigItem<E> configItem;
119 switch (info.getFormat()) {
120 case BOOLEAN:
121 configItem = new ConfigItemBoolean<E>(info);
122 break;
123 case COLOR:
124 configItem = new ConfigItemColor<E>(info);
125 break;
126 case FILE:
127 configItem = new ConfigItemBrowse<E>(info, false);
128 break;
129 case DIRECTORY:
130 configItem = new ConfigItemBrowse<E>(info, true);
131 break;
132 case COMBO_LIST:
133 configItem = new ConfigItemCombobox<E>(info, true);
134 break;
135 case FIXED_LIST:
136 configItem = new ConfigItemCombobox<E>(info, false);
137 break;
138 case INT:
139 configItem = new ConfigItemInteger<E>(info);
140 break;
141 case PASSWORD:
142 configItem = new ConfigItemPassword<E>(info);
143 break;
144 case STRING:
145 case LOCALE: // TODO?
146 default:
147 configItem = new ConfigItemString<E>(info);
148 break;
149 }
150
151 if (info.isArray()) {
152 this.setLayout(new BorderLayout());
153 add(label(nhgap), BorderLayout.WEST);
154
155 final JPanel main = new JPanel();
156 main.setLayout(new BoxLayout(main, BoxLayout.Y_AXIS));
157 int size = info.getListSize(false);
158 for (int i = 0; i < size; i++) {
159 JComponent field = configItem.createComponent(i);
160 main.add(field);
161 }
162
163 final JButton add = new JButton();
164 setImage(add, addImage64, "+");
165
166 final ConfigItem<E> fconfigItem = configItem;
167 add.addActionListener(new ActionListener() {
168 @Override
169 public void actionPerformed(ActionEvent e) {
170 JComponent field = fconfigItem
171 .createComponent(fconfigItem.info
172 .getListSize(false));
173 main.add(field);
174
175 main.revalidate();
176 main.repaint();
177 }
178 });
179
180 JPanel tmp = new JPanel(new BorderLayout());
181 tmp.add(add, BorderLayout.WEST);
182
183 JPanel mainPlus = new JPanel(new BorderLayout());
184 mainPlus.add(main, BorderLayout.CENTER);
185 mainPlus.add(tmp, BorderLayout.SOUTH);
186
187 add(mainPlus, BorderLayout.CENTER);
188 } else {
189 this.setLayout(new BorderLayout());
190 add(label(nhgap), BorderLayout.WEST);
191
192 JComponent field = configItem.createComponent(-1);
193 add(field, BorderLayout.CENTER);
194 }
195 }
196
197 /**
198 * Prepare a new {@link ConfigItem} instance, linked to the given
199 * {@link MetaInfo}.
200 *
201 * @param info
202 * the info
203 * @param autoDirtyHandling
204 * TRUE to automatically manage the setDirty/Save operations,
205 * FALSE if you want to do it yourself via
206 * {@link ConfigItem#setDirtyItem(int)}
207 */
208 protected ConfigItem(MetaInfo<E> info, boolean autoDirtyHandling) {
209 this.info = info;
210 if (!autoDirtyHandling) {
211 dirtyBits = new ArrayList<Integer>();
212 }
213 }
214
215 /**
216 * Create an empty graphical component to be used later by
217 * {@link ConfigItem#getField(int)}.
218 * <p>
219 * Note that {@link ConfigItem#reload(int)} will be called after it was
220 * created.
221 *
222 * @param item
223 * the item number to get for an array of values, or -1 to get
224 * the whole value (has no effect if {@link MetaInfo#isArray()}
225 * is FALSE)
226 *
227 * @return the graphical component
228 */
229 protected JComponent createField(@SuppressWarnings("unused") int item) {
230 // Not used by the main class, only the sublasses
231 return null;
232 }
233
234 /**
235 * Get the information from the {@link MetaInfo} in the subclass preferred
236 * format.
237 *
238 * @param item
239 * the item number to get for an array of values, or -1 to get
240 * the whole value (has no effect if {@link MetaInfo#isArray()}
241 * is FALSE)
242 *
243 * @return the information in the subclass preferred format
244 */
245 protected Object getFromInfo(@SuppressWarnings("unused") int item) {
246 // Not used by the main class, only the subclasses
247 return null;
248 }
249
250 /**
251 * Set the value to the {@link MetaInfo}.
252 *
253 * @param value
254 * the value in the subclass preferred format
255 * @param item
256 * the item number to get for an array of values, or -1 to get
257 * the whole value (has no effect if {@link MetaInfo#isArray()}
258 * is FALSE)
259 */
260 protected void setToInfo(@SuppressWarnings("unused") Object value,
261 @SuppressWarnings("unused") int item) {
262 // Not used by the main class, only the subclasses
263 }
264
265 /**
266 * @param item
267 * the item number to get for an array of values, or -1 to get
268 * the whole value (has no effect if {@link MetaInfo#isArray()}
269 * is FALSE)
270 *
271 * @return
272 */
273 protected Object getFromField(@SuppressWarnings("unused") int item) {
274 // Not used by the main class, only the subclasses
275 return null;
276 }
277
278 /**
279 * Set the value (in the subclass preferred format) into the field.
280 *
281 * @param value
282 * the value in the subclass preferred format
283 * @param item
284 * the item number to get for an array of values, or -1 to get
285 * the whole value (has no effect if {@link MetaInfo#isArray()}
286 * is FALSE)
287 */
288 protected void setToField(@SuppressWarnings("unused") Object value,
289 @SuppressWarnings("unused") int item) {
290 // Not used by the main class, only the subclasses
291 }
292
293 /**
294 * Create a new field for the given graphical component at the given index
295 * (note that the component is usually created by
296 * {@link ConfigItem#createField(int)}).
297 *
298 * @param item
299 * the item number to get for an array of values, or -1 to get
300 * the whole value (has no effect if {@link MetaInfo#isArray()}
301 * is FALSE)
302 * @param field
303 * the graphical component
304 */
305 private void setField(int item, JComponent field) {
306 if (item < 0) {
307 this.field = field;
308 return;
309 }
310
311 for (int i = fields.size(); i <= item; i++) {
312 fields.add(null);
313 }
314
315 fields.set(item, field);
316 }
317
318 /**
319 * Retrieve the associated graphical component that was created with
320 * {@link ConfigItem#createField(int)}.
321 *
322 * @param item
323 * the item number to get for an array of values, or -1 to get
324 * the whole value (has no effect if {@link MetaInfo#isArray()}
325 * is FALSE)
326 *
327 * @return the graphical component
328 */
329 protected JComponent getField(int item) {
330 if (item < 0) {
331 return field;
332 }
333
334 if (item < fields.size()) {
335 return fields.get(item);
336 }
337
338 return null;
339 }
340
341 /**
342 * Manually specify that the given item is "dirty" and thus should be saved
343 * when asked.
344 * <p>
345 * Has no effect if the class is using automatic dirty handling (see
346 * {@link ConfigItem#ConfigItem(MetaInfo, boolean)}).
347 *
348 * @param item
349 * the item number to get for an array of values, or -1 to get
350 * the whole value (has no effect if {@link MetaInfo#isArray()}
351 * is FALSE)
352 */
353 protected void setDirtyItem(int item) {
354 if (dirtyBits != null) {
355 dirtyBits.add(item);
356 }
357 }
358
359 /**
360 * Check if the value changed since the last load/save into the linked
361 * {@link MetaInfo}.
362 * <p>
363 * Note that we consider NULL and an Empty {@link String} to be equals.
364 *
365 * @param value
366 * the value to test
367 *
368 * @return TRUE if it has
369 */
370 protected boolean hasValueChanged(Object value) {
371 // We consider "" and NULL to be equals
372 return !orig.equals(value == null ? "" : value);
373 }
374
375 /**
376 * Reload the values to what they currently are in the {@link MetaInfo}.
377 *
378 * @param item
379 * the item number to get for an array of values, or -1 to get
380 * the whole value (has no effect if {@link MetaInfo#isArray()}
381 * is FALSE)
382 */
383 protected void reload(int item) {
384 Object value = getFromInfo(item);
385 setToField(value, item);
386
387 // We consider "" and NULL to be equals
388 orig = (value == null ? "" : value);
389 }
390
391 /**
392 * If the item has been modified, set the {@link MetaInfo} to dirty then
393 * modify it to, reflect the changes so it can be saved later.
394 * <p>
395 * This method does <b>not</b> call {@link MetaInfo#save(boolean)}.
396 *
397 * @param item
398 * the item number to get for an array of values, or -1 to get
399 * the whole value (has no effect if {@link MetaInfo#isArray()}
400 * is FALSE)
401 */
402 protected void save(int item) {
403 Object value = getFromField(item);
404
405 boolean dirty = false;
406 if (dirtyBits != null) {
407 dirty = dirtyBits.remove((Integer) item);
408 } else {
409 // We consider "" and NULL to be equals
410 dirty = hasValueChanged(value);
411 }
412
413 if (dirty) {
414 info.setDirty();
415 setToInfo(value, item);
416 orig = (value == null ? "" : value);
417 }
418 }
419
420 /**
421 *
422 * @param item
423 * the item number to get for an array of values, or -1 to get
424 * the whole value (has no effect if {@link MetaInfo#isArray()}
425 * is FALSE)
426 * @param addTo
427 * @param nhgap
428 *
429 * @return
430 */
431 protected JComponent createComponent(final int item) {
432 setField(item, createField(item));
433 reload(item);
434
435 info.addReloadedListener(new Runnable() {
436 @Override
437 public void run() {
438 reload(item);
439 }
440 });
441 info.addSaveListener(new Runnable() {
442 @Override
443 public void run() {
444 save(item);
445 }
446 });
447
448 JComponent field = getField(item);
449 setPreferredSize(field);
450
451 return field;
452 }
453
454 /**
455 * Create a label which width is constrained in lock steps.
456 *
457 * @param nhgap
458 * negative horisontal gap in pixel to use for the label, i.e.,
459 * the step lock sized labels will start smaller by that amount
460 * (the use case would be to align controls that start at a
461 * different horisontal position)
462 *
463 * @return the label
464 */
465 protected JComponent label(int nhgap) {
466 final JLabel label = new JLabel(info.getName());
467
468 Dimension ps = label.getPreferredSize();
469 if (ps == null) {
470 ps = label.getSize();
471 }
472
473 ps.height = Math.max(ps.height, getMinimumHeight());
474
475 int w = ps.width;
476 int step = 150;
477 for (int i = 2 * step - nhgap; i < 10 * step; i += step) {
478 if (w < i) {
479 w = i;
480 break;
481 }
482 }
483
484 final Runnable showInfo = new Runnable() {
485 @Override
486 public void run() {
487 StringBuilder builder = new StringBuilder();
488 String text = (info.getDescription().replace("\\n", "\n"))
489 .trim();
490 for (String line : StringUtils.justifyText(text, 80,
491 Alignment.LEFT)) {
492 if (builder.length() > 0) {
493 builder.append("\n");
494 }
495 builder.append(line);
496 }
497 text = builder.toString();
498 JOptionPane.showMessageDialog(ConfigItem.this, text,
499 info.getName(), JOptionPane.INFORMATION_MESSAGE);
500 }
501 };
502
503 JLabel help = new JLabel("");
504 help.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
505 setImage(help, infoImage64, "?");
506
507 help.addMouseListener(new MouseAdapter() {
508 @Override
509 public void mouseClicked(MouseEvent e) {
510 showInfo.run();
511 }
512 });
513
514 JPanel pane2 = new JPanel(new BorderLayout());
515 pane2.add(help, BorderLayout.WEST);
516 pane2.add(new JLabel(" "), BorderLayout.CENTER);
517
518 JPanel contentPane = new JPanel(new BorderLayout());
519 contentPane.add(label, BorderLayout.WEST);
520 contentPane.add(pane2, BorderLayout.CENTER);
521
522 ps.width = w + 30; // 30 for the (?) sign
523 contentPane.setSize(ps);
524 contentPane.setPreferredSize(ps);
525
526 JPanel pane = new JPanel(new BorderLayout());
527 pane.add(contentPane, BorderLayout.NORTH);
528
529 return pane;
530 }
531
532 protected void setPreferredSize(JComponent field) {
533 int height = Math
534 .max(getMinimumHeight(), field.getMinimumSize().height);
535 setPreferredSize(new Dimension(200, height));
536 }
537
538 static private int getMinimumHeight() {
539 if (minimumHeight < 0) {
540 minimumHeight = new JTextField("Test").getMinimumSize().height;
541 }
542
543 return minimumHeight;
544 }
545
546 /**
547 * Set an image to the given {@link JButton}, with a fallback text if it
548 * fails.
549 *
550 * @param button
551 * the button to set
552 * @param image64
553 * the image in BASE64 (should be PNG or similar)
554 * @param fallbackText
555 * text to use in case the image cannot be created
556 */
557 static private void setImage(JLabel button, String image64,
558 String fallbackText) {
559 try {
560 Image img = new Image(image64);
561 try {
562 BufferedImage bImg = ImageUtilsAwt.fromImage(img);
563 button.setIcon(new ImageIcon(bImg));
564 } finally {
565 img.close();
566 }
567 } catch (IOException e) {
568 // This is an hard-coded image, should not happen
569 button.setText(fallbackText);
570 }
571 }
572
573 /**
574 * Set an image to the given {@link JButton}, with a fallback text if it
575 * fails.
576 *
577 * @param button
578 * the button to set
579 * @param image64
580 * the image in BASE64 (should be PNG or similar)
581 * @param fallbackText
582 * text to use in case the image cannot be created
583 */
584 static private void setImage(JButton button, String image64,
585 String fallbackText) {
586 try {
587 Image img = new Image(image64);
588 try {
589 BufferedImage bImg = ImageUtilsAwt.fromImage(img);
590 button.setIcon(new ImageIcon(bImg));
591 } finally {
592 img.close();
593 }
594 } catch (IOException e) {
595 // This is an hard-coded image, should not happen
596 button.setText(fallbackText);
597 }
598 }
599 }