Improve cache + jdoc, traces
[fanfix.git] / src / be / nikiroo / utils / ui / ConfigItem.java
CommitLineData
d350b96b
NR
1package be.nikiroo.utils.ui;
2
3import java.awt.BorderLayout;
424dcb0d 4import java.awt.Color;
daf0fd5a 5import java.awt.Cursor;
c637d2e0 6import java.awt.Dimension;
424dcb0d
NR
7import java.awt.Graphics2D;
8import java.awt.event.ActionEvent;
9import java.awt.event.ActionListener;
daf0fd5a
NR
10import java.awt.event.MouseAdapter;
11import java.awt.event.MouseEvent;
424dcb0d 12import java.awt.image.BufferedImage;
0877d6f5 13import java.io.File;
daf0fd5a 14import java.io.IOException;
424dcb0d
NR
15
16import javax.swing.Icon;
17import javax.swing.ImageIcon;
18import javax.swing.JButton;
9e834013 19import javax.swing.JCheckBox;
424dcb0d 20import javax.swing.JColorChooser;
0877d6f5
NR
21import javax.swing.JComboBox;
22import javax.swing.JComponent;
23import javax.swing.JFileChooser;
8517b60c 24import javax.swing.JLabel;
0877d6f5 25import javax.swing.JOptionPane;
d350b96b 26import javax.swing.JPanel;
0877d6f5 27import javax.swing.JPasswordField;
d18e136e 28import javax.swing.JSpinner;
d350b96b 29import javax.swing.JTextField;
d350b96b 30
daf0fd5a 31import be.nikiroo.utils.Image;
0877d6f5
NR
32import be.nikiroo.utils.StringUtils;
33import be.nikiroo.utils.StringUtils.Alignment;
d350b96b 34import be.nikiroo.utils.resources.Bundle;
76b51de9 35import be.nikiroo.utils.resources.Meta.Format;
9e834013 36import be.nikiroo.utils.resources.MetaInfo;
d350b96b
NR
37
38/**
39 * A graphical item that reflect a configuration option from the given
40 * {@link Bundle}.
76b51de9
NR
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.
d350b96b
NR
45 *
46 * @author niki
db31c358 47 *
d350b96b
NR
48 * @param <E>
49 * the type of {@link Bundle} to edit
50 */
51public class ConfigItem<E extends Enum<E>> extends JPanel {
52 private static final long serialVersionUID = 1L;
db31c358 53
e95f4fb6
NR
54 private static int minimumHeight = -1;
55
daf0fd5a
NR
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
fde375c1
NR
68 /** The original value before current changes. */
69 private Object orig;
70
76b51de9
NR
71 /**
72 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
73 *
74 * @param info
75 * the {@link MetaInfo}
d18e136e
NR
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)
76b51de9 81 */
d18e136e 82 public ConfigItem(MetaInfo<E> info, int nhgap) {
d350b96b 83 this.setLayout(new BorderLayout());
d350b96b 84
76b51de9
NR
85 // TODO: support arrays
86 Format fmt = info.getFormat();
87 if (info.isArray()) {
88 fmt = Format.STRING;
89 }
90
91 switch (fmt) {
0877d6f5 92 case BOOLEAN:
d18e136e 93 addBooleanField(info, nhgap);
0877d6f5
NR
94 break;
95 case COLOR:
d18e136e 96 addColorField(info, nhgap);
0877d6f5
NR
97 break;
98 case FILE:
d18e136e 99 addBrowseField(info, nhgap, false);
0877d6f5
NR
100 break;
101 case DIRECTORY:
d18e136e 102 addBrowseField(info, nhgap, true);
0877d6f5
NR
103 break;
104 case COMBO_LIST:
d18e136e 105 addComboboxField(info, nhgap, true);
0877d6f5
NR
106 break;
107 case FIXED_LIST:
d18e136e 108 addComboboxField(info, nhgap, false);
0877d6f5
NR
109 break;
110 case INT:
d18e136e 111 addIntField(info, nhgap);
0877d6f5
NR
112 break;
113 case PASSWORD:
d18e136e 114 addPasswordField(info, nhgap);
0877d6f5
NR
115 break;
116 case STRING:
117 case LOCALE: // TODO?
118 default:
d18e136e 119 addStringField(info, nhgap);
0877d6f5
NR
120 break;
121 }
122 }
d350b96b 123
fde375c1
NR
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
d18e136e 145 private void addStringField(final MetaInfo<E> info, int nhgap) {
0877d6f5
NR
146 final JTextField field = new JTextField();
147 field.setToolTipText(info.getDescription());
fde375c1
NR
148 String value = info.getString(false);
149 reload(value);
150 field.setText(value);
0877d6f5
NR
151
152 info.addReloadedListener(new Runnable() {
153 @Override
154 public void run() {
fde375c1
NR
155 String value = info.getString(false);
156 reload(value);
157 field.setText(value);
0877d6f5
NR
158 }
159 });
160 info.addSaveListener(new Runnable() {
161 @Override
162 public void run() {
fde375c1
NR
163 String value = field.getText();
164 if (isChanged(value)) {
165 info.setString(value);
166 }
9e834013 167 }
0877d6f5 168 });
d350b96b 169
d18e136e 170 this.add(label(info, nhgap), BorderLayout.WEST);
0877d6f5 171 this.add(field, BorderLayout.CENTER);
d18e136e
NR
172
173 setPreferredSize(field);
0877d6f5 174 }
9e834013 175
d18e136e 176 private void addBooleanField(final MetaInfo<E> info, int nhgap) {
0877d6f5
NR
177 final JCheckBox field = new JCheckBox();
178 field.setToolTipText(info.getDescription());
d18e136e 179 Boolean state = info.getBoolean(true);
8517b60c 180
0877d6f5
NR
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
fde375c1 189 reload(state);
0877d6f5
NR
190 field.setSelected(state);
191
192 info.addReloadedListener(new Runnable() {
193 @Override
194 public void run() {
d18e136e 195 Boolean state = info.getBoolean(true);
0877d6f5
NR
196 if (state == null) {
197 state = false;
9e834013 198 }
0877d6f5 199
fde375c1 200 reload(state);
0877d6f5
NR
201 field.setSelected(state);
202 }
203 });
204 info.addSaveListener(new Runnable() {
205 @Override
206 public void run() {
fde375c1
NR
207 boolean state = field.isSelected();
208 if (isChanged(state)) {
209 info.setBoolean(state);
210 }
0877d6f5
NR
211 }
212 });
213
d18e136e 214 this.add(label(info, nhgap), BorderLayout.WEST);
0877d6f5 215 this.add(field, BorderLayout.CENTER);
d18e136e
NR
216
217 setPreferredSize(field);
0877d6f5
NR
218 }
219
d18e136e 220 private void addColorField(final MetaInfo<E> info, int nhgap) {
0877d6f5
NR
221 final JTextField field = new JTextField();
222 field.setToolTipText(info.getDescription());
fde375c1
NR
223 String value = info.getString(false);
224 reload(value);
225 field.setText(value);
0877d6f5
NR
226
227 info.addReloadedListener(new Runnable() {
228 @Override
229 public void run() {
fde375c1
NR
230 String value = info.getString(false);
231 reload(value);
232 field.setText(value);
0877d6f5
NR
233 }
234 });
235 info.addSaveListener(new Runnable() {
236 @Override
237 public void run() {
fde375c1
NR
238 String value = field.getText();
239 if (isChanged(value)) {
240 info.setString(value);
241 }
0877d6f5
NR
242 }
243 });
244
d18e136e 245 this.add(label(info, nhgap), BorderLayout.WEST);
0877d6f5
NR
246 JPanel pane = new JPanel(new BorderLayout());
247
248 final JButton colorWheel = new JButton();
d18e136e 249 colorWheel.setIcon(getIcon(17, info.getColor(true)));
0877d6f5
NR
250 colorWheel.addActionListener(new ActionListener() {
251 @Override
252 public void actionPerformed(ActionEvent e) {
fde375c1
NR
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);
0877d6f5
NR
258 Color newColor = JColorChooser.showDialog(ConfigItem.this,
259 info.getName(), initialColor);
260 if (newColor != null) {
261 info.setColor(newColor.getRGB());
d18e136e
NR
262 field.setText(info.getString(false));
263 colorWheel.setIcon(getIcon(17, info.getColor(true)));
424dcb0d 264 }
0877d6f5
NR
265 }
266 });
267 pane.add(colorWheel, BorderLayout.WEST);
268 pane.add(field, BorderLayout.CENTER);
269 this.add(pane, BorderLayout.CENTER);
d18e136e
NR
270
271 setPreferredSize(pane);
0877d6f5
NR
272 }
273
d18e136e
NR
274 private void addBrowseField(final MetaInfo<E> info, int nhgap,
275 final boolean dir) {
0877d6f5
NR
276 final JTextField field = new JTextField();
277 field.setToolTipText(info.getDescription());
fde375c1
NR
278 String value = info.getString(false);
279 reload(value);
280 field.setText(value);
0877d6f5
NR
281
282 info.addReloadedListener(new Runnable() {
283 @Override
284 public void run() {
fde375c1
NR
285 String value = info.getString(false);
286 reload(value);
287 field.setText(value);
0877d6f5
NR
288 }
289 });
290 info.addSaveListener(new Runnable() {
291 @Override
292 public void run() {
fde375c1
NR
293 String value = field.getText();
294 if (isChanged(value)) {
295 info.setString(value);
296 }
0877d6f5
NR
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) {
fde375c1
NR
311 String value = file.getAbsolutePath();
312 if (isChanged(value)) {
313 info.setString(value);
314 }
315 field.setText(value);
424dcb0d
NR
316 }
317 }
0877d6f5
NR
318 }
319 });
320
321 JPanel pane = new JPanel(new BorderLayout());
d18e136e 322 this.add(label(info, nhgap), BorderLayout.WEST);
0877d6f5
NR
323 pane.add(browseButton, BorderLayout.WEST);
324 pane.add(field, BorderLayout.CENTER);
325 this.add(pane, BorderLayout.CENTER);
d18e136e
NR
326
327 setPreferredSize(pane);
0877d6f5
NR
328 }
329
d18e136e
NR
330 private void addComboboxField(final MetaInfo<E> info, int nhgap,
331 boolean editable) {
0877d6f5
NR
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);
fde375c1
NR
336 String value = info.getString(false);
337 reload(value);
338 field.setSelectedItem(value);
0877d6f5
NR
339
340 info.addReloadedListener(new Runnable() {
341 @Override
342 public void run() {
fde375c1
NR
343 String value = info.getString(false);
344 reload(value);
345 field.setSelectedItem(value);
0877d6f5
NR
346 }
347 });
348 info.addSaveListener(new Runnable() {
349 @Override
350 public void run() {
9cc3f77b
NR
351 Object item = field.getSelectedItem();
352 String value = item == null ? null : item.toString();
fde375c1
NR
353 if (isChanged(value)) {
354 info.setString(value);
355 }
0877d6f5
NR
356 }
357 });
358
d18e136e 359 this.add(label(info, nhgap), BorderLayout.WEST);
0877d6f5 360 this.add(field, BorderLayout.CENTER);
d18e136e
NR
361
362 setPreferredSize(field);
0877d6f5
NR
363 }
364
d18e136e 365 private void addPasswordField(final MetaInfo<E> info, int nhgap) {
0877d6f5
NR
366 final JPasswordField field = new JPasswordField();
367 field.setToolTipText(info.getDescription());
fde375c1
NR
368 String value = info.getString(false);
369 reload(value);
370 field.setText(value);
0877d6f5
NR
371
372 info.addReloadedListener(new Runnable() {
373 @Override
374 public void run() {
fde375c1
NR
375 String value = info.getString(false);
376 reload(value);
377 field.setText(value);
0877d6f5
NR
378 }
379 });
380 info.addSaveListener(new Runnable() {
381 @Override
382 public void run() {
fde375c1
NR
383 String value = new String(field.getPassword());
384 if (isChanged(value)) {
385 info.setString(value);
386 }
0877d6f5
NR
387 }
388 });
389
d18e136e 390 this.add(label(info, nhgap), BorderLayout.WEST);
0877d6f5 391 this.add(field, BorderLayout.CENTER);
d18e136e
NR
392
393 setPreferredSize(field);
0877d6f5
NR
394 }
395
d18e136e
NR
396 private void addIntField(final MetaInfo<E> info, int nhgap) {
397 final JSpinner field = new JSpinner();
0877d6f5 398 field.setToolTipText(info.getDescription());
fde375c1
NR
399 int value = info.getInteger(true) == null ? 0 : info.getInteger(true);
400 reload(value);
401 field.setValue(value);
0877d6f5
NR
402
403 info.addReloadedListener(new Runnable() {
404 @Override
405 public void run() {
fde375c1
NR
406 int value = info.getInteger(true) == null ? 0 : info
407 .getInteger(true);
408 reload(value);
409 field.setValue(value);
0877d6f5
NR
410 }
411 });
412 info.addSaveListener(new Runnable() {
413 @Override
414 public void run() {
fde375c1
NR
415 int value = field.getValue() == null ? 0 : (Integer) field
416 .getValue();
417 if (isChanged(value)) {
418 info.setInteger(value);
8517b60c 419 }
0877d6f5
NR
420 }
421 });
9e834013 422
d18e136e
NR
423 this.add(label(info, nhgap), BorderLayout.WEST);
424 this.add(field, BorderLayout.CENTER);
0877d6f5 425
d18e136e 426 setPreferredSize(field);
d350b96b 427 }
c637d2e0
NR
428
429 /**
430 * Create a label which width is constrained in lock steps.
431 *
0877d6f5
NR
432 * @param info
433 * the {@link MetaInfo} for which we want to add a label
d18e136e
NR
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)
c637d2e0
NR
439 *
440 * @return the label
441 */
d18e136e 442 private JComponent label(final MetaInfo<E> info, int nhgap) {
0877d6f5 443 final JLabel label = new JLabel(info.getName());
c637d2e0
NR
444
445 Dimension ps = label.getPreferredSize();
446 if (ps == null) {
447 ps = label.getSize();
448 }
449
e95f4fb6
NR
450 ps.height = Math.max(ps.height, getMinimumHeight());
451
c637d2e0 452 int w = ps.width;
daf0fd5a 453 int step = 150;
d18e136e 454 for (int i = 2 * step - nhgap; i < 10 * step; i += step) {
c637d2e0
NR
455 if (w < i) {
456 w = i;
457 break;
458 }
459 }
460
daf0fd5a 461 final Runnable showInfo = new Runnable() {
0877d6f5 462 @Override
daf0fd5a 463 public void run() {
0877d6f5 464 StringBuilder builder = new StringBuilder();
d18e136e
NR
465 String text = (info.getDescription().replace("\\n", "\n"))
466 .trim();
0877d6f5
NR
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();
daf0fd5a
NR
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();
0877d6f5
NR
499 }
500 });
501
502 JPanel pane2 = new JPanel(new BorderLayout());
503 pane2.add(help, BorderLayout.WEST);
504 pane2.add(new JLabel(" "), BorderLayout.CENTER);
505
e95f4fb6
NR
506 JPanel contentPane = new JPanel(new BorderLayout());
507 contentPane.add(label, BorderLayout.WEST);
508 contentPane.add(pane2, BorderLayout.CENTER);
0877d6f5
NR
509
510 ps.width = w + 30; // 30 for the (?) sign
e95f4fb6
NR
511 contentPane.setSize(ps);
512 contentPane.setPreferredSize(ps);
513
514 JPanel pane = new JPanel(new BorderLayout());
515 pane.add(contentPane, BorderLayout.NORTH);
c637d2e0 516
0877d6f5 517 return pane;
c637d2e0 518 }
424dcb0d
NR
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
fde375c1
NR
527 * the colour of the badge, which can be NULL (will return
528 * transparent white)
424dcb0d
NR
529 *
530 * @return the badge
531 */
fde375c1
NR
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
424dcb0d
NR
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 }
d18e136e
NR
557
558 private void setPreferredSize(JComponent field) {
e95f4fb6
NR
559 int height = Math
560 .max(getMinimumHeight(), field.getMinimumSize().height);
d18e136e
NR
561 setPreferredSize(new Dimension(200, height));
562 }
e95f4fb6
NR
563
564 static private int getMinimumHeight() {
565 if (minimumHeight < 0) {
566 minimumHeight = new JTextField("Test").getMinimumSize().height;
567 }
568
569 return minimumHeight;
570 }
d350b96b 571}