ConfigItem: assure label placement if taller pane
[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 String value = field.getSelectedItem().toString();
352 if (isChanged(value)) {
353 info.setString(value);
354 }
355 }
356 });
357
358 this.add(label(info, nhgap), BorderLayout.WEST);
359 this.add(field, BorderLayout.CENTER);
360
361 setPreferredSize(field);
362 }
363
364 private void addPasswordField(final MetaInfo<E> info, int nhgap) {
365 final JPasswordField field = new JPasswordField();
366 field.setToolTipText(info.getDescription());
367 String value = info.getString(false);
368 reload(value);
369 field.setText(value);
370
371 info.addReloadedListener(new Runnable() {
372 @Override
373 public void run() {
374 String value = info.getString(false);
375 reload(value);
376 field.setText(value);
377 }
378 });
379 info.addSaveListener(new Runnable() {
380 @Override
381 public void run() {
382 String value = new String(field.getPassword());
383 if (isChanged(value)) {
384 info.setString(value);
385 }
386 }
387 });
388
389 this.add(label(info, nhgap), BorderLayout.WEST);
390 this.add(field, BorderLayout.CENTER);
391
392 setPreferredSize(field);
393 }
394
395 private void addIntField(final MetaInfo<E> info, int nhgap) {
396 final JSpinner field = new JSpinner();
397 field.setToolTipText(info.getDescription());
398 int value = info.getInteger(true) == null ? 0 : info.getInteger(true);
399 reload(value);
400 field.setValue(value);
401
402 info.addReloadedListener(new Runnable() {
403 @Override
404 public void run() {
405 int value = info.getInteger(true) == null ? 0 : info
406 .getInteger(true);
407 reload(value);
408 field.setValue(value);
409 }
410 });
411 info.addSaveListener(new Runnable() {
412 @Override
413 public void run() {
414 int value = field.getValue() == null ? 0 : (Integer) field
415 .getValue();
416 if (isChanged(value)) {
417 info.setInteger(value);
418 }
419 }
420 });
421
422 this.add(label(info, nhgap), BorderLayout.WEST);
423 this.add(field, BorderLayout.CENTER);
424
425 setPreferredSize(field);
426 }
427
428 /**
429 * Create a label which width is constrained in lock steps.
430 *
431 * @param info
432 * the {@link MetaInfo} for which we want to add a label
433 * @param nhgap
434 * negative horisontal gap in pixel to use for the label, i.e.,
435 * the step lock sized labels will start smaller by that amount
436 * (the use case would be to align controls that start at a
437 * different horisontal position)
438 *
439 * @return the label
440 */
441 private JComponent label(final MetaInfo<E> info, int nhgap) {
442 final JLabel label = new JLabel(info.getName());
443
444 Dimension ps = label.getPreferredSize();
445 if (ps == null) {
446 ps = label.getSize();
447 }
448
449 ps.height = Math.max(ps.height, getMinimumHeight());
450
451 int w = ps.width;
452 int step = 150;
453 for (int i = 2 * step - nhgap; i < 10 * step; i += step) {
454 if (w < i) {
455 w = i;
456 break;
457 }
458 }
459
460 final Runnable showInfo = new Runnable() {
461 @Override
462 public void run() {
463 StringBuilder builder = new StringBuilder();
464 String text = (info.getDescription().replace("\\n", "\n"))
465 .trim();
466 for (String line : StringUtils.justifyText(text, 80,
467 Alignment.LEFT)) {
468 if (builder.length() > 0) {
469 builder.append("\n");
470 }
471 builder.append(line);
472 }
473 text = builder.toString();
474 JOptionPane.showMessageDialog(ConfigItem.this, text,
475 info.getName(), JOptionPane.INFORMATION_MESSAGE);
476 }
477 };
478
479 JLabel help = new JLabel("");
480 help.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
481 try {
482 Image img = new Image(infoImage64);
483 try {
484 BufferedImage bImg = ImageUtilsAwt.fromImage(img);
485 help.setIcon(new ImageIcon(bImg));
486 } finally {
487 img.close();
488 }
489 } catch (IOException e) {
490 // This is an hard-coded image, should not happen
491 help.setText("?");
492 }
493
494 help.addMouseListener(new MouseAdapter() {
495 @Override
496 public void mouseClicked(MouseEvent e) {
497 showInfo.run();
498 }
499 });
500
501 JPanel pane2 = new JPanel(new BorderLayout());
502 pane2.add(help, BorderLayout.WEST);
503 pane2.add(new JLabel(" "), BorderLayout.CENTER);
504
505 JPanel contentPane = new JPanel(new BorderLayout());
506 contentPane.add(label, BorderLayout.WEST);
507 contentPane.add(pane2, BorderLayout.CENTER);
508
509 ps.width = w + 30; // 30 for the (?) sign
510 contentPane.setSize(ps);
511 contentPane.setPreferredSize(ps);
512
513 JPanel pane = new JPanel(new BorderLayout());
514 pane.add(contentPane, BorderLayout.NORTH);
515
516 return pane;
517 }
518
519 /**
520 * Return an {@link Icon} to use as a colour badge for the colour field
521 * controls.
522 *
523 * @param size
524 * the size of the badge
525 * @param color
526 * the colour of the badge, which can be NULL (will return
527 * transparent white)
528 *
529 * @return the badge
530 */
531 private Icon getIcon(int size, Integer color) {
532 // Allow null values
533 if (color == null) {
534 color = new Color(255, 255, 255, 255).getRGB();
535 }
536
537 Color c = new Color(color, true);
538 int avg = (c.getRed() + c.getGreen() + c.getBlue()) / 3;
539 Color border = (avg >= 128 ? Color.BLACK : Color.WHITE);
540
541 BufferedImage img = new BufferedImage(size, size,
542 BufferedImage.TYPE_4BYTE_ABGR);
543
544 Graphics2D g = img.createGraphics();
545 try {
546 g.setColor(c);
547 g.fillRect(0, 0, img.getWidth(), img.getHeight());
548 g.setColor(border);
549 g.drawRect(0, 0, img.getWidth() - 1, img.getHeight() - 1);
550 } finally {
551 g.dispose();
552 }
553
554 return new ImageIcon(img);
555 }
556
557 private void setPreferredSize(JComponent field) {
558 int height = Math
559 .max(getMinimumHeight(), field.getMinimumSize().height);
560 setPreferredSize(new Dimension(200, height));
561 }
562
563 static private int getMinimumHeight() {
564 if (minimumHeight < 0) {
565 minimumHeight = new JTextField("Test").getMinimumSize().height;
566 }
567
568 return minimumHeight;
569 }
570 }