package abbot.editor;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import abbot.*;
import abbot.finder.matchers.NameMatcher;
import abbot.finder.BasicFinder;
import abbot.editor.actions.*;
import abbot.editor.widgets.*;
import abbot.i18n.Strings;
import com.apple.mrj.*;
/**
* Provides the primary frame for the Costello script editor. Maintains the
* LAF used when first created, restoring it temporarily when displaying any
* new components.
*
* FIXME needs major refactoring:
* Export actions (via ActionMap)
* Use generic menu setup provided by a special array of actions
*
* @author Kyle Girard, twall
*/
public class ScriptEditorFrame
extends JFrame implements EditorConstants, abbot.Version {
private static int STEP_EDITOR_MIN_HEIGHT = 75;
private JLabel currentTestSuiteLabel;
private JTextField testScriptDescription;
private JButton testSuiteSelectionButton;
private JButton runButton;
private JComboBox testScriptSelector;
// script on left, step editor on right
private JSplitPane scriptSplit;
// script split above, component browser below
private JSplitPane scriptBrowserSplit;
private ScriptTable scriptTable;
private JTextArea statusBar;
private JDialog statusWindow;
private boolean statusShown;
private JTextArea statusText;
private ComponentBrowser componentBrowser;
private ActionMap actionMap;
private Preferences prefs;
private ImageIcon logo;
private JMenu insertMenu;
private int INSERT_BASE_COUNT;
private JMenu captureMenu;
private JMenu actionMenu;
private TwoStateEditorMenu assertMenu;
private TwoStateEditorMenu waitMenu;
private JDialog aboutBox;
private JPanel lastEditor;
private LookAndFeelPreserver preserver;
/**
* Constructs a ScriptEditorFrame with a title and a scriptable
*/
public ScriptEditorFrame(String[][] menus, ActionMap actionMap,
ActionListener listener,
String title, ScriptTable scriptTable,
Preferences preferences) {
super(title);
setName("ScriptEditor");
prefs = preferences;
this.scriptTable = scriptTable;
this.actionMap = actionMap;
java.net.URL url = getClass().getResource("icons/abbot.gif");
logo = new ImageIcon(url);
setIconImage(logo.getImage());
// Allow us to cancel out of a close.
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
initComponents(listener);
setJMenuBar(createMenus(menus));
// Make sure we always use the same LAF, regardless of changes made by
// the code under test.
preserver = new LookAndFeelPreserver(this);
}
private JMenuBar createMenus(String[][] menus) {
JMenuBar menuBar = new JMenuBar();
for (int i=0;i < menus.length;i++) {
String[] keys = menus[i];
if (keys.length > 0) {
JMenu menu = new EditorMenu(keys[0]);
for (int j=1;j < keys.length;j++) {
String key = keys[j];
if (key == null) {
menu.add(new JSeparator());
}
else {
javax.swing.Action action = actionMap.get(key);
menu.add(createMenuItem(action));
}
}
if (i == menus.length-1) {
menuBar.add(Box.createHorizontalGlue());
}
menuBar.add(menu);
}
}
return menuBar;
}
/**
* Returns the componentBrowser.
* @return ComponentBrowser
*/
public ComponentBrowser getComponentBrowser() {
return componentBrowser;
}
/**
* Sets the componentBrowser.
* @param componentBrowser The componentBrowser to set
*/
public void setComponentBrowser(ComponentBrowser componentBrowser) {
this.componentBrowser = componentBrowser;
scriptBrowserSplit.setBottomComponent(componentBrowser);
}
/**
* Returns the scriptTable.
* @return ScriptTable
*/
public ScriptTable getScriptTable() {
return scriptTable;
}
public String getStatus() {
return statusBar.getText();
}
/** Set the initial size based on saved prefs. */
private void setInitialBounds() {
Log.debug("bounds=" + getBounds());
Dimension size =
new Dimension(prefs.getIntegerProperty("width", getWidth()),
prefs.getIntegerProperty("height", getHeight()));
if (size.width < 200 || size.height < 200) {
Log.warn("Size is rather small: " + size
+ ", using defaults");
}
else {
setSize(size);
}
Rectangle screen = getGraphicsConfiguration().getBounds();
int x = screen.x + (screen.width - getWidth()) / 2;
int y = screen.y + (screen.height - getHeight()) / 2;
Point where = new Point(prefs.getIntegerProperty("x", x),
prefs.getIntegerProperty("y", y));
setLocation(where);
int split1 = prefs.getIntegerProperty("split.script.stepeditor", -1);
if (split1 < 10 || split1 > getHeight() - 10)
split1 = -1;
scriptSplit.setDividerLocation(split1);
int split2 = prefs.getIntegerProperty("split.script.browser", -1);
if (split2 < 10 || split2 > getWidth() - 10)
split2 = -1;
scriptBrowserSplit.setDividerLocation(split2);
Log.debug("post=" + getBounds()
+ " split1=" + split1 + " split2=" + split2);
}
/** Save size and position information before hiding. */
boolean firstShow = true;
public void show() {
if (firstShow) {
firstShow = false;
setInitialBounds();
}
super.show();
}
public void hide() {
if (isShowing()) {
prefs.setProperty("x", String.valueOf(getX()));
prefs.setProperty("y", String.valueOf(getY()));
prefs.setProperty("width", String.valueOf(getWidth()));
prefs.setProperty("height", String.valueOf(getHeight()));
prefs.setProperty("split.script.stepeditor",
String.valueOf(scriptSplit.getDividerLocation()));
prefs.setProperty("split.script.browser",
String.valueOf(scriptBrowserSplit.getDividerLocation()));
prefs.save();
}
super.hide();
}
/** Set the text for the status window. The first argument is the short
text and the second is additional optional text to be displayed in a
larger dialog.
*/
public void setStatus(final String msg, final String extended,
final Color color) {
// setText is thread-safe w/r/t the dispatch thread, but setForeground
// is not
statusBar.setText(msg);
statusText.setText(msg + (extended != null
? "\n" + extended : ""));
Runnable action = new Runnable() {
public void run() {
statusBar.setForeground(color);
statusText.setForeground(color);
if (statusWindow.isShowing())
resizeStatusWindow();
}
};
if (!color.equals(statusBar.getForeground()))
abbot.util.AWT.invokeAction(action);
}
public Dimension getPreferredSize() {
Dimension pref = super.getPreferredSize();
Rectangle screen = getGraphicsConfiguration().getBounds();
pref.width = Math.min(pref.width, screen.width);
pref.height = Math.min(pref.height, screen.height);
return pref;
}
/**
* Returns the testSuiteDescription.
* @return JLabel
*/
public JLabel getCurrentTestSuiteLabel() {
return currentTestSuiteLabel;
}
/**
* Returns the testScriptSelector.
* @return JComboBox
*/
public JComboBox getTestScriptSelector() {
return testScriptSelector;
}
/**
* Returns the testScriptDescription.
* @return JTextField
*/
public JTextField getTestScriptDescription() {
return testScriptDescription;
}
private void initComponents(ActionListener al) {
JPanel pane = (JPanel)getContentPane();
JPanel top = new JPanel();
top.setLayout(new BoxLayout(top, BoxLayout.X_AXIS));
top.setBorder(new EmptyBorder(4,4,4,4));
{
JLabel suiteLabel = new JLabel(getString("AbbotSuite")) {
public Dimension getMaximumSize() {
return super.getPreferredSize();
}
};
suiteLabel.setHorizontalAlignment(SwingConstants.RIGHT);
currentTestSuiteLabel = new JLabel(getString("NoSuite"));
currentTestSuiteLabel.setHorizontalAlignment(SwingConstants.LEFT);
top.add(suiteLabel);
top.add(Box.createHorizontalStrut(8));
top.add(currentTestSuiteLabel);
top.add(Box.createHorizontalGlue());
top.add(Box.createHorizontalStrut(8));
Action action = actionMap.get(ScriptEditor.ACTION_SELECT_TESTSUITE);
if (action != null) {
testSuiteSelectionButton = new EditorButton(action);
top.add(testSuiteSelectionButton);
top.add(Box.createHorizontalStrut(8));
}
action = actionMap.get(ScriptEditor.ACTION_RUN);
if (action != null) {
runButton = new EditorButton(action);
top.add(runButton);
}
}
JPanel center = new JPanel(new BorderLayout());
{
testScriptSelector = new JComboBox();
JPanel scriptPane = new JPanel(new BorderLayout());
{
testScriptDescription = new abbot.editor.widgets.TextField("");
testScriptDescription.addActionListener(al);
String tip = TextFormat.
tooltip(Strings.get("editor.script_description.tip"));
testScriptDescription.setToolTipText(tip);
JScrollPane scroll = new JScrollPane(scriptTable) {
public Dimension getPreferredSize() {
return new Dimension(250, 200);
}
public Dimension getMinimumSize() {
return new Dimension(250, super.getMinimumSize().height);
}
};
scroll.setBorder(null);
scroll.getViewport().setBackground(scriptTable.getBackground());
scriptSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
// script gets extra space
scriptSplit.setResizeWeight(1.0);
scriptSplit.setDividerSize(4);
scriptSplit.setBorder(null);
scriptSplit.setLeftComponent(scroll);
scriptBrowserSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
// script gets extra space
scriptBrowserSplit.setResizeWeight(1.0);
scriptBrowserSplit.setDividerSize(4);
scriptBrowserSplit.setBorder(null);
scriptBrowserSplit.setTopComponent(scriptSplit);
scriptPane.add(testScriptDescription, BorderLayout.NORTH);
scriptPane.add(scriptBrowserSplit, BorderLayout.CENTER);
}
//center.add(buttons, BorderLayout.NORTH);
center.add(testScriptSelector, BorderLayout.NORTH);
center.add(scriptPane, BorderLayout.CENTER);
}
statusText = new JTextArea("");
statusText.setEditable(false);
statusText.setBackground(getContentPane().getBackground());
statusText.setColumns(80);
statusText.setLineWrap(true);
statusText.setWrapStyleWord(true);
statusWindow = createStatusWindow();
JPanel wp = (JPanel)statusWindow.getContentPane();
wp.add(new JScrollPane(statusText));
statusBar = new JTextArea(getString("Initializing"));
statusBar.setEditable(false);
statusBar.setBackground(getContentPane().getBackground());
statusBar.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
boolean hasMoreText =
!statusBar.getText().equals(statusText.getText());
boolean hasWideText =
statusBar.getPreferredSize().width > statusBar.getSize().width;
Log.debug("has more=" + hasMoreText + ", hasWide=" + hasWideText);
if (hasMoreText || hasWideText) {
resizeStatusWindow();
statusWindow.show();
resizeStatusWindow();
}
}
});
statusBar.setToolTipText(getString("Status.tip"));
pane.setLayout(new BorderLayout());
pane.setBorder(new EmptyBorder(4,4,4,4));
pane.add(top, BorderLayout.NORTH);
pane.add(center, BorderLayout.CENTER);
pane.add(statusBar, BorderLayout.SOUTH);
componentBrowser = null;
}
/** Create a checkbox or regular menu item as appropriate. */
private JMenuItem createMenuItem(javax.swing.Action action) {
JMenuItem item = action instanceof EditorToggleAction
? (JMenuItem)new CustomCheckBoxMenuItem((EditorToggleAction)action)
: (JMenuItem)new EditorMenuItem(action);
return item;
}
public void showAboutBox() {
if (aboutBox == null) {
String title = Strings.get("actions.editor-about.title");
aboutBox = new JDialog(this, title, true);
JPanel pane = (JPanel)aboutBox.getContentPane();
pane.setLayout(new BorderLayout());
pane.add(new LogoLabel(), BorderLayout.CENTER);
JLabel label = new JLabel(VERSION);
label.setHorizontalAlignment(SwingConstants.CENTER);
pane.add(label, BorderLayout.SOUTH);
pane.setBorder(new EmptyBorder(4,4,4,4));
aboutBox.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
aboutBox.hide();
}
});
aboutBox.pack();
aboutBox.setResizable(false);
}
// Center on the parent frame
aboutBox.setLocation(getLocation().x + getWidth()/2
- aboutBox.getWidth()/2,
getLocation().y + getHeight()/2
- aboutBox.getHeight()*2/3);
aboutBox.show();
}
public void setAssertOptions(boolean wait, boolean invert) {
getComponentBrowser().updateAssertText(wait, invert);
assertMenu.setSecondary(invert);
waitMenu.setSecondary(invert);
}
private void populateMenu(JMenu menu, ArrayList actions) {
Iterator iter = actions.iterator();
while (iter.hasNext()) {
Action action = (Action)iter.next();
if (action == null)
menu.add(new JSeparator());
else {
JMenuItem mi = new EditorMenuItem(action);
menu.add(mi);
}
}
}
/** Fill the menu with available actionXXX methods for the given class. */
public void populateInsertMenu(ArrayList actions) {
if (INSERT_BASE_COUNT == 0) {
INSERT_BASE_COUNT = insertMenu.getItemCount();
}
else {
while (insertMenu.getItemCount() > INSERT_BASE_COUNT) {
insertMenu.remove(INSERT_BASE_COUNT);
}
}
insertMenu.add(new JSeparator());
insertMenu.add(actionMenu = new EditorMenu("menus.insert-action"));
insertMenu.add(assertMenu =
new TwoStateEditorMenu("menus.insert-assert",
"menus.insert-assert.neg"));
insertMenu.add(waitMenu =
new TwoStateEditorMenu("menus.insert-wait",
"menus.insert-wait.neg"));
Collections.sort(actions);
populateMenu(actionMenu, actions);
}
/** Fill the menu with available assertXXX methods for the given class. */
public void populateAssertMenu(ArrayList actions) {
assertMenu.removeAll();
populateMenu(assertMenu, actions);
}
/** Same as populateAssertMenu, but makes them waits instead. */
public void populateWaitMenu(ArrayList actions) {
waitMenu.removeAll();
populateMenu(waitMenu, actions);
}
/** Create the list of recordable GUI actions. */
public void populateCaptureMenu(ArrayList actions) {
captureMenu.removeAll();
populateMenu(captureMenu, actions);
}
public JPanel getEditor() { return lastEditor; }
public void setEditor(final JPanel editor) {
if (editor != null) {
// preserve the divider location as the editor is changed
JScrollPane scroll = new JScrollPane(editor);
int loc = scriptSplit.getDividerLocation();
scroll.getViewport().setBackground(editor.getBackground());
Dimension minSize = editor.getMinimumSize();
minSize.height = STEP_EDITOR_MIN_HEIGHT;
scroll.getViewport().setMinimumSize(minSize);
scriptSplit.setRightComponent(scroll);
// Preserve the divider location as the editor changes
if (lastEditor != null)
scriptSplit.setDividerLocation(loc);
}
else {
scriptSplit.setRightComponent(null);
}
lastEditor = editor;
}
/** If a resource happens to be missing, use its key instead. */
private static String getString(String key) {
String value = Strings.get(key);
if (value == null)
value = key;
return value;
}
private void resizeStatusWindow() {
statusWindow.setResizable(true);
statusWindow.pack();
if (!statusShown) {
Dimension size = statusWindow.getSize();
Point vwhere = getLocationOnScreen();
if (size.width < getWidth()) {
size.width = getWidth();
statusWindow.setSize(size);
}
Point where = new Point();
where.x = vwhere.x + 10;
where.y = vwhere.y + getHeight() - size.height;
if (where.y < vwhere.y + 10) {
where.y = vwhere.y + 10;
}
statusWindow.setLocation(where);
statusShown = true;
}
statusWindow.setResizable(false);
statusWindow.repaint();
}
private JDialog createStatusWindow() {
JDialog dialog =
new JDialog(this, Strings.get("Status.title"), false) {
public Dimension getMaximumSize() {
Dimension max = super.getMaximumSize();
Rectangle screen = getGraphicsConfiguration().getBounds();
max.height = Math.min(Math.min(max.height,
screen.height * 3 / 4),
ScriptEditorFrame.this.
getMaximumSize().height);
max.width = Math.min(Math.min(max.width,
screen.width * 3 / 4),
ScriptEditorFrame.this.
getMaximumSize().width);
Log.debug("maximum size is " + max);
return max;
}
public Dimension getMinimumSize() {
Dimension min = super.getMinimumSize();
min.height = Math.max(150, min.height);
Log.debug("minimum size is " + min);
return min;
}
public Dimension getPreferredSize() {
Dimension pref = super.getPreferredSize();
pref.width = Math.min(pref.width,
ScriptEditorFrame.this.
getPreferredSize().width);
Log.debug("preferred size is " + pref);
return pref;
}
};
return dialog;
}
/** Display a confirmation dialog. */
public int showConfirmation(String msg) {
return showConfirmation(msg, JOptionPane.YES_NO_OPTION);
}
/** Display a confirmation dialog. */
public int showConfirmation(String msg, int opts) {
msg = TextFormat.dialog(msg);
return JOptionPane.showConfirmDialog(this, msg,
Strings.get("Confirm"), opts);
}
/** Global facility for obtaining a user input String. */
public String showInputDialog(String title, String msg,
String initial) {
msg = TextFormat.dialog(msg);
return (String)JOptionPane.showInputDialog(this, msg, title,
JOptionPane.PLAIN_MESSAGE,
null, null, initial);
}
/** Global facility for message dialogs. */
public void showMessage(String title, String msg) {
msg = TextFormat.dialog(msg);
JOptionPane.showMessageDialog(this, msg);
}
/** Global facility for warning dialog. */
public void showWarning(String msg) {
showWarning(Strings.get("Warning.title"), msg);
}
/** Global facility for warning dialog. */
public void showWarning(String title, String msg) {
msg = TextFormat.dialog(msg);
JOptionPane.showMessageDialog(this, msg, title,
JOptionPane.WARNING_MESSAGE);
}
/** Global facility for error dialogs. */
public void showError(String msg) {
showError(Strings.get("Error.title"), msg);
}
/** Global facility for error dialogs. */
public void showError(String title, String msg) {
msg = TextFormat.dialog(msg);
JOptionPane.showMessageDialog(this, msg, title,
JOptionPane.ERROR_MESSAGE);
}
private class EditorMenu extends JMenu {
public EditorMenu(String key) {
super(getString(key));
setName(key);
// sort of a hack; update member variables when these special
// menus get created.
if (key.equals(MENU_INSERT)) {
insertMenu = this;
}
else if (key.equals(MENU_CAPTURE)) {
captureMenu = this;
}
setMnemonic(key);
}
protected void setMnemonic(String key) {
Mnemonic mnemonic = Mnemonic.getMnemonic(Strings.get(key));
mnemonic.setMnemonic(this);
// This can go away once the properties files have been
// updated to use ampersands instead of VK keycodes
if (mnemonic.keycode == KeyEvent.VK_UNDEFINED) {
int code = EditorAction.getMnemonic(key);
if (code != KeyEvent.VK_UNDEFINED) {
setMnemonic(code);
}
}
}
}
private class TwoStateEditorMenu extends EditorMenu {
private String primary, secondary;
public TwoStateEditorMenu(String primary, String secondary) {
super(primary);
this.primary = primary;
this.secondary = secondary;
}
public void setSecondary(boolean state) {
setMnemonic(state ? secondary : primary);
}
}
private class EditorButton extends JButton {
public EditorButton(Action action) {
super(action);
setName((String)action.getValue(EditorAction.NAME));
Integer i = (Integer)action.getValue(EditorAction.MNEMONIC_INDEX);
if (i != null)
Mnemonic.setDisplayedMnemonicIndex(this, i.intValue());
}
}
private class EditorMenuItem extends JMenuItem {
public EditorMenuItem(Action action) {
super(action);
setName((String)action.getValue(EditorAction.NAME));
Integer i = (Integer)action.getValue(EditorAction.MNEMONIC_INDEX);
if (i != null)
Mnemonic.setDisplayedMnemonicIndex(this, i.intValue());
// prior to 1.4, the accelerator key is not automatically set
setAccelerator((KeyStroke)action.getValue(Action.ACCELERATOR_KEY));
}
}
}