package abbot.editor.editors;
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.Constructor;
import java.util.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
import javax.swing.text.*;
import abbot.Log;
import abbot.i18n.Strings;
import abbot.script.*;
import abbot.editor.widgets.ArrayEditor;
import abbot.editor.widgets.TextArea;
import abbot.editor.widgets.TextField;
/** Provide base-level step editor support with step change notification. */
// NOTE: this should really be done with beans instead...
public abstract class StepEditor extends JPanel
implements ActionListener, Scrollable, XMLConstants {
private Step step;
private JLabel label;
JTextField description;
private LayoutManager layout;
private ArrayList listeners = new ArrayList();
protected static final int MARGIN = 4;
private boolean fieldChanging = false;
protected static Color DEFAULT_FOREGROUND = null;
protected static Color ERROR_FOREGROUND = Color.red;
public StepEditor(Step step) {
setBorder(new EmptyBorder(2,2,2,2));
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
layout = getLayout();
this.step = step;
label = new JLabel(step.getXMLTag());
label.setFont(label.getFont().deriveFont(Font.BOLD));
label.setToolTipText(step.getUsage());
add(label);
description = addTextField(null, step.getDescription());
description.setName(TAG_DESC);
description.setToolTipText(Strings.get("editor.step.description.tip"));
if (DEFAULT_FOREGROUND == null) {
DEFAULT_FOREGROUND = description.getForeground();
}
}
/** Keep a reasonable minimum width. */
public Dimension getMinimumSize() {
Dimension min = super.getMinimumSize();
min.width = 200;
return min;
}
/** Keep a reasonable minimum width. */
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
size.width = 200;
return size;
}
/** We don't want to become infinitely wide due to text fields. */
public Dimension getMaximumSize() {
Dimension max = super.getMaximumSize();
max.width = 400;
return max;
}
protected JCheckBox addCheckBox(String title, boolean value) {
JCheckBox cb = new JCheckBox(title);
cb.setSelected(value);
cb.addActionListener(this);
add(cb);
return cb;
}
/** Provide a combo box that short-circuits unnecessary and
problem-causing event notifications.
*/
private class ComboBox extends JComboBox {
private JTextField editor;
private boolean configuringEditor;
public ComboBox() { }
public ComboBox(ComboBoxModel model) { super(model); }
public ComboBox(Object[] values) { super(values); }
public void addImpl(Component c, Object constraints, int index) {
if (c instanceof JTextField) {
editor = (JTextField)c;
TextField.decorate(editor);
}
super.addImpl(c, constraints, index);
}
/** Disallow recursive calls, which occur when someone sets the combo
box contents in response to the combo box selection. */
public void configureEditor(ComboBoxEditor editor, Object item) {
// Avoids IllegalStateExceptions from the text field ("Attempt to
// mutate in notification" errors).
if (!configuringEditor) {
configuringEditor = true;
super.configureEditor(editor, item);
configuringEditor = false;
}
}
/** Sets the foreground color of the editor text. */
public void setForeground(Color c) {
if (editor != null) {
editor.setForeground(c);
}
}
/** Overridden to prevent recursive calls. */
public void fireActionEvent() {
if (!fieldChanging) {
fieldChanging = true;
super.fireActionEvent();
fieldChanging = false;
}
}
/** Overridden to prevent recursive calls. */
public void fireItemStateChanged(ItemEvent e) {
if (!fieldChanging) {
fieldChanging = true;
super.fireItemStateChanged(e);
fieldChanging = false;
}
}
}
private static final String NONE = "<None>";
private class RefModel
extends AbstractListModel implements ComboBoxModel {
private Resolver resolver;
private boolean includeNone;
private Object selected;
private Collection set;
public RefModel(Resolver r, boolean includeNone) {
resolver = r;
this.includeNone = includeNone;
set = resolver.getComponentReferences();
}
public Object getElementAt(int i) {
checkContents();
if (includeNone) {
if (i == 0)
return NONE;
--i;
}
return ((ComponentReference)set.toArray()[i]).getID();
}
public int getSize() {
checkContents();
int size = set.size();
return includeNone ? size + 1 : size;
}
public void setSelectedItem(Object o) {
if (o instanceof ComponentReference)
o = ((ComponentReference)o).getID();
else if (o == null)
o = NONE;
selected = o;
checkContents();
}
public Object getSelectedItem() {
checkContents();
return selected == NONE ? null : selected;
}
// Always check whether this model is synched with the resolver's set
// of references.
private void checkContents() {
Collection current = resolver.getComponentReferences();
if (set.size() != current.size()) {
set = current;
if (!fieldChanging) {
fieldChanging = true;
fireContentsChanged(this, 0, set.size()-1);
fieldChanging = false;
}
}
}
}
protected JComboBox addComponentSelector(String title, String refid,
Resolver resolver,
boolean allowNone) {
// NOTE: the combo box has no method of refreshing its contents when
// references are added/removed/changed in the resolver
JComboBox cb = new ComboBox(new RefModel(resolver, allowNone));
cb.setSelectedItem(refid);
cb.addActionListener(this);
add(title, cb);
return cb;
}
protected JComboBox addComboBox(String title,
Object value, Object[] values) {
JComboBox cb = new ComboBox(values);
cb.setEditable(true);
cb.setSelectedItem(value);
cb.addActionListener(this);
add(title, cb);
return cb;
}
protected JTextField addTextField(String title, String value) {
return addTextField(title, value, null);
}
protected JTextField addTextField(String title, String value,
String defaultValue) {
JTextField field =
new abbot.editor.widgets.TextField(value, defaultValue);
field.addActionListener(this);
add(title, field);
return field;
}
protected ArrayEditor addArrayEditor(String title, Object[] values) {
ArrayEditor ed = new ArrayEditor(values);
ed.addActionListener(this);
// Make sure we resize/repaint when items are added or removed
ed.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand() != ArrayEditor.ACTION_ITEM_CHANGED) {
revalidate();
repaint();
}
}
});
add(title, ed);
return ed;
}
protected JButton addButton(String title) {
JButton button = new JButton(title);
button.addActionListener(this);
add(button);
return button;
}
protected JTextArea addTextArea(String title, String value) {
final TextArea text = new TextArea(value != null ? value : "");
text.setLineWrap(true);
text.setWrapStyleWord(true);
text.setBorder(UIManager.getBorder("TextField.border"));
text.addActionListener(this);
add(title, text);
return text;
}
/** Automatically remove the strut spacing and the component. */
public void remove(Component comp) {
if (getLayout() == layout) {
Component[] children = super.getComponents();
for (int i=1;i < children.length;i++) {
if (children[i] == comp) {
super.remove(children[i-1]);
break;
}
}
}
super.remove(comp);
}
/** Auto-add a label with a component. */
public Component add(String name, Component comp) {
if (name != null) {
JLabel label = new JLabel(name);
label.setLabelFor(comp);
add(label);
}
return add(comp);
}
/** Automatically add a vertical struct with a component. */
public Component add(Component comp) {
if (getLayout() == layout) {
super.add(Box.createVerticalStrut(MARGIN));
if (comp instanceof JComponent) {
((JComponent)comp).setAlignmentX(JComponent.LEFT_ALIGNMENT);
}
}
return super.add(comp);
}
/** Respond to UI changes by updating the step data. */
public void actionPerformed(ActionEvent ev) {
Object src = ev.getSource();
if (src == description) {
// When the description is cleared (but only when entered by ENTER
// or FOCUS events), reset it to the default
String text = description.getText();
String cmd = ev.getActionCommand();
if ("".equals(text)) {
if (!TextField.isDocumentAction(ev.getActionCommand())) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
description.setText(step.getDefaultDescription());
description.selectAll();
}
});
step.setDescription(null);
fireStepChanged();
}
}
// Only explicitly set the step data if the data is different
// from the default.
else if (cmd == TextField.ACTION_TEXT_REVERTED
|| !text.equals(step.getDefaultDescription())) {
step.setDescription(text);
fireStepChanged();
}
}
}
public void addStepChangeListener(StepChangeListener scl) {
synchronized(listeners) {
listeners.add(scl);
}
}
public void removeStepChangeListener(StepChangeListener scl) {
synchronized(listeners) {
listeners.remove(scl);
}
}
/** This method should be invoked after any change to step data. */
protected void fireStepChanged() {
synchronized(listeners) {
Iterator iter = listeners.iterator();
while (iter.hasNext()) {
StepChangeListener scl = (StepChangeListener)iter.next();
scl.stepChanged(step);
}
}
// The default description may have changed; ensure the text field is
// up to date
if (!description.getText().equals(step.getDescription())) {
description.setText(step.getDescription());
}
}
/** Return the appropriate editor panel for the given Step.
* Custom editors must be named after the step class name, and be defined
* in the abbot.editor.editors package, e.g. abbot.script.Launch expects
* abbot.editor.editors.LaunchEditor, abbot.script.Assert expects
* abbot.editor.editors.AssertEditor.
*/
public static StepEditor getEditor(Step step) {
Class stepClass = step.getClass();
Log.debug("Looking up editor for " + step + " using " + stepClass);
String className = stepClass.getName();
className = "abbot.editor.editors."
+ className.substring(className.lastIndexOf(".") + 1) + "Editor";
try {
Log.debug("Trying " + className);
Class cls = Class.forName(className);
Class[] types = new Class[] { stepClass };
Constructor ctor = cls.getConstructor(types);
return (StepEditor)ctor.newInstance(new Object[] { step });
}
catch(ClassNotFoundException e) {
// ignore this one
}
catch(Exception e) {
Log.warn(e);
}
return null;
}
/** Always maintain the minimum width. */
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
public int getScrollableBlockIncrement(Rectangle visible,
int orient, int direction) {
return orient == SwingConstants.HORIZONTAL
? visible.width : visible.height;
}
public boolean getScrollableTracksViewportHeight() {
return false;
}
public boolean getScrollableTracksViewportWidth() {
return true;
}
public int getScrollableUnitIncrement(Rectangle visible,
int orient, int direction) {
return orient == SwingConstants.HORIZONTAL
? 10 : description.getSize().height;
}
public String toString() {
return getClass().getName() + " for " + label.getText();
}
}