![JAR search and dependency download from the Maven repository](/logo.png)
abbot.editor.recorder.ComponentRecorder Maven / Gradle / Ivy
package abbot.editor.recorder;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import abbot.BugReport;
import abbot.Log;
import abbot.Platform;
import abbot.finder.matchers.JMenuItemMatcher;
import abbot.i18n.Strings;
import abbot.script.*;
import abbot.script.Action;
import abbot.script.Resolver;
import abbot.tester.*;
import abbot.tester.Robot;
import abbot.util.AWT;
/**
* Record basic semantic events you might find on any component. This class
* handles the following actions:
*
* - window actions
*
- popup menus
*
- click
*
- typed keys
*
- basic drag and drop
*
- InputMethod events (extended character input)
*
* Clicks, popup menus, and drag/drop actions may be based on coordinates or
* component substructure (cell, row, tab, etc) locations.
*
* Window Actions
* While these nominally might be handled in a WindowRecorder, they are so
* common that it's easier to handle here instead. Currently supports
* tracking show/hide/activate. TODO: move/resize/iconfify/deiconify.
* Popup Menus
* Currently only the click/select/click sequence is supported. The
* press/drag/release version shouldn't be hard to implement, though.
* Click
* Simple press/release on a component, storing the exact coordinate of the
* click. Most things with selectability will want to override this. Culling
* accidental intervening drags would be nice but probably not worth the
* effort or complexity (better just to be less sloppy with your mouse).
* Key Type
* Capture only events that result in actual output. No plain modifiers,
* shortcuts, or mnemonics.
* Drag/Drop
* Basic drag from one component and drop on another, storing exact
* coordinates of the press/release actions. Should definitely override this
* to represent your component's internal objects (e.g. cells in a table).
* Note that these are two distinct actions, even though they always appear
* together. The source is responsible for identifying the drag, and the
* target is responsible for identifying the drop.
* InputMethod
* Catch extended character input.
*/
// NOTE: Mac OSX robot will actually generate key modifiers prior
// to button2/3
// NOTE: Mac OSX CTRL/ALT+MB1 invokes MB2
// CTRL+MB1->CTRL+MB2
// ALT+MB1->MB2
// TODO: test recorders by sending an event stream; test platform stream
// by generating robot events and verifying stream seen; this splits the
// tests into separate concerns.
public class ComponentRecorder extends SemanticRecorder {
private static final String[] TYPES = {
"any", "window", "menu", "click", "key",
"drag", "drop", "text", "input method"
};
/** Mappings for special keys. */
private static java.util.HashMap specialMap;
static {
// Make explicit some special key mappings which we DON'T want to save
// as the resulting characters (b/c they may not actually be
// characters, or they're not particularly good to save as
// characters.
int[][] mappings = {
{ '\t', KeyEvent.VK_TAB },
{ '', KeyEvent.VK_ESCAPE }, // No escape sequence exists
{ '\b', KeyEvent.VK_BACK_SPACE },
{ '', KeyEvent.VK_DELETE }, // No escape sequence exists
{ '\n', KeyEvent.VK_ENTER },
{ '\r', KeyEvent.VK_ENTER },
};
specialMap = new java.util.HashMap();
for (int i=0;i < mappings.length;i++) {
specialMap.put(String.valueOf((char)mappings[i][0]),
AWT.getKeyCode(mappings[i][1]));
}
}
// For windows
private Window window = null;
private boolean isClose = false;
// For key events
private char keychar = KeyEvent.CHAR_UNDEFINED;
private int modifiers;
// For clicks
private Component target;
private Component forwardedTarget;
private int x, y;
private boolean released;
private int clickCount;
// For menu events
private Component invoker;
private int menux, menuy;
private MenuItem awtMenuTarget;
private Component menuTarget;
private boolean isPopup;
private boolean hasAWTPopup;
private MenuListener menuListener;
private boolean menuCanceled;
// For drag events
// This class is responsible for handling drag/drop once the action has
// been recognized by a derived class
private Component dragSource;
private int dragx, dragy;
// For drop events
private Component dropTarget;
private int dropx, dropy;
// InputMethod
private ArrayList imKeyCodes = new ArrayList();
private StringBuffer imText = new StringBuffer();
/** Keep a short-term memory of windows we've seen open/close already. */
private static WeakHashMap closeEventWindows = new WeakHashMap();
private static WeakHashMap openEventWindows = new WeakHashMap();
/** Create a ComponentRecorder for use in capturing the semantics of a GUI
* action.
*/
public ComponentRecorder(Resolver resolver) {
super(resolver);
}
/** Does the given event indicate a window was shown? */
protected boolean isOpen(AWTEvent event) {
int id = event.getID();
// 1.3 VMs may generate a WINDOW_OPEN without a COMPONENT_SHOWN
// (see EventRecorderTest.testClickWithDialog)
// NOTE: COMPONENT_SHOWN precedes WINDOW_OPENED, but we don't really
// care in this case, since we're just recording the event, not
// watching for the component's validity.
if (((id == WindowEvent.WINDOW_OPENED
&& !openEventWindows.containsKey(event.getSource()))
|| id == ComponentEvent.COMPONENT_SHOWN)) {
return true;
}
return false;
}
/** Does the given event indicate a window was closed? */
protected boolean isClose(AWTEvent event) {
int id = event.getID();
// Window.dispose doesn't generate a HIDDEN event, but it does
// generate a WINDOW_CLOSED event (1.3/1.4)
if (((id == WindowEvent.WINDOW_CLOSED
&& !closeEventWindows.containsKey(event.getSource()))
|| id == ComponentEvent.COMPONENT_HIDDEN)) {
return true;
}
return false;
}
/** Returns whether this ComponentRecorder wishes to accept the given
* event. If the event is accepted, the recorder must invoke init() with
* the appropriate semantic event type.
*/
public boolean accept(AWTEvent event) {
int rtype = SE_NONE;
if (isWindowEvent(event)) {
rtype = SE_WINDOW;
}
else if (isMenuEvent(event)) {
rtype = SE_MENU;
}
else if (isKeyTyped(event)) {
rtype = SE_KEY;
}
else if (isClick(event)) {
rtype = SE_CLICK;
}
else if (isDragDrop(event)) {
rtype = SE_DROP;
}
else if (isInputMethod(event)) {
rtype = SE_IM;
}
else {
if (Log.isClassDebugEnabled(ComponentRecorder.class))
Log.debug("Ignoring " + Robot.toString(event));
}
init(rtype);
boolean accepted = rtype != SE_NONE;
if (accepted && Log.isClassDebugEnabled(ComponentRecorder.class))
Log.debug("Accepted " + ComponentTester.toString(event));
return accepted;
}
/** Test whether the given event is a trigger for a window event.
* Allow derived classes to change definition of a click.
*/
protected boolean isWindowEvent(AWTEvent event) {
// Ignore activate and deactivate. They are unreliable.
// We only want open/close events on non-tooltip and non-popup windows
return (event.getSource() instanceof Window)
&& !AWT.isHeavyweightPopup((Window)event.getSource())
&& !isToolTip(event.getSource())
&& (isClose(event) || isOpen(event));
}
/**
* Return true if the given event source is a tooltip.
* Such events look like window events, but we check for them before other
* kinds of window events so as to be able to filter them out.
*
* TODO: emit steps to confirm value of tooltip?
*
* @param source the object to examine
* @return true if this event source is a tooltip
*/
protected boolean isToolTip(Object source){
// Tooltips appear to be a direct subclass of JWindow and
// have a single component of class JToolTip
if (source instanceof JWindow && !(source instanceof JFrame)){
Container pane = ((JWindow)source).getContentPane();
while (pane.getComponentCount() == 1){
Component child = pane.getComponent(0);
if (child instanceof JToolTip)
return true;
if (!(child instanceof Container))
break;
pane = (Container)child;
}
}
return false;
}
protected boolean isMenuEvent(AWTEvent event) {
if (event.getID() == ActionEvent.ACTION_PERFORMED
&& event.getSource() instanceof java.awt.MenuItem) {
return true;
}
else if (event.getID() == MouseEvent.MOUSE_PRESSED) {
MouseEvent me = (MouseEvent)event;
return me.isPopupTrigger()
|| ((me.getModifiers() & AWTConstants.POPUP_MASK) != 0)
|| me.getSource() instanceof JMenu;
}
return false;
}
protected boolean isKeyTyped(AWTEvent event) {
return event.getID() == KeyEvent.KEY_TYPED;
}
/** Test whether the given event is a trigger for a mouse button click.
* Allow derived classes to change definition of a click.
*/
protected boolean isClick(AWTEvent event) {
if (event.getID() == MouseEvent.MOUSE_PRESSED) {
MouseEvent me = (MouseEvent)event;
return (me.getModifiers() & MouseEvent.BUTTON1_MASK) != 0;
}
return false;
}
/** Test whether the given event precurses a drop. */
protected boolean isDragDrop(AWTEvent event) {
return event.getID() == MouseEvent.MOUSE_DRAGGED;
}
/** Default to recording a drag if it looks like one. */
// FIXME may be some better detection, like checking for DND interfaces. */
protected boolean canDrag() {
return true;
}
/** Default to waiting for multiple clicks. */
protected boolean canMultipleClick() {
return true;
}
/** Is this the start of an input method event? */
private boolean isInputMethod(AWTEvent event) {
// NOTE: HALF_WIDTH signals start of kanji input
// NOTE: Mac uses input method for some dual-keystroke chars (option-e)
return (event.getID() == KeyEvent.KEY_RELEASED
&& ((KeyEvent)event).getKeyCode() == KeyEvent.VK_HALF_WIDTH)
|| event.getID() == InputMethodEvent.INPUT_METHOD_TEXT_CHANGED;
}
/** Provide standard parsing of mouse button events. */
protected boolean parseClick(AWTEvent event) {
boolean consumed = true;
int id = event.getID();
if (id == MouseEvent.MOUSE_PRESSED) {
Log.debug("Parsing mouse down");
MouseEvent me = (MouseEvent)event;
if (clickCount == 0) {
target = me.getComponent();
x = me.getX();
y = me.getY();
modifiers = me.getModifiers();
clickCount = 1;
// Add the component immediately, just in case it gets removed
// from the hierarchy as a result of the click.
getResolver().addComponent(target);
}
else {
if (target == me.getComponent()) {
clickCount = me.getClickCount();
}
else if (!released) {
// It's possible to get two consecutive MOUSE_PRESSED
// events for different targets (e.g. double click on a
// table cell to get the default editor) (OSX 1.3.1, XP
// 1.4.1_01). Ignore the second click, since it is
// artificial, and wait for the original click to finish.
// i.e. w32 1.3.1
// MOUSE_PRESSED JTable
// MOUSE_PRESSED JTextField
// FOCUS_LOST JTable
// FOCUS_GAINED JTextField
// MOUSE_EXITED JTable
// MOUSE_ENTERED JTextField
// MOUSE_RELEASED JTable
// MOUSE_RELEASED JTextField
forwardedTarget = me.getComponent();
}
}
released = false;
}
else if (id == MouseEvent.MOUSE_RELEASED) {
Log.debug("Parsing mouse up");
released = true;
// Optionally disallow multiple clicks
if (!canMultipleClick())
setFinished(true);
}
else if (id == MouseEvent.MOUSE_CLICKED) {
// optionally wait for multiple clicks
if (!canMultipleClick())
setFinished(true);
}
else if (id == MouseEvent.MOUSE_EXITED) {
Log.debug("exit event, released=" + released);
if (event.getSource() != target || released) {
consumed = false;
setFinished(true);
}
else if (!released) {
// May not see any DRAGGED events if it's a native drag;
// 1.3 posts MOUSE_EXITED after MOUSE_PRESSED, no drag events
if (clickCount == 1) {
setRecordingType(SE_DRAG);
consumed = dragStarted(target, x, y, modifiers,
(MouseEvent)event);
}
}
}
else if (id == MouseEvent.MOUSE_ENTERED) {
if (event.getSource() == target && !released) {
// nothing
}
else if (event.getSource() != forwardedTarget) {
consumed = false;
setFinished(true);
}
}
else if (id == MouseEvent.MOUSE_DRAGGED && canDrag()) {
Log.debug("Changing click to drag start");
MouseEvent me = (MouseEvent)event;
Point where = SwingUtilities.convertPoint(me.getComponent(),
me.getX(), me.getY(),
target);
int threshold = Integer.getInteger("abbot.capture.drag_threshold",
5).intValue();
if (Math.abs(where.x - x) >= threshold
|| Math.abs(where.y - y) >= threshold) {
// Was actually a drag; pass off to drag handler
setRecordingType(SE_DRAG);
consumed = dragStarted(target, x, y, modifiers, me);
}
else {
Log.debug("Drag too small: " + me + " (from " + dragSource
+ " (" + x + ", " + y + "))");
}
}
// These events will not prevent a multi-click from being registered.
else if ((id >= ComponentEvent.COMPONENT_FIRST
&& id <= ComponentEvent.COMPONENT_LAST)
|| (event instanceof ContainerEvent)
|| (event instanceof FocusEvent)
|| (id == HierarchyEvent.HIERARCHY_CHANGED
&& (((HierarchyEvent)event).getChangeFlags()
& HierarchyEvent.SHOWING_CHANGED) == 0)) {
// Ignore most hierarchy change and component events between
// clicks.
// The focus event is sporadic on w32 1.4.1_02
}
else {
// All other events should cause the click to finish,
// but don't register a click unless we've received the release
// event.
if (released) {
consumed = false;
setFinished(true);
}
}
return consumed;
}
protected boolean parseWindowEvent(AWTEvent event) {
boolean consumed = true;
isClose = isClose(event);
// Keep track of window open/close state so we don't parse the same
// semantic event twice (e.g. COMPONENT_SHOWN + WINDOW_OPENED or
// multiple WINDOW_CLOSED events).
if (isClose) {
closeEventWindows.put(event.getSource(), Boolean.TRUE);
openEventWindows.remove(event.getSource());
}
else {
openEventWindows.put(event.getSource(), Boolean.TRUE);
closeEventWindows.remove(event.getSource());
}
Log.log("close=" + isClose + " (" + Robot.toString(event) + ")");
window = (Window)event.getSource();
setFinished(true);
return consumed;
}
protected boolean parseKeyEvent(AWTEvent e) {
int id = e.getID();
boolean consumed = true;
if (id == KeyEvent.KEY_TYPED) {
KeyEvent typed = (KeyEvent)e;
target = typed.getComponent();
keychar = typed.getKeyChar();
modifiers = typed.getModifiers();
if ((modifiers & KeyEvent.ALT_MASK) == KeyEvent.ALT_MASK) {
Log.debug("Waiting for potential focus accelerator on '"
+ keychar + "'");
}
else {
// Ignore KEY_TYPED input for control and alt modifiers, since
// the generated characters are not accepted as text input.
// Add others if you encounter them, but err on the side of
// accepting input that can later be removed.
if ((modifiers & InputEvent.CTRL_MASK) == InputEvent.CTRL_MASK
|| (modifiers & InputEvent.ALT_MASK) == InputEvent.ALT_MASK) {
Log.debug("Ignoring modifiers: " + modifiers);
setRecordingType(SE_NONE);
}
setFinished(true);
}
}
else if (id == FocusEvent.FOCUS_LOST) {
// Ignore and wait for FOCUS_GAINED
}
else if (id == FocusEvent.FOCUS_GAINED) {
// Looks like a focus accelerator focus change. Ignore the
// KEY_TYPED event.
Object o = e.getSource();
char ch = KeyEvent.CHAR_UNDEFINED;
if (o instanceof JTextComponent) {
ch = ((JTextComponent)o).getFocusAccelerator();
Log.debug("focus accelerator is '" + ch + "'");
}
if (Character.toUpperCase(ch) == Character.toUpperCase(keychar)) {
setRecordingType(SE_NONE);
setFinished(true);
}
else {
setFinished(true);
consumed = false;
}
}
else {
setRecordingType(SE_NONE);
setFinished(true);
consumed = false;
}
return consumed;
}
/** Base implementation handles context (popup) menus. */
protected boolean parseMenuSelection(AWTEvent event) {
int id = event.getID();
boolean consumed = true;
// press, release, show, [move, show,] press, release
// press, [drag, show,] release (FIXME not done)
// ACTION_PERFORMED and ITEM_STATE_CHANGED are only
// produced by AWT components (wxp/1.4.2)
if (id == ActionEvent.ACTION_PERFORMED
|| id == ItemEvent.ITEM_STATE_CHANGED) {
awtMenuTarget = (MenuItem)event.getSource();
invoker = AWT.getInvoker(awtMenuTarget);
// If there is no invoker, the selection came from the MenuBar
if (invoker != null) {
isPopup = true;
}
Log.debug("AWT menu selection, invoker="
+ Robot.toString(invoker));
if (event instanceof ActionEvent) {
modifiers = ((ActionEvent)event).getModifiers();
}
else {
// ItemEvent doesn't report modifiers, so ask use internal
// tracking to see if any modifiers are active.
modifiers = Robot.getState().getModifiers();
}
setFinished(true);
}
else if (id == MouseEvent.MOUSE_PRESSED) {
MouseEvent me = (MouseEvent)event;
// On the first press, we haven't yet set the invoker, which
// is either a JMenu or the component holding the popup.
if (invoker == null) {
invoker = me.getComponent();
menux = me.getX();
menuy = me.getY();
modifiers = me.getModifiers();
isPopup = me.isPopupTrigger();
// Must add the listener now, b/c on w32 release/click events
// are not generated until *after* the awt popup selection.
if (isPopup || (modifiers & AWTConstants.POPUP_MASK) != 0) {
hasAWTPopup = addMenuListener(invoker);
}
// It's possible for a popup menu to be triggered by some
// other event (e.g. a button click). Assume that action is
// already recorded and simply make note of the appropriate
// menu selection.
if (invoker instanceof JMenuItem
&& !(invoker instanceof JMenu)) {
menuTarget = invoker;
invoker = null;
menux = menuy = -1;
modifiers = 0;
isPopup = true;
}
}
else if (event.getSource() instanceof JMenu) {
// ignore
}
else if (event.getSource() instanceof JMenuItem) {
// Click to select the menu item; this will be the second
// press event received
menuTarget = (Component)event.getSource();
}
else {
// Mouse press in something other than the menu, assume it was
// canceled.
// Popup was canceled. Discard subsequent release/click.
menuCanceled = true;
setStatus("Popup menu selection canceled");
}
Log.log("Menu mouse press");
}
else if (id == MouseEvent.MOUSE_RELEASED) {
MouseEvent me = (MouseEvent)event;
// The menu target won't be set until the second mouse press
if (menuCanceled) {
setRecordingType(SE_NONE);
setFinished(true);
}
else if (menuTarget == null) {
// This is the first mouse release
if (!isPopup) {
isPopup = me.isPopupTrigger();
}
}
else {
if (menuTarget != null)
setFinished(true);
}
Log.log("Menu mouse release");
}
else if (id == MouseEvent.MOUSE_CLICKED && isPopup) {
// If it was a popup trigger, make sure there was a popup,
// otherwise record it as a click.
// Note that we won't likely get any events with an AWT popup, so
// just assume it was invoked if there is one.
if (!hasAWTPopup
&& AWT.findActivePopupMenu() == null) {
setRecordingType(SE_CLICK);
target = invoker;
x = menux;
y = menuy;
setFinished(true);
}
}
else {
Log.debug("Ignoring " + ComponentTester.toString(event));
}
return consumed;
}
protected boolean parseDrop(AWTEvent event) {
int id = event.getID();
switch(id) {
case MouseEvent.MOUSE_DRAGGED:
case MouseEvent.MOUSE_ENTERED:
updateDropTarget((MouseEvent)event);
break;
case MouseEvent.MOUSE_RELEASED:
updateDropTarget((MouseEvent)event);
setFinished(true);
break;
case MouseEvent.MOUSE_MOVED:
setFinished(true);
break;
case MouseEvent.MOUSE_EXITED:
// ignore this; wait for the next event to get
// a new drop target
break;
default:
if (Log.isClassDebugEnabled(ComponentRecorder.class))
Log.debug("Ignoring " + ComponentTester.toString(event));
}
return true;
}
/** Non-native drags use the drag source as the source for all mouse events
* during the drag except for enter/exit events. Native drags use the
* parent window as the source for all SunDropTargetEvents. In both cases,
* extract the actual target and location relative to the target.
*/
private void updateDropTarget(MouseEvent me) {
dropTarget = SwingUtilities.getDeepestComponentAt(me.getComponent(), me.getX(), me.getY());
Point where = SwingUtilities.convertPoint(me.getComponent(), me.getX(), me.getY(), dropTarget);
dropx = where.x;
dropy = where.y;
}
protected boolean parseInputMethod(AWTEvent event) {
boolean consumed = true;
int id = event.getID();
if (id == KeyEvent.KEY_RELEASED) {
KeyEvent ke = (KeyEvent)event;
int code = ke.getKeyCode();
switch (code) {
case KeyEvent.VK_HALF_WIDTH:
// This indicates the input method start (for kanji, anyway)
break;
case KeyEvent.VK_FULL_WIDTH:
// This indicates the input method end (for kanji, anyway)
Log.log("Captured " + imText);
setFinished(true);
break;
case KeyEvent.VK_ALT_GRAPH:
case KeyEvent.VK_CONTROL:
case KeyEvent.VK_SHIFT:
case KeyEvent.VK_META:
case KeyEvent.VK_ALT:
Log.debug("Modifier indicates end of InputMethod");
consumed = true;
setFinished(true);
break;
default:
// Consume other key release events, assuming there was no
// corresponding key press event.
imKeyCodes.add(new Integer(code));
break;
}
}
else if (event instanceof InputMethodEvent) {
InputMethodEvent ime = (InputMethodEvent)event;
if (id == InputMethodEvent.INPUT_METHOD_TEXT_CHANGED) {
if (ime.getCommittedCharacterCount() > 0) {
AttributedCharacterIterator iter = ime.getText();
StringBuffer sb = new StringBuffer();
for (char ch = iter.first();
ch != CharacterIterator.DONE;
ch = iter.next()) {
sb.append(ch);
}
target = (Component)ime.getSource();
imText.append(sb.toString());
Log.debug("Partial capture " + sb.toString());
}
setFinished(true);
}
}
else {
consumed = false;
setFinished(true);
}
return consumed;
}
/** Handle an event. Return whether the event was consumed. */
public boolean parse(AWTEvent event) {
if (Log.isClassDebugEnabled(ComponentRecorder.class))
Log.debug("Parsing " + ComponentTester.toString(event)
+ " as " + TYPES[getRecordingType()]);
// Default handling is event consumed, and assume not finished
boolean consumed = true;
switch(getRecordingType()) {
case SE_WINDOW:
consumed = parseWindowEvent(event);
break;
case SE_KEY:
consumed = parseKeyEvent(event);
break;
case SE_CLICK:
consumed = parseClick(event);
break;
case SE_MENU:
consumed = parseMenuSelection(event);
break;
case SE_DROP:
consumed = parseDrop(event);
break;
case SE_IM:
consumed = parseInputMethod(event);
break;
default:
Log.warn("Unknown input type: " + getRecordingType());
// error
break;
}
if (isFinished()) {
try {
Step step = createStep();
setStep(step);
Log.log("Semantic event recorded: " + step);
}
catch(Throwable thr) {
String msg = Strings.get("editor.recording.error");
BugReport br = new BugReport(msg, thr);
Log.log("Semantic recorder error: " + br.toString());
setStatus(Strings.get("editor.see_console"));
setRecordingType(SE_NONE);
throw br;
}
}
return consumed;
}
/** Returns whether the first drag motion event should be consumed.
* Derived classes may override this to provide custom drag behavior.
* Default behavior saves the drag initiation event by itself.
*/
protected boolean dragStarted(Component target,
int x, int y,
int modifiers,
MouseEvent dragEvent) {
dragSource = target;
dragx = x;
dragy = y;
setFinished(true);
return false;
}
/** Returns the script step generated from the events recorded so far. */
protected Step createStep() {
Step step = null;
int type = getRecordingType();
Log.debug("Creating step for semantic recorder, type: "
+ (type >= 0 && type < TYPES.length
? TYPES[getRecordingType()] : String.valueOf(type)));
switch(type) {
case SE_WINDOW:
step = createWindowEvent(window, isClose);
break;
case SE_MENU:
if (awtMenuTarget != null) {
if (invoker == null) {
MenuContainer mc = awtMenuTarget.getParent();
while (mc instanceof MenuComponent
&& !(mc instanceof Component)) {
mc = ((MenuComponent)mc).getParent();
}
if (mc == null) {
throw new Error("AWT MenuItem " + awtMenuTarget
+ " has no Component ancestor");
}
invoker = (Component)mc;
}
step = createAWTMenuSelection(invoker, awtMenuTarget, isPopup);
}
else if (isPopup) {
step = createPopupMenuSelection(invoker, menux, menuy,
menuTarget);
}
else if (menuTarget != null) {
step = createMenuSelection(menuTarget);
}
break;
case SE_KEY: {
if (keychar != KeyEvent.CHAR_UNDEFINED) {
step = createKey(target, keychar, modifiers);
}
else {
step = null;
}
break;
}
case SE_CLICK: {
step = createClick(target, x, y, modifiers,
canMultipleClick() ? clickCount : 1);
break;
}
case SE_DRAG: {
step = createDrag(dragSource, dragx, dragy);
break;
}
case SE_DROP:
step = createDrop(dropTarget, dropx, dropy);
break;
case SE_IM:
if (imText.length() > 0)
step = createInputMethod(target, imKeyCodes, imText.toString());
else {
Log.debug("Input method resulted in no text");
step = null;
}
break;
default:
step = null;
break;
}
return step;
}
/** Create a wait for the window show/hide. Use an appropriate identifier
string, which might be the name, title, or component reference.
*/
protected Step createWindowEvent(Window window, boolean isClose) {
ComponentReference ref = getResolver().addComponent(window);
String method = "assertComponentShowing";
Assert step = new Assert(getResolver(), null,
ComponentTester.class.getName(),
method,
new String[] { ref.getID() },
"true", isClose);
step.setWait(true);
return step;
}
protected Step createMenuSelection(Component menuItem) {
// For for issue 3411849, record the full path to
// the menu item to make sure that lazily loaded
// menu items are selected.
// Get the owning window, in the case of a heavyweight
// popup make sure we go up just one more level
Window owningWindow = SwingUtilities.getWindowAncestor(menuItem);
if (owningWindow.getClass().getName().startsWith("javax.swing.Popup")) {
owningWindow = (Window)owningWindow.getParent();
}
ComponentReference cr = getResolver().addComponent(owningWindow);
String path = JMenuItemMatcher.getPath((JMenuItem)menuItem);
Step step = new Action(getResolver(),
null, "actionSelectMenuItem",
new String[] { cr.getID(), path });
return step;
}
protected Step createAWTMenuSelection(Component parent, MenuItem menuItem,
boolean isPopup) {
ComponentReference ref = getResolver().addComponent(parent);
String method = "actionSelectAWTMenuItem";
if (isPopup)
method = "actionSelectAWTPopupMenuItem";
// Get a unique path for the MenuItem
String path = AWT.getPath(menuItem);
// Do a quick search on the invoker for other popups. If there are
// duplicates, include the menu item name
Step step = new Action(getResolver(),
null, method,
new String[] { ref.getID(), path });
return step;
}
protected Step createPopupMenuSelection(Component invoker, int x, int y,
Component menuItem) {
Step step;
if (invoker != null) {
ComponentReference inv = getResolver().addComponent(invoker);
JMenuItem mi = (JMenuItem)menuItem;
String where = getLocationArgument(invoker, x, y);
step = new Action(getResolver(),
null, "actionSelectPopupMenuItem",
new String[] { inv.getID(), where,
JMenuItemMatcher.getPath(mi)
}, invoker.getClass());
}
else {
ComponentReference ref = getResolver().addComponent(menuItem);
step = new Action(getResolver(),
null, "actionSelectMenuItem",
new String[] { ref.getID() });
}
return step;
}
protected Step createKey(Component comp, char keychar, int mods) {
ComponentReference cr = getResolver().addComponent(comp);
// NOTE: Any keys which might have effects as key press/release should
// be encoded as a keystroke, rather than a keystring.
// NOTE: We encode strings rather than integer values, since the
// names are more useful.
String code = (String)specialMap.get(String.valueOf(keychar));
if (code != null) {
String[] args = mods != 0
? new String[] { cr.getID(), code,
AWT.getKeyModifiers(mods) }
: new String[] { cr.getID(), code };
return new Action(getResolver(), null, "actionKeyStroke", args);
}
return new Action(getResolver(), null,
"actionKeyString",
new String[] { cr.getID(),
String.valueOf(keychar) });
}
protected Step createDrop(Component comp, int x, int y) {
Step step = null;
if (comp != null) {
ComponentReference cr = getResolver().addComponent(comp);
String where = getLocationArgument(comp, x, y);
step = new Action(getResolver(),
null, "actionDrop", new String[] {
cr.getID(), where
}, comp.getClass());
}
return step;
}
protected Step createDrag(Component comp, int x, int y) {
ComponentReference ref = getResolver().addComponent(comp);
String where = getLocationArgument(comp, x, y);
Step step = new Action(getResolver(),
null, "actionDrag", new String[] {
ref.getID(), where,
}, comp.getClass());
return step;
}
/** Create a click event with the given event information. */
protected Step createClick(Component target, int x, int y,
int mods, int count) {
Log.debug("creating click");
ComponentReference cr = getResolver().addComponent(target);
ArrayList args = new ArrayList();
args.add(cr.getID());
args.add(getLocationArgument(target, x, y));
if ((mods != 0 && mods != MouseEvent.BUTTON1_MASK)
|| count > 1) {
// NOTE: this currently saves POPUP or TERTIARY, rather than
// an explicit button 2 or 3. I figure that makes more sense
// than a hard coded button number.
args.add(AWT.getMouseModifiers(mods));
if (count > 1) {
args.add(String.valueOf(count));
}
}
return new Action(getResolver(), null, "actionClick",
(String[])args.toArray(new String[args.size()]),
target.getClass());
}
protected Step createInputMethod(Component comp,
ArrayList codes, String text) {
Log.debug("Text length is " + text.length());
ComponentReference ref = getResolver().addComponent(comp);
return new Action(getResolver(), null,
"actionKeyString",
new String[] { ref.getID(), text });
}
protected void init(int recordingType) {
super.init(recordingType);
target = null;
forwardedTarget = null;
released = false;
clickCount = 0;
keychar = KeyEvent.CHAR_UNDEFINED;
invoker = null;
awtMenuTarget = null;
isPopup = false;
hasAWTPopup = false;
menuListener = null;
menuTarget = null;
menuCanceled = false;
dragSource = dropTarget = null;
window = null;
isClose = false;
imKeyCodes.clear();
imText.delete(0, imText.length());
}
/** Invoke when end of the semantic event has been seen. */
protected void setFinished(boolean state) {
MenuListener listener = null;
synchronized(this) {
super.setFinished(state);
listener = menuListener;
menuListener = null;
}
if (listener != null)
listener.dispose();
}
private boolean addMenuListener(Component invoker) {
PopupMenu[] popups = AWT.getPopupMenus(invoker);
if (popups.length > 0) {
menuListener = new MenuListener(popups);
return true;
}
return false;
}
private class MenuListener implements ItemListener {
private ArrayList items = new ArrayList();
public MenuListener(PopupMenu[] popups) {
for (int i=0;i < popups.length;i++) {
addRecursive(popups[i]);
}
}
private void addRecursive(Menu menu) {
for (int i=0;i < menu.getItemCount();i++) {
MenuItem item = menu.getItem(i);
if (item instanceof Menu)
addRecursive((Menu)item);
else if (item instanceof CheckboxMenuItem) {
((CheckboxMenuItem)item).addItemListener(this);
items.add(item);
}
}
}
public void itemStateChanged(ItemEvent e) {
dispose();
parse(e);
}
public void dispose() {
while (items.size() > 0) {
((CheckboxMenuItem)items.get(0)).removeItemListener(this);
items.remove(0);
}
}
}
/** Obtain the String representation of the Component-specific location. */
protected String getLocationArgument(Component c, int x, int y) {
return getLocation(c, x, y).toString();
}
/** Obtain a more precise location than the given coordinate, if
* possible.
*/
protected ComponentLocation getLocation(Component c, int x, int y) {
ComponentTester tester = ComponentTester.getTester(c);
return tester.getLocation(c, new Point(x, y));
}
}