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 (?) 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 /** The original value before current changes. */
62 private List
<Integer
> dirtyBits
;
64 protected MetaInfo
<E
> info
;
66 private JComponent field
;
67 private List
<JComponent
> fields
= new ArrayList
<JComponent
>();
70 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
73 * the {@link MetaInfo}
75 * negative horisontal gap in pixel to use for the label, i.e.,
76 * the step lock sized labels will start smaller by that amount
77 * (the use case would be to align controls that start at a
78 * different horisontal position)
80 public ConfigItem(MetaInfo
<E
> info
, int nhgap
) {
83 ConfigItem
<E
> configItem
;
84 switch (info
.getFormat()) {
86 configItem
= new ConfigItemBoolean
<E
>(info
);
89 configItem
= new ConfigItemColor
<E
>(info
);
92 configItem
= new ConfigItemBrowse
<E
>(info
, false);
95 configItem
= new ConfigItemBrowse
<E
>(info
, true);
98 configItem
= new ConfigItemCombobox
<E
>(info
, true);
101 configItem
= new ConfigItemCombobox
<E
>(info
, false);
104 configItem
= new ConfigItemInteger
<E
>(info
);
107 configItem
= new ConfigItemPassword
<E
>(info
);
110 case LOCALE
: // TODO?
112 configItem
= new ConfigItemString
<E
>(info
);
116 if (info
.isArray()) {
117 this.setLayout(new BorderLayout());
118 add(label(nhgap
), BorderLayout
.WEST
);
120 final JPanel main
= new JPanel();
121 main
.setLayout(new BoxLayout(main
, BoxLayout
.Y_AXIS
));
122 int size
= info
.getListSize(false);
123 for (int i
= 0; i
< size
; i
++) {
124 JComponent field
= configItem
.createComponent(i
);
129 final JButton add
= new JButton("+");
130 final ConfigItem
<E
> fconfigItem
= configItem
;
131 add
.addActionListener(new ActionListener() {
133 public void actionPerformed(ActionEvent e
) {
134 JComponent field
= fconfigItem
135 .createComponent(fconfigItem
.info
136 .getListSize(false));
139 // TODO this doesn't woooooorkk
143 ConfigItem
.this.repaint();
144 ConfigItem
.this.validate();
145 ConfigItem
.this.repaint();
149 JPanel tmp
= new JPanel(new BorderLayout());
150 tmp
.add(add
, BorderLayout
.WEST
);
152 JPanel mainPlus
= new JPanel(new BorderLayout());
153 mainPlus
.add(main
, BorderLayout
.CENTER
);
154 mainPlus
.add(tmp
, BorderLayout
.SOUTH
);
156 add(mainPlus
, BorderLayout
.CENTER
);
158 this.setLayout(new BorderLayout());
159 add(label(nhgap
), BorderLayout
.WEST
);
161 JComponent field
= configItem
.createComponent(-1);
162 add(field
, BorderLayout
.CENTER
);
167 * Prepare a new {@link ConfigItem} instance, linked to the given
172 * @param autoDirtyHandling
173 * TRUE to automatically manage the setDirty/Save operations,
174 * FALSE if you want to do it yourself via
175 * {@link ConfigItem#setDirtyItem(int)}
177 protected ConfigItem(MetaInfo
<E
> info
, boolean autoDirtyHandling
) {
179 if (!autoDirtyHandling
) {
180 dirtyBits
= new ArrayList
<Integer
>();
185 * Create an empty graphical component to be used later by
186 * {@link ConfigItem#getField(int)}.
188 * Note that {@link ConfigItem#reload(int)} will be called after it was
192 * the item number to get for an array of values, or -1 to get
193 * the whole value (has no effect if {@link MetaInfo#isArray()}
196 * @return the graphical component
198 protected JComponent
createField(@SuppressWarnings("unused") int item
) {
199 // Not used by the main class, only the sublasses
204 * Get the information from the {@link MetaInfo} in the subclass preferred
208 * the item number to get for an array of values, or -1 to get
209 * the whole value (has no effect if {@link MetaInfo#isArray()}
212 * @return the information in the subclass preferred format
214 protected Object
getFromInfo(@SuppressWarnings("unused") int item
) {
215 // Not used by the main class, only the subclasses
220 * Set the value to the {@link MetaInfo}.
223 * the value in the subclass preferred format
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 protected void setToInfo(@SuppressWarnings("unused") Object value
,
230 @SuppressWarnings("unused") int item
) {
231 // Not used by the main class, only the subclasses
236 * the item number to get for an array of values, or -1 to get
237 * the whole value (has no effect if {@link MetaInfo#isArray()}
242 protected Object
getFromField(@SuppressWarnings("unused") int item
) {
243 // Not used by the main class, only the subclasses
248 * Set the value (in the subclass preferred format) into the field.
251 * the value in the subclass preferred format
253 * the item number to get for an array of values, or -1 to get
254 * the whole value (has no effect if {@link MetaInfo#isArray()}
257 protected void setToField(@SuppressWarnings("unused") Object value
,
258 @SuppressWarnings("unused") int item
) {
259 // Not used by the main class, only the subclasses
263 * Create a new field for the given graphical component at the given index
264 * (note that the component is usually created by
265 * {@link ConfigItem#createField(int)}).
268 * the item number to get for an array of values, or -1 to get
269 * the whole value (has no effect if {@link MetaInfo#isArray()}
272 * the graphical component
274 private void setField(int item
, JComponent field
) {
280 for (int i
= fields
.size(); i
<= item
; i
++) {
284 fields
.set(item
, field
);
288 * Retrieve the associated graphical component that was created with
289 * {@link ConfigItem#createField(int)}.
292 * the item number to get for an array of values, or -1 to get
293 * the whole value (has no effect if {@link MetaInfo#isArray()}
296 * @return the graphical component
298 protected JComponent
getField(int item
) {
303 if (item
< fields
.size()) {
304 return fields
.get(item
);
311 * Manually specify that the given item is "dirty" and thus should be saved
314 * Has no effect if the class is using automatic dirty handling (see
315 * {@link ConfigItem#ConfigItem(MetaInfo, boolean)}).
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 protected void setDirtyItem(int item
) {
323 if (dirtyBits
!= null) {
329 * Check if the value changed since the last load/save into the linked
332 * Note that we consider NULL and an Empty {@link String} to be equals.
337 * @return TRUE if it has
339 protected boolean hasValueChanged(Object value
) {
340 // We consider "" and NULL to be equals
341 return !orig
.equals(value
== null ?
"" : value
);
345 * Reload the values to what they currently are in the {@link MetaInfo}.
348 * the item number to get for an array of values, or -1 to get
349 * the whole value (has no effect if {@link MetaInfo#isArray()}
352 protected void reload(int item
) {
353 Object value
= getFromInfo(item
);
354 setToField(value
, item
);
356 // We consider "" and NULL to be equals
357 orig
= (value
== null ?
"" : value
);
361 * If the item has been modified, set the {@link MetaInfo} to dirty then
362 * modify it to, reflect the changes so it can be saved later.
364 * This method does <b>not</b> call {@link MetaInfo#save(boolean)}.
367 * the item number to get for an array of values, or -1 to get
368 * the whole value (has no effect if {@link MetaInfo#isArray()}
371 protected void save(int item
) {
372 Object value
= getFromField(item
);
374 boolean dirty
= false;
375 if (dirtyBits
!= null) {
376 dirty
= dirtyBits
.remove((Integer
) item
);
378 // We consider "" and NULL to be equals
379 dirty
= hasValueChanged(value
);
384 setToInfo(value
, item
);
385 orig
= (value
== null ?
"" : value
);
392 * the item number to get for an array of values, or -1 to get
393 * the whole value (has no effect if {@link MetaInfo#isArray()}
398 protected JComponent
createComponent(final int item
) {
399 setField(item
, createField(item
));
402 info
.addReloadedListener(new Runnable() {
408 info
.addSaveListener(new Runnable() {
415 JComponent field
= getField(item
);
416 setPreferredSize(field
);
422 * Create a label which width is constrained in lock steps.
425 * negative horisontal gap in pixel to use for the label, i.e.,
426 * the step lock sized labels will start smaller by that amount
427 * (the use case would be to align controls that start at a
428 * different horisontal position)
432 protected JComponent
label(int nhgap
) {
433 final JLabel label
= new JLabel(info
.getName());
435 Dimension ps
= label
.getPreferredSize();
437 ps
= label
.getSize();
440 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
444 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
451 final Runnable showInfo
= new Runnable() {
454 StringBuilder builder
= new StringBuilder();
455 String text
= (info
.getDescription().replace("\\n", "\n"))
457 for (String line
: StringUtils
.justifyText(text
, 80,
459 if (builder
.length() > 0) {
460 builder
.append("\n");
462 builder
.append(line
);
464 text
= builder
.toString();
465 JOptionPane
.showMessageDialog(ConfigItem
.this, text
,
466 info
.getName(), JOptionPane
.INFORMATION_MESSAGE
);
470 JLabel help
= new JLabel("");
471 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
473 Image img
= new Image(infoImage64
);
475 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
476 help
.setIcon(new ImageIcon(bImg
));
480 } catch (IOException e
) {
481 // This is an hard-coded image, should not happen
485 help
.addMouseListener(new MouseAdapter() {
487 public void mouseClicked(MouseEvent e
) {
492 JPanel pane2
= new JPanel(new BorderLayout());
493 pane2
.add(help
, BorderLayout
.WEST
);
494 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
496 JPanel contentPane
= new JPanel(new BorderLayout());
497 contentPane
.add(label
, BorderLayout
.WEST
);
498 contentPane
.add(pane2
, BorderLayout
.CENTER
);
500 ps
.width
= w
+ 30; // 30 for the (?) sign
501 contentPane
.setSize(ps
);
502 contentPane
.setPreferredSize(ps
);
504 JPanel pane
= new JPanel(new BorderLayout());
505 pane
.add(contentPane
, BorderLayout
.NORTH
);
510 protected void setPreferredSize(JComponent field
) {
512 .max(getMinimumHeight(), field
.getMinimumSize().height
);
513 setPreferredSize(new Dimension(200, height
));
516 static private int getMinimumHeight() {
517 if (minimumHeight
< 0) {
518 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
521 return minimumHeight
;