fix ConfigItem NPE and ConfigEditor Save/Reset/Defaults
[nikiroo-utils.git] / src / be / nikiroo / utils / ui / ConfigItem.java
1 package be.nikiroo.utils.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Cursor;
6 import java.awt.Dimension;
7 import java.awt.Graphics2D;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.MouseAdapter;
11 import java.awt.event.MouseEvent;
12 import java.awt.image.BufferedImage;
13 import java.io.File;
14 import java.io.IOException;
15
16 import javax.swing.Icon;
17 import javax.swing.ImageIcon;
18 import javax.swing.JButton;
19 import javax.swing.JCheckBox;
20 import javax.swing.JColorChooser;
21 import javax.swing.JComboBox;
22 import javax.swing.JComponent;
23 import javax.swing.JFileChooser;
24 import javax.swing.JLabel;
25 import javax.swing.JOptionPane;
26 import javax.swing.JPanel;
27 import javax.swing.JPasswordField;
28 import javax.swing.JSpinner;
29 import javax.swing.JTextField;
30
31 import be.nikiroo.utils.Image;
32 import be.nikiroo.utils.StringUtils;
33 import be.nikiroo.utils.StringUtils.Alignment;
34 import be.nikiroo.utils.resources.Bundle;
35 import be.nikiroo.utils.resources.Meta.Format;
36 import be.nikiroo.utils.resources.MetaInfo;
37
38 /**
39 * A graphical item that reflect a configuration option from the given
40 * {@link Bundle}.
41 * <p>
42 * This graphical item can be edited, and the result will be saved back into the
43 * linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should
44 * you wish to, of course.
45 *
46 * @author niki
47 *
48 * @param <E>
49 * the type of {@link Bundle} to edit
50 */
51 public class ConfigItem<E extends Enum<E>> extends JPanel {
52 private static final long serialVersionUID = 1L;
53
54 private static int minimumHeight = -1;
55
56 /** A small (?) blue in PNG, base64 encoded. */
57 private static String infoImage64 = //
58 ""
59 + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
60 + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wURFRg6IrtcdgAAATdJREFUOMvtkj8sQ1EUxr9z/71G"
61 + "m1RDogYxq7WDDYMYTSajSG4n6YRYzSaSLibWbiaDIGwdiLIYDFKDNJEgKu969xi8UNHy7H7LPcN3"
62 + "v/Odcy+hG9oOIeIcBCJS9MAvlZtOMtHxsrFrJHGqe0RVGnHAHpcIbPlng8BS3HmKBJYzabGUzcrJ"
63 + "XK+ckIrqANYR2JEv2nYDEVck0WKGfHzyq82Go+btxoX3XAcAIqTj8wPqOH6mtMeM4bGCLhyfhTMA"
64 + "qlLhKHqujCfaweCAmV0p50dPzsNpEKpK01V/n55HIvTnfDC2odKlfeYadZN/T+AqDACUsnkhqaU1"
65 + "LRIVuX1x7ciuSWQxVIrunONrfq3dI6oh+T94Z8453vEem/HTqT8ZpFJ0qDXtGkPbAGAMeSRngQCA"
66 + "eUvgn195AwlZWyvjtQdhAAAAAElFTkSuQmCC";
67
68 /** The original value before current changes. */
69 private Object orig;
70
71 /**
72 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
73 *
74 * @param info
75 * the {@link MetaInfo}
76 * @param nhgap
77 * negative horisontal gap in pixel to use for the label, i.e.,
78 * the step lock sized labels will start smaller by that amount
79 * (the use case would be to align controls that start at a
80 * different horisontal position)
81 */
82 public ConfigItem(MetaInfo<E> info, int nhgap) {
83 this.setLayout(new BorderLayout());
84
85 // TODO: support arrays
86 Format fmt = info.getFormat();
87 if (info.isArray()) {
88 fmt = Format.STRING;
89 }
90
91 switch (fmt) {
92 case BOOLEAN:
93 addBooleanField(info, nhgap);
94 break;
95 case COLOR:
96 addColorField(info, nhgap);
97 break;
98 case FILE:
99 addBrowseField(info, nhgap, false);
100 break;
101 case DIRECTORY:
102 addBrowseField(info, nhgap, true);
103 break;
104 case COMBO_LIST:
105 addComboboxField(info, nhgap, true);
106 break;
107 case FIXED_LIST:
108 addComboboxField(info, nhgap, false);
109 break;
110 case INT:
111 addIntField(info, nhgap);
112 break;
113 case PASSWORD:
114 addPasswordField(info, nhgap);
115 break;
116 case STRING:
117 case LOCALE: // TODO?
118 default:
119 addStringField(info, nhgap);
120 break;
121 }
122 }
123
124 private void reload(Object value) {
125 // We consider "" and NULL to be equals
126 if ("".equals(value)) {
127 value = null;
128 }
129 orig = value;
130 }
131
132 private boolean isChanged(Object newValue) {
133 // We consider "" and NULL to be equals
134 if ("".equals(newValue)) {
135 newValue = null;
136 }
137
138 if (newValue == null) {
139 return orig != null;
140 }
141
142 return !newValue.equals(orig);
143 }
144
145 private void addStringField(final MetaInfo<E> info, int nhgap) {
146 final JTextField field = new JTextField();
147 field.setToolTipText(info.getDescription());
148 String value = info.getString(false);
149 reload(value);
150 field.setText(value);
151
152 info.addReloadedListener(new Runnable() {
153 @Override
154 public void run() {
155 String value = info.getString(false);
156 reload(value);
157 field.setText(value);
158 }
159 });
160 info.addSaveListener(new Runnable() {
161 @Override
162 public void run() {
163 String value = field.getText();
164 if (isChanged(value)) {
165 info.setString(value);
166 }
167 }
168 });
169
170 this.add(label(info, nhgap), BorderLayout.WEST);
171 this.add(field, BorderLayout.CENTER);
172
173 setPreferredSize(field);
174 }
175
176 private void addBooleanField(final MetaInfo<E> info, int nhgap) {
177 final JCheckBox field = new JCheckBox();
178 field.setToolTipText(info.getDescription());
179 Boolean state = info.getBoolean(true);
180
181 // Should not happen!
182 if (state == null) {
183 System.err
184 .println("No default value given for BOOLEAN parameter \""
185 + info.getName() + "\", we consider it is FALSE");
186 state = false;
187 }
188
189 reload(state);
190 field.setSelected(state);
191
192 info.addReloadedListener(new Runnable() {
193 @Override
194 public void run() {
195 Boolean state = info.getBoolean(true);
196 if (state == null) {
197 state = false;
198 }
199
200 reload(state);
201 field.setSelected(state);
202 }
203 });
204 info.addSaveListener(new Runnable() {
205 @Override
206 public void run() {
207 boolean state = field.isSelected();
208 if (isChanged(state)) {
209 info.setBoolean(state);
210 }
211 }
212 });
213
214 this.add(label(info, nhgap), BorderLayout.WEST);
215 this.add(field, BorderLayout.CENTER);
216
217 setPreferredSize(field);
218 }
219
220 private void addColorField(final MetaInfo<E> info, int nhgap) {
221 final JTextField field = new JTextField();
222 field.setToolTipText(info.getDescription());
223 String value = info.getString(false);
224 reload(value);
225 field.setText(value);
226
227 info.addReloadedListener(new Runnable() {
228 @Override
229 public void run() {
230 String value = info.getString(false);
231 reload(value);
232 field.setText(value);
233 }
234 });
235 info.addSaveListener(new Runnable() {
236 @Override
237 public void run() {
238 String value = field.getText();
239 if (isChanged(value)) {
240 info.setString(value);
241 }
242 }
243 });
244
245 this.add(label(info, nhgap), BorderLayout.WEST);
246 JPanel pane = new JPanel(new BorderLayout());
247
248 final JButton colorWheel = new JButton();
249 colorWheel.setIcon(getIcon(17, info.getColor(true)));
250 colorWheel.addActionListener(new ActionListener() {
251 @Override
252 public void actionPerformed(ActionEvent e) {
253 Integer icol = info.getColor(true);
254 if (icol == null) {
255 icol = new Color(255, 255, 255, 255).getRGB();
256 }
257 Color initialColor = new Color(icol, true);
258 Color newColor = JColorChooser.showDialog(ConfigItem.this,
259 info.getName(), initialColor);
260 if (newColor != null) {
261 info.setColor(newColor.getRGB());
262 field.setText(info.getString(false));
263 colorWheel.setIcon(getIcon(17, info.getColor(true)));
264 }
265 }
266 });
267 pane.add(colorWheel, BorderLayout.WEST);
268 pane.add(field, BorderLayout.CENTER);
269 this.add(pane, BorderLayout.CENTER);
270
271 setPreferredSize(pane);
272 }
273
274 private void addBrowseField(final MetaInfo<E> info, int nhgap,
275 final boolean dir) {
276 final JTextField field = new JTextField();
277 field.setToolTipText(info.getDescription());
278 String value = info.getString(false);
279 reload(value);
280 field.setText(value);
281
282 info.addReloadedListener(new Runnable() {
283 @Override
284 public void run() {
285 String value = info.getString(false);
286 reload(value);
287 field.setText(value);
288 }
289 });
290 info.addSaveListener(new Runnable() {
291 @Override
292 public void run() {
293 String value = field.getText();
294 if (isChanged(value)) {
295 info.setString(value);
296 }
297 }
298 });
299
300 JButton browseButton = new JButton("...");
301 browseButton.addActionListener(new ActionListener() {
302 @Override
303 public void actionPerformed(ActionEvent e) {
304 JFileChooser chooser = new JFileChooser();
305 chooser.setCurrentDirectory(null);
306 chooser.setFileSelectionMode(dir ? JFileChooser.DIRECTORIES_ONLY
307 : JFileChooser.FILES_ONLY);
308 if (chooser.showOpenDialog(ConfigItem.this) == JFileChooser.APPROVE_OPTION) {
309 File file = chooser.getSelectedFile();
310 if (file != null) {
311 String value = file.getAbsolutePath();
312 if (isChanged(value)) {
313 info.setString(value);
314 }
315 field.setText(value);
316 }
317 }
318 }
319 });
320
321 JPanel pane = new JPanel(new BorderLayout());
322 this.add(label(info, nhgap), BorderLayout.WEST);
323 pane.add(browseButton, BorderLayout.WEST);
324 pane.add(field, BorderLayout.CENTER);
325 this.add(pane, BorderLayout.CENTER);
326
327 setPreferredSize(pane);
328 }
329
330 private void addComboboxField(final MetaInfo<E> info, int nhgap,
331 boolean editable) {
332 // rawtypes for Java 1.6 (and 1.7 ?) support
333 @SuppressWarnings({ "rawtypes", "unchecked" })
334 final JComboBox field = new JComboBox(info.getAllowedValues());
335 field.setEditable(editable);
336 String value = info.getString(false);
337 reload(value);
338 field.setSelectedItem(value);
339
340 info.addReloadedListener(new Runnable() {
341 @Override
342 public void run() {
343 String value = info.getString(false);
344 reload(value);
345 field.setSelectedItem(value);
346 }
347 });
348 info.addSaveListener(new Runnable() {
349 @Override
350 public void run() {
351 Object item = field.getSelectedItem();
352 String value = item == null ? null : item.toString();
353 if (isChanged(value)) {
354 info.setString(value);
355 }
356 }
357 });
358
359 this.add(label(info, nhgap), BorderLayout.WEST);
360 this.add(field, BorderLayout.CENTER);
361
362 setPreferredSize(field);
363 }
364
365 private void addPasswordField(final MetaInfo<E> info, int nhgap) {
366 final JPasswordField field = new JPasswordField();
367 field.setToolTipText(info.getDescription());
368 String value = info.getString(false);
369 reload(value);
370 field.setText(value);
371
372 info.addReloadedListener(new Runnable() {
373 @Override
374 public void run() {
375 String value = info.getString(false);
376 reload(value);
377 field.setText(value);
378 }
379 });
380 info.addSaveListener(new Runnable() {
381 @Override
382 public void run() {
383 String value = new String(field.getPassword());
384 if (isChanged(value)) {
385 info.setString(value);
386 }
387 }
388 });
389
390 this.add(label(info, nhgap), BorderLayout.WEST);
391 this.add(field, BorderLayout.CENTER);
392
393 setPreferredSize(field);
394 }
395
396 private void addIntField(final MetaInfo<E> info, int nhgap) {
397 final JSpinner field = new JSpinner();
398 field.setToolTipText(info.getDescription());
399 int value = info.getInteger(true) == null ? 0 : info.getInteger(true);
400 reload(value);
401 field.setValue(value);
402
403 info.addReloadedListener(new Runnable() {
404 @Override
405 public void run() {
406 int value = info.getInteger(true) == null ? 0 : info
407 .getInteger(true);
408 reload(value);
409 field.setValue(value);
410 }
411 });
412 info.addSaveListener(new Runnable() {
413 @Override
414 public void run() {
415 int value = field.getValue() == null ? 0 : (Integer) field
416 .getValue();
417 if (isChanged(value)) {
418 info.setInteger(value);
419 }
420 }
421 });
422
423 this.add(label(info, nhgap), BorderLayout.WEST);
424 this.add(field, BorderLayout.CENTER);
425
426 setPreferredSize(field);
427 }
428
429 /**
430 * Create a label which width is constrained in lock steps.
431 *
432 * @param info
433 * the {@link MetaInfo} for which we want to add a label
434 * @param nhgap
435 * negative horisontal gap in pixel to use for the label, i.e.,
436 * the step lock sized labels will start smaller by that amount
437 * (the use case would be to align controls that start at a
438 * different horisontal position)
439 *
440 * @return the label
441 */
442 private JComponent label(final MetaInfo<E> info, int nhgap) {
443 final JLabel label = new JLabel(info.getName());
444
445 Dimension ps = label.getPreferredSize();
446 if (ps == null) {
447 ps = label.getSize();
448 }
449
450 ps.height = Math.max(ps.height, getMinimumHeight());
451
452 int w = ps.width;
453 int step = 150;
454 for (int i = 2 * step - nhgap; i < 10 * step; i += step) {
455 if (w < i) {
456 w = i;
457 break;
458 }
459 }
460
461 final Runnable showInfo = new Runnable() {
462 @Override
463 public void run() {
464 StringBuilder builder = new StringBuilder();
465 String text = (info.getDescription().replace("\\n", "\n"))
466 .trim();
467 for (String line : StringUtils.justifyText(text, 80,
468 Alignment.LEFT)) {
469 if (builder.length() > 0) {
470 builder.append("\n");
471 }
472 builder.append(line);
473 }
474 text = builder.toString();
475 JOptionPane.showMessageDialog(ConfigItem.this, text,
476 info.getName(), JOptionPane.INFORMATION_MESSAGE);
477 }
478 };
479
480 JLabel help = new JLabel("");
481 help.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
482 try {
483 Image img = new Image(infoImage64);
484 try {
485 BufferedImage bImg = ImageUtilsAwt.fromImage(img);
486 help.setIcon(new ImageIcon(bImg));
487 } finally {
488 img.close();
489 }
490 } catch (IOException e) {
491 // This is an hard-coded image, should not happen
492 help.setText("?");
493 }
494
495 help.addMouseListener(new MouseAdapter() {
496 @Override
497 public void mouseClicked(MouseEvent e) {
498 showInfo.run();
499 }
500 });
501
502 JPanel pane2 = new JPanel(new BorderLayout());
503 pane2.add(help, BorderLayout.WEST);
504 pane2.add(new JLabel(" "), BorderLayout.CENTER);
505
506 JPanel contentPane = new JPanel(new BorderLayout());
507 contentPane.add(label, BorderLayout.WEST);
508 contentPane.add(pane2, BorderLayout.CENTER);
509
510 ps.width = w + 30; // 30 for the (?) sign
511 contentPane.setSize(ps);
512 contentPane.setPreferredSize(ps);
513
514 JPanel pane = new JPanel(new BorderLayout());
515 pane.add(contentPane, BorderLayout.NORTH);
516
517 return pane;
518 }
519
520 /**
521 * Return an {@link Icon} to use as a colour badge for the colour field
522 * controls.
523 *
524 * @param size
525 * the size of the badge
526 * @param color
527 * the colour of the badge, which can be NULL (will return
528 * transparent white)
529 *
530 * @return the badge
531 */
532 private Icon getIcon(int size, Integer color) {
533 // Allow null values
534 if (color == null) {
535 color = new Color(255, 255, 255, 255).getRGB();
536 }
537
538 Color c = new Color(color, true);
539 int avg = (c.getRed() + c.getGreen() + c.getBlue()) / 3;
540 Color border = (avg >= 128 ? Color.BLACK : Color.WHITE);
541
542 BufferedImage img = new BufferedImage(size, size,
543 BufferedImage.TYPE_4BYTE_ABGR);
544
545 Graphics2D g = img.createGraphics();
546 try {
547 g.setColor(c);
548 g.fillRect(0, 0, img.getWidth(), img.getHeight());
549 g.setColor(border);
550 g.drawRect(0, 0, img.getWidth() - 1, img.getHeight() - 1);
551 } finally {
552 g.dispose();
553 }
554
555 return new ImageIcon(img);
556 }
557
558 private void setPreferredSize(JComponent field) {
559 int height = Math
560 .max(getMinimumHeight(), field.getMinimumSize().height);
561 setPreferredSize(new Dimension(200, height));
562 }
563
564 static private int getMinimumHeight() {
565 if (minimumHeight < 0) {
566 minimumHeight = new JTextField("Test").getMinimumSize().height;
567 }
568
569 return minimumHeight;
570 }
571 }