/*
* $Id$
*
* Copyright (c) 2008-2009 Brent Easton
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.tools.icon;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import VASSAL.build.AbstractConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.configure.Configurer;
import VASSAL.configure.ImageConfigurer;
import VASSAL.configure.StringConfigurer;
import VASSAL.i18n.Resources;
import VASSAL.tools.ArchiveWriter;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.filechooser.ImageFileFilter;
import VASSAL.tools.image.ImageUtils;
import VASSAL.tools.imageop.Op;
import VASSAL.tools.imageop.OpIcon;
import VASSAL.tools.swing.Dialogs;
/**
* An IconFamily is a named set of Icons in the four standard Tango sizes.
*
* Each IconFamily consists of at least a Scalable Icon, plus zero or more
* specifically sized icons.
*
* If a specific sized Icon is missing, the IconFamily will supply a scaled icon
* based on the Scalable icon.
*
* Icons are created as lazily as possible.
*
* IconFamilys are created in two ways: - For Vassal inbuilt Icons by
* IconFactory when it scans the Vengine for inbuilt Icons - For Modules,
* IconFamilys can be added to IconFamilyContainer by the module designer.
*
* Each IconFamily consists of at least a Scalable Icon, plus zero or more
* specifically sized icons. If an
*/
public class IconFamily extends AbstractConfigurable {
public static final String SCALABLE_ICON = "scalableIcon"; //$NON-NLS-1$
public static final String ICON0 = "icon0"; //$NON-NLS-1$
public static final String ICON1 = "icon1"; //$NON-NLS-1$
public static final String ICON2 = "icon2"; //$NON-NLS-1$
public static final String ICON3 = "icon3"; //$NON-NLS-1$
private final PropertyChangeSupport propSupport = new PropertyChangeSupport(
this);
// Tango Icon sizes
public static final int XSMALL = 0;
public static final int SMALL = 1;
public static final int MEDIUM = 2;
public static final int LARGE = 3;
static final int MAX_SIZE = LARGE;
static final int SIZE_COUNT = MAX_SIZE + 1;
// Pixel size of each Tango Size
static final int[] SIZES = new int[] { 16, 22, 32, 48 };
// Directories within the icons directory to locate each Tango Size
static final String[] SIZE_DIRS = new String[] {
"16x16/", //$NON-NLS-1$
"22x22/", //$NON-NLS-1$
"32x32/", //$NON-NLS-1$
"48x48/" //$NON-NLS-1$
};
// Names of sizes in local language
static final String[] SIZE_NAMES = new String[SIZE_COUNT];;
// Directory within the icons directory holding the Scalable versions of the
// icons
static final String SCALABLE_DIR = "scalable/"; //$NON-NLS-1$
// Cache of the icons in this family
protected OpIcon[] icons;
protected OpIcon scalableIcon;
// Paths to the source of the icons in this family
protected String scalablePath;
protected String[] sizePaths = new String[SIZE_COUNT];
/**
* Return list of Icon Size names in local language
*
* @return
*/
public static String[] getIconSizeNames() {
synchronized (SIZE_NAMES) {
if (SIZE_NAMES[0] == null) {
SIZE_NAMES[XSMALL] = Resources.getString("Icon.extra_small"); //$NON-NLS-1$
SIZE_NAMES[SMALL] = Resources.getString("Icon.small"); //$NON-NLS-1$
SIZE_NAMES[MEDIUM] = Resources.getString("Icon.medium"); //$NON-NLS-1$
SIZE_NAMES[LARGE] = Resources.getString("Icon.large"); //$NON-NLS-1$
}
}
return SIZE_NAMES;
}
/**
* Return an Icon Size based on the local language name
*/
public static int getIconSize(String name) {
int size = SMALL;
final String[] options = getIconSizeNames();
for (int i = 0; i < options.length; i++) {
if (options[i].equals(name)) {
return i;
}
}
return size;
}
public static int getIconHeight(int size) {
if (size < 0 || size > MAX_SIZE) {
return 0;
}
return SIZES[size];
}
/**
* Create a new IconFamily with the given name. The name supplied will
* normally be the name of an IconFamily, with no suffix.
*
* These constructors are used by IconFactory to create IconFamilys for the
* Vassal inbuilt Icons
*
* FIXME: Write this bit...Will be needed once Toolbar Icon support is added
* Backward Compatibility: If the name supplied does have a file type suffix,
* then it is a specific Icon name from a pre-IconFamily module. By throwing
* away the suffix, IconFamily will use the supplied icon as a base icon to
* create the full IconFamily.
*
* @param familyName
* IconFamily name or Icon name
* @param scalableName
* Name of the scalable icon
* @param sizeNames
* Names of the sized Icons
*/
public IconFamily(String familyName, String scalableName, String[] sizeName) {
this(familyName);
setScalableIconPath(scalableName);
for (int i = 0; i < MAX_SIZE; i++) {
setSizeIconPath(i, sizeName[i]);
}
}
public IconFamily(String familyName) {
this();
setConfigureName(familyName);
}
public IconFamily() {
icons = new OpIcon[SIZE_COUNT];
setConfigureName(""); //$NON-NLS-1$
}
public void setScalableIconPath(String s) {
scalablePath = s;
scalableIcon = null;
}
public void setSizeIconPath(int size, String path) {
sizePaths[size] = path;
icons[size] = null;
}
public boolean isLegacy() {
return getName().contains("."); //$NON-NLS-1$
}
/**
* Return a particular sized icon. If it can't be found, then build it by
* scaling the base Icon.
*
* @param size
* Icon size
* @return Icon
*/
public Icon getIcon(int size) {
if (size < 0 || size > MAX_SIZE) {
return null;
}
synchronized (this) {
if (icons[size] == null) {
icons[size] = buildIcon(size);
}
}
return icons[size];
}
/**
* Return a particular sized Icon, but do not build one from the scalable Icon
* if it is not found.
*
* @param size
* @return
*/
public Icon getRawIcon(int size) {
if (size < 0 || size > MAX_SIZE || sizePaths[size] == null) {
return null;
}
return getIcon(size);
}
/**
* Return the scalable icon directly (used by {@link IconImageConfigurer})
*
* @return
*/
public Icon getScalableIcon() {
synchronized (this) {
buildScalableIcon();
}
return scalableIcon;
}
public BufferedImage getImage(int size) {
if (size < 0 || size > MAX_SIZE) {
return null;
}
getIcon(size);
return (BufferedImage) (icons[size] == null ? null : icons[size].getImage());
}
protected OpIcon buildIcon(int size) {
// Do we have it ready to go?
if (icons[size] != null) {
return icons[size];
}
// This size exists?
if (sizePaths[size] != null) {
icons[size] = new OpIcon(Op.load(sizePaths[size]));
icons[size].getImage();
return icons[size];
}
// No, So we need to build it from the Scalable version
buildScalableIcon();
icons[size] = scaleIcon(scalableIcon, SIZES[size]);
icons[size].getImage();
return icons[size];
}
protected void buildScalableIcon() {
if (scalableIcon == null) {
if (scalablePath != null) {
scalableIcon = new OpIcon(Op.load(scalablePath));
}
}
}
/**
* Scale an Icon to desired size
*
* @param base
* Base Icon
* @param toSizePixels
* Required Size in Pixels
* @return Scaled Icon
*/
protected OpIcon scaleIcon(OpIcon base, int toSizePixels) {
if (base == null) {
return null;
}
final int baseHeight = base.getIconHeight();
if (baseHeight == toSizePixels) {
return base;
}
return new OpIcon(Op.scale(base.getOp(), ((double) toSizePixels)
/ base.getIconHeight()));
}
public String getName() {
return getConfigureName();
}
public void addPropertyChangeListener(PropertyChangeListener l) {
propSupport.addPropertyChangeListener(l);
}
public void setConfigureName(String s) {
String oldName = name;
this.name = s;
propSupport.firePropertyChange(NAME_PROPERTY, oldName, name);
}
// Note: Custom Configurer
public static String getConfigureTypeName() {
return Resources.getString("Editor.IconFamily.component_type"); //$NON-NLS-1$
}
public String[] getAttributeDescriptions() {
return new String[0];
}
public Class<?>[] getAttributeTypes() {
return new Class[0];
}
public String[] getAttributeNames() {
return new String[] { NAME_PROPERTY, SCALABLE_ICON, ICON0, ICON1, ICON2,
ICON3 };
}
public String getAttributeValueString(String key) {
if (NAME_PROPERTY.equals(key)) {
return getConfigureName();
}
else if (SCALABLE_ICON.equals(key)) {
return scalablePath;
}
else if (ICON0.equals(key)) {
return sizePaths[0];
}
else if (ICON1.equals(key)) {
return sizePaths[1];
}
else if (ICON2.equals(key)) {
return sizePaths[2];
}
else if (ICON3.equals(key)) {
return sizePaths[3];
}
return null;
}
public void setAttribute(String key, Object value) {
if (NAME_PROPERTY.equals(key)) {
setConfigureName((String) value);
}
else if (SCALABLE_ICON.equals(key)) {
setScalableIconPath((String) value);
}
else if (ICON0.equals(key)) {
setSizeIconPath(0, (String) value);
}
else if (ICON1.equals(key)) {
setSizeIconPath(1, (String) value);
}
else if (ICON2.equals(key)) {
setSizeIconPath(2, (String) value);
}
else if (ICON3.equals(key)) {
setSizeIconPath(3, (String) value);
}
}
public Class<?>[] getAllowableConfigureComponents() {
return new Class[0];
}
public HelpFile getHelpFile() {
return null;
}
public void removeFrom(Buildable parent) {
}
public void addTo(Buildable parent) {
}
public Configurer getConfigurer() {
return new IconFamilyConfig(this);
}
/*******************************************************
* Custom Configurer for Icon Family
*
*/
static class IconFamilyConfig extends Configurer {
protected IconFamily family;
protected JPanel controls;
protected StringConfigurer title;
protected JLabel errorLabel;
protected ImageConfigurer scalableConfig;
public IconFamilyConfig(IconFamily f) {
super(null, null);
family = f;
controls = new JPanel();
controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS));
final JPanel mig = new JPanel(new MigLayout("inset 5")); //$NON-NLS-1$
title = new StringConfigurer(null, "", family.getConfigureName()); //$NON-NLS-1$
title.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getNewValue() != null) {
family.setConfigureName((String) evt.getNewValue());
}
}
});
mig.add(new JLabel(Resources.getString("Editor.IconFamily.family_name"))); //$NON-NLS-1$
mig.add(title.getControls(), "wrap"); //$NON-NLS-1$
errorLabel = new JLabel(Resources.getString("Editor.IconFamily.name_taken")); //$NON-NLS-1$
errorLabel.setForeground(Color.red);
errorLabel.setVisible(false);
family.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (Configurable.NAME_PROPERTY.equals(evt.getPropertyName())) {
final IconFamily savedFamily = IconFactory.getIconFamily(family
.getName());
errorLabel.setVisible(savedFamily != null && savedFamily != family);
}
}
});
mig.add(errorLabel, "span 2,wrap"); //$NON-NLS-1$
final IconImageConfigurer scalableConfig = new IconImageConfigurer(family);
mig.add(new JLabel(Resources.getString("Editor.IconFamily.scalable_icon_label"))); //$NON-NLS-1$
mig.add(scalableConfig.getControls(), "wrap"); //$NON-NLS-1$
final IconImageConfigurer[] sizeConfig = new IconImageConfigurer[IconFamily.SIZE_COUNT];
for (int size = 0; size < IconFamily.SIZE_COUNT; size++) {
sizeConfig[size] = new IconImageConfigurer(family, size);
final String px = String.valueOf(IconFamily.SIZES[size]);
mig.add(new JLabel(Resources.getString("Editor.IconFamily.icon_label", IconFamily.getIconSizeNames()[size], px))); //$NON-NLS-1$
mig.add(sizeConfig[size].getControls(), "wrap"); //$NON-NLS-1$
}
controls.add(mig);
}
public Component getControls() {
return controls;
}
public String getValueString() {
return null;
}
public void setValue(String s) {
}
}
/**************************************************
* Configure an individual Icon Image
*/
static class IconImageConfigurer extends Configurer {
protected int size;
protected JPanel controls;
protected IconFamily family;
protected int px;
protected JLabel warningLabel;
public IconImageConfigurer(IconFamily family, int size) {
super(null, null);
this.size = size;
this.family = family;
if (size < 0) {
px = IconFamily.SIZES[IconFamily.LARGE];
}
else {
px = IconFamily.SIZES[size];
}
}
/**
* Constructor to Configure the scalable icon;
*
* @param key
* @param name
* @param testElementName
*/
public IconImageConfigurer(IconFamily family) {
this(family, -1);
}
public Component getControls() {
if (controls == null) {
controls = new JPanel(new MigLayout());
controls.add(new JLabel(getName()));
final JPanel p = new JPanel() {
private static final long serialVersionUID = 1L;
public void paint(Graphics g) {
g.clearRect(0, 0, getSize().width, getSize().height);
final Icon i = getIconValue();
if (i != null) {
i.paintIcon(this, g, getSize().width / 2 - i.getIconWidth() / 2,
getSize().height / 2 - i.getIconHeight() / 2);
}
}
};
p.setPreferredSize(new Dimension(px, px));
controls.add(p);
final JButton select = new JButton(Resources.getString(Resources.SELECT));
select.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
selectImage();
p.repaint();
}
});
controls.add(select, "wrap"); //$NON-NLS-1$
warningLabel = new JLabel();
warningLabel.setForeground(Color.red);
warningLabel.setVisible(false);
checkIconSize();
controls.add(warningLabel, "span,wrap"); //$NON-NLS-1$
}
return controls;
}
public String getValueString() {
if (size < 0) {
return family.scalablePath;
}
else {
return family.sizePaths[size];
}
}
public Icon getIconValue() {
Icon icon = null;
if (family != null) {
if (size < 0) {
if (family.scalablePath != null) {
icon = family.getScalableIcon();
}
}
else {
if (family.sizePaths[size] != null) {
icon = family.getIcon(size);
}
}
}
return icon;
}
public void setValue(String s) {
if (size < 0) {
family.setScalableIconPath(buildPath(s));
}
else {
family.setSizeIconPath(size, buildPath(s));
checkIconSize();
}
}
protected void checkIconSize() {
final Icon check = family.getRawIcon(size);
if (check != null) {
if (check.getIconHeight() != IconFamily.SIZES[size]) {
setWarning(Resources.getString("Editor.IconFamily.size_warning", IconFamily.SIZES[size], check.getIconHeight())); //$NON-NLS-1$
}
}
}
protected String buildPath(String s) {
if (s == null || s.length() == 0) {
return null;
}
if (size < 0) {
return ArchiveWriter.ICON_DIR + IconFamily.SCALABLE_DIR + s; //$NON-NLS-1$
}
else {
return ArchiveWriter.ICON_DIR + IconFamily.SIZE_DIRS[size] + s; //$NON-NLS-1$
}
}
protected void setWarning(String warning) {
warningLabel.setText(warning);
warningLabel.setVisible(warning != null && warning.length() > 0);
repack();
}
protected void repack() {
Window w = SwingUtilities.getWindowAncestor(controls);
if (w != null) {
w.pack();
}
}
protected void selectImage() {
final FileChooser fc = GameModule.getGameModule().getFileChooser();
fc.setFileFilter(new FamilyImageFilter(family.getName()));
fc.setSelectedFile(new File(family.getName() + ".*")); //$NON-NLS-1$
if (fc.showOpenDialog(getControls()) != FileChooser.APPROVE_OPTION) {
setWarning(""); //$NON-NLS-1$
setValue(null);
}
else {
final File f = fc.getSelectedFile();
if (f != null && f.exists()) {
final String name = f.getName();
if (name.split("\\.").length != 2) { //$NON-NLS-1$
showError(Resources.getString("Editor.IconFamily.illegal_icon_name")); //$NON-NLS-1$
}
else if (!name.startsWith(family.getName())) {
showError(Resources.getString("Editor.IconFamily.bad_icon_name", family.getName())); //$NON-NLS-1$
}
else if (!ImageUtils.hasImageSuffix(name)) {
showError(Resources.getString("Editor.IconFamily.bad_icon_file")); //$NON-NLS-1$
}
else {
GameModule.getGameModule().getArchiveWriter().addImage(f.getPath(),
buildPath(f.getName()));
setWarning(""); //$NON-NLS-1$
setValue(name);
}
}
else {
setWarning(""); //$NON-NLS-1$
setValue(null);
}
}
}
protected void showError(String message) {
Dialogs.showMessageDialog(SwingUtilities.getWindowAncestor(controls),
Resources.getString("Editor.IconFamily.icon_load_error"), //$NON-NLS-1$
Resources.getString("Editor.IconFamily.cannot_load_icon"), message, //$NON-NLS-1$
JOptionPane.ERROR_MESSAGE);
}
}
/**
* Filter Icon files matching this family
*
*/
static class FamilyImageFilter extends ImageFileFilter {
private String familyName;
public FamilyImageFilter(String family) {
super();
familyName = family;
}
public boolean accept(File f) {
if (super.accept(f)) {
final String s = f.getName().split("\\.")[0]; //$NON-NLS-1$
return s.equals(familyName);
}
return false;
}
}
}