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 configItem
= new ConfigItemLocale
<E
>(info
);
149 configItem
= new ConfigItemString
<E
>(info
);
153 if (info
.isArray()) {
154 this.setLayout(new BorderLayout());
155 add(label(nhgap
), BorderLayout
.WEST
);
157 final JPanel main
= new JPanel();
158 main
.setLayout(new BoxLayout(main
, BoxLayout
.Y_AXIS
));
159 int size
= info
.getListSize(false);
160 for (int i
= 0; i
< size
; i
++) {
161 JComponent field
= configItem
.createComponent(i
);
165 final JButton add
= new JButton();
166 setImage(add
, addImage64
, "+");
168 final ConfigItem
<E
> fconfigItem
= configItem
;
169 add
.addActionListener(new ActionListener() {
171 public void actionPerformed(ActionEvent e
) {
172 JComponent field
= fconfigItem
173 .createComponent(fconfigItem
.info
174 .getListSize(false));
182 JPanel tmp
= new JPanel(new BorderLayout());
183 tmp
.add(add
, BorderLayout
.WEST
);
185 JPanel mainPlus
= new JPanel(new BorderLayout());
186 mainPlus
.add(main
, BorderLayout
.CENTER
);
187 mainPlus
.add(tmp
, BorderLayout
.SOUTH
);
189 add(mainPlus
, BorderLayout
.CENTER
);
191 this.setLayout(new BorderLayout());
192 add(label(nhgap
), BorderLayout
.WEST
);
194 JComponent field
= configItem
.createComponent(-1);
195 add(field
, BorderLayout
.CENTER
);
200 * Prepare a new {@link ConfigItem} instance, linked to the given
205 * @param autoDirtyHandling
206 * TRUE to automatically manage the setDirty/Save operations,
207 * FALSE if you want to do it yourself via
208 * {@link ConfigItem#setDirtyItem(int)}
210 protected ConfigItem(MetaInfo
<E
> info
, boolean autoDirtyHandling
) {
212 if (!autoDirtyHandling
) {
213 dirtyBits
= new ArrayList
<Integer
>();
218 * Create an empty graphical component to be used later by
219 * {@link ConfigItem#getField(int)}.
221 * Note that {@link ConfigItem#reload(int)} will be called after it was
225 * the item number to get for an array of values, or -1 to get
226 * the whole value (has no effect if {@link MetaInfo#isArray()}
229 * @return the graphical component
231 protected JComponent
createField(@SuppressWarnings("unused") int item
) {
232 // Not used by the main class, only the sublasses
237 * Get the information from the {@link MetaInfo} in the subclass preferred
241 * the item number to get for an array of values, or -1 to get
242 * the whole value (has no effect if {@link MetaInfo#isArray()}
245 * @return the information in the subclass preferred format
247 protected Object
getFromInfo(@SuppressWarnings("unused") int item
) {
248 // Not used by the main class, only the subclasses
253 * Set the value to the {@link MetaInfo}.
256 * the value in the subclass preferred format
258 * the item number to get for an array of values, or -1 to get
259 * the whole value (has no effect if {@link MetaInfo#isArray()}
262 protected void setToInfo(@SuppressWarnings("unused") Object value
,
263 @SuppressWarnings("unused") int item
) {
264 // Not used by the main class, only the subclasses
269 * the item number to get for an array of values, or -1 to get
270 * the whole value (has no effect if {@link MetaInfo#isArray()}
275 protected Object
getFromField(@SuppressWarnings("unused") int item
) {
276 // Not used by the main class, only the subclasses
281 * Set the value (in the subclass preferred format) into the field.
284 * the value in the subclass preferred format
286 * the item number to get for an array of values, or -1 to get
287 * the whole value (has no effect if {@link MetaInfo#isArray()}
290 protected void setToField(@SuppressWarnings("unused") Object value
,
291 @SuppressWarnings("unused") int item
) {
292 // Not used by the main class, only the subclasses
296 * Create a new field for the given graphical component at the given index
297 * (note that the component is usually created by
298 * {@link ConfigItem#createField(int)}).
301 * the item number to get for an array of values, or -1 to get
302 * the whole value (has no effect if {@link MetaInfo#isArray()}
305 * the graphical component
307 private void setField(int item
, JComponent field
) {
313 for (int i
= fields
.size(); i
<= item
; i
++) {
317 fields
.set(item
, field
);
321 * Retrieve the associated graphical component that was created with
322 * {@link ConfigItem#createField(int)}.
325 * the item number to get for an array of values, or -1 to get
326 * the whole value (has no effect if {@link MetaInfo#isArray()}
329 * @return the graphical component
331 protected JComponent
getField(int item
) {
336 if (item
< fields
.size()) {
337 return fields
.get(item
);
344 * Manually specify that the given item is "dirty" and thus should be saved
347 * Has no effect if the class is using automatic dirty handling (see
348 * {@link ConfigItem#ConfigItem(MetaInfo, boolean)}).
351 * the item number to get for an array of values, or -1 to get
352 * the whole value (has no effect if {@link MetaInfo#isArray()}
355 protected void setDirtyItem(int item
) {
356 if (dirtyBits
!= null) {
362 * Check if the value changed since the last load/save into the linked
365 * Note that we consider NULL and an Empty {@link String} to be equals.
370 * @return TRUE if it has
372 protected boolean hasValueChanged(Object value
) {
373 // We consider "" and NULL to be equals
374 return !orig
.equals(value
== null ?
"" : value
);
378 * Reload the values to what they currently are in the {@link MetaInfo}.
381 * the item number to get for an array of values, or -1 to get
382 * the whole value (has no effect if {@link MetaInfo#isArray()}
385 protected void reload(int item
) {
386 Object value
= getFromInfo(item
);
387 setToField(value
, item
);
389 // We consider "" and NULL to be equals
390 orig
= (value
== null ?
"" : value
);
394 * If the item has been modified, set the {@link MetaInfo} to dirty then
395 * modify it to, reflect the changes so it can be saved later.
397 * This method does <b>not</b> call {@link MetaInfo#save(boolean)}.
400 * the item number to get for an array of values, or -1 to get
401 * the whole value (has no effect if {@link MetaInfo#isArray()}
404 protected void save(int item
) {
405 Object value
= getFromField(item
);
407 boolean dirty
= false;
408 if (dirtyBits
!= null) {
409 dirty
= dirtyBits
.remove((Integer
) item
);
411 // We consider "" and NULL to be equals
412 dirty
= hasValueChanged(value
);
417 setToInfo(value
, item
);
418 orig
= (value
== null ?
"" : value
);
425 * the item number to get for an array of values, or -1 to get
426 * the whole value (has no effect if {@link MetaInfo#isArray()}
433 protected JComponent
createComponent(final int item
) {
434 setField(item
, createField(item
));
437 info
.addReloadedListener(new Runnable() {
443 info
.addSaveListener(new Runnable() {
450 JComponent field
= getField(item
);
451 setPreferredSize(field
);
457 * Create a label which width is constrained in lock steps.
460 * negative horisontal gap in pixel to use for the label, i.e.,
461 * the step lock sized labels will start smaller by that amount
462 * (the use case would be to align controls that start at a
463 * different horisontal position)
467 protected JComponent
label(int nhgap
) {
468 final JLabel label
= new JLabel(info
.getName());
470 Dimension ps
= label
.getPreferredSize();
472 ps
= label
.getSize();
475 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
479 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
486 final Runnable showInfo
= new Runnable() {
489 StringBuilder builder
= new StringBuilder();
490 String text
= (info
.getDescription().replace("\\n", "\n"))
492 for (String line
: StringUtils
.justifyText(text
, 80,
494 if (builder
.length() > 0) {
495 builder
.append("\n");
497 builder
.append(line
);
499 text
= builder
.toString();
500 JOptionPane
.showMessageDialog(ConfigItem
.this, text
,
501 info
.getName(), JOptionPane
.INFORMATION_MESSAGE
);
505 JLabel help
= new JLabel("");
506 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
507 setImage(help
, infoImage64
, "?");
509 help
.addMouseListener(new MouseAdapter() {
511 public void mouseClicked(MouseEvent e
) {
516 JPanel pane2
= new JPanel(new BorderLayout());
517 pane2
.add(help
, BorderLayout
.WEST
);
518 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
520 JPanel contentPane
= new JPanel(new BorderLayout());
521 contentPane
.add(label
, BorderLayout
.WEST
);
522 contentPane
.add(pane2
, BorderLayout
.CENTER
);
524 ps
.width
= w
+ 30; // 30 for the (?) sign
525 contentPane
.setSize(ps
);
526 contentPane
.setPreferredSize(ps
);
528 JPanel pane
= new JPanel(new BorderLayout());
529 pane
.add(contentPane
, BorderLayout
.NORTH
);
534 protected void setPreferredSize(JComponent field
) {
536 .max(getMinimumHeight(), field
.getMinimumSize().height
);
537 setPreferredSize(new Dimension(200, height
));
540 static private int getMinimumHeight() {
541 if (minimumHeight
< 0) {
542 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
545 return minimumHeight
;
549 * Set an image to the given {@link JButton}, with a fallback text if it
555 * the image in BASE64 (should be PNG or similar)
556 * @param fallbackText
557 * text to use in case the image cannot be created
559 static private void setImage(JLabel button
, String image64
,
560 String fallbackText
) {
562 Image img
= new Image(image64
);
564 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
565 button
.setIcon(new ImageIcon(bImg
));
569 } catch (IOException e
) {
570 // This is an hard-coded image, should not happen
571 button
.setText(fallbackText
);
576 * Set an image to the given {@link JButton}, with a fallback text if it
582 * the image in BASE64 (should be PNG or similar)
583 * @param fallbackText
584 * text to use in case the image cannot be created
586 static private void setImage(JButton button
, String image64
,
587 String fallbackText
) {
589 Image img
= new Image(image64
);
591 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
592 button
.setIcon(new ImageIcon(bImg
));
596 } catch (IOException e
) {
597 // This is an hard-coded image, should not happen
598 button
.setText(fallbackText
);