1 package be
.nikiroo
.utils
.ui
;
3 import java
.awt
.BorderLayout
;
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
;
14 import java
.io
.IOException
;
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
;
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
;
39 * A graphical item that reflect a configuration option from the given
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.
49 * the type of {@link Bundle} to edit
51 public class ConfigItem
<E
extends Enum
<E
>> extends JPanel
{
52 private static final long serialVersionUID
= 1L;
54 private static int minimumHeight
= -1;
56 /** A small (?) blue in PNG, base64 encoded. */
57 private static String infoImage64
= //
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";
68 /** The original value before current changes. */
72 * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
75 * the {@link MetaInfo}
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)
82 public ConfigItem(MetaInfo
<E
> info
, int nhgap
) {
83 this.setLayout(new BorderLayout());
85 // TODO: support arrays
86 Format fmt
= info
.getFormat();
93 addBooleanField(info
, nhgap
);
96 addColorField(info
, nhgap
);
99 addBrowseField(info
, nhgap
, false);
102 addBrowseField(info
, nhgap
, true);
105 addComboboxField(info
, nhgap
, true);
108 addComboboxField(info
, nhgap
, false);
111 addIntField(info
, nhgap
);
114 addPasswordField(info
, nhgap
);
117 case LOCALE
: // TODO?
119 addStringField(info
, nhgap
);
124 private void reload(Object value
) {
125 // We consider "" and NULL to be equals
126 if ("".equals(value
)) {
132 private boolean isChanged(Object newValue
) {
133 // We consider "" and NULL to be equals
134 if ("".equals(newValue
)) {
138 if (newValue
== null) {
142 return !newValue
.equals(orig
);
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);
150 field
.setText(value
);
152 info
.addReloadedListener(new Runnable() {
155 String value
= info
.getString(false);
157 field
.setText(value
);
160 info
.addSaveListener(new Runnable() {
163 String value
= field
.getText();
164 if (isChanged(value
)) {
165 info
.setString(value
);
170 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
171 this.add(field
, BorderLayout
.CENTER
);
173 setPreferredSize(field
);
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);
181 // Should not happen!
184 .println("No default value given for BOOLEAN parameter \""
185 + info
.getName() + "\", we consider it is FALSE");
190 field
.setSelected(state
);
192 info
.addReloadedListener(new Runnable() {
195 Boolean state
= info
.getBoolean(true);
201 field
.setSelected(state
);
204 info
.addSaveListener(new Runnable() {
207 boolean state
= field
.isSelected();
208 if (isChanged(state
)) {
209 info
.setBoolean(state
);
214 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
215 this.add(field
, BorderLayout
.CENTER
);
217 setPreferredSize(field
);
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);
225 field
.setText(value
);
227 info
.addReloadedListener(new Runnable() {
230 String value
= info
.getString(false);
232 field
.setText(value
);
235 info
.addSaveListener(new Runnable() {
238 String value
= field
.getText();
239 if (isChanged(value
)) {
240 info
.setString(value
);
245 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
246 JPanel pane
= new JPanel(new BorderLayout());
248 final JButton colorWheel
= new JButton();
249 colorWheel
.setIcon(getIcon(17, info
.getColor(true)));
250 colorWheel
.addActionListener(new ActionListener() {
252 public void actionPerformed(ActionEvent e
) {
253 Integer icol
= info
.getColor(true);
255 icol
= new Color(255, 255, 255, 255).getRGB();
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)));
267 pane
.add(colorWheel
, BorderLayout
.WEST
);
268 pane
.add(field
, BorderLayout
.CENTER
);
269 this.add(pane
, BorderLayout
.CENTER
);
271 setPreferredSize(pane
);
274 private void addBrowseField(final MetaInfo
<E
> info
, int nhgap
,
276 final JTextField field
= new JTextField();
277 field
.setToolTipText(info
.getDescription());
278 String value
= info
.getString(false);
280 field
.setText(value
);
282 info
.addReloadedListener(new Runnable() {
285 String value
= info
.getString(false);
287 field
.setText(value
);
290 info
.addSaveListener(new Runnable() {
293 String value
= field
.getText();
294 if (isChanged(value
)) {
295 info
.setString(value
);
300 JButton browseButton
= new JButton("...");
301 browseButton
.addActionListener(new ActionListener() {
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();
311 String value
= file
.getAbsolutePath();
312 if (isChanged(value
)) {
313 info
.setString(value
);
315 field
.setText(value
);
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
);
327 setPreferredSize(pane
);
330 private void addComboboxField(final MetaInfo
<E
> info
, int nhgap
,
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);
338 field
.setSelectedItem(value
);
340 info
.addReloadedListener(new Runnable() {
343 String value
= info
.getString(false);
345 field
.setSelectedItem(value
);
348 info
.addSaveListener(new Runnable() {
351 Object item
= field
.getSelectedItem();
352 String value
= item
== null ?
null : item
.toString();
353 if (isChanged(value
)) {
354 info
.setString(value
);
359 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
360 this.add(field
, BorderLayout
.CENTER
);
362 setPreferredSize(field
);
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);
370 field
.setText(value
);
372 info
.addReloadedListener(new Runnable() {
375 String value
= info
.getString(false);
377 field
.setText(value
);
380 info
.addSaveListener(new Runnable() {
383 String value
= new String(field
.getPassword());
384 if (isChanged(value
)) {
385 info
.setString(value
);
390 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
391 this.add(field
, BorderLayout
.CENTER
);
393 setPreferredSize(field
);
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);
401 field
.setValue(value
);
403 info
.addReloadedListener(new Runnable() {
406 int value
= info
.getInteger(true) == null ?
0 : info
409 field
.setValue(value
);
412 info
.addSaveListener(new Runnable() {
415 int value
= field
.getValue() == null ?
0 : (Integer
) field
417 if (isChanged(value
)) {
418 info
.setInteger(value
);
423 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
424 this.add(field
, BorderLayout
.CENTER
);
426 setPreferredSize(field
);
430 * Create a label which width is constrained in lock steps.
433 * the {@link MetaInfo} for which we want to add a label
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)
442 private JComponent
label(final MetaInfo
<E
> info
, int nhgap
) {
443 final JLabel label
= new JLabel(info
.getName());
445 Dimension ps
= label
.getPreferredSize();
447 ps
= label
.getSize();
450 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
454 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
461 final Runnable showInfo
= new Runnable() {
464 StringBuilder builder
= new StringBuilder();
465 String text
= (info
.getDescription().replace("\\n", "\n"))
467 for (String line
: StringUtils
.justifyText(text
, 80,
469 if (builder
.length() > 0) {
470 builder
.append("\n");
472 builder
.append(line
);
474 text
= builder
.toString();
475 JOptionPane
.showMessageDialog(ConfigItem
.this, text
,
476 info
.getName(), JOptionPane
.INFORMATION_MESSAGE
);
480 JLabel help
= new JLabel("");
481 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
483 Image img
= new Image(infoImage64
);
485 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
486 help
.setIcon(new ImageIcon(bImg
));
490 } catch (IOException e
) {
491 // This is an hard-coded image, should not happen
495 help
.addMouseListener(new MouseAdapter() {
497 public void mouseClicked(MouseEvent e
) {
502 JPanel pane2
= new JPanel(new BorderLayout());
503 pane2
.add(help
, BorderLayout
.WEST
);
504 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
506 JPanel contentPane
= new JPanel(new BorderLayout());
507 contentPane
.add(label
, BorderLayout
.WEST
);
508 contentPane
.add(pane2
, BorderLayout
.CENTER
);
510 ps
.width
= w
+ 30; // 30 for the (?) sign
511 contentPane
.setSize(ps
);
512 contentPane
.setPreferredSize(ps
);
514 JPanel pane
= new JPanel(new BorderLayout());
515 pane
.add(contentPane
, BorderLayout
.NORTH
);
521 * Return an {@link Icon} to use as a colour badge for the colour field
525 * the size of the badge
527 * the colour of the badge, which can be NULL (will return
532 private Icon
getIcon(int size
, Integer color
) {
535 color
= new Color(255, 255, 255, 255).getRGB();
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
);
542 BufferedImage img
= new BufferedImage(size
, size
,
543 BufferedImage
.TYPE_4BYTE_ABGR
);
545 Graphics2D g
= img
.createGraphics();
548 g
.fillRect(0, 0, img
.getWidth(), img
.getHeight());
550 g
.drawRect(0, 0, img
.getWidth() - 1, img
.getHeight() - 1);
555 return new ImageIcon(img
);
558 private void setPreferredSize(JComponent field
) {
560 .max(getMinimumHeight(), field
.getMinimumSize().height
);
561 setPreferredSize(new Dimension(200, height
));
564 static private int getMinimumHeight() {
565 if (minimumHeight
< 0) {
566 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
569 return minimumHeight
;