/*
* $Id$
*
* Copyright (c) 2000-2003 by Rodney Kinney, Jim Urbas
*
* 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.counters;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableModel;
import javax.swing.text.JTextComponent;
import VASSAL.build.GameModule;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.command.ChangePiece;
import VASSAL.command.Command;
import VASSAL.configure.NamedHotKeyConfigurer;
import VASSAL.i18n.PieceI18nData;
import VASSAL.i18n.TranslatablePiece;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.ScrollPane;
import VASSAL.tools.SequenceEncoder;
/**
* A Decorator class that endows a GamePiece with a dialog.
*/
public class PropertySheet extends Decorator implements TranslatablePiece {
public static final String ID = "propertysheet;";
protected String oldState;
// properties
protected String menuName;
protected NamedKeyStroke launchKeyStroke;
protected KeyCommand launch;
protected Color backgroundColor;
protected String m_definition;
protected PropertySheetDialog frame;
protected JButton applyButton;
// Commit type definitions
final static String[] COMMIT_VALUES = {"Every Keystroke", "Apply Button or Enter Key", "Close Window or Enter Key"};
static final int COMMIT_IMMEDIATELY = 0;
static final int COMMIT_ON_APPLY = 1;
static final int COMMIT_ON_CLOSE = 2;
static final int COMMIT_DEFAULT = COMMIT_IMMEDIATELY;
// Field type definitions
static final String[] TYPE_VALUES = {"Text", "Multi-line text", "Label Only", "Tick Marks", "Tick Marks with Max Field", "Tick Marks with Value Field", "Tick Marks with Value & Max", "Spinner"};
static final int TEXT_FIELD = 0;
static final int TEXT_AREA = 1;
static final int LABEL_ONLY = 2;
static final int TICKS = 3;
static final int TICKS_MAX = 4;
static final int TICKS_VAL = 5;
static final int TICKS_VALMAX = 6;
static final int SPINNER = 7;
protected int commitStyle = COMMIT_DEFAULT;
protected boolean isUpdating;
protected String state;
protected Map<String,Object> properties = new HashMap<String,Object>();
protected List<JComponent> m_fields;
static final char TYPE_DELIMITOR = ';';
static final char DEF_DELIMITOR = '~';
static final char STATE_DELIMITOR = '~';
static final char LINE_DELIMINATOR = '|';
static final char VALUE_DELIMINATOR = '/';
class PropertySheetDialog extends JDialog implements ActionListener {
private static final long serialVersionUID = 1L;
public PropertySheetDialog(Frame owner) {
super(owner, false);
}
public void actionPerformed(ActionEvent event) {
if (applyButton != null) {
applyButton.setEnabled(false);
}
updateStateFromFields();
}
}
public PropertySheet() {
// format is propertysheet;menu-name;keystroke;commitStyle;backgroundRed;backgroundGreen;backgroundBlue
this(ID + ";Properties;P;;;;", null);
}
public PropertySheet(String type, GamePiece p) {
mySetType(type);
setInner(p);
}
/** Changes the "type" definition this decoration, which discards all value data and structures.
* Format: definition; name; keystroke
*/
public void mySetType(String s) {
s = s.substring(ID.length());
SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(s, TYPE_DELIMITOR);
m_definition = st.nextToken();
menuName = st.nextToken();
final String launchKeyToken = st.nextToken("");
commitStyle = st.nextInt(COMMIT_DEFAULT);
String red = st.hasMoreTokens() ? st.nextToken() : "";
String green = st.hasMoreTokens() ? st.nextToken() : "";
String blue = st.hasMoreTokens() ? st.nextToken() : "";
final String launchKeyStrokeToken = st.nextToken("");
backgroundColor = red.equals("") ? null : new Color(atoi(red), atoi(green), atoi(blue));
frame = null;
// Handle conversion from old character only key
if (launchKeyStrokeToken.length() > 0) {
launchKeyStroke = NamedHotKeyConfigurer.decode(launchKeyStrokeToken);
}
else if (launchKeyToken.length() > 0) {
launchKeyStroke = new NamedKeyStroke(launchKeyToken.charAt(0), InputEvent.CTRL_MASK);
}
else {
launchKeyStroke = new NamedKeyStroke('P', InputEvent.CTRL_MASK);
}
}
public void draw(java.awt.Graphics g, int x, int y, java.awt.Component obs, double zoom) {
piece.draw(g, x, y, obs, zoom);
}
public String getName() {
return piece.getName();
}
public java.awt.Rectangle boundingBox() {
return piece.boundingBox();
}
public Shape getShape() {
return piece.getShape();
}
public String myGetState() {
return state;
}
public void mySetState(String state) {
this.state = state;
updateFieldsFromState();
}
/** returns string defining the field types */
public String myGetType() {
SequenceEncoder se = new SequenceEncoder(TYPE_DELIMITOR);
String red = backgroundColor == null ? "" : Integer.toString(backgroundColor.getRed());
String green = backgroundColor == null ? "" : Integer.toString(backgroundColor.getGreen());
String blue = backgroundColor == null ? "" : Integer.toString(backgroundColor.getBlue());
String commit = Integer.toString(commitStyle);
se.append(m_definition).append(menuName).append("").append(commit).
append(red).append(green).append(blue).append(launchKeyStroke);
return ID + se.getValue();
}
protected KeyCommand[] myGetKeyCommands() {
launch = new KeyCommand(menuName, launchKeyStroke, Decorator.getOutermost(this), this);
return new KeyCommand[]{launch};
}
/** parses leading integer from string */
int atoi(String s) {
int value = 0;
if (s != null) {
for (int i = 0; i < s.length() && Character.isDigit(s.charAt(i)); ++i) {
value = value * 10 + s.charAt(i) - '0';
}
}
return value;
}
/** parses trailing integer from string */
int atoiRight(String s) {
int value = 0;
if (s != null) {
int base = 1;
for (int i = s.length() - 1; i >= 0 && Character.isDigit(s.charAt(i)); --i, base *= 10) {
value = value + base * (s.charAt(i) - '0');
}
}
return value;
}
// stores field values
private void updateStateFromFields() {
SequenceEncoder encoder = new SequenceEncoder(STATE_DELIMITOR);
for (Object field : m_fields) {
if (field instanceof JTextComponent) {
encoder.append(((JTextComponent) field).getText()
.replace('\n', LINE_DELIMINATOR));
}
else if (field instanceof TickPanel) {
encoder.append(((TickPanel) field).getValue());
}
else {
encoder.append("Unknown");
}
}
if (encoder.getValue() != null && !encoder.getValue().equals(state)) {
mySetState(encoder.getValue());
GamePiece outer = Decorator.getOutermost(PropertySheet.this);
if (outer.getId() != null) {
GameModule.getGameModule().sendAndLog(
new ChangePiece(outer.getId(), oldState, outer.getState()));
}
}
}
private void updateFieldsFromState() {
isUpdating = true;
properties.clear();
final SequenceEncoder.Decoder defDecoder =
new SequenceEncoder.Decoder(m_definition, DEF_DELIMITOR);
final SequenceEncoder.Decoder stateDecoder =
new SequenceEncoder.Decoder(state, STATE_DELIMITOR);
for (int iField = 0; defDecoder.hasMoreTokens(); ++iField) {
String name = defDecoder.nextToken();
if (name.length() == 0) {
continue;
}
final int type = name.charAt(0) - '0';
name = name.substring(1);
String value = stateDecoder.nextToken("");
switch (type) {
case TICKS:
case TICKS_VAL:
case TICKS_MAX:
case TICKS_VALMAX:
final int index = value.indexOf('/');
properties.put(name, index > 0 ? value.substring(0, index) : value);
break;
default:
properties.put(name, value);
}
value = value.replace(LINE_DELIMINATOR, '\n');
if (frame != null) {
final Object field = m_fields.get(iField);
if (field instanceof JTextComponent) {
final JTextComponent tf = (JTextComponent) field;
final int pos = Math.min(tf.getCaretPosition(), value.length());
tf.setText(value);
tf.setCaretPosition(pos);
}
else if (field instanceof TickPanel) {
((TickPanel) field).updateValue(value);
}
}
}
if (applyButton != null) {
applyButton.setEnabled(false);
}
isUpdating = false;
}
public Command myKeyEvent(KeyStroke stroke) {
myGetKeyCommands();
if (launch.matches(stroke)) {
if (frame == null) {
m_fields = new ArrayList<JComponent>();
VASSAL.build.module.Map map = piece.getMap();
Frame parent = null;
if (map != null && map.getView() != null) {
Container topWin = map.getView().getTopLevelAncestor();
if (topWin instanceof JFrame) {
parent = (Frame) topWin;
}
}
else {
parent = GameModule.getGameModule().getFrame();
}
frame = new PropertySheetDialog(parent);
JPanel pane = new JPanel();
JScrollPane scroll =
new JScrollPane(pane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
frame.add(scroll);
// set up Apply button
if (commitStyle == COMMIT_ON_APPLY) {
applyButton = new JButton("Apply");
applyButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
if (applyButton != null) {
applyButton.setEnabled(false);
}
updateStateFromFields();
}
});
applyButton.setMnemonic(java.awt.event.KeyEvent.VK_A); // respond to Alt+A
applyButton.setEnabled(false);
}
// ... enable APPLY button when field changes
DocumentListener changeListener = new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
update(e);
}
public void removeUpdate(DocumentEvent e) {
update(e);
}
public void changedUpdate(DocumentEvent e) {
update(e);
}
public void update(DocumentEvent e) {
if (!isUpdating) {
switch (commitStyle) {
case COMMIT_IMMEDIATELY:
// queue commit operation because it could do something
// unsafe in a an event update
SwingUtilities.invokeLater(new Runnable() {
public void run() {
updateStateFromFields();
}
});
break;
case COMMIT_ON_APPLY:
applyButton.setEnabled(true);
break;
case COMMIT_ON_CLOSE:
break;
default:
throw new IllegalStateException();
}
}
}
};
pane.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.BOTH;
c.insets = new Insets(1, 3, 1, 3);
c.gridx = 0;
c.gridy = 0;
c.fill = GridBagConstraints.BOTH;
SequenceEncoder.Decoder defDecoder =
new SequenceEncoder.Decoder(m_definition, DEF_DELIMITOR);
SequenceEncoder.Decoder stateDecoder =
new SequenceEncoder.Decoder(state, STATE_DELIMITOR);
while (defDecoder.hasMoreTokens()) {
String code = defDecoder.nextToken();
if (code.length() == 0) {
break;
}
int type = code.charAt(0) - '0';
String name = code.substring(1);
JComponent field;
switch (type) {
case TEXT_FIELD:
field = new JTextField(stateDecoder.nextToken(""));
((JTextComponent) field).getDocument()
.addDocumentListener(changeListener);
((JTextField) field).addActionListener(frame);
m_fields.add(field);
break;
case TEXT_AREA:
field = new JTextArea(
stateDecoder.nextToken("").replace(LINE_DELIMINATOR, '\n'));
((JTextComponent) field).getDocument()
.addDocumentListener(changeListener);
m_fields.add(field);
field = new ScrollPane(field);
break;
case TICKS:
case TICKS_VAL:
case TICKS_MAX:
case TICKS_VALMAX:
field = new TickPanel(stateDecoder.nextToken(""), type);
((TickPanel) field).addDocumentListener(changeListener);
((TickPanel) field).addActionListener(frame);
if (backgroundColor != null)
field.setBackground(backgroundColor);
m_fields.add(field);
break;
case SPINNER:
JSpinner spinner = new JSpinner();
JTextField textField =
((JSpinner.DefaultEditor) spinner.getEditor()).getTextField();
textField.setText(stateDecoder.nextToken(""));
textField.getDocument().addDocumentListener(changeListener);
m_fields.add(textField);
field = spinner;
break;
case LABEL_ONLY:
default :
stateDecoder.nextToken("");
field = null;
m_fields.add(field);
break;
}
c.gridwidth = type == TEXT_AREA || type == LABEL_ONLY ? 2 : 1;
if (name != null && !name.equals("")) {
c.gridx = 0;
c.weighty = 0.0;
c.weightx = c.gridwidth == 2 ? 1.0 : 0.0;
pane.add(new JLabel(getTranslation(name)), c);
if (c.gridwidth == 2) {
++c.gridy;
}
}
if (field != null) {
c.weightx = 1.0;
c.weighty = type == TEXT_AREA ? 1.0 : 0.0;
c.gridx = type == TEXT_AREA ? 0 : 1;
pane.add(field, c);
++c.gridy;
}
}
if (backgroundColor != null) {
pane.setBackground(backgroundColor);
}
if (commitStyle == COMMIT_ON_APPLY) {
// setup Close button
JButton closeButton = new JButton("Close");
closeButton.setMnemonic(java.awt.event.KeyEvent.VK_C); // respond to Alt+C // key event cannot be resolved
closeButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
updateStateFromFields();
frame.setVisible(false);
}
});
c.gridwidth = 1;
c.weighty = 0.0;
c.anchor = GridBagConstraints.SOUTHEAST;
c.gridwidth = 2; // use the whole row
c.gridx = 0;
c.weightx = 0.0;
JPanel buttonRow = new JPanel();
buttonRow.add(applyButton);
buttonRow.add(closeButton);
if (backgroundColor != null) {
applyButton.setBackground(backgroundColor);
closeButton.setBackground(backgroundColor);
buttonRow.setBackground(backgroundColor);
}
pane.add(buttonRow, c);
}
// move window
Point p = GameModule.getGameModule().getFrame().getLocation();
if (getMap() != null) {
p = getMap().getView().getLocationOnScreen();
Point p2 = getMap().componentCoordinates(getPosition());
p.translate(p2.x, p2.y);
}
frame.setLocation(p.x, p.y);
// watch for window closing - save state
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent evt) {
updateStateFromFields();
}
});
frame.pack();
}
// Name window and make it visible
frame.setTitle(getLocalizedName());
oldState = Decorator.getOutermost(this).getState();
frame.setVisible(true);
return null;
}
else {
return null;
}
}
@Override
public Object getLocalizedProperty(Object key) {
final Object value = properties.get(key);
return value == null ? super.getLocalizedProperty(key) : value;
}
public Object getProperty(Object key) {
final Object value = properties.get(key);
return value == null ? super.getProperty(key) : value;
}
public String getDescription() {
return "Property Sheet";
}
public VASSAL.build.module.documentation.HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("PropertySheet.htm");
}
public PieceEditor getEditor() {
return new Ed(this);
}
/**
* A generic panel for editing unit traits.
* Not directly related to "PropertySheets".
*/
static class PropertyPanel extends JPanel implements FocusListener {
private static final long serialVersionUID = 1L;
GridBagConstraints c = new GridBagConstraints();
public PropertyPanel() {
super(new GridBagLayout());
c.insets = new Insets(0, 4, 0, 4);
//c.ipadx = 5;
c.anchor = GridBagConstraints.WEST;
}
public NamedHotKeyConfigurer addKeyStrokeConfig(NamedKeyStroke value) {
++c.gridy;
c.gridx = 0;
c.weightx = 0.0;
c.fill = GridBagConstraints.NONE;
add(new JLabel("Keystroke:"), c);
++c.gridx;
final NamedHotKeyConfigurer config = new NamedHotKeyConfigurer(null, "", value);
final Component field = config.getControls();
add(field, c);
return config;
}
public JTextField addStringCtrl(String name, String value) {
++c.gridy;
c.gridx = 0;
c.weightx = 0.0;
c.fill = GridBagConstraints.HORIZONTAL;
add(new JLabel(name), c);
c.weightx = 1.0;
JTextField field = new JTextField(value);
++c.gridx;
add(field, c);
return field;
}
public JButton addColorCtrl(String name, Color value) {
++c.gridy;
c.gridx = 0;
c.weightx = 0.0;
c.fill = GridBagConstraints.NONE;
add(new JLabel(name), c);
c.weightx = 0.0;
JButton button = new JButton("Default");
if (value != null) {
button.setBackground(value);
button.setText("sample");
}
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
JButton button = (JButton) event.getSource();
Color value = button.getBackground();
Color newColor = JColorChooser.showDialog(PropertyPanel.this, "Choose background color or CANCEL to use default color scheme", value);
if (newColor != null) {
button.setBackground(newColor);
button.setText("sample");
}
else {
button.setBackground(PropertyPanel.this.getBackground());
button.setText("Default");
}
}
});
++c.gridx;
add(button, c);
return button;
}
public JComboBox addComboBox(String name, String[] values, int initialRow) {
JComboBox comboBox = new JComboBox(values);
comboBox.setEditable(false);
comboBox.setSelectedIndex(initialRow);
addCtrl(name, comboBox);
return comboBox;
}
public void addCtrl(String name, JComponent ctrl) {
++c.gridy;
c.gridx = 0;
c.weightx = 0.0;
c.fill = GridBagConstraints.NONE;
add(new JLabel(name), c);
c.weightx = 0.0;
++c.gridx;
add(ctrl, c);
}
DefaultTableModel tableModel;
String[] defaultValues;
public static class SmartTable extends JTable {
private static final long serialVersionUID = 1L;
SmartTable(TableModel m) {
super(m);
setSurrendersFocusOnKeystroke(true);
}
//Prepares the editor by querying the data model for the value and selection state of the cell at row, column. }
public Component prepareEditor(TableCellEditor editor, int row, int column) {
if (row == getRowCount() - 1) {
((DefaultTableModel) getModel()).addRow(Ed.DEFAULT_ROW);
}
Component component = super.prepareEditor(editor, row, column);
if (component instanceof JTextComponent) {
((JTextComponent) component).grabFocus();
((JTextComponent) component).selectAll();
}
return component;
}
}
public JTable addTableCtrl(String name, DefaultTableModel theTableModel, String[] theDefaultValues) {
tableModel = theTableModel;
this.defaultValues = theDefaultValues;
++c.gridy;
c.gridx = 0;
c.weighty = 0.0;
c.weightx = 1.0;
c.gridwidth = 2;
c.fill = GridBagConstraints.HORIZONTAL;
add(new JLabel(name), c);
++c.gridy;
c.weighty = 1.0;
c.fill = GridBagConstraints.BOTH;
final SmartTable table = new SmartTable(tableModel);
add(new ScrollPane(table), c);
c.gridwidth = 1;
++c.gridy;
c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.EAST;
c.gridwidth = 2;
c.weighty = 0.0;
c.weightx = 1.0;
JPanel buttonPanel = new JPanel();
// add button
JButton addButton = new JButton("Insert Row");
buttonPanel.add(addButton);
addButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (table.isEditing()) {
table.getCellEditor().stopCellEditing();
}
ListSelectionModel selection = table.getSelectionModel();
int iSelection;
if (selection.isSelectionEmpty()) {
tableModel.addRow(defaultValues);
iSelection = tableModel.getRowCount() - 1;
}
else {
iSelection = selection.getMaxSelectionIndex();
tableModel.insertRow(iSelection, defaultValues);
}
tableModel.fireTableDataChanged(); // BING BING BING
selection.setSelectionInterval(iSelection, iSelection);
table.grabFocus();
table.editCellAt(iSelection, 0);
Component comp = table.getCellEditor().getTableCellEditorComponent(table, null, true, iSelection, 0);
if (comp instanceof JComponent) {
((JComponent) comp).grabFocus();
}
}
});
// delete button
final JButton deleteButton = new JButton("Delete Row");
deleteButton.setEnabled(false);
buttonPanel.add(deleteButton);
deleteButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (table.isEditing()) {
table.getCellEditor().stopCellEditing();
}
ListSelectionModel selection = table.getSelectionModel();
for (int i = selection.getMaxSelectionIndex(); i >= selection.getMinSelectionIndex(); --i) {
if (selection.isSelectedIndex(i)) {
tableModel.removeRow(i);
}
}
tableModel.fireTableDataChanged(); // BING BING BING
}
});
// Ask to be notified of selection changes.
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent event) {
//Ignore extra messages.
if (!event.getValueIsAdjusting()) {
ListSelectionModel lsm = (ListSelectionModel) event.getSource();
deleteButton.setEnabled(!lsm.isSelectionEmpty());
}
}
});
add(buttonPanel, c);
c.anchor = GridBagConstraints.WEST;
c.weightx = 0.0;
return table;
}
public void focusGained(FocusEvent event) {
}
// make sure we save user's changes
public void focusLost(FocusEvent event) {
if (event.getComponent() instanceof JTable) {
JTable table = (JTable) event.getComponent();
if (table.isEditing()) {
table.getCellEditor().stopCellEditing();
}
}
}
}
public PieceI18nData getI18nData() {
final ArrayList<String> items = new ArrayList<String>();
final SequenceEncoder.Decoder defDecoder =
new SequenceEncoder.Decoder(m_definition, DEF_DELIMITOR);
while (defDecoder.hasMoreTokens()) {
final String item = defDecoder.nextToken();
items.add(item.length() == 0 ? "" : item.substring(1));
}
final String[] menuNames = new String[items.size()+1];
final String[] descriptions = new String[items.size()+1];
menuNames[0] = menuName;
descriptions[0] = "Property Sheet command";
int j = 1;
for (String s : items) {
menuNames[j] = s;
descriptions[j] = "Property Sheet item " + j;
j++;
}
return getI18nData(menuNames, descriptions);
}
/**
* Return Property names exposed by this trait
*/
public List<String> getPropertyNames() {
ArrayList<String> l = new ArrayList<String>();
for (String prop : properties.keySet()) {
l.add(prop);
}
return l;
}
private static class Ed implements PieceEditor {
private PropertyPanel m_panel;
private JTextField menuNameCtrl;
private NamedHotKeyConfigurer keyStrokeConfig;
private JButton colorCtrl;
private JTable propertyTable;
private JComboBox commitCtrl;
static final String[] COLUMN_NAMES = {"Name", "Type"};
static final String[] DEFAULT_ROW = {"*new property*", "Text"};
public Ed(PropertySheet propertySheet) {
m_panel = new PropertyPanel();
menuNameCtrl = m_panel.addStringCtrl("Menu Text:", propertySheet.menuName);
keyStrokeConfig = m_panel.addKeyStrokeConfig(propertySheet.launchKeyStroke);
commitCtrl = m_panel.addComboBox("Commit changes on:", COMMIT_VALUES, propertySheet.commitStyle);
colorCtrl = m_panel.addColorCtrl("Background Color:", propertySheet.backgroundColor);
DefaultTableModel dataModel = new DefaultTableModel(getTableData(propertySheet.m_definition), COLUMN_NAMES);
AddCreateRow(dataModel);
propertyTable = m_panel.addTableCtrl("Properties:", dataModel, DEFAULT_ROW);
DefaultCellEditor typePicklist = new DefaultCellEditor(new JComboBox(TYPE_VALUES));
propertyTable.getColumnModel().getColumn(1).setCellEditor(typePicklist);
}
protected void AddCreateRow(DefaultTableModel data) {
data.addRow(DEFAULT_ROW);
}
protected String[][] getTableData(String definition) {
SequenceEncoder.Decoder decoder = new SequenceEncoder.Decoder(definition, DEF_DELIMITOR);
int numRows = !definition.equals("") && decoder.hasMoreTokens() ? 1 : 0;
for (int iDef = -1; (iDef = definition.indexOf(DEF_DELIMITOR, iDef + 1)) >= 0;) {
++numRows;
}
String[][] rows = new String[numRows][2];
for (int iRow = 0; decoder.hasMoreTokens() && iRow < numRows; ++iRow) {
String token = decoder.nextToken();
rows[iRow][0] = token.substring(1);
rows[iRow][1] = TYPE_VALUES[token.charAt(0) - '0'];
}
return rows;
}
public java.awt.Component getControls() {
return m_panel;
}
/** returns the type-definition in the format:
definition, name, keystroke, commit, red, green, blue
*/
public String getType() {
if (propertyTable.isEditing()) {
propertyTable.getCellEditor().stopCellEditing();
}
SequenceEncoder defEncoder = new SequenceEncoder(DEF_DELIMITOR);
// StringBuilder definition = new StringBuilder();
// int numRows = propertyTable.getRowCount();
for (int iRow = 0; iRow < propertyTable.getRowCount(); ++iRow) {
String typeString = (String) propertyTable.getValueAt(iRow, 1);
for (int iType = 0; iType < TYPE_VALUES.length; ++iType) {
if (typeString.matches(TYPE_VALUES[iType]) &&
!DEFAULT_ROW[0].equals(propertyTable.getValueAt(iRow, 0))) {
defEncoder.append(iType + (String) propertyTable.getValueAt(iRow, 0));
break;
}
}
}
SequenceEncoder typeEncoder = new SequenceEncoder(TYPE_DELIMITOR);
// calc color strings
String red, green, blue;
if (colorCtrl.getText().equals("Default")) {
red = "";
green = "";
blue = "";
}
else {
red = Integer.toString(colorCtrl.getBackground().getRed());
green = Integer.toString(colorCtrl.getBackground().getGreen());
blue = Integer.toString(colorCtrl.getBackground().getBlue());
}
String definitionString = defEncoder.getValue();
typeEncoder.append(definitionString == null ? "" : definitionString).
append(menuNameCtrl.getText()).
append("").
append(Integer.toString(commitCtrl.getSelectedIndex())).
append(red).append(green).append(blue).append(keyStrokeConfig.getValueString());
return ID + typeEncoder.getValue();
}
/** returns a default value-string for the given definition */
public String getState() {
final StringBuilder buf = new StringBuilder();
for (int i = 0; i < propertyTable.getRowCount(); ++i) {
buf.append(STATE_DELIMITOR);
}
return buf.toString();
}
}
class TickPanel extends JPanel implements ActionListener, FocusListener, DocumentListener {
private static final long serialVersionUID = 1L;
private int numTicks;
private int maxTicks;
private int panelType;
private JTextField valField;
private JTextField maxField;
private TickLabel ticks;
private List<ActionListener> actionListeners =
new ArrayList<ActionListener>();
private List<DocumentListener> documentListeners =
new ArrayList<DocumentListener>();
public TickPanel(String value, int type) {
super(new GridBagLayout());
set(value);
panelType = type;
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.BOTH;
c.ipadx = 1;
c.weightx = 0.0;
c.gridx = 0;
c.gridy = 0;
Dimension minSize;
if (panelType == TICKS_VAL || panelType == TICKS_VALMAX) {
valField = new JTextField(Integer.toString(numTicks));
minSize = valField.getMinimumSize();
minSize.width = 24;
valField.setMinimumSize(minSize);
valField.setPreferredSize(minSize);
valField.addActionListener(this);
valField.addFocusListener(this);
add(valField, c);
++c.gridx;
}
if (panelType == TICKS_MAX || panelType == TICKS_VALMAX) {
maxField = new JTextField(Integer.toString(maxTicks));
minSize = maxField.getMinimumSize();
minSize.width = 24;
maxField.setMinimumSize(minSize);
maxField.setPreferredSize(minSize);
maxField.addActionListener(this);
maxField.addFocusListener(this);
if (panelType == TICKS_VALMAX) {
add(new JLabel("/"), c);
++c.gridx;
}
add(maxField, c);
++c.gridx;
}
ticks = new TickLabel(numTicks, maxTicks, panelType);
ticks.addActionListener(this);
c.weightx = 1.0;
add(ticks, c);
doLayout();
}
public void updateValue(String value) {
set(value);
updateFields();
}
private void set(String value) {
set(atoi(value), atoiRight(value));
}
private boolean set(int num, int max) {
boolean changed = false;
if (numTicks == 0 && maxTicks == 0 && num == 0 && max > 0) {
// num = max; // This causes a bug in which ticks set to zero go to max after save/reload of a game
changed = true;
}
if (numTicks == 0 && maxTicks == 0 && max == 0 && num > 0) {
max = num;
changed = true;
}
numTicks = Math.min(max, num);
maxTicks = max;
return changed;
}
public String getValue() {
commitTextFields();
return Integer.toString(numTicks) + VALUE_DELIMINATOR + maxTicks;
}
private void commitTextFields() {
if (valField != null || maxField != null) {
if (set(valField != null ? atoi(valField.getText()) : numTicks,
maxField != null ? atoi(maxField.getText()) : maxTicks)) {
updateFields();
}
}
}
private void updateFields() {
if (valField != null) {
valField.setText(Integer.toString(numTicks));
}
if (maxField != null) {
maxField.setText(Integer.toString(maxTicks));
}
ticks.set(numTicks, maxTicks);
}
private boolean areFieldValuesValid() {
int max = maxField == null ? maxTicks : atoi(maxField.getText());
int val = valField == null ? numTicks : atoi(valField.getText());
return val < max && val >= 0;
}
// field changed
public void actionPerformed(ActionEvent event) {
if (event.getSource() == maxField || event.getSource() == valField) {
commitTextFields();
ticks.set(numTicks, maxTicks);
fireActionEvent();
}
else if (event.getSource() == ticks) {
commitTextFields();
numTicks = ticks.getNumTicks();
if (maxField == null) {
maxTicks = ticks.getMaxTicks();
}
updateFields();
fireDocumentEvent();
}
}
public void addActionListener(ActionListener listener) {
actionListeners.add(listener);
}
public void fireActionEvent() {
for (ActionListener l : actionListeners) {
l.actionPerformed(new ActionEvent(this, 0, null));
}
}
void addDocumentListener(DocumentListener listener) {
if (valField != null)
valField.getDocument().addDocumentListener(this);
if (maxField != null)
maxField.getDocument().addDocumentListener(this);
documentListeners.add(listener);
}
public void fireDocumentEvent() {
for (DocumentListener l : documentListeners) {
l.changedUpdate(null);
}
}
// FocusListener Interface
public void focusLost(FocusEvent event) {
commitTextFields();
ticks.set(numTicks, maxTicks);
}
public void focusGained(FocusEvent event) {
}
public void changedUpdate(DocumentEvent event) {
// do not propagate events unless min/max is valid. We don't want to trigger both
// a state update. A state update might trigger a fix-min/max operation, which would
// cause the text fields to update which will throw an IllegalStateException
if (areFieldValuesValid()) {
fireDocumentEvent();
}
}
public void insertUpdate(DocumentEvent event) {
changedUpdate(event);
}
public void removeUpdate(DocumentEvent event) {
changedUpdate(event);
}
}
class TickLabel extends JLabel implements MouseListener {
private static final long serialVersionUID = 1L;
private int numTicks = 0;
private int maxTicks = 0;
protected int panelType;
private List<ActionListener> actionListeners =
new ArrayList<ActionListener>();
public int getNumTicks() {
return numTicks;
}
public int getMaxTicks() {
return maxTicks;
}
public void addActionListener(ActionListener listener) {
actionListeners.add(listener);
}
public TickLabel(int numTicks, int maxTicks, int panelType) {
super(" ");
//Debug.trace("TickLabel( " + maxTicks + ", " + numTicks + " )");
set(numTicks, maxTicks);
this.panelType = panelType;
addMouseListener(this);
}
protected int topMargin = 2;
protected int leftMargin = 2;
protected int numRows;
protected int numCols;
protected int dx = 1;
public void paint(Graphics g) {
//Debug.trace("TickLabcmdel.paint(" + numTicks + "/" + maxTicks + ")");
//Debug.trace(" width=" + getWidth() + " height=" + getHeight());
if (maxTicks > 0) {
// prefered width is 10
// min width before resize is 6
int displayWidth = getWidth() - 2;
dx = Math.min(displayWidth / maxTicks, 10);
// if dx < 2, we have a problem
numRows = dx < 3 ? 3 : dx < 6 ? 2 : 1;
dx = Math.min(displayWidth * numRows / maxTicks, Math.min(getHeight() / numRows, 10));
if (dx < 1) dx = 1;
numCols = (maxTicks + numRows - 1) / numRows;
int dy = dx;
topMargin = (getHeight() - dy * numRows + 2) / 2;
int tick = 0;
int row, col;
if (dx > 4) {
for (; tick < maxTicks; ++tick) {
row = tick / numCols;
col = tick % numCols;
g.setColor(Color.BLACK);
g.drawRect(leftMargin + col * dx, topMargin + row * dy, dx - 3, dy - 3);
g.setColor(tick < numTicks ? Color.BLACK : Color.WHITE);
g.fillRect(leftMargin + 1 + col * dx, topMargin + 1 + row * dy, dx - 4, dy - 4);
}
}
else {
g.setColor(Color.GRAY);
g.fillRect(0, topMargin - 2, numCols * dx + leftMargin * 2, numRows * dy + 4);
for (; tick < maxTicks; ++tick) {
row = tick / numCols;
col = tick % numCols;
g.setColor(tick < numTicks ? Color.BLACK : Color.WHITE);
g.fillRect(leftMargin + col * dx, topMargin + row * dy, dx - 1, dy - 1);
}
}
}
}
public void mouseClicked(MouseEvent event) {
if ((event.isMetaDown() || event.isShiftDown()) && panelType != TICKS_VALMAX) {
new EditTickLabelValueDialog(this);
return;
}
int col = Math.min((event.getX() - leftMargin + 1) / dx, numCols - 1);
int row = Math.min((event.getY() - topMargin + 1) / dx, numRows - 1);
int num = row * numCols + col + 1;
// Checkbox behavior; toggle the box clicked on
// numTicks = num > numTicks ? num : num - 1;
// Slider behavior; set the box clicked on and all to left and clear all boxes to right,
// UNLESS user clicked on last set box (which would do nothing), in this case, toggle the
// box clicked on. This is the only way the user can clear the first box.
numTicks = (num == numTicks) ? num - 1 : num;
fireActionEvent();
repaint();
}
public void fireActionEvent() {
for (ActionListener l : actionListeners) {
l.actionPerformed(new ActionEvent(this, 0, null));
}
}
public void set(int newNumTicks, int newMaxTicks) {
//Debug.trace("TickLabel.set( " + newNumTicks + "," + newMaxTicks + " ) was " + numTicks + "/" + maxTicks);
numTicks = newNumTicks;
maxTicks = newMaxTicks;
String tip = numTicks + "/" + maxTicks;
if (panelType != TICKS_VALMAX) {
tip += " (right-click to edit)";
}
setToolTipText(tip);
repaint();
}
public void mouseEntered(MouseEvent event) {
}
public void mouseExited(MouseEvent event) {
}
public void mousePressed(MouseEvent event) {
}
public void mouseReleased(MouseEvent event) {
}
public class EditTickLabelValueDialog extends JPanel implements ActionListener, DocumentListener, FocusListener {
private static final long serialVersionUID = 1L;
TickLabel theTickLabel;
JLayeredPane editorParent;
JTextField valueField;
public EditTickLabelValueDialog(TickLabel owner) {
super(new BorderLayout());
theTickLabel = owner;
// Find containing dialog
Container theDialog = theTickLabel.getParent();
while (!(theDialog instanceof JDialog) && theDialog != null) {
theDialog = theDialog.getParent();
}
if (theDialog != null) {
editorParent = ((JDialog) theDialog).getLayeredPane();
Rectangle newBounds = SwingUtilities.convertRectangle(theTickLabel.getParent(), theTickLabel.getBounds(), editorParent);
setBounds(newBounds);
JButton okButton = new JButton("Ok");
switch (panelType) {
case TICKS_VAL:
valueField = new JTextField(Integer.toString(owner.getMaxTicks()));
valueField.setToolTipText("max value");
break;
case TICKS_MAX:
valueField = new JTextField(Integer.toString(owner.getNumTicks()));
valueField.setToolTipText("current value");
break;
case TICKS:
default:
valueField = new JTextField(owner.numTicks + "/" + owner.maxTicks);
valueField.setToolTipText("current value / max value");
break;
}
valueField.addActionListener(this);
valueField.addFocusListener(this);
valueField.getDocument().addDocumentListener(this);
add(valueField, BorderLayout.CENTER);
okButton.addActionListener(this);
add(okButton, BorderLayout.EAST);
editorParent.add(this, JLayeredPane.MODAL_LAYER);
setVisible(true);
valueField.grabFocus();
}
}
public void storeValues() {
String value = valueField.getText();
switch (panelType) {
case TICKS_VAL:
theTickLabel.set(theTickLabel.getNumTicks(), atoi(value));
break;
case TICKS_MAX:
theTickLabel.set(atoi(value), theTickLabel.getMaxTicks());
break;
case TICKS:
default:
theTickLabel.set(atoi(value), atoiRight(value));
break;
}
theTickLabel.fireActionEvent();
}
public void actionPerformed(ActionEvent event) {
storeValues();
editorParent.remove(this);
}
public void changedUpdate(DocumentEvent event) {
theTickLabel.fireActionEvent();
}
public void insertUpdate(DocumentEvent event) {
theTickLabel.fireActionEvent();
}
public void removeUpdate(DocumentEvent event) {
theTickLabel.fireActionEvent();
}
public void focusGained(FocusEvent event) {
}
public void focusLost(FocusEvent event) {
storeValues();
}
}
}
}