1 package be
.nikiroo
.utils
.ui
;
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
.HashMap
;
14 import java
.util
.List
;
17 import javax
.swing
.BoxLayout
;
18 import javax
.swing
.ImageIcon
;
19 import javax
.swing
.JButton
;
20 import javax
.swing
.JComponent
;
21 import javax
.swing
.JLabel
;
22 import javax
.swing
.JOptionPane
;
23 import javax
.swing
.JPanel
;
24 import javax
.swing
.JTextField
;
26 import be
.nikiroo
.utils
.Image
;
27 import be
.nikiroo
.utils
.StringUtils
;
28 import be
.nikiroo
.utils
.StringUtils
.Alignment
;
29 import be
.nikiroo
.utils
.resources
.Bundle
;
30 import be
.nikiroo
.utils
.resources
.MetaInfo
;
33 * A graphical item that reflect a configuration option from the given
36 * This graphical item can be edited, and the result will be saved back into the
37 * linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should
38 * you wish to, of course.
43 * the type of {@link Bundle} to edit
45 public abstract class ConfigItem
<E
extends Enum
<E
>> extends JPanel
{
46 private static final long serialVersionUID
= 1L;
48 private static int minimumHeight
= -1;
50 /** A small 16x16 "?" blue in PNG, base64 encoded. */
51 private static String img64info
= //
53 + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
54 + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wURFRg6IrtcdgAAATdJREFUOMvtkj8sQ1EUxr9z/71G"
55 + "m1RDogYxq7WDDYMYTSajSG4n6YRYzSaSLibWbiaDIGwdiLIYDFKDNJEgKu969xi8UNHy7H7LPcN3"
56 + "v/Odcy+hG9oOIeIcBCJS9MAvlZtOMtHxsrFrJHGqe0RVGnHAHpcIbPlng8BS3HmKBJYzabGUzcrJ"
57 + "XK+ckIrqANYR2JEv2nYDEVck0WKGfHzyq82Go+btxoX3XAcAIqTj8wPqOH6mtMeM4bGCLhyfhTMA"
58 + "qlLhKHqujCfaweCAmV0p50dPzsNpEKpK01V/n55HIvTnfDC2odKlfeYadZN/T+AqDACUsnkhqaU1"
59 + "LRIVuX1x7ciuSWQxVIrunONrfq3dI6oh+T94Z8453vEem/HTqT8ZpFJ0qDXtGkPbAGAMeSRngQCA"
60 + "eUvgn195AwlZWyvjtQdhAAAAAElFTkSuQmCC";
62 /** A small 16x16 "+" image with colours */
63 private static String img64add
= //
65 + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
66 + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeES0QBFvvnAAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
67 + "YXRlZCB3aXRoIEdJTVBkLmUHAAACH0lEQVQ4y42Tz0sVURTHP+fMmC7CQMpH1EjgIimCsEVBEIg/"
68 + "qIbcBAW2Uai1m/oH2rlJXLQpeRJt2gQhTO0iTTKC1I2JBf5gKCJCRPvhPOed22LmvV70Fn7hwr3c"
69 + "+z3ne+73HCFHEClxaASRHgduA91AW369BkwDI3Foy0GkEofmACQnSxyaCyItAkMClMzYdeCAJgVP"
70 + "tJJrPA7tVoUjNZlngXMAiRmXClfoK/Tjq09x7T6LW+8RxOVJ5+LQzgSRojm5WCEDlMrQVbjIQNtN"
71 + "rh0d5FTzaTLBmWKgM4h0Ig4NzWseohYCJUuqx123Sx0MBpF2+MAdyWUnlqX4lf4bIDHjR+rwJJPR"
72 + "qNCgCjDsA10lM/oKIRcO9lByCYklnG/pqQa4euQ6J5tPoKI0yD6ef33Ku40Z80R7CSJNWyZxT+Ki"
73 + "2ytGP911hyZxQaRp1RtPPPYKD4+sGJwPrDUp7Q9Xxnj9fYrUUnaszEAwQHfrZQAerT/g7cYMiuCp"
74 + "z8LmLI0qBqz6wLQn2v5he57FrXkAtlPH2ZZOuskCzG2+4dnnx3iSuSgCKqLAlAIjmXPiVIRsgYjU"
75 + "usrfO0Gq7cA9jUNbBsZrmiQnac1e6n3FeBzakpf39OSBG9IPHAZwzlFoagVg5edHXn57wZed9dpA"
76 + "C3FoYRDpf8M0AQwKwu9yubxjeA7Y72ENqlp3mOqMcwcwDPQCx8gGchV4BYzGoS1V3gL8AVA5C5/0"
77 + "oRFoAAAAAElFTkSuQmCC";
79 /** A small 32x32 "-" image with colours */
80 private static String img64remove
= //
82 + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
83 + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeESw5X/JGsQAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
84 + "YXRlZCB3aXRoIEdJTVBkLmUHAAACKUlEQVQ4y5WTO2iTYRSG3+//v/+SJrG5SSABh1JQBHFJNUNR"
85 + "YodCLoMoTkK0YKhQtBmsl01wKVZRBwcrgosg3SwFW9Cippe0VmlpB6uYqYIaNSZtbv/lOKRx0iR9"
86 + "4YOzvOc8vOd8wLbG4nYGAKP9tshKr3Pq0zFXORt0UzbopvUeZ2ml1/niUcIWAYBzwwqr+xgAjCSt"
87 + "wpXjWzx105Ha+1XsMgT8U6IJfPAacyfO50OXJi3VwbtbxMbidtZ3tiClbzi/eAuCmxgai4AfNvNn"
88 + "KJn3X5xWKgwA0lHHYud3MdDUXMcmIOMx0oGJXJCN9tuiJ98p4//DbtTk2cFKhB/OSBcMgQHVMkir"
89 + "AqwJBhGYrIIkCQc2eJK3aewI9Crko2FIh0K1Jo0mcwmV6XFUlmfRXhK7eXuRKaRVIYdiUGKnW8Kn"
90 + "0ia0t6/hKHJVqCcLzncQgLhtIvBfbWbZZahq+cl96AuvQLre2Mw59NUlkCwjZ6USL0uYgSj26B/X"
91 + "oK+vtkYgMAhMRF4x5oWlPdod0UQtfUFo7YEBBKz59BEGAAtRx1xHVgzu5AYyHmMmMJHrZolhhU3t"
92 + "05XJe7s2PJuCq9k1MgKyNjOXiBf8kWW5JDy4XKHBl2ql6+pvX8ZjzDOqrcWsFQAAE/T3H3z2GG/6"
93 + "zhT8sfdKeehWkUQAeJ7WcH23xTz1uPBwf1hclA3mBZjPojFOIOSsVPpmN1OznfpA+Gn+2kCHqg/d"
94 + "LhIA/AFU5d0V6gTjtQAAAABJRU5ErkJggg==";
96 /** The original value before current changes. */
98 private List
<Object
> origs
= new ArrayList
<Object
>();
99 private List
<Integer
> dirtyBits
;
101 /** The fields (one for non-array, a list for arrays). */
102 private JComponent field
;
103 private List
<JComponent
> fields
= new ArrayList
<JComponent
>();
105 /** The fields to panel map to get the actual item added to 'main'. */
106 private Map
<Integer
, JComponent
> itemFields
= new HashMap
<Integer
, JComponent
>();
108 /** The main panel with all the fields in it. */
111 /** The {@link MetaInfo} linked to the field. */
112 protected MetaInfo
<E
> info
;
115 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
118 * negative horisontal gap in pixel to use for the label, i.e.,
119 * the step lock sized labels will start smaller by that amount
120 * (the use case would be to align controls that start at a
121 * different horisontal position)
123 public void init(int nhgap
) {
124 if (info
.isArray()) {
125 this.setLayout(new BorderLayout());
126 add(label(nhgap
), BorderLayout
.WEST
);
130 main
.setLayout(new BoxLayout(main
, BoxLayout
.Y_AXIS
));
131 int size
= info
.getListSize(false);
132 for (int i
= 0; i
< size
; i
++) {
138 final JButton add
= new JButton();
139 setImage(add
, img64add
, "+");
141 add
.addActionListener(new ActionListener() {
143 public void actionPerformed(ActionEvent e
) {
144 addItem(fields
.size());
150 JPanel tmp
= new JPanel(new BorderLayout());
151 tmp
.add(add
, BorderLayout
.WEST
);
153 JPanel mainPlus
= new JPanel(new BorderLayout());
154 mainPlus
.add(main
, BorderLayout
.CENTER
);
155 mainPlus
.add(tmp
, BorderLayout
.SOUTH
);
157 add(mainPlus
, BorderLayout
.CENTER
);
159 this.setLayout(new BorderLayout());
160 add(label(nhgap
), BorderLayout
.WEST
);
162 JComponent field
= createField(-1);
163 add(field
, BorderLayout
.CENTER
);
167 private void addItem(final int item
) {
168 JPanel minusPanel
= new JPanel(new BorderLayout());
169 itemFields
.put(item
, minusPanel
);
171 JComponent field
= createField(item
);
173 final JButton remove
= new JButton();
174 setImage(remove
, img64remove
, "-");
176 remove
.addActionListener(new ActionListener() {
178 public void actionPerformed(ActionEvent e
) {
183 minusPanel
.add(field
, BorderLayout
.CENTER
);
184 minusPanel
.add(remove
, BorderLayout
.EAST
);
186 main
.add(minusPanel
);
191 private void removeItem(int item
) {
192 int last
= itemFields
.size() - 1;
194 for (int i
= item
; i
<= last
; i
++) {
197 value
= getFromField(i
+ 1);
199 setToField(value
, i
);
204 main
.remove(itemFields
.remove(last
));
210 * Prepare a new {@link ConfigItem} instance, linked to the given
215 * @param autoDirtyHandling
216 * TRUE to automatically manage the setDirty/Save operations,
217 * FALSE if you want to do it yourself via
218 * {@link ConfigItem#setDirtyItem(int)}
220 protected ConfigItem(MetaInfo
<E
> info
, boolean autoDirtyHandling
) {
222 if (!autoDirtyHandling
) {
223 dirtyBits
= new ArrayList
<Integer
>();
228 * Create an empty graphical component to be used later by
229 * {@link ConfigItem#createField(int)}.
231 * Note that {@link ConfigItem#reload(int)} will be called after it was
232 * created by {@link ConfigItem#createField(int)}.
235 * the item number to get for an array of values, or -1 to get
236 * the whole value (has no effect if {@link MetaInfo#isArray()}
239 * @return the graphical component
241 abstract protected JComponent
createEmptyField(int item
);
244 * Get the information from the {@link MetaInfo} in the subclass preferred
248 * the item number to get for an array of values, or -1 to get
249 * the whole value (has no effect if {@link MetaInfo#isArray()}
252 * @return the information in the subclass preferred format
254 abstract protected Object
getFromInfo(int item
);
257 * Set the value to the {@link MetaInfo}.
260 * the value in the subclass preferred format
262 * the item number to get for an array of values, or -1 to get
263 * the whole value (has no effect if {@link MetaInfo#isArray()}
266 abstract protected void setToInfo(Object value
, int item
);
269 * The value present in the given item's related field in the subclass
273 * the item number to get for an array of values, or -1 to get
274 * the whole value (has no effect if {@link MetaInfo#isArray()}
277 * @return the value present in the given item's related field in the
278 * subclass preferred format
280 abstract protected Object
getFromField(int item
);
283 * Set the value (in the subclass preferred format) into the field.
286 * the value in the subclass preferred format
288 * the item number to get for an array of values, or -1 to get
289 * the whole value (has no effect if {@link MetaInfo#isArray()}
292 abstract protected void setToField(Object value
, int item
);
295 * Create a new field for the given graphical component at the given index
296 * (note that the component is usually created by
297 * {@link ConfigItem#createEmptyField(int)}).
300 * the item number to get for an array of values, or -1 to get
301 * the whole value (has no effect if {@link MetaInfo#isArray()}
304 * the graphical component
306 private void setField(int item
, JComponent field
) {
312 for (int i
= fields
.size(); i
<= item
; i
++) {
316 fields
.set(item
, field
);
320 * Retrieve the associated graphical component that was created with
321 * {@link ConfigItem#createEmptyField(int)}.
324 * the item number to get for an array of values, or -1 to get
325 * the whole value (has no effect if {@link MetaInfo#isArray()}
328 * @return the graphical component
330 protected JComponent
getField(int item
) {
335 if (item
< fields
.size()) {
336 return fields
.get(item
);
343 * The original value (before any changes to the {@link MetaInfo}) for this
347 * the item number to get for an array of values, or -1 to get
348 * the whole value (has no effect if {@link MetaInfo#isArray()}
351 * @return the original value
353 private Object
getOrig(int item
) {
358 if (item
< origs
.size()) {
359 return origs
.get(item
);
366 * The original value (before any changes to the {@link MetaInfo}) for this
370 * the item number to get for an array of values, or -1 to get
371 * the whole value (has no effect if {@link MetaInfo#isArray()}
374 * the new original value
376 private void setOrig(Object value
, int item
) {
380 while (item
>= origs
.size()) {
384 origs
.set(item
, value
);
389 * Manually specify that the given item is "dirty" and thus should be saved
392 * Has no effect if the class is using automatic dirty handling (see
393 * {@link ConfigItem#ConfigItem(MetaInfo, boolean)}).
396 * the item number to get for an array of values, or -1 to get
397 * the whole value (has no effect if {@link MetaInfo#isArray()}
400 protected void setDirtyItem(int item
) {
401 if (dirtyBits
!= null) {
407 * Check if the value changed since the last load/save into the linked
410 * Note that we consider NULL and an Empty {@link String} to be equals.
415 * the item number to get for an array of values, or -1 to get
416 * the whole value (has no effect if {@link MetaInfo#isArray()}
419 * @return TRUE if it has
421 protected boolean hasValueChanged(Object value
, int item
) {
422 // We consider "" and NULL to be equals
423 Object orig
= getOrig(item
);
427 return !orig
.equals(value
== null ?
"" : value
);
431 * Reload the values to what they currently are in the {@link MetaInfo}.
433 private void reload() {
434 if (info
.isArray()) {
435 while (!itemFields
.isEmpty()) {
436 main
.remove(itemFields
.remove(itemFields
.size() - 1));
440 for (int item
= 0; item
< info
.getListSize(false); item
++) {
449 * Reload the values to what they currently are in the {@link MetaInfo}.
452 * the item number to get for an array of values, or -1 to get
453 * the whole value (has no effect if {@link MetaInfo#isArray()}
456 private void reload(int item
) {
457 if (item
>= 0 && !itemFields
.containsKey(item
)) {
461 Object value
= getFromInfo(item
);
462 setToField(value
, item
);
463 setOrig(value
== null ?
"" : value
, item
);
467 * If the item has been modified, set the {@link MetaInfo} to dirty then
468 * modify it to, reflect the changes so it can be saved later.
470 * This method does <b>not</b> call {@link MetaInfo#save(boolean)}.
472 private void save() {
473 if (info
.isArray()) {
474 boolean dirty
= itemFields
.size() != info
.getListSize(false);
475 for (int item
= 0; item
< itemFields
.size(); item
++) {
476 if (getDirtyBit(item
)) {
483 info
.setString(null, -1);
485 for (int item
= 0; item
< itemFields
.size(); item
++) {
487 if (getField(item
) != null) {
488 value
= getFromField(item
);
489 if ("".equals(value
)) {
494 setToInfo(value
, item
);
495 setOrig(value
, item
);
499 if (getDirtyBit(-1)) {
500 Object value
= getFromField(-1);
503 setToInfo(value
, -1);
510 * Check if the item is dirty, and clear the dirty bit if set.
513 * the item number to get for an array of values, or -1 to get
514 * the whole value (has no effect if {@link MetaInfo#isArray()}
517 * @return TRUE if it was dirty, FALSE if not
519 private boolean getDirtyBit(int item
) {
520 if (dirtyBits
!= null) {
521 return dirtyBits
.remove((Integer
) item
);
525 if (getField(item
) != null) {
526 value
= getFromField(item
);
529 return hasValueChanged(value
, item
);
533 * Create a new field for the given item.
536 * the item number to get for an array of values, or -1 to get
537 * the whole value (has no effect if {@link MetaInfo#isArray()}
540 * @return the newly created field
542 protected JComponent
createField(final int item
) {
543 JComponent field
= createEmptyField(item
);
544 setField(item
, field
);
547 info
.addReloadedListener(new Runnable() {
553 info
.addSaveListener(new Runnable() {
561 .max(getMinimumHeight(), field
.getMinimumSize().height
);
562 field
.setPreferredSize(new Dimension(200, height
));
568 * Create a label which width is constrained in lock steps.
571 * negative horisontal gap in pixel to use for the label, i.e.,
572 * the step lock sized labels will start smaller by that amount
573 * (the use case would be to align controls that start at a
574 * different horisontal position)
578 protected JComponent
label(int nhgap
) {
579 final JLabel label
= new JLabel(info
.getName());
581 Dimension ps
= label
.getPreferredSize();
583 ps
= label
.getSize();
586 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
590 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
597 final Runnable showInfo
= new Runnable() {
600 StringBuilder builder
= new StringBuilder();
601 String text
= (info
.getDescription().replace("\\n", "\n"))
603 for (String line
: StringUtils
.justifyText(text
, 80,
605 if (builder
.length() > 0) {
606 builder
.append("\n");
608 builder
.append(line
);
610 text
= builder
.toString();
611 JOptionPane
.showMessageDialog(ConfigItem
.this, text
,
612 info
.getName(), JOptionPane
.INFORMATION_MESSAGE
);
616 JLabel help
= new JLabel("");
617 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
618 setImage(help
, img64info
, "?");
620 help
.addMouseListener(new MouseAdapter() {
622 public void mouseClicked(MouseEvent e
) {
627 JPanel pane2
= new JPanel(new BorderLayout());
628 pane2
.add(help
, BorderLayout
.WEST
);
629 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
631 JPanel contentPane
= new JPanel(new BorderLayout());
632 contentPane
.add(label
, BorderLayout
.WEST
);
633 contentPane
.add(pane2
, BorderLayout
.CENTER
);
635 ps
.width
= w
+ 30; // 30 for the (?) sign
636 contentPane
.setSize(ps
);
637 contentPane
.setPreferredSize(ps
);
639 JPanel pane
= new JPanel(new BorderLayout());
640 pane
.add(contentPane
, BorderLayout
.NORTH
);
646 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
649 * the type of {@link Bundle} to edit
652 * the {@link MetaInfo}
654 * negative horisontal gap in pixel to use for the label, i.e.,
655 * the step lock sized labels will start smaller by that amount
656 * (the use case would be to align controls that start at a
657 * different horisontal position)
659 * @return the new {@link ConfigItem}
661 static public <E
extends Enum
<E
>> ConfigItem
<E
> createItem(
662 MetaInfo
<E
> info
, int nhgap
) {
664 ConfigItem
<E
> configItem
;
665 switch (info
.getFormat()) {
667 configItem
= new ConfigItemBoolean
<E
>(info
);
670 configItem
= new ConfigItemColor
<E
>(info
);
673 configItem
= new ConfigItemBrowse
<E
>(info
, false);
676 configItem
= new ConfigItemBrowse
<E
>(info
, true);
679 configItem
= new ConfigItemCombobox
<E
>(info
, true);
682 configItem
= new ConfigItemCombobox
<E
>(info
, false);
685 configItem
= new ConfigItemInteger
<E
>(info
);
688 configItem
= new ConfigItemPassword
<E
>(info
);
691 configItem
= new ConfigItemLocale
<E
>(info
);
695 configItem
= new ConfigItemString
<E
>(info
);
699 configItem
.init(nhgap
);
704 * Set an image to the given {@link JButton}, with a fallback text if it
710 * the image in BASE64 (should be PNG or similar)
711 * @param fallbackText
712 * text to use in case the image cannot be created
714 static protected void setImage(JLabel button
, String image64
,
715 String fallbackText
) {
717 Image img
= new Image(image64
);
719 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
720 button
.setIcon(new ImageIcon(bImg
));
724 } catch (IOException e
) {
725 // This is an hard-coded image, should not happen
726 button
.setText(fallbackText
);
731 * Set an image to the given {@link JButton}, with a fallback text if it
737 * the image in BASE64 (should be PNG or similar)
738 * @param fallbackText
739 * text to use in case the image cannot be created
741 static protected void setImage(JButton button
, String image64
,
742 String fallbackText
) {
744 Image img
= new Image(image64
);
746 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
747 button
.setIcon(new ImageIcon(bImg
));
751 } catch (IOException e
) {
752 // This is an hard-coded image, should not happen
753 button
.setText(fallbackText
);
757 static private int getMinimumHeight() {
758 if (minimumHeight
< 0) {
759 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
762 return minimumHeight
;