001: package abbot.editor;
002:
003: import java.awt.*;
004: import java.awt.event.*;
005: import java.util.*;
006:
007: import javax.swing.*;
008: import javax.swing.border.EmptyBorder;
009:
010: import abbot.*;
011: import abbot.editor.actions.*;
012: import abbot.editor.widgets.*;
013: import abbot.i18n.Strings;
014:
015: /**
016: * Provides the primary frame for the Costello script editor. Maintains the
017: * LAF used when first created, restoring it temporarily when displaying any
018: * new components.
019: *
020: * FIXME needs major refactoring:
021: * Export actions (via ActionMap)
022: * Use generic menu setup provided by a special array of actions
023: *
024: * @author Kyle Girard, twall
025: */
026: public class ScriptEditorFrame extends JFrame implements
027: EditorConstants, abbot.Version {
028:
029: private static int STEP_EDITOR_MIN_HEIGHT = 75;
030:
031: private JLabel currentTestSuiteLabel;
032: private JTextField testScriptDescription;
033: private JButton testSuiteSelectionButton;
034: private JButton runButton;
035: private JComboBox testScriptSelector;
036: // script on left, step editor on right
037: private JSplitPane scriptSplit;
038: // script split above, component browser below
039: private JSplitPane scriptBrowserSplit;
040: private ScriptTable scriptTable;
041: private JTextArea statusBar;
042: private JDialog statusWindow;
043: private boolean statusShown;
044: private JTextArea statusText;
045: private ComponentBrowser componentBrowser;
046: private ActionMap actionMap;
047: private Preferences prefs;
048: private ImageIcon logo;
049: private JMenu insertMenu;
050: private int INSERT_BASE_COUNT;
051: private JMenu captureMenu;
052: private JMenu actionMenu;
053: private TwoStateEditorMenu assertMenu;
054: private TwoStateEditorMenu waitMenu;
055: private JDialog aboutBox;
056: private JPanel lastEditor;
057:
058: /**
059: * Constructs a ScriptEditorFrame with a title and a scriptable
060: */
061: public ScriptEditorFrame(String[][] menus, ActionMap actionMap,
062: ActionListener listener, String title,
063: ScriptTable scriptTable, Preferences preferences) {
064: super (title);
065: setName("ScriptEditor");
066: prefs = preferences;
067:
068: this .scriptTable = scriptTable;
069: this .actionMap = actionMap;
070: java.net.URL url = getClass().getResource("icons/abbot.gif");
071: logo = new ImageIcon(url);
072: setIconImage(logo.getImage());
073: // Allow us to cancel out of a close.
074: setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
075:
076: initComponents(listener);
077: setJMenuBar(createMenus(menus));
078:
079: // LookAndFeelPreserver causes OSX to chew CPU cycles
080: if (!Platform.isMacintosh()) {
081: // Make sure we always use the same LAF, regardless of changes made by
082: // the code under test.
083: getRootPane().putClientProperty("LAFpreserver",
084: new LookAndFeelPreserver(this ));
085: }
086: }
087:
088: private JMenuBar createMenus(String[][] menus) {
089: JMenuBar menuBar = new JMenuBar();
090: for (int i = 0; i < menus.length; i++) {
091: String[] keys = menus[i];
092: if (keys.length > 0) {
093: JMenu menu = new EditorMenu(keys[0]);
094: for (int j = 1; j < keys.length; j++) {
095: String key = keys[j];
096: if (key == null) {
097: menu.add(new JSeparator());
098: } else {
099: Action action = actionMap.get(key);
100: menu.add(createMenuItem(action));
101: }
102: }
103: if (i == menus.length - 1) {
104: menuBar.add(Box.createHorizontalGlue());
105: }
106: menuBar.add(menu);
107: }
108: }
109: return menuBar;
110: }
111:
112: /**
113: * Returns the componentBrowser.
114: * @return ComponentBrowser
115: */
116: public ComponentBrowser getComponentBrowser() {
117: return componentBrowser;
118: }
119:
120: /**
121: * Sets the componentBrowser.
122: * @param componentBrowser The componentBrowser to set
123: */
124: public void setComponentBrowser(ComponentBrowser componentBrowser) {
125: this .componentBrowser = componentBrowser;
126: scriptBrowserSplit.setBottomComponent(componentBrowser);
127: }
128:
129: /**
130: * Returns the scriptTable.
131: * @return ScriptTable
132: */
133: public ScriptTable getScriptTable() {
134: return scriptTable;
135: }
136:
137: public String getStatus() {
138: return statusBar.getText();
139: }
140:
141: /** Set the initial size based on saved prefs. */
142: private void setInitialBounds() {
143: Log.debug("bounds=" + getBounds());
144: Dimension size = new Dimension(prefs.getIntegerProperty(
145: "width", getWidth()), prefs.getIntegerProperty(
146: "height", getHeight()));
147: if (size.width < 200 || size.height < 200) {
148: Log.warn("Size is rather small: " + size
149: + ", using defaults");
150: } else {
151: setSize(size);
152: }
153:
154: Rectangle screen = getGraphicsConfiguration().getBounds();
155: int x = screen.x + (screen.width - getWidth()) / 2;
156: int y = screen.y + (screen.height - getHeight()) / 2;
157: Point where = new Point(prefs.getIntegerProperty("x", x), prefs
158: .getIntegerProperty("y", y));
159: setLocation(where);
160:
161: int split1 = prefs.getIntegerProperty(
162: "split.script.stepeditor", -1);
163: if (split1 < 10 || split1 > getHeight() - 10)
164: split1 = -1;
165: scriptSplit.setDividerLocation(split1);
166:
167: int split2 = prefs.getIntegerProperty("split.script.browser",
168: -1);
169: if (split2 < 10 || split2 > getWidth() - 10)
170: split2 = -1;
171: scriptBrowserSplit.setDividerLocation(split2);
172: Log.debug("post=" + getBounds() + " split1=" + split1
173: + " split2=" + split2);
174: }
175:
176: /** Save size and position information before hiding. */
177: boolean firstShow = true;
178:
179: /** @deprecated */
180: public void show() {
181: if (firstShow) {
182: firstShow = false;
183: setInitialBounds();
184: }
185: super .show();
186: }
187:
188: /** @deprecated */
189: public void hide() {
190: if (isShowing()) {
191: prefs.setProperty("x", String.valueOf(getX()));
192: prefs.setProperty("y", String.valueOf(getY()));
193: prefs.setProperty("width", String.valueOf(getWidth()));
194: prefs.setProperty("height", String.valueOf(getHeight()));
195: prefs.setProperty("split.script.stepeditor", String
196: .valueOf(scriptSplit.getDividerLocation()));
197: prefs.setProperty("split.script.browser", String
198: .valueOf(scriptBrowserSplit.getDividerLocation()));
199: prefs.save();
200: }
201: super .hide();
202: }
203:
204: /** Set the text for the status window. The first argument is the short
205: text and the second is additional optional text to be displayed in a
206: larger dialog.
207: */
208: public void setStatus(final String msg, final String extended,
209: final Color color) {
210: // setText is thread-safe w/r/t the dispatch thread, but setForeground
211: // is not
212: statusBar.setText(msg);
213: statusText.setText(msg
214: + (extended != null ? "\n" + extended : ""));
215: Runnable action = new Runnable() {
216: public void run() {
217: statusBar.setForeground(color);
218: statusText.setForeground(color);
219: if (statusWindow.isShowing())
220: resizeStatusWindow();
221: }
222: };
223: if (!color.equals(statusBar.getForeground()))
224: abbot.util.AWT.invokeAction(action);
225: }
226:
227: public Dimension getPreferredSize() {
228: Dimension pref = super .getPreferredSize();
229: Rectangle screen = getGraphicsConfiguration().getBounds();
230: pref.width = Math.min(pref.width, screen.width);
231: pref.height = Math.min(pref.height, screen.height);
232: return pref;
233: }
234:
235: /**
236: * Returns the testSuiteDescription.
237: * @return JLabel
238: */
239: public JLabel getCurrentTestSuiteLabel() {
240: return currentTestSuiteLabel;
241: }
242:
243: /**
244: * Returns the testScriptSelector.
245: * @return JComboBox
246: */
247: public JComboBox getTestScriptSelector() {
248: return testScriptSelector;
249: }
250:
251: /**
252: * Returns the testScriptDescription.
253: * @return JTextField
254: */
255: public JTextField getTestScriptDescription() {
256: return testScriptDescription;
257: }
258:
259: private void initComponents(ActionListener al) {
260: JPanel pane = (JPanel) getContentPane();
261:
262: JPanel top = new JPanel();
263: top.setLayout(new BoxLayout(top, BoxLayout.X_AXIS));
264: top.setBorder(new EmptyBorder(4, 4, 4, 4));
265: {
266: JLabel suiteLabel = new JLabel(getString("AbbotSuite")) {
267: public Dimension getMaximumSize() {
268: return super .getPreferredSize();
269: }
270: };
271: suiteLabel.setHorizontalAlignment(SwingConstants.RIGHT);
272: currentTestSuiteLabel = new JLabel(getString("NoSuite"));
273: currentTestSuiteLabel
274: .setHorizontalAlignment(SwingConstants.LEFT);
275: top.add(suiteLabel);
276: top.add(Box.createHorizontalStrut(8));
277: top.add(currentTestSuiteLabel);
278: top.add(Box.createHorizontalGlue());
279: top.add(Box.createHorizontalStrut(8));
280:
281: Action action = actionMap
282: .get(ScriptEditor.ACTION_SELECT_TESTSUITE);
283: if (action != null) {
284: testSuiteSelectionButton = new EditorButton(action);
285: top.add(testSuiteSelectionButton);
286: top.add(Box.createHorizontalStrut(8));
287: }
288:
289: action = actionMap.get(ScriptEditor.ACTION_RUN);
290: if (action != null) {
291: runButton = new EditorButton(action);
292: top.add(runButton);
293: }
294: }
295:
296: JPanel center = new JPanel(new BorderLayout());
297: {
298: testScriptSelector = new JComboBox();
299: JPanel scriptPane = new JPanel(new BorderLayout());
300: {
301: testScriptDescription = new abbot.editor.widgets.TextField(
302: "");
303: testScriptDescription.addActionListener(al);
304: String tip = TextFormat.tooltip(Strings
305: .get("editor.script_description.tip"));
306: testScriptDescription.setToolTipText(tip);
307:
308: JScrollPane scroll = new JScrollPane(scriptTable) {
309: public Dimension getPreferredSize() {
310: return new Dimension(250, 200);
311: }
312:
313: public Dimension getMinimumSize() {
314: return new Dimension(250, super
315: .getMinimumSize().height);
316: }
317: };
318: scroll.setBorder(null);
319: scroll.getViewport().setBackground(
320: scriptTable.getBackground());
321:
322: scriptSplit = new JSplitPane(
323: JSplitPane.HORIZONTAL_SPLIT);
324: // script gets extra space
325: scriptSplit.setResizeWeight(1.0);
326: scriptSplit.setDividerSize(4);
327: scriptSplit.setBorder(null);
328: scriptSplit.setLeftComponent(scroll);
329: scriptBrowserSplit = new JSplitPane(
330: JSplitPane.VERTICAL_SPLIT);
331: // script gets extra space
332: scriptBrowserSplit.setResizeWeight(1.0);
333: scriptBrowserSplit.setDividerSize(4);
334: scriptBrowserSplit.setBorder(null);
335: scriptBrowserSplit.setTopComponent(scriptSplit);
336: scriptPane.add(testScriptDescription,
337: BorderLayout.NORTH);
338: scriptPane.add(scriptBrowserSplit, BorderLayout.CENTER);
339: }
340: //center.add(buttons, BorderLayout.NORTH);
341: center.add(testScriptSelector, BorderLayout.NORTH);
342: center.add(scriptPane, BorderLayout.CENTER);
343: }
344:
345: statusText = new JTextArea("");
346: statusText.setEditable(false);
347: statusText.setBackground(getContentPane().getBackground());
348: statusText.setColumns(80);
349: statusText.setLineWrap(true);
350: statusText.setWrapStyleWord(true);
351: statusWindow = createStatusWindow();
352: JPanel wp = (JPanel) statusWindow.getContentPane();
353: wp.add(new JScrollPane(statusText));
354:
355: statusBar = new JTextArea(getString("Initializing"));
356: statusBar.setEditable(false);
357: statusBar.setBackground(getContentPane().getBackground());
358: statusBar.addMouseListener(new MouseAdapter() {
359: public void mouseClicked(MouseEvent me) {
360: boolean hasMoreText = !statusBar.getText().equals(
361: statusText.getText());
362: boolean hasWideText = statusBar.getPreferredSize().width > statusBar
363: .getSize().width;
364: Log.debug("has more=" + hasMoreText + ", hasWide="
365: + hasWideText);
366: if (hasMoreText || hasWideText) {
367: resizeStatusWindow();
368: statusWindow.setVisible(true);
369: resizeStatusWindow();
370: }
371: }
372: });
373: statusBar.setToolTipText(getString("Status.tip"));
374:
375: pane.setLayout(new BorderLayout());
376: pane.setBorder(new EmptyBorder(4, 4, 4, 4));
377: pane.add(top, BorderLayout.NORTH);
378: pane.add(center, BorderLayout.CENTER);
379: pane.add(statusBar, BorderLayout.SOUTH);
380:
381: componentBrowser = null;
382: }
383:
384: /** Create a checkbox or regular menu item as appropriate. */
385: private JMenuItem createMenuItem(javax.swing.Action action) {
386: JMenuItem item = action instanceof EditorToggleAction ? (JMenuItem) new CustomCheckBoxMenuItem(
387: (EditorToggleAction) action)
388: : (JMenuItem) new EditorMenuItem(action);
389: return item;
390: }
391:
392: public void showAboutBox() {
393: if (aboutBox == null) {
394: String title = Strings.get("actions.editor-about.title");
395: aboutBox = new JDialog(this , title, true);
396: JPanel pane = (JPanel) aboutBox.getContentPane();
397: pane.setLayout(new BorderLayout());
398: pane.add(new LogoLabel(), BorderLayout.CENTER);
399: JLabel label = new JLabel(VERSION);
400: label.setHorizontalAlignment(SwingConstants.CENTER);
401: pane.add(label, BorderLayout.SOUTH);
402: pane.setBorder(new EmptyBorder(4, 4, 4, 4));
403: aboutBox.addMouseListener(new MouseAdapter() {
404: public void mouseClicked(MouseEvent me) {
405: aboutBox.setVisible(false);
406: }
407: });
408: aboutBox.pack();
409: aboutBox.setResizable(false);
410: }
411: // Center on the parent frame
412: aboutBox.setLocation(getLocation().x + getWidth() / 2
413: - aboutBox.getWidth() / 2, getLocation().y
414: + getHeight() / 2 - aboutBox.getHeight() * 2 / 3);
415: aboutBox.setVisible(true);
416: }
417:
418: public void setAssertOptions(boolean wait, boolean invert) {
419: getComponentBrowser().updateAssertText(wait, invert);
420: assertMenu.setSecondary(invert);
421: waitMenu.setSecondary(invert);
422: }
423:
424: private void populateMenu(JMenu menu, ArrayList actions) {
425: Iterator iter = actions.iterator();
426: while (iter.hasNext()) {
427: Action action = (Action) iter.next();
428: if (action == null)
429: menu.add(new JSeparator());
430: else {
431: JMenuItem mi = new EditorMenuItem(action);
432: menu.add(mi);
433: }
434: }
435: }
436:
437: /** Fill the menu with available actionXXX methods for the given class. */
438: public void populateInsertMenu(ArrayList actions) {
439: if (INSERT_BASE_COUNT == 0) {
440: INSERT_BASE_COUNT = insertMenu.getItemCount();
441: } else {
442: while (insertMenu.getItemCount() > INSERT_BASE_COUNT) {
443: insertMenu.remove(INSERT_BASE_COUNT);
444: }
445: }
446: insertMenu.add(new JSeparator());
447: insertMenu.add(actionMenu = new EditorMenu(
448: "menus.insert-action"));
449: insertMenu.add(assertMenu = new TwoStateEditorMenu(
450: "menus.insert-assert", "menus.insert-assert.neg"));
451: insertMenu.add(waitMenu = new TwoStateEditorMenu(
452: "menus.insert-wait", "menus.insert-wait.neg"));
453:
454: Collections.sort(actions);
455: populateMenu(actionMenu, actions);
456: }
457:
458: /** Fill the menu with available assertXXX methods for the given class. */
459: public void populateAssertMenu(ArrayList actions) {
460: assertMenu.removeAll();
461: populateMenu(assertMenu, actions);
462: }
463:
464: /** Same as populateAssertMenu, but makes them waits instead. */
465: public void populateWaitMenu(ArrayList actions) {
466: waitMenu.removeAll();
467: populateMenu(waitMenu, actions);
468: }
469:
470: /** Create the list of recordable GUI actions. */
471: public void populateCaptureMenu(ArrayList actions) {
472: captureMenu.removeAll();
473: populateMenu(captureMenu, actions);
474: }
475:
476: public JPanel getEditor() {
477: return lastEditor;
478: }
479:
480: public void setEditor(final JPanel editor) {
481: if (editor != null) {
482: // preserve the divider location as the editor is changed
483: JScrollPane scroll = new JScrollPane(editor);
484: int loc = scriptSplit.getDividerLocation();
485: scroll.getViewport().setBackground(editor.getBackground());
486: Dimension minSize = editor.getMinimumSize();
487: minSize.height = STEP_EDITOR_MIN_HEIGHT;
488: scroll.getViewport().setMinimumSize(minSize);
489: scriptSplit.setRightComponent(scroll);
490: // Preserve the divider location as the editor changes
491: if (lastEditor != null)
492: scriptSplit.setDividerLocation(loc);
493: } else {
494: scriptSplit.setRightComponent(null);
495: }
496: lastEditor = editor;
497: }
498:
499: /** If a resource happens to be missing, use its key instead. */
500: private static String getString(String key) {
501: String value = Strings.get(key);
502: if (value == null)
503: value = key;
504: return value;
505: }
506:
507: private void resizeStatusWindow() {
508: statusWindow.setResizable(true);
509: statusWindow.pack();
510: if (!statusShown) {
511: Dimension size = statusWindow.getSize();
512: Point vwhere = getLocationOnScreen();
513: if (size.width < getWidth()) {
514: size.width = getWidth();
515: statusWindow.setSize(size);
516: }
517: Point where = new Point();
518: where.x = vwhere.x + 10;
519: where.y = vwhere.y + getHeight() - size.height;
520: if (where.y < vwhere.y + 10) {
521: where.y = vwhere.y + 10;
522: }
523: statusWindow.setLocation(where);
524: statusShown = true;
525: }
526: statusWindow.setResizable(false);
527: statusWindow.repaint();
528: }
529:
530: private JDialog createStatusWindow() {
531: JDialog dialog = new JDialog(this , Strings.get("Status.title"),
532: false) {
533: public Dimension getMaximumSize() {
534: Dimension max = super .getMaximumSize();
535: Rectangle screen = getGraphicsConfiguration()
536: .getBounds();
537: max.height = Math.min(Math.min(max.height,
538: screen.height * 3 / 4), ScriptEditorFrame.this
539: .getMaximumSize().height);
540: max.width = Math.min(Math.min(max.width,
541: screen.width * 3 / 4), ScriptEditorFrame.this
542: .getMaximumSize().width);
543: Log.debug("maximum size is " + max);
544: return max;
545: }
546:
547: public Dimension getMinimumSize() {
548: Dimension min = super .getMinimumSize();
549: min.height = Math.max(150, min.height);
550: Log.debug("minimum size is " + min);
551: return min;
552: }
553:
554: public Dimension getPreferredSize() {
555: Dimension pref = super .getPreferredSize();
556: pref.width = Math
557: .min(pref.width, ScriptEditorFrame.this
558: .getPreferredSize().width);
559:
560: Log.debug("preferred size is " + pref);
561: return pref;
562: }
563: };
564: return dialog;
565: }
566:
567: /** Display a confirmation dialog. */
568: public int showConfirmation(String msg) {
569: return showConfirmation(msg, JOptionPane.YES_NO_OPTION);
570: }
571:
572: /** Display a confirmation dialog. */
573: public int showConfirmation(String msg, int opts) {
574: msg = TextFormat.dialog(msg);
575: return JOptionPane.showConfirmDialog(this , msg, Strings
576: .get("Confirm"), opts);
577: }
578:
579: /** Global facility for obtaining a user input String. */
580: public String showInputDialog(String title, String msg,
581: String initial) {
582: msg = TextFormat.dialog(msg);
583: return (String) JOptionPane.showInputDialog(this , msg, title,
584: JOptionPane.PLAIN_MESSAGE, null, null, initial);
585: }
586:
587: /** Global facility for message dialogs. */
588: public void showMessage(String title, String msg) {
589: msg = TextFormat.dialog(msg);
590: JOptionPane.showMessageDialog(this , msg);
591: }
592:
593: /** Global facility for warning dialog. */
594: public void showWarning(String msg) {
595: showWarning(Strings.get("Warning.title"), msg);
596: }
597:
598: /** Global facility for warning dialog. */
599: public void showWarning(String title, String msg) {
600: msg = TextFormat.dialog(msg);
601: JOptionPane.showMessageDialog(this , msg, title,
602: JOptionPane.WARNING_MESSAGE);
603: }
604:
605: /** Global facility for error dialogs. */
606: public void showError(String msg) {
607: showError(Strings.get("Error.title"), msg);
608: }
609:
610: /** Global facility for error dialogs. */
611: public void showError(String title, String msg) {
612: msg = TextFormat.dialog(msg);
613: JOptionPane.showMessageDialog(this , msg, title,
614: JOptionPane.ERROR_MESSAGE);
615: }
616:
617: private class EditorMenu extends JMenu {
618: public EditorMenu(String key) {
619: super (getString(key));
620: setName(key);
621: // sort of a hack; update member variables when these special
622: // menus get created.
623: if (key.equals(MENU_INSERT)) {
624: insertMenu = this ;
625: } else if (key.equals(MENU_CAPTURE)) {
626: captureMenu = this ;
627: }
628: setMnemonic(key);
629: }
630:
631: protected void setMnemonic(String key) {
632: Mnemonic mnemonic = Mnemonic.getMnemonic(Strings.get(key));
633: mnemonic.setMnemonic(this );
634:
635: // This can go away once the properties files have been
636: // updated to use ampersands instead of VK keycodes
637: if (mnemonic.keycode == KeyEvent.VK_UNDEFINED) {
638: int code = EditorAction.getMnemonic(key);
639: if (code != KeyEvent.VK_UNDEFINED) {
640: setMnemonic(code);
641: }
642: }
643: }
644: }
645:
646: private class TwoStateEditorMenu extends EditorMenu {
647: private String primary, secondary;
648:
649: public TwoStateEditorMenu(String primary, String secondary) {
650: super (primary);
651: this .primary = primary;
652: this .secondary = secondary;
653: }
654:
655: public void setSecondary(boolean state) {
656: setMnemonic(state ? secondary : primary);
657: }
658: }
659:
660: private class EditorButton extends JButton {
661: public EditorButton(Action action) {
662: super (action);
663: setName((String) action.getValue(EditorAction.NAME));
664: Integer i = (Integer) action
665: .getValue(EditorAction.MNEMONIC_INDEX);
666: if (i != null)
667: Mnemonic.setDisplayedMnemonicIndex(this , i.intValue());
668: }
669: }
670:
671: private class EditorMenuItem extends JMenuItem {
672: public EditorMenuItem(Action action) {
673: super (action);
674: setName((String) action.getValue(EditorAction.NAME));
675: Integer i = (Integer) action
676: .getValue(EditorAction.MNEMONIC_INDEX);
677: if (i != null)
678: Mnemonic.setDisplayedMnemonicIndex(this , i.intValue());
679: // prior to 1.4, the accelerator key is not automatically set
680: setAccelerator((KeyStroke) action
681: .getValue(Action.ACCELERATOR_KEY));
682: }
683: }
684: }
|