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
.List
;
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
;
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
;
30 * A graphical item that reflect a configuration option from the given
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.
40 * the type of {@link Bundle} to edit
42 public abstract class ConfigItem
<E
extends Enum
<E
>> extends JPanel
{
43 private static final long serialVersionUID
= 1L;
45 private static int minimumHeight
= -1;
47 /** A small 16x16 "?" blue in PNG, base64 encoded. */
48 private static String img64info
= //
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";
59 /** A small 16x16 "+" image with colours */
60 private static String img64add
= //
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";
76 /** A small 32x32 "-" image with colours */
77 private static String img64remove
= //
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==";
94 private final ConfigItemBase
<JComponent
, E
> base
;
96 /** The main panel with all the fields in it. */
100 * Prepare a new {@link ConfigItem} instance, linked to the given
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)}
110 protected ConfigItem(MetaInfo
<E
> info
, boolean autoDirtyHandling
) {
111 base
= new ConfigItemBase
<JComponent
, E
>(info
, autoDirtyHandling
) {
113 protected JComponent
createEmptyField(int item
) {
114 return ConfigItem
.this.createEmptyField(item
);
118 protected Object
getFromInfo(int item
) {
119 return ConfigItem
.this.getFromInfo(item
);
123 protected void setToInfo(Object value
, int item
) {
124 ConfigItem
.this.setToInfo(value
, item
);
128 protected Object
getFromField(int item
) {
129 return ConfigItem
.this.getFromField(item
);
133 protected void setToField(Object value
, int item
) {
134 ConfigItem
.this.setToField(value
, item
);
138 public JComponent
createField(int item
) {
139 JComponent field
= super.createField(item
);
141 int height
= Math
.max(getMinimumHeight(),
142 field
.getMinimumSize().height
);
143 field
.setPreferredSize(new Dimension(200, height
));
149 public List
<JComponent
> reload() {
150 List
<JComponent
> removed
= base
.reload();
151 if (!removed
.isEmpty()) {
152 for (JComponent c
: removed
) {
163 protected JComponent
removeItem(int item
) {
164 JComponent removed
= super.removeItem(item
);
165 main
.remove(removed
);
175 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
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)
183 public void init(int nhgap
) {
184 if (getInfo().isArray()) {
185 this.setLayout(new BorderLayout());
186 add(label(nhgap
), BorderLayout
.WEST
);
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
);
198 final JButton add
= new JButton();
199 setImage(add
, img64add
, "+");
201 add
.addActionListener(new ActionListener() {
203 public void actionPerformed(ActionEvent e
) {
204 addItemWithMinusPanel(base
.getFieldsSize());
210 JPanel tmp
= new JPanel(new BorderLayout());
211 tmp
.add(add
, BorderLayout
.WEST
);
213 JPanel mainPlus
= new JPanel(new BorderLayout());
214 mainPlus
.add(main
, BorderLayout
.CENTER
);
215 mainPlus
.add(tmp
, BorderLayout
.SOUTH
);
217 add(mainPlus
, BorderLayout
.CENTER
);
219 this.setLayout(new BorderLayout());
220 add(label(nhgap
), BorderLayout
.WEST
);
222 JComponent field
= base
.createField(-1);
223 add(field
, BorderLayout
.CENTER
);
227 /** The {@link MetaInfo} linked to the field. */
228 public MetaInfo
<E
> getInfo() {
229 return base
.getInfo();
233 * Retrieve the associated graphical component that was created with
234 * {@link ConfigItemBase#createEmptyField(int)}.
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()}
241 * @return the graphical component
243 protected JComponent
getField(int item
) {
244 return base
.getField(item
);
248 * Manually specify that the given item is "dirty" and thus should be saved
251 * Has no effect if the class is using automatic dirty handling (see
252 * {@link ConfigItemBase#ConfigItem(MetaInfo, boolean)}).
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()}
259 protected void setDirtyItem(int item
) {
260 base
.setDirtyItem(item
);
264 * Check if the value changed since the last load/save into the linked
267 * Note that we consider NULL and an Empty {@link String} to be equals.
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()}
276 * @return TRUE if it has
278 protected boolean hasValueChanged(Object value
, int item
) {
279 return base
.hasValueChanged(value
, item
);
282 private void addItemWithMinusPanel(int item
) {
283 JPanel minusPanel
= createMinusPanel(item
);
284 JComponent field
= base
.addItem(item
, minusPanel
);
285 minusPanel
.add(field
, BorderLayout
.CENTER
);
288 private JPanel
createMinusPanel(final int item
) {
289 JPanel minusPanel
= new JPanel(new BorderLayout());
291 final JButton remove
= new JButton();
292 setImage(remove
, img64remove
, "-");
294 remove
.addActionListener(new ActionListener() {
296 public void actionPerformed(ActionEvent e
) {
297 base
.removeItem(item
);
301 minusPanel
.add(remove
, BorderLayout
.EAST
);
303 main
.add(minusPanel
);
311 * Create an empty graphical component to be used later by
312 * {@link ConfigItem#createField(int)}.
314 * Note that {@link ConfigItem#reload(int)} will be called after it was
315 * created by {@link ConfigItem#createField(int)}.
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()}
322 * @return the graphical component
324 abstract protected JComponent
createEmptyField(int item
);
327 * Get the information from the {@link MetaInfo} in the subclass preferred
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()}
335 * @return the information in the subclass preferred format
337 abstract protected Object
getFromInfo(int item
);
340 * Set the value to the {@link MetaInfo}.
343 * the value in the subclass preferred format
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()}
349 abstract protected void setToInfo(Object value
, int item
);
352 * The value present in the given item's related field in the subclass
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()}
360 * @return the value present in the given item's related field in the
361 * subclass preferred format
363 abstract protected Object
getFromField(int item
);
366 * Set the value (in the subclass preferred format) into the field.
369 * the value in the subclass preferred format
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()}
375 abstract protected void setToField(Object value
, int item
);
378 * Create a label which width is constrained in lock steps.
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)
388 protected JComponent
label(int nhgap
) {
389 final JLabel label
= new JLabel(getInfo().getName());
391 Dimension ps
= label
.getPreferredSize();
393 ps
= label
.getSize();
396 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
400 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
407 final Runnable showInfo
= new Runnable() {
410 StringBuilder builder
= new StringBuilder();
411 String text
= (getInfo().getDescription().replace("\\n", "\n"))
413 for (String line
: StringUtils
.justifyText(text
, 80,
415 if (builder
.length() > 0) {
416 builder
.append("\n");
418 builder
.append(line
);
420 text
= builder
.toString();
421 JOptionPane
.showMessageDialog(ConfigItem
.this, text
, getInfo()
422 .getName(), JOptionPane
.INFORMATION_MESSAGE
);
426 JLabel help
= new JLabel("");
427 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
428 setImage(help
, img64info
, "?");
430 help
.addMouseListener(new MouseAdapter() {
432 public void mouseClicked(MouseEvent e
) {
437 JPanel pane2
= new JPanel(new BorderLayout());
438 pane2
.add(help
, BorderLayout
.WEST
);
439 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
441 JPanel contentPane
= new JPanel(new BorderLayout());
442 contentPane
.add(label
, BorderLayout
.WEST
);
443 contentPane
.add(pane2
, BorderLayout
.CENTER
);
445 ps
.width
= w
+ 30; // 30 for the (?) sign
446 contentPane
.setSize(ps
);
447 contentPane
.setPreferredSize(ps
);
449 JPanel pane
= new JPanel(new BorderLayout());
450 pane
.add(contentPane
, BorderLayout
.NORTH
);
456 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
459 * the type of {@link Bundle} to edit
462 * the {@link MetaInfo}
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)
469 * @return the new {@link ConfigItem}
471 static public <E
extends Enum
<E
>> ConfigItem
<E
> createItem(
472 MetaInfo
<E
> info
, int nhgap
) {
474 ConfigItem
<E
> configItem
;
475 switch (info
.getFormat()) {
477 configItem
= new ConfigItemBoolean
<E
>(info
);
480 configItem
= new ConfigItemColor
<E
>(info
);
483 configItem
= new ConfigItemBrowse
<E
>(info
, false);
486 configItem
= new ConfigItemBrowse
<E
>(info
, true);
489 configItem
= new ConfigItemCombobox
<E
>(info
, true);
492 configItem
= new ConfigItemCombobox
<E
>(info
, false);
495 configItem
= new ConfigItemInteger
<E
>(info
);
498 configItem
= new ConfigItemPassword
<E
>(info
);
501 configItem
= new ConfigItemLocale
<E
>(info
);
505 configItem
= new ConfigItemString
<E
>(info
);
509 configItem
.init(nhgap
);
514 * Set an image to the given {@link JButton}, with a fallback text if it
520 * the image in BASE64 (should be PNG or similar)
521 * @param fallbackText
522 * text to use in case the image cannot be created
524 static protected void setImage(JLabel button
, String image64
,
525 String fallbackText
) {
527 Image img
= new Image(image64
);
529 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
530 button
.setIcon(new ImageIcon(bImg
));
534 } catch (IOException e
) {
535 // This is an hard-coded image, should not happen
536 button
.setText(fallbackText
);
541 * Set an image to the given {@link JButton}, with a fallback text if it
547 * the image in BASE64 (should be PNG or similar)
548 * @param fallbackText
549 * text to use in case the image cannot be created
551 static protected void setImage(JButton button
, String image64
,
552 String fallbackText
) {
554 Image img
= new Image(image64
);
556 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
557 button
.setIcon(new ImageIcon(bImg
));
561 } catch (IOException e
) {
562 // This is an hard-coded image, should not happen
563 button
.setText(fallbackText
);
567 static private int getMinimumHeight() {
568 if (minimumHeight
< 0) {
569 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
572 return minimumHeight
;