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
.List
;
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
;
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
;
31 * A graphical item that reflect a configuration option from the given
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.
41 * the type of {@link Bundle} to edit
43 public class ConfigItem
<E
extends Enum
<E
>> extends JPanel
{
44 private static final long serialVersionUID
= 1L;
46 private static int minimumHeight
= -1;
48 /** A small 16x16 "?" blue in PNG, base64 encoded. */
49 private static String infoImage64
= //
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";
60 // A small 16x16 "+" image with colours
61 private static String addImage64
= //
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";
77 // A small 32x32 "-" image with colours
78 private static String removeImage64
= //
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==";
94 /** The original value before current changes. */
96 private List
<Integer
> dirtyBits
;
98 private JComponent field
;
99 private List
<JComponent
> fields
= new ArrayList
<JComponent
>();
101 /** The {@link MetaInfo} linked to the field. */
102 protected MetaInfo
<E
> info
;
105 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
108 * the {@link MetaInfo}
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)
115 public ConfigItem(MetaInfo
<E
> info
, int nhgap
) {
118 ConfigItem
<E
> configItem
;
119 switch (info
.getFormat()) {
121 configItem
= new ConfigItemBoolean
<E
>(info
);
124 configItem
= new ConfigItemColor
<E
>(info
);
127 configItem
= new ConfigItemBrowse
<E
>(info
, false);
130 configItem
= new ConfigItemBrowse
<E
>(info
, true);
133 configItem
= new ConfigItemCombobox
<E
>(info
, true);
136 configItem
= new ConfigItemCombobox
<E
>(info
, false);
139 configItem
= new ConfigItemInteger
<E
>(info
);
142 configItem
= new ConfigItemPassword
<E
>(info
);
145 case LOCALE
: // TODO?
147 configItem
= new ConfigItemString
<E
>(info
);
151 if (info
.isArray()) {
152 this.setLayout(new BorderLayout());
153 add(label(nhgap
), BorderLayout
.WEST
);
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
);
163 final JButton add
= new JButton();
164 setImage(add
, addImage64
, "+");
166 final ConfigItem
<E
> fconfigItem
= configItem
;
167 add
.addActionListener(new ActionListener() {
169 public void actionPerformed(ActionEvent e
) {
170 JComponent field
= fconfigItem
171 .createComponent(fconfigItem
.info
172 .getListSize(false));
180 JPanel tmp
= new JPanel(new BorderLayout());
181 tmp
.add(add
, BorderLayout
.WEST
);
183 JPanel mainPlus
= new JPanel(new BorderLayout());
184 mainPlus
.add(main
, BorderLayout
.CENTER
);
185 mainPlus
.add(tmp
, BorderLayout
.SOUTH
);
187 add(mainPlus
, BorderLayout
.CENTER
);
189 this.setLayout(new BorderLayout());
190 add(label(nhgap
), BorderLayout
.WEST
);
192 JComponent field
= configItem
.createComponent(-1);
193 add(field
, BorderLayout
.CENTER
);
198 * Prepare a new {@link ConfigItem} instance, linked to the given
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)}
208 protected ConfigItem(MetaInfo
<E
> info
, boolean autoDirtyHandling
) {
210 if (!autoDirtyHandling
) {
211 dirtyBits
= new ArrayList
<Integer
>();
216 * Create an empty graphical component to be used later by
217 * {@link ConfigItem#getField(int)}.
219 * Note that {@link ConfigItem#reload(int)} will be called after it was
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()}
227 * @return the graphical component
229 protected JComponent
createField(@SuppressWarnings("unused") int item
) {
230 // Not used by the main class, only the sublasses
235 * Get the information from the {@link MetaInfo} in the subclass preferred
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()}
243 * @return the information in the subclass preferred format
245 protected Object
getFromInfo(@SuppressWarnings("unused") int item
) {
246 // Not used by the main class, only the subclasses
251 * Set the value to the {@link MetaInfo}.
254 * the value in the subclass preferred format
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()}
260 protected void setToInfo(@SuppressWarnings("unused") Object value
,
261 @SuppressWarnings("unused") int item
) {
262 // Not used by the main class, only the subclasses
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()}
273 protected Object
getFromField(@SuppressWarnings("unused") int item
) {
274 // Not used by the main class, only the subclasses
279 * Set the value (in the subclass preferred format) into the field.
282 * the value in the subclass preferred format
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()}
288 protected void setToField(@SuppressWarnings("unused") Object value
,
289 @SuppressWarnings("unused") int item
) {
290 // Not used by the main class, only the subclasses
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)}).
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()}
303 * the graphical component
305 private void setField(int item
, JComponent field
) {
311 for (int i
= fields
.size(); i
<= item
; i
++) {
315 fields
.set(item
, field
);
319 * Retrieve the associated graphical component that was created with
320 * {@link ConfigItem#createField(int)}.
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()}
327 * @return the graphical component
329 protected JComponent
getField(int item
) {
334 if (item
< fields
.size()) {
335 return fields
.get(item
);
342 * Manually specify that the given item is "dirty" and thus should be saved
345 * Has no effect if the class is using automatic dirty handling (see
346 * {@link ConfigItem#ConfigItem(MetaInfo, boolean)}).
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()}
353 protected void setDirtyItem(int item
) {
354 if (dirtyBits
!= null) {
360 * Check if the value changed since the last load/save into the linked
363 * Note that we consider NULL and an Empty {@link String} to be equals.
368 * @return TRUE if it has
370 protected boolean hasValueChanged(Object value
) {
371 // We consider "" and NULL to be equals
372 return !orig
.equals(value
== null ?
"" : value
);
376 * Reload the values to what they currently are in the {@link MetaInfo}.
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()}
383 protected void reload(int item
) {
384 Object value
= getFromInfo(item
);
385 setToField(value
, item
);
387 // We consider "" and NULL to be equals
388 orig
= (value
== null ?
"" : value
);
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.
395 * This method does <b>not</b> call {@link MetaInfo#save(boolean)}.
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()}
402 protected void save(int item
) {
403 Object value
= getFromField(item
);
405 boolean dirty
= false;
406 if (dirtyBits
!= null) {
407 dirty
= dirtyBits
.remove((Integer
) item
);
409 // We consider "" and NULL to be equals
410 dirty
= hasValueChanged(value
);
415 setToInfo(value
, item
);
416 orig
= (value
== null ?
"" : value
);
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()}
431 protected JComponent
createComponent(final int item
) {
432 setField(item
, createField(item
));
435 info
.addReloadedListener(new Runnable() {
441 info
.addSaveListener(new Runnable() {
448 JComponent field
= getField(item
);
449 setPreferredSize(field
);
455 * Create a label which width is constrained in lock steps.
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)
465 protected JComponent
label(int nhgap
) {
466 final JLabel label
= new JLabel(info
.getName());
468 Dimension ps
= label
.getPreferredSize();
470 ps
= label
.getSize();
473 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
477 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
484 final Runnable showInfo
= new Runnable() {
487 StringBuilder builder
= new StringBuilder();
488 String text
= (info
.getDescription().replace("\\n", "\n"))
490 for (String line
: StringUtils
.justifyText(text
, 80,
492 if (builder
.length() > 0) {
493 builder
.append("\n");
495 builder
.append(line
);
497 text
= builder
.toString();
498 JOptionPane
.showMessageDialog(ConfigItem
.this, text
,
499 info
.getName(), JOptionPane
.INFORMATION_MESSAGE
);
503 JLabel help
= new JLabel("");
504 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
505 setImage(help
, infoImage64
, "?");
507 help
.addMouseListener(new MouseAdapter() {
509 public void mouseClicked(MouseEvent e
) {
514 JPanel pane2
= new JPanel(new BorderLayout());
515 pane2
.add(help
, BorderLayout
.WEST
);
516 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
518 JPanel contentPane
= new JPanel(new BorderLayout());
519 contentPane
.add(label
, BorderLayout
.WEST
);
520 contentPane
.add(pane2
, BorderLayout
.CENTER
);
522 ps
.width
= w
+ 30; // 30 for the (?) sign
523 contentPane
.setSize(ps
);
524 contentPane
.setPreferredSize(ps
);
526 JPanel pane
= new JPanel(new BorderLayout());
527 pane
.add(contentPane
, BorderLayout
.NORTH
);
532 protected void setPreferredSize(JComponent field
) {
534 .max(getMinimumHeight(), field
.getMinimumSize().height
);
535 setPreferredSize(new Dimension(200, height
));
538 static private int getMinimumHeight() {
539 if (minimumHeight
< 0) {
540 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
543 return minimumHeight
;
547 * Set an image to the given {@link JButton}, with a fallback text if it
553 * the image in BASE64 (should be PNG or similar)
554 * @param fallbackText
555 * text to use in case the image cannot be created
557 static private void setImage(JLabel button
, String image64
,
558 String fallbackText
) {
560 Image img
= new Image(image64
);
562 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
563 button
.setIcon(new ImageIcon(bImg
));
567 } catch (IOException e
) {
568 // This is an hard-coded image, should not happen
569 button
.setText(fallbackText
);
574 * Set an image to the given {@link JButton}, with a fallback text if it
580 * the image in BASE64 (should be PNG or similar)
581 * @param fallbackText
582 * text to use in case the image cannot be created
584 static private void setImage(JButton button
, String image64
,
585 String fallbackText
) {
587 Image img
= new Image(image64
);
589 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
590 button
.setIcon(new ImageIcon(bImg
));
594 } catch (IOException e
) {
595 // This is an hard-coded image, should not happen
596 button
.setText(fallbackText
);