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 String value
= field
.getSelectedItem().toString();
352 if (isChanged(value
)) {
353 info
.setString(value
);
358 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
359 this.add(field
, BorderLayout
.CENTER
);
361 setPreferredSize(field
);
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);
369 field
.setText(value
);
371 info
.addReloadedListener(new Runnable() {
374 String value
= info
.getString(false);
376 field
.setText(value
);
379 info
.addSaveListener(new Runnable() {
382 String value
= new String(field
.getPassword());
383 if (isChanged(value
)) {
384 info
.setString(value
);
389 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
390 this.add(field
, BorderLayout
.CENTER
);
392 setPreferredSize(field
);
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);
400 field
.setValue(value
);
402 info
.addReloadedListener(new Runnable() {
405 int value
= info
.getInteger(true) == null ?
0 : info
408 field
.setValue(value
);
411 info
.addSaveListener(new Runnable() {
414 int value
= field
.getValue() == null ?
0 : (Integer
) field
416 if (isChanged(value
)) {
417 info
.setInteger(value
);
422 this.add(label(info
, nhgap
), BorderLayout
.WEST
);
423 this.add(field
, BorderLayout
.CENTER
);
425 setPreferredSize(field
);
429 * Create a label which width is constrained in lock steps.
432 * the {@link MetaInfo} for which we want to add a label
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)
441 private JComponent
label(final MetaInfo
<E
> info
, int nhgap
) {
442 final JLabel label
= new JLabel(info
.getName());
444 Dimension ps
= label
.getPreferredSize();
446 ps
= label
.getSize();
449 ps
.height
= Math
.max(ps
.height
, getMinimumHeight());
453 for (int i
= 2 * step
- nhgap
; i
< 10 * step
; i
+= step
) {
460 final Runnable showInfo
= new Runnable() {
463 StringBuilder builder
= new StringBuilder();
464 String text
= (info
.getDescription().replace("\\n", "\n"))
466 for (String line
: StringUtils
.justifyText(text
, 80,
468 if (builder
.length() > 0) {
469 builder
.append("\n");
471 builder
.append(line
);
473 text
= builder
.toString();
474 JOptionPane
.showMessageDialog(ConfigItem
.this, text
,
475 info
.getName(), JOptionPane
.INFORMATION_MESSAGE
);
479 JLabel help
= new JLabel("");
480 help
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
482 Image img
= new Image(infoImage64
);
484 BufferedImage bImg
= ImageUtilsAwt
.fromImage(img
);
485 help
.setIcon(new ImageIcon(bImg
));
489 } catch (IOException e
) {
490 // This is an hard-coded image, should not happen
494 help
.addMouseListener(new MouseAdapter() {
496 public void mouseClicked(MouseEvent e
) {
501 JPanel pane2
= new JPanel(new BorderLayout());
502 pane2
.add(help
, BorderLayout
.WEST
);
503 pane2
.add(new JLabel(" "), BorderLayout
.CENTER
);
505 JPanel contentPane
= new JPanel(new BorderLayout());
506 contentPane
.add(label
, BorderLayout
.WEST
);
507 contentPane
.add(pane2
, BorderLayout
.CENTER
);
509 ps
.width
= w
+ 30; // 30 for the (?) sign
510 contentPane
.setSize(ps
);
511 contentPane
.setPreferredSize(ps
);
513 JPanel pane
= new JPanel(new BorderLayout());
514 pane
.add(contentPane
, BorderLayout
.NORTH
);
520 * Return an {@link Icon} to use as a colour badge for the colour field
524 * the size of the badge
526 * the colour of the badge, which can be NULL (will return
531 private Icon
getIcon(int size
, Integer color
) {
534 color
= new Color(255, 255, 255, 255).getRGB();
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
);
541 BufferedImage img
= new BufferedImage(size
, size
,
542 BufferedImage
.TYPE_4BYTE_ABGR
);
544 Graphics2D g
= img
.createGraphics();
547 g
.fillRect(0, 0, img
.getWidth(), img
.getHeight());
549 g
.drawRect(0, 0, img
.getWidth() - 1, img
.getHeight() - 1);
554 return new ImageIcon(img
);
557 private void setPreferredSize(JComponent field
) {
559 .max(getMinimumHeight(), field
.getMinimumSize().height
);
560 setPreferredSize(new Dimension(200, height
));
563 static private int getMinimumHeight() {
564 if (minimumHeight
< 0) {
565 minimumHeight
= new JTextField("Test").getMinimumSize().height
;
568 return minimumHeight
;