001: package abbot.editor.editors;
002:
003: import java.awt.*;
004: import java.awt.event.*;
005: import java.lang.reflect.Constructor;
006: import java.util.*;
007:
008: import javax.swing.*;
009: import javax.swing.border.EmptyBorder;
010: import abbot.Log;
011: import abbot.i18n.Strings;
012: import abbot.script.*;
013: import abbot.editor.widgets.ArrayEditor;
014: import abbot.editor.widgets.TextArea;
015: import abbot.editor.widgets.TextField;
016:
017: /** Provide base-level step editor support with step change notification. */
018: // NOTE: this should really be done with beans instead...
019: public abstract class StepEditor extends JPanel implements
020: ActionListener, Scrollable, XMLConstants {
021:
022: private Step step;
023: private JLabel label;
024: JTextField description;
025: private LayoutManager layout;
026: private ArrayList listeners = new ArrayList();
027:
028: protected static final int MARGIN = 4;
029: private boolean fieldChanging = false;
030:
031: protected static Color DEFAULT_FOREGROUND = null;
032: protected static Color ERROR_FOREGROUND = Color.red;
033:
034: public StepEditor(Step step) {
035: setBorder(new EmptyBorder(2, 2, 2, 2));
036: setLayout(new BoxLayout(this , BoxLayout.Y_AXIS));
037: layout = getLayout();
038: this .step = step;
039: label = new JLabel(step.getXMLTag());
040: label.setFont(label.getFont().deriveFont(Font.BOLD));
041: label.setToolTipText(step.getUsage());
042: add(label);
043: description = addTextField(null, step.getDescription());
044: description.setName(TAG_DESC);
045: description.setToolTipText(Strings
046: .get("editor.step.description.tip"));
047: if (DEFAULT_FOREGROUND == null) {
048: DEFAULT_FOREGROUND = description.getForeground();
049: }
050: }
051:
052: /** Keep a reasonable minimum width. */
053: public Dimension getMinimumSize() {
054: Dimension min = super .getMinimumSize();
055: min.width = 200;
056: return min;
057: }
058:
059: /** Keep a reasonable minimum width. */
060: public Dimension getPreferredSize() {
061: Dimension size = super .getPreferredSize();
062: size.width = 200;
063: return size;
064: }
065:
066: /** We don't want to become infinitely wide due to text fields. */
067: public Dimension getMaximumSize() {
068: Dimension max = super .getMaximumSize();
069: max.width = 400;
070: return max;
071: }
072:
073: protected JCheckBox addCheckBox(String title, boolean value) {
074: JCheckBox cb = new JCheckBox(title);
075: cb.setSelected(value);
076: cb.addActionListener(this );
077: add(cb);
078: return cb;
079: }
080:
081: /** Provide a combo box that short-circuits unnecessary and
082: problem-causing event notifications.
083: */
084: private class ComboBox extends JComboBox {
085: private JTextField editor;
086: private boolean configuringEditor;
087:
088: public ComboBox() {
089: }
090:
091: public ComboBox(ComboBoxModel model) {
092: super (model);
093: }
094:
095: public ComboBox(Object[] values) {
096: super (values);
097: }
098:
099: public void addImpl(Component c, Object constraints, int index) {
100: if (c instanceof JTextField) {
101: editor = (JTextField) c;
102: TextField.decorate(editor);
103: }
104: super .addImpl(c, constraints, index);
105: }
106:
107: /** Disallow recursive calls, which occur when someone sets the combo
108: box contents in response to the combo box selection. */
109: public void configureEditor(ComboBoxEditor editor, Object item) {
110: // Avoids IllegalStateExceptions from the text field ("Attempt to
111: // mutate in notification" errors).
112: if (!configuringEditor) {
113: configuringEditor = true;
114: super .configureEditor(editor, item);
115: configuringEditor = false;
116: }
117: }
118:
119: /** Sets the foreground color of the editor text. */
120: public void setForeground(Color c) {
121: if (editor != null) {
122: editor.setForeground(c);
123: }
124: }
125:
126: /** Overridden to prevent recursive calls. */
127: public void fireActionEvent() {
128: if (!fieldChanging) {
129: fieldChanging = true;
130: super .fireActionEvent();
131: fieldChanging = false;
132: }
133: }
134:
135: /** Overridden to prevent recursive calls. */
136: public void fireItemStateChanged(ItemEvent e) {
137: if (!fieldChanging) {
138: fieldChanging = true;
139: super .fireItemStateChanged(e);
140: fieldChanging = false;
141: }
142: }
143: }
144:
145: private static final String NONE = "<None>";
146:
147: private class RefModel extends AbstractListModel implements
148: ComboBoxModel {
149: private Resolver resolver;
150: private boolean includeNone;
151: private Object selected;
152: private Collection set;
153:
154: public RefModel(Resolver r, boolean includeNone) {
155: resolver = r;
156: this .includeNone = includeNone;
157: set = resolver.getComponentReferences();
158: }
159:
160: public Object getElementAt(int i) {
161: checkContents();
162: if (includeNone) {
163: if (i == 0)
164: return NONE;
165: --i;
166: }
167: return ((ComponentReference) set.toArray()[i]).getID();
168: }
169:
170: public int getSize() {
171: checkContents();
172: int size = set.size();
173: return includeNone ? size + 1 : size;
174: }
175:
176: public void setSelectedItem(Object o) {
177: if (o instanceof ComponentReference)
178: o = ((ComponentReference) o).getID();
179: else if (o == null)
180: o = NONE;
181: selected = o;
182: checkContents();
183: }
184:
185: public Object getSelectedItem() {
186: checkContents();
187: return selected == NONE ? null : selected;
188: }
189:
190: // Always check whether this model is synched with the resolver's set
191: // of references.
192: private void checkContents() {
193: Collection current = resolver.getComponentReferences();
194: if (set.size() != current.size()) {
195: set = current;
196: if (!fieldChanging) {
197: fieldChanging = true;
198: fireContentsChanged(this , 0, set.size() - 1);
199: fieldChanging = false;
200: }
201: }
202: }
203: }
204:
205: protected JComboBox addComponentSelector(String title,
206: String refid, Resolver resolver, boolean allowNone) {
207: // NOTE: the combo box has no method of refreshing its contents when
208: // references are added/removed/changed in the resolver
209: JComboBox cb = new ComboBox(new RefModel(resolver, allowNone));
210: cb.setSelectedItem(refid);
211: cb.addActionListener(this );
212: add(title, cb);
213: return cb;
214: }
215:
216: protected JComboBox addComboBox(String title, Object value,
217: Object[] values) {
218: JComboBox cb = new ComboBox(values);
219: cb.setEditable(true);
220: cb.setSelectedItem(value);
221: cb.addActionListener(this );
222: add(title, cb);
223: return cb;
224: }
225:
226: protected JTextField addTextField(String title, String value) {
227: return addTextField(title, value, null);
228: }
229:
230: protected JTextField addTextField(String title, String value,
231: String defaultValue) {
232: JTextField field = new abbot.editor.widgets.TextField(value,
233: defaultValue);
234: field.addActionListener(this );
235: add(title, field);
236: return field;
237: }
238:
239: protected ArrayEditor addArrayEditor(String title, Object[] values) {
240: ArrayEditor ed = new ArrayEditor(values);
241: ed.addActionListener(this );
242: // Make sure we resize/repaint when items are added or removed
243: ed.addActionListener(new ActionListener() {
244: public void actionPerformed(ActionEvent e) {
245: if (e.getActionCommand() != ArrayEditor.ACTION_ITEM_CHANGED) {
246: revalidate();
247: repaint();
248: }
249: }
250: });
251: add(title, ed);
252: return ed;
253: }
254:
255: protected JButton addButton(String title) {
256: JButton button = new JButton(title);
257: button.addActionListener(this );
258: add(button);
259: return button;
260: }
261:
262: protected JTextArea addTextArea(String title, String value) {
263: final TextArea text = new TextArea(value != null ? value : "");
264: text.setLineWrap(true);
265: text.setWrapStyleWord(true);
266: text.setBorder(UIManager.getBorder("TextField.border"));
267: text.addActionListener(this );
268: text.setRows(10);
269: add(title, new JScrollPane(text,
270: JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
271: JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED));
272: return text;
273: }
274:
275: /** Automatically remove the strut spacing and the component. */
276: public void remove(Component comp) {
277: if (getLayout() == layout) {
278: Component[] children = super .getComponents();
279: for (int i = 1; i < children.length; i++) {
280: if (children[i] == comp) {
281: super .remove(children[i - 1]);
282: break;
283: }
284: }
285: }
286: super .remove(comp);
287: }
288:
289: /** Auto-add a label with a component. */
290: public Component add(String name, Component comp) {
291: if (name != null) {
292: JLabel label = new JLabel(name);
293: label.setLabelFor(comp);
294: add(label);
295: }
296: return add(comp);
297: }
298:
299: /** Automatically add a vertical struct with a component. */
300: public Component add(Component comp) {
301: if (getLayout() == layout) {
302: super .add(Box.createVerticalStrut(MARGIN));
303: if (comp instanceof JComponent) {
304: ((JComponent) comp)
305: .setAlignmentX(JComponent.LEFT_ALIGNMENT);
306: }
307: }
308: return super .add(comp);
309: }
310:
311: /** Respond to UI changes by updating the step data. */
312: public void actionPerformed(ActionEvent ev) {
313: Object src = ev.getSource();
314: if (src == description) {
315: // When the description is cleared (but only when entered by ENTER
316: // or FOCUS events), reset it to the default
317: String text = description.getText();
318: String cmd = ev.getActionCommand();
319: if ("".equals(text)) {
320: if (!TextField.isDocumentAction(ev.getActionCommand())) {
321: SwingUtilities.invokeLater(new Runnable() {
322: public void run() {
323: description.setText(step
324: .getDefaultDescription());
325: description.selectAll();
326: }
327: });
328: step.setDescription(null);
329: fireStepChanged();
330: }
331: }
332: // Only explicitly set the step data if the data is different
333: // from the default.
334: else if (cmd == TextField.ACTION_TEXT_REVERTED
335: || !text.equals(step.getDefaultDescription())) {
336: step.setDescription(text);
337: fireStepChanged();
338: }
339: }
340: }
341:
342: public void addStepChangeListener(StepChangeListener scl) {
343: synchronized (listeners) {
344: listeners.add(scl);
345: }
346: }
347:
348: public void removeStepChangeListener(StepChangeListener scl) {
349: synchronized (listeners) {
350: listeners.remove(scl);
351: }
352: }
353:
354: /** This method should be invoked after any change to step data. */
355: protected void fireStepChanged() {
356: synchronized (listeners) {
357: Iterator iter = listeners.iterator();
358: while (iter.hasNext()) {
359: StepChangeListener scl = (StepChangeListener) iter
360: .next();
361: scl.stepChanged(step);
362: }
363: }
364: // The default description may have changed; ensure the text field is
365: // up to date
366: if (!description.getText().equals(step.getDescription())) {
367: description.setText(step.getDescription());
368: }
369: }
370:
371: /** Return the appropriate editor panel for the given Step.
372: * Custom editors must be named after the step class name, and be defined
373: * in the abbot.editor.editors package, e.g. abbot.script.Launch expects
374: * abbot.editor.editors.LaunchEditor, abbot.script.Assert expects
375: * abbot.editor.editors.AssertEditor.
376: */
377: public static StepEditor getEditor(Step step) {
378: Class stepClass = step.getClass();
379: Log.debug("Looking up editor for " + step + " using "
380: + stepClass);
381: String className = stepClass.getName();
382: className = "abbot.editor.editors."
383: + className.substring(className.lastIndexOf(".") + 1)
384: + "Editor";
385: try {
386: Log.debug("Trying " + className);
387: Class cls = Class.forName(className);
388: Class[] types = new Class[] { stepClass };
389: Constructor ctor = cls.getConstructor(types);
390: return (StepEditor) ctor.newInstance(new Object[] { step });
391: } catch (ClassNotFoundException e) {
392: // ignore this one
393: } catch (Exception e) {
394: Log.warn(e);
395: }
396: return null;
397: }
398:
399: /** Always maintain the minimum width. */
400: public Dimension getPreferredScrollableViewportSize() {
401: return getPreferredSize();
402: }
403:
404: public int getScrollableBlockIncrement(Rectangle visible,
405: int orient, int direction) {
406: return orient == SwingConstants.HORIZONTAL ? visible.width
407: : visible.height;
408: }
409:
410: public boolean getScrollableTracksViewportHeight() {
411: return false;
412: }
413:
414: public boolean getScrollableTracksViewportWidth() {
415: return true;
416: }
417:
418: public int getScrollableUnitIncrement(Rectangle visible,
419: int orient, int direction) {
420: return orient == SwingConstants.HORIZONTAL ? 10 : description
421: .getSize().height;
422: }
423:
424: public String toString() {
425: return getClass().getName() + " for " + label.getText();
426: }
427: }
|