package be.nikiroo.utils.ui;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.List;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import be.nikiroo.utils.Image;
import be.nikiroo.utils.StringUtils;
import be.nikiroo.utils.StringUtils.Alignment;
import be.nikiroo.utils.resources.Bundle;
import be.nikiroo.utils.resources.MetaInfo;
/**
* A graphical item that reflect a configuration option from the given
* {@link Bundle}.
*
* This graphical item can be edited, and the result will be saved back into the
* linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should
* you wish to, of course.
*
* @author niki
*
* @param
* the type of {@link Bundle} to edit
*/
public abstract class ConfigItem> extends JPanel {
private static final long serialVersionUID = 1L;
private static int minimumHeight = -1;
/** A small 16x16 "?" blue in PNG, base64 encoded. */
private static String img64info = //
""
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
+ "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wURFRg6IrtcdgAAATdJREFUOMvtkj8sQ1EUxr9z/71G"
+ "m1RDogYxq7WDDYMYTSajSG4n6YRYzSaSLibWbiaDIGwdiLIYDFKDNJEgKu969xi8UNHy7H7LPcN3"
+ "v/Odcy+hG9oOIeIcBCJS9MAvlZtOMtHxsrFrJHGqe0RVGnHAHpcIbPlng8BS3HmKBJYzabGUzcrJ"
+ "XK+ckIrqANYR2JEv2nYDEVck0WKGfHzyq82Go+btxoX3XAcAIqTj8wPqOH6mtMeM4bGCLhyfhTMA"
+ "qlLhKHqujCfaweCAmV0p50dPzsNpEKpK01V/n55HIvTnfDC2odKlfeYadZN/T+AqDACUsnkhqaU1"
+ "LRIVuX1x7ciuSWQxVIrunONrfq3dI6oh+T94Z8453vEem/HTqT8ZpFJ0qDXtGkPbAGAMeSRngQCA"
+ "eUvgn195AwlZWyvjtQdhAAAAAElFTkSuQmCC";
/** A small 16x16 "+" image with colours */
private static String img64add = //
""
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
+ "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeES0QBFvvnAAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
+ "YXRlZCB3aXRoIEdJTVBkLmUHAAACH0lEQVQ4y42Tz0sVURTHP+fMmC7CQMpH1EjgIimCsEVBEIg/"
+ "qIbcBAW2Uai1m/oH2rlJXLQpeRJt2gQhTO0iTTKC1I2JBf5gKCJCRPvhPOed22LmvV70Fn7hwr3c"
+ "+z3ne+73HCFHEClxaASRHgduA91AW369BkwDI3Foy0GkEofmACQnSxyaCyItAkMClMzYdeCAJgVP"
+ "tJJrPA7tVoUjNZlngXMAiRmXClfoK/Tjq09x7T6LW+8RxOVJ5+LQzgSRojm5WCEDlMrQVbjIQNtN"
+ "rh0d5FTzaTLBmWKgM4h0Ig4NzWseohYCJUuqx123Sx0MBpF2+MAdyWUnlqX4lf4bIDHjR+rwJJPR"
+ "qNCgCjDsA10lM/oKIRcO9lByCYklnG/pqQa4euQ6J5tPoKI0yD6ef33Ku40Z80R7CSJNWyZxT+Ki"
+ "2ytGP911hyZxQaRp1RtPPPYKD4+sGJwPrDUp7Q9Xxnj9fYrUUnaszEAwQHfrZQAerT/g7cYMiuCp"
+ "z8LmLI0qBqz6wLQn2v5he57FrXkAtlPH2ZZOuskCzG2+4dnnx3iSuSgCKqLAlAIjmXPiVIRsgYjU"
+ "usrfO0Gq7cA9jUNbBsZrmiQnac1e6n3FeBzakpf39OSBG9IPHAZwzlFoagVg5edHXn57wZed9dpA"
+ "C3FoYRDpf8M0AQwKwu9yubxjeA7Y72ENqlp3mOqMcwcwDPQCx8gGchV4BYzGoS1V3gL8AVA5C5/0"
+ "oRFoAAAAAElFTkSuQmCC";
/** A small 32x32 "-" image with colours */
private static String img64remove = //
""
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
+ "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeESw5X/JGsQAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
+ "YXRlZCB3aXRoIEdJTVBkLmUHAAACKUlEQVQ4y5WTO2iTYRSG3+//v/+SJrG5SSABh1JQBHFJNUNR"
+ "YodCLoMoTkK0YKhQtBmsl01wKVZRBwcrgosg3SwFW9Cippe0VmlpB6uYqYIaNSZtbv/lOKRx0iR9"
+ "4YOzvOc8vOd8wLbG4nYGAKP9tshKr3Pq0zFXORt0UzbopvUeZ2ml1/niUcIWAYBzwwqr+xgAjCSt"
+ "wpXjWzx105Ha+1XsMgT8U6IJfPAacyfO50OXJi3VwbtbxMbidtZ3tiClbzi/eAuCmxgai4AfNvNn"
+ "KJn3X5xWKgwA0lHHYud3MdDUXMcmIOMx0oGJXJCN9tuiJ98p4//DbtTk2cFKhB/OSBcMgQHVMkir"
+ "AqwJBhGYrIIkCQc2eJK3aewI9Crko2FIh0K1Jo0mcwmV6XFUlmfRXhK7eXuRKaRVIYdiUGKnW8Kn"
+ "0ia0t6/hKHJVqCcLzncQgLhtIvBfbWbZZahq+cl96AuvQLre2Mw59NUlkCwjZ6USL0uYgSj26B/X"
+ "oK+vtkYgMAhMRF4x5oWlPdod0UQtfUFo7YEBBKz59BEGAAtRx1xHVgzu5AYyHmMmMJHrZolhhU3t"
+ "05XJe7s2PJuCq9k1MgKyNjOXiBf8kWW5JDy4XKHBl2ql6+pvX8ZjzDOqrcWsFQAAE/T3H3z2GG/6"
+ "zhT8sfdKeehWkUQAeJ7WcH23xTz1uPBwf1hclA3mBZjPojFOIOSsVPpmN1OznfpA+Gn+2kCHqg/d"
+ "LhIA/AFU5d0V6gTjtQAAAABJRU5ErkJggg==";
/** The code base */
private final ConfigItemBase base;
/** The main panel with all the fields in it. */
private JPanel main;
/**
* Prepare a new {@link ConfigItem} instance, linked to the given
* {@link MetaInfo}.
*
* @param info
* the info
* @param autoDirtyHandling
* TRUE to automatically manage the setDirty/Save operations,
* FALSE if you want to do it yourself via
* {@link ConfigItem#setDirtyItem(int)}
*/
protected ConfigItem(MetaInfo info, boolean autoDirtyHandling) {
base = new ConfigItemBase(info, autoDirtyHandling) {
@Override
protected JComponent createEmptyField(int item) {
return ConfigItem.this.createEmptyField(item);
}
@Override
protected Object getFromInfo(int item) {
return ConfigItem.this.getFromInfo(item);
}
@Override
protected void setToInfo(Object value, int item) {
ConfigItem.this.setToInfo(value, item);
}
@Override
protected Object getFromField(int item) {
return ConfigItem.this.getFromField(item);
}
@Override
protected void setToField(Object value, int item) {
ConfigItem.this.setToField(value, item);
}
@Override
public JComponent createField(int item) {
JComponent field = super.createField(item);
int height = Math.max(getMinimumHeight(),
field.getMinimumSize().height);
field.setPreferredSize(new Dimension(200, height));
return field;
}
@Override
public List reload() {
List removed = base.reload();
if (!removed.isEmpty()) {
for (JComponent c : removed) {
main.remove(c);
}
main.revalidate();
main.repaint();
}
return removed;
}
@Override
protected JComponent removeItem(int item) {
JComponent removed = super.removeItem(item);
main.remove(removed);
main.revalidate();
main.repaint();
return removed;
}
};
}
/**
* Create a new {@link ConfigItem} for the given {@link MetaInfo}.
*
* @param nhgap
* negative horisontal gap in pixel to use for the label, i.e.,
* the step lock sized labels will start smaller by that amount
* (the use case would be to align controls that start at a
* different horisontal position)
*/
public void init(int nhgap) {
if (getInfo().isArray()) {
this.setLayout(new BorderLayout());
add(label(nhgap), BorderLayout.WEST);
main = new JPanel();
main.setLayout(new BoxLayout(main, BoxLayout.Y_AXIS));
int size = getInfo().getListSize(false);
for (int i = 0; i < size; i++) {
addItemWithMinusPanel(i);
}
main.revalidate();
main.repaint();
final JButton add = new JButton();
setImage(add, img64add, "+");
add.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
addItemWithMinusPanel(base.getFieldsSize());
main.revalidate();
main.repaint();
}
});
JPanel tmp = new JPanel(new BorderLayout());
tmp.add(add, BorderLayout.WEST);
JPanel mainPlus = new JPanel(new BorderLayout());
mainPlus.add(main, BorderLayout.CENTER);
mainPlus.add(tmp, BorderLayout.SOUTH);
add(mainPlus, BorderLayout.CENTER);
} else {
this.setLayout(new BorderLayout());
add(label(nhgap), BorderLayout.WEST);
JComponent field = base.createField(-1);
add(field, BorderLayout.CENTER);
}
}
/** The {@link MetaInfo} linked to the field. */
public MetaInfo getInfo() {
return base.getInfo();
}
/**
* Retrieve the associated graphical component that was created with
* {@link ConfigItemBase#createEmptyField(int)}.
*
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*
* @return the graphical component
*/
protected JComponent getField(int item) {
return base.getField(item);
}
/**
* Manually specify that the given item is "dirty" and thus should be saved
* when asked.
*
* Has no effect if the class is using automatic dirty handling (see
* {@link ConfigItemBase#ConfigItem(MetaInfo, boolean)}).
*
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*/
protected void setDirtyItem(int item) {
base.setDirtyItem(item);
}
/**
* Check if the value changed since the last load/save into the linked
* {@link MetaInfo}.
*
* Note that we consider NULL and an Empty {@link String} to be equals.
*
* @param value
* the value to test
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*
* @return TRUE if it has
*/
protected boolean hasValueChanged(Object value, int item) {
return base.hasValueChanged(value, item);
}
private void addItemWithMinusPanel(int item) {
JPanel minusPanel = createMinusPanel(item);
JComponent field = base.addItem(item, minusPanel);
minusPanel.add(field, BorderLayout.CENTER);
}
private JPanel createMinusPanel(final int item) {
JPanel minusPanel = new JPanel(new BorderLayout());
final JButton remove = new JButton();
setImage(remove, img64remove, "-");
remove.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
base.removeItem(item);
}
});
minusPanel.add(remove, BorderLayout.EAST);
main.add(minusPanel);
main.revalidate();
main.repaint();
return minusPanel;
}
/**
* Create an empty graphical component to be used later by
* {@link ConfigItem#createField(int)}.
*
* Note that {@link ConfigItem#reload(int)} will be called after it was
* created by {@link ConfigItem#createField(int)}.
*
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*
* @return the graphical component
*/
abstract protected JComponent createEmptyField(int item);
/**
* Get the information from the {@link MetaInfo} in the subclass preferred
* format.
*
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*
* @return the information in the subclass preferred format
*/
abstract protected Object getFromInfo(int item);
/**
* Set the value to the {@link MetaInfo}.
*
* @param value
* the value in the subclass preferred format
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*/
abstract protected void setToInfo(Object value, int item);
/**
* The value present in the given item's related field in the subclass
* preferred format.
*
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*
* @return the value present in the given item's related field in the
* subclass preferred format
*/
abstract protected Object getFromField(int item);
/**
* Set the value (in the subclass preferred format) into the field.
*
* @param value
* the value in the subclass preferred format
* @param item
* the item number to get for an array of values, or -1 to get
* the whole value (has no effect if {@link MetaInfo#isArray()}
* is FALSE)
*/
abstract protected void setToField(Object value, int item);
/**
* Create a label which width is constrained in lock steps.
*
* @param nhgap
* negative horisontal gap in pixel to use for the label, i.e.,
* the step lock sized labels will start smaller by that amount
* (the use case would be to align controls that start at a
* different horisontal position)
*
* @return the label
*/
protected JComponent label(int nhgap) {
final JLabel label = new JLabel(getInfo().getName());
Dimension ps = label.getPreferredSize();
if (ps == null) {
ps = label.getSize();
}
ps.height = Math.max(ps.height, getMinimumHeight());
int w = ps.width;
int step = 150;
for (int i = 2 * step - nhgap; i < 10 * step; i += step) {
if (w < i) {
w = i;
break;
}
}
final Runnable showInfo = new Runnable() {
@Override
public void run() {
StringBuilder builder = new StringBuilder();
String text = (getInfo().getDescription().replace("\\n", "\n"))
.trim();
for (String line : StringUtils.justifyText(text, 80,
Alignment.LEFT)) {
if (builder.length() > 0) {
builder.append("\n");
}
builder.append(line);
}
text = builder.toString();
JOptionPane.showMessageDialog(ConfigItem.this, text, getInfo()
.getName(), JOptionPane.INFORMATION_MESSAGE);
}
};
JLabel help = new JLabel("");
help.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
setImage(help, img64info, "?");
help.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
showInfo.run();
}
});
JPanel pane2 = new JPanel(new BorderLayout());
pane2.add(help, BorderLayout.WEST);
pane2.add(new JLabel(" "), BorderLayout.CENTER);
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.add(label, BorderLayout.WEST);
contentPane.add(pane2, BorderLayout.CENTER);
ps.width = w + 30; // 30 for the (?) sign
contentPane.setSize(ps);
contentPane.setPreferredSize(ps);
JPanel pane = new JPanel(new BorderLayout());
pane.add(contentPane, BorderLayout.NORTH);
return pane;
}
/**
* Create a new {@link ConfigItem} for the given {@link MetaInfo}.
*
* @param
* the type of {@link Bundle} to edit
*
* @param info
* the {@link MetaInfo}
* @param nhgap
* negative horisontal gap in pixel to use for the label, i.e.,
* the step lock sized labels will start smaller by that amount
* (the use case would be to align controls that start at a
* different horisontal position)
*
* @return the new {@link ConfigItem}
*/
static public > ConfigItem createItem(
MetaInfo info, int nhgap) {
ConfigItem configItem;
switch (info.getFormat()) {
case BOOLEAN:
configItem = new ConfigItemBoolean(info);
break;
case COLOR:
configItem = new ConfigItemColor(info);
break;
case FILE:
configItem = new ConfigItemBrowse(info, false);
break;
case DIRECTORY:
configItem = new ConfigItemBrowse(info, true);
break;
case COMBO_LIST:
configItem = new ConfigItemCombobox(info, true);
break;
case FIXED_LIST:
configItem = new ConfigItemCombobox(info, false);
break;
case INT:
configItem = new ConfigItemInteger(info);
break;
case PASSWORD:
configItem = new ConfigItemPassword(info);
break;
case LOCALE:
configItem = new ConfigItemLocale(info);
break;
case STRING:
default:
configItem = new ConfigItemString(info);
break;
}
configItem.init(nhgap);
return configItem;
}
/**
* Set an image to the given {@link JButton}, with a fallback text if it
* fails.
*
* @param button
* the button to set
* @param image64
* the image in BASE64 (should be PNG or similar)
* @param fallbackText
* text to use in case the image cannot be created
*/
static protected void setImage(JLabel button, String image64,
String fallbackText) {
try {
Image img = new Image(image64);
try {
BufferedImage bImg = ImageUtilsAwt.fromImage(img);
button.setIcon(new ImageIcon(bImg));
} finally {
img.close();
}
} catch (IOException e) {
// This is an hard-coded image, should not happen
button.setText(fallbackText);
}
}
/**
* Set an image to the given {@link JButton}, with a fallback text if it
* fails.
*
* @param button
* the button to set
* @param image64
* the image in BASE64 (should be PNG or similar)
* @param fallbackText
* text to use in case the image cannot be created
*/
static protected void setImage(JButton button, String image64,
String fallbackText) {
try {
Image img = new Image(image64);
try {
BufferedImage bImg = ImageUtilsAwt.fromImage(img);
button.setIcon(new ImageIcon(bImg));
} finally {
img.close();
}
} catch (IOException e) {
// This is an hard-coded image, should not happen
button.setText(fallbackText);
}
}
static private int getMinimumHeight() {
if (minimumHeight < 0) {
minimumHeight = new JTextField("Test").getMinimumSize().height;
}
return minimumHeight;
}
}