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));
144 JPanel tmp
= new JPanel(new BorderLayout());
145 tmp
.add(add
, BorderLayout
.WEST
);
147 JPanel mainPlus
= new JPanel(new BorderLayout());
148 mainPlus
.add(main
, BorderLayout
.CENTER
);
149 mainPlus
.add(tmp
, BorderLayout
.SOUTH
);
151 add(mainPlus
, BorderLayout
.CENTER
);
153 this.setLayout(new BorderLayout());
154 add(label(nhgap
), BorderLayout
.WEST
);
156 JComponent field
= configItem
.createComponent(-1);
157 add(field
, BorderLayout
.CENTER
);
162 * Prepare a new {@link ConfigItem} instance, linked to the given
167 * @param autoDirtyHandling
168 * TRUE to automatically manage the setDirty/Save operations,
169 * FALSE if you want to do it yourself via
170 * {@link ConfigItem#setDirtyItem(int)}
172 protected ConfigItem(MetaInfo
<E
> info
, boolean autoDirtyHandling
) {
174 if (!autoDirtyHandling
) {
175 dirtyBits
= new ArrayList
<Integer
>();
180 * Create an empty graphical component to be used later by
181 * {@link ConfigItem#getField(int)}.
183 * Note that {@link ConfigItem#reload(int)} will be called after it was
187 * the item number to get for an array of values, or -1 to get
188 * the whole value (has no effect if {@link MetaInfo#isArray()}
191 * @return the graphical component
193 protected JComponent
createField(@SuppressWarnings("unused") int item
) {
194 // Not used by the main class, only the sublasses
199 * Get the information from the {@link MetaInfo} in the subclass preferred
203 * the item number to get for an array of values, or -1 to get
204 * the whole value (has no effect if {@link MetaInfo#isArray()}
207 * @return the information in the subclass preferred format
209 protected Object
getFromInfo(@SuppressWarnings("unused") int item
) {
210 // Not used by the main class, only the subclasses
215 * Set the value to the {@link MetaInfo}.
218 * the value in the subclass preferred format
220 * the item number to get for an array of values, or -1 to get
221 * the whole value (has no effect if {@link MetaInfo#isArray()}
224 protected void setToInfo(@SuppressWarnings("unused") Object value
,
225 @SuppressWarnings("unused") int item
) {
226 // Not used by the main class, only the subclasses
231 * the item number to get for an array of values, or -1 to get
232 * the whole value (has no effect if {@link MetaInfo#isArray()}
237 protected Object
getFromField(@SuppressWarnings("unused") int item
) {
238 // Not used by the main class, only the subclasses
243 * Set the value (in the subclass preferred format) into the field.
246 * the value in the subclass preferred format
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 protected void setToField(@SuppressWarnings("unused") Object value
,
253 @SuppressWarnings("unused") int item
) {
254 // Not used by the main class, only the subclasses
258 * Create a new field for the given graphical component at the given index
259 * (note that the component is usually created by
260 * {@link ConfigItem#createField(int)}).
263 * the item number to get for an array of values, or -1 to get
264 * the whole value (has no effect if {@link MetaInfo#isArray()}
267 * the graphical component
269 private void setField(int item
, JComponent field
) {
275 for (int i
= fields
.size(); i
<= item
; i
++) {
279 fields
.set(item
, field
);
283 * Retrieve the associated graphical component that was created with
284 * {@link ConfigItem#createField(int)}.
287 * the item number to get for an array of values, or -1 to get
288 * the whole value (has no effect if {@link MetaInfo#isArray()}
291 * @return the graphical component
293 protected JComponent
getField(int item
) {
298 if (item
< fields
.size()) {
299 return fields
.get(item
);
306 * Manually specify that the given item is "dirty" and thus should be saved
309 * Has no effect if the class is using automatic dirty handling (see
310 * {@link ConfigItem#ConfigItem(MetaInfo, boolean)}).
313 * the item number to get for an array of values, or -1 to get
314 * the whole value (has no effect if {@link MetaInfo#isArray()}
317 protected void setDirtyItem(int item
) {
318 if (dirtyBits
!= null) {
324 * Check if the value changed since the last load/save into the linked
327 * Note that we consider NULL and an Empty {@link String} to be equals.
332 * @return TRUE if it has
334 protected boolean hasValueChanged(Object value
) {
335 // We consider "" and NULL to be equals
336 return !orig
.equals(value
== null ?
"" : value
);
340 * Reload the values to what they currently are in the {@link MetaInfo}.
343 * the item number to get for an array of values, or -1 to get
344 * the whole value (has no effect if {@link MetaInfo#isArray()}
347 protected void reload(int item
) {
348 Object value
= getFromInfo(item
);
349 setToField(value
, item
);
351 // We consider "" and NULL to be equals
352 orig
= (value
== null ?
"" : value
);
356 * If the item has been modified, set the {@link MetaInfo} to dirty then
357 * modify it to, reflect the changes so it can be saved later.
359 * This method does <b>not</b> call {@link MetaInfo#save(boolean)}.
362 * the item number to get for an array of values, or -1 to get
363 * the whole value (has no effect if {@link MetaInfo#isArray()}
366 protected void save(int item
) {
367 Object value
= getFromField(item
);
369 boolean dirty
= false;
370 if (dirtyBits
!= null) {
371 dirty
= dirtyBits
.remove((Integer
) item
);
373 // We consider "" and NULL to be equals
374 dirty
= hasValueChanged(value
);
379 setToInfo(value
, item
);
380 orig
= (value
== null ?
"" : value
);
387 * the item number to get for an array of values, or -1 to get
388 * the whole value (has no effect if {@link MetaInfo#isArray()}
393 protected JComponent
createComponent(final int item
) {
394 setField(item
, createField(item
));
397 info
.addReloadedListener(new Runnable() {
403 info
.addSaveListener(new Runnable() {
410 JComponent field
= getField(item
);
411 setPreferredSize(field
);
417 * Create a label which width is constrained in lock steps.
420 * negative horisontal gap in pixel to use for the label, i.e.,
421 * the step lock sized labels will start smaller by that amount
422 * (the use case would be to align controls that start at a
423 * different horisontal position)
427 protected JComponent
label(int nhgap
) {
428 final JLabel label
= new JLabel(info
.getName());
430 Dimension ps
= label
.getPreferredSize();
432 ps
= label
.getSize();
435 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
439 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
446 final Runnable showInfo
= new Runnable() {
449 StringBuilder builder
= new StringBuilder();
450 String text
= (info
.getDescription().replace("\\n", "\n"))
452 for (String line
: StringUtils
.justifyText(text
, 80,
454 if (builder
.length() > 0) {
455 builder
.append("\n");
457 builder
.append(line
);
459 text
= builder
.toString();
460 JOptionPane
.showMessageDialog(ConfigItem
.this, text
,
461 info
.getName(), JOptionPane
.INFORMATION_MESSAGE
);
465 JLabel help
= new JLabel("");
466 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
468 Image img
= new Image(infoImage64
);
470 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
471 help
.setIcon(new ImageIcon(bImg
));
475 } catch (IOException e
) {
476 // This is an hard-coded image, should not happen
480 help
.addMouseListener(new MouseAdapter() {
482 public void mouseClicked(MouseEvent e
) {
487 JPanel pane2
= new JPanel(new BorderLayout());
488 pane2
.add(help
, BorderLayout
.WEST
);
489 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
491 JPanel contentPane
= new JPanel(new BorderLayout());
492 contentPane
.add(label
, BorderLayout
.WEST
);
493 contentPane
.add(pane2
, BorderLayout
.CENTER
);
495 ps
.width
= w
+ 30; // 30 for the (?) sign
496 contentPane
.setSize(ps
);
497 contentPane
.setPreferredSize(ps
);
499 JPanel pane
= new JPanel(new BorderLayout());
500 pane
.add(contentPane
, BorderLayout
.NORTH
);
505 protected void setPreferredSize(JComponent field
) {
507 .max(getMinimumHeight(), field
.getMinimumSize().height
);
508 setPreferredSize(new Dimension(200, height
));
511 static private int getMinimumHeight() {
512 if (minimumHeight
< 0) {
513 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
516 return minimumHeight
;