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