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