0001: package abbot.editor.recorder;
0002:
0003: import java.awt.*;
0004: import java.awt.event.*;
0005: import java.text.*;
0006: import java.util.*;
0007:
0008: import javax.swing.*;
0009: import javax.swing.text.JTextComponent;
0010:
0011: import abbot.BugReport;
0012: import abbot.Log;
0013: import abbot.Platform;
0014:
0015: import abbot.finder.matchers.JMenuItemMatcher;
0016:
0017: import abbot.i18n.Strings;
0018: import abbot.script.*;
0019: import abbot.script.Action;
0020: import abbot.script.Resolver;
0021: import abbot.tester.*;
0022: import abbot.tester.Robot;
0023: import abbot.util.AWT;
0024:
0025: /**
0026: * Record basic semantic events you might find on any component. This class
0027: * handles the following actions:<p>
0028: * <ul>
0029: * <li>window actions
0030: * <li>popup menus
0031: * <li>click
0032: * <li>typed keys
0033: * <li>basic drag and drop
0034: * <li>InputMethod events (extended character input)
0035: * </ul>
0036: * Clicks, popup menus, and drag/drop actions may be based on coordinates or
0037: * component substructure (cell, row, tab, etc) locations.
0038: *
0039: * <h3>Window Actions</h3>
0040: * While these nominally might be handled in a WindowRecorder, they are so
0041: * common that it's easier to handle here instead. Currently supports
0042: * tracking show/hide/activate. TODO: move/resize/iconfify/deiconify.
0043: * <h3>Popup Menus</h3>
0044: * Currently only the click/select/click sequence is supported. The
0045: * press/drag/release version shouldn't be hard to implement, though.
0046: * <h3>Click</h3>
0047: * Simple press/release on a component, storing the exact coordinate of the
0048: * click. Most things with selectability will want to override this. Culling
0049: * accidental intervening drags would be nice but probably not worth the
0050: * effort or complexity (better just to be less sloppy with your mouse).
0051: * <h3>Key Type</h3>
0052: * Capture only events that result in actual output. No plain modifiers,
0053: * shortcuts, or mnemonics.
0054: * <h3>Drag/Drop</h3>
0055: * Basic drag from one component and drop on another, storing exact
0056: * coordinates of the press/release actions. Should definitely override this
0057: * to represent your component's internal objects (e.g. cells in a table).
0058: * Note that these are two distinct actions, even though they always appear
0059: * together. The source is responsible for identifying the drag, and the
0060: * target is responsible for identifying the drop.
0061: * <h3>InputMethod</h3>
0062: * Catch extended character input.
0063: */
0064: // NOTE: Mac OSX robot will actually generate key modifiers prior
0065: // to button2/3
0066: // NOTE: Mac OSX CTRL/ALT+MB1 invokes MB2
0067: // CTRL+MB1->CTRL+MB2
0068: // ALT+MB1->MB2
0069: // TODO: test recorders by sending an event stream; test platform stream
0070: // by generating robot events and verifying stream seen; this splits the
0071: // tests into separate concerns.
0072: public class ComponentRecorder extends SemanticRecorder {
0073:
0074: private static final String[] TYPES = { "any", "window", "menu",
0075: "click", "key", "drag", "drop", "text", "input method" };
0076:
0077: /** Mappings for special keys. */
0078: private static java.util.HashMap specialMap;
0079:
0080: static {
0081: // Make explicit some special key mappings which we DON'T want to save
0082: // as the resulting characters (b/c they may not actually be
0083: // characters, or they're not particularly good to save as
0084: // characters.
0085: int[][] mappings = {
0086: { '\t', KeyEvent.VK_TAB },
0087: { '', KeyEvent.VK_ESCAPE }, // No escape sequence exists
0088: { '\b', KeyEvent.VK_BACK_SPACE },
0089: { '', KeyEvent.VK_DELETE }, // No escape sequence exists
0090: { '\n', KeyEvent.VK_ENTER },
0091: { '\r', KeyEvent.VK_ENTER }, };
0092: specialMap = new java.util.HashMap();
0093: for (int i = 0; i < mappings.length; i++) {
0094: specialMap.put(String.valueOf((char) mappings[i][0]), AWT
0095: .getKeyCode(mappings[i][1]));
0096: }
0097: }
0098:
0099: // For windows
0100: private Window window = null;
0101: private boolean isClose = false;
0102: // For key events
0103: private char keychar = KeyEvent.CHAR_UNDEFINED;
0104: private int modifiers;
0105: // For clicks
0106: private Component target;
0107: private Component forwardedTarget;
0108: private int x, y;
0109: private boolean released;
0110: private int clickCount;
0111: // For menu events
0112: private Component invoker;
0113: private int menux, menuy;
0114: private MenuItem awtMenuTarget;
0115: private Component menuTarget;
0116: private boolean isPopup;
0117: private boolean hasAWTPopup;
0118: private MenuListener menuListener;
0119: private boolean menuCanceled;
0120: // For drag events
0121: // This class is responsible for handling drag/drop once the action has
0122: // been recognized by a derived class
0123: private Component dragSource;
0124: private int dragx, dragy;
0125: // For drop events
0126: private Component dropTarget;
0127: private int dropx, dropy;
0128: // InputMethod
0129: private ArrayList imKeyCodes = new ArrayList();
0130: private StringBuffer imText = new StringBuffer();
0131: /** Keep a short-term memory of windows we've seen open/close already. */
0132: private static WeakHashMap closeEventWindows = new WeakHashMap();
0133: private static WeakHashMap openEventWindows = new WeakHashMap();
0134:
0135: /** Create a ComponentRecorder for use in capturing the semantics of a GUI
0136: * action.
0137: */
0138: public ComponentRecorder(Resolver resolver) {
0139: super (resolver);
0140: }
0141:
0142: /** Does the given event indicate a window was shown? */
0143: protected boolean isOpen(AWTEvent event) {
0144: int id = event.getID();
0145: // 1.3 VMs may generate a WINDOW_OPEN without a COMPONENT_SHOWN
0146: // (see EventRecorderTest.testClickWithDialog)
0147: // NOTE: COMPONENT_SHOWN precedes WINDOW_OPENED, but we don't really
0148: // care in this case, since we're just recording the event, not
0149: // watching for the component's validity.
0150: if (((id == WindowEvent.WINDOW_OPENED && !openEventWindows
0151: .containsKey(event.getSource())) || id == ComponentEvent.COMPONENT_SHOWN)) {
0152: return true;
0153: }
0154: return false;
0155: }
0156:
0157: /** Does the given event indicate a window was closed? */
0158: protected boolean isClose(AWTEvent event) {
0159: int id = event.getID();
0160: // Window.dispose doesn't generate a HIDDEN event, but it does
0161: // generate a WINDOW_CLOSED event (1.3/1.4)
0162: if (((id == WindowEvent.WINDOW_CLOSED && !closeEventWindows
0163: .containsKey(event.getSource())) || id == ComponentEvent.COMPONENT_HIDDEN)) {
0164: return true;
0165: }
0166: return false;
0167: }
0168:
0169: /** Returns whether this ComponentRecorder wishes to accept the given
0170: * event. If the event is accepted, the recorder must invoke init() with
0171: * the appropriate semantic event type.
0172: */
0173: public boolean accept(AWTEvent event) {
0174: int rtype = SE_NONE;
0175:
0176: if (isWindowEvent(event)) {
0177: rtype = SE_WINDOW;
0178: } else if (isMenuEvent(event)) {
0179: rtype = SE_MENU;
0180: } else if (isKeyTyped(event)) {
0181: rtype = SE_KEY;
0182: } else if (isClick(event)) {
0183: rtype = SE_CLICK;
0184: } else if (isDragDrop(event)) {
0185: rtype = SE_DROP;
0186: } else if (isInputMethod(event)) {
0187: rtype = SE_IM;
0188: } else {
0189: if (Log.isClassDebugEnabled(ComponentRecorder.class))
0190: Log.debug("Ignoring " + Robot.toString(event));
0191: }
0192:
0193: init(rtype);
0194: boolean accepted = rtype != SE_NONE;
0195: if (accepted
0196: && Log.isClassDebugEnabled(ComponentRecorder.class))
0197: Log.debug("Accepted " + ComponentTester.toString(event));
0198: return accepted;
0199: }
0200:
0201: /** Test whether the given event is a trigger for a window event.
0202: * Allow derived classes to change definition of a click.
0203: */
0204: protected boolean isWindowEvent(AWTEvent event) {
0205: // Ignore activate and deactivate. They are unreliable.
0206: // We only want open/close events on non-tooltip and non-popup windows
0207: return (event.getSource() instanceof Window)
0208: && !AWT.isHeavyweightPopup((Window) event.getSource())
0209: && !isToolTip(event.getSource())
0210: && (isClose(event) || isOpen(event));
0211: }
0212:
0213: /**
0214: * Return true if the given event source is a tooltip.
0215: * Such events look like window events, but we check for them before other
0216: * kinds of window events so as to be able to filter them out.
0217: * <P>
0218: * TODO: emit steps to confirm value of tooltip?
0219: * <P>
0220: * @param source the object to examine
0221: * @return true if this event source is a tooltip
0222: */
0223: protected boolean isToolTip(Object source) {
0224: // Tooltips appear to be a direct subclass of JWindow and
0225: // have a single component of class JToolTip
0226: if (source instanceof JWindow && !(source instanceof JFrame)) {
0227: Container pane = ((JWindow) source).getContentPane();
0228: while (pane.getComponentCount() == 1) {
0229: Component child = pane.getComponent(0);
0230: if (child instanceof JToolTip)
0231: return true;
0232: if (!(child instanceof Container))
0233: break;
0234: pane = (Container) child;
0235: }
0236: }
0237: return false;
0238: }
0239:
0240: protected boolean isMenuEvent(AWTEvent event) {
0241: if (event.getID() == ActionEvent.ACTION_PERFORMED
0242: && event.getSource() instanceof java.awt.MenuItem) {
0243: return true;
0244: } else if (event.getID() == MouseEvent.MOUSE_PRESSED) {
0245: MouseEvent me = (MouseEvent) event;
0246: return me.isPopupTrigger()
0247: || ((me.getModifiers() & AWTConstants.POPUP_MASK) != 0)
0248: || me.getSource() instanceof JMenu;
0249: }
0250: return false;
0251: }
0252:
0253: protected boolean isKeyTyped(AWTEvent event) {
0254: return event.getID() == KeyEvent.KEY_TYPED;
0255: }
0256:
0257: /** Test whether the given event is a trigger for a mouse button click.
0258: * Allow derived classes to change definition of a click.
0259: */
0260: protected boolean isClick(AWTEvent event) {
0261: if (event.getID() == MouseEvent.MOUSE_PRESSED) {
0262: MouseEvent me = (MouseEvent) event;
0263: return (me.getModifiers() & MouseEvent.BUTTON1_MASK) != 0;
0264: }
0265: return false;
0266: }
0267:
0268: /** Test whether the given event precurses a drop. */
0269: protected boolean isDragDrop(AWTEvent event) {
0270: return event.getID() == MouseEvent.MOUSE_DRAGGED;
0271: }
0272:
0273: /** Default to recording a drag if it looks like one. */
0274: // FIXME may be some better detection, like checking for DND interfaces. */
0275: protected boolean canDrag() {
0276: return true;
0277: }
0278:
0279: /** Default to waiting for multiple clicks. */
0280: protected boolean canMultipleClick() {
0281: return true;
0282: }
0283:
0284: /** Is this the start of an input method event? */
0285: private boolean isInputMethod(AWTEvent event) {
0286: // NOTE: HALF_WIDTH signals start of kanji input
0287: // NOTE: Mac uses input method for some dual-keystroke chars (option-e)
0288: return (event.getID() == KeyEvent.KEY_RELEASED && ((KeyEvent) event)
0289: .getKeyCode() == KeyEvent.VK_HALF_WIDTH)
0290: || event.getID() == InputMethodEvent.INPUT_METHOD_TEXT_CHANGED;
0291: }
0292:
0293: /** Provide standard parsing of mouse button events. */
0294: protected boolean parseClick(AWTEvent event) {
0295: boolean consumed = true;
0296: int id = event.getID();
0297: if (id == MouseEvent.MOUSE_PRESSED) {
0298: Log.debug("Parsing mouse down");
0299: MouseEvent me = (MouseEvent) event;
0300: if (clickCount == 0) {
0301: target = me.getComponent();
0302: x = me.getX();
0303: y = me.getY();
0304: modifiers = me.getModifiers();
0305: clickCount = 1;
0306: // Add the component immediately, just in case it gets removed
0307: // from the hierarchy as a result of the click.
0308: getResolver().addComponent(target);
0309: } else {
0310: if (target == me.getComponent()) {
0311: clickCount = me.getClickCount();
0312: } else if (!released) {
0313: // It's possible to get two consecutive MOUSE_PRESSED
0314: // events for different targets (e.g. double click on a
0315: // table cell to get the default editor) (OSX 1.3.1, XP
0316: // 1.4.1_01). Ignore the second click, since it is
0317: // artificial, and wait for the original click to finish.
0318: // i.e. w32 1.3.1
0319: // MOUSE_PRESSED JTable
0320: // MOUSE_PRESSED JTextField
0321: // FOCUS_LOST JTable
0322: // FOCUS_GAINED JTextField
0323: // MOUSE_EXITED JTable
0324: // MOUSE_ENTERED JTextField
0325: // MOUSE_RELEASED JTable
0326: // MOUSE_RELEASED JTextField
0327: forwardedTarget = me.getComponent();
0328: }
0329: }
0330: released = false;
0331: } else if (id == MouseEvent.MOUSE_RELEASED) {
0332: Log.debug("Parsing mouse up");
0333: released = true;
0334: // Optionally disallow multiple clicks
0335: if (!canMultipleClick())
0336: setFinished(true);
0337: } else if (id == MouseEvent.MOUSE_CLICKED) {
0338: // optionally wait for multiple clicks
0339: if (!canMultipleClick())
0340: setFinished(true);
0341: } else if (id == MouseEvent.MOUSE_EXITED) {
0342: Log.debug("exit event, released=" + released);
0343: if (event.getSource() != target || released) {
0344: consumed = false;
0345: setFinished(true);
0346: } else if (!released) {
0347: // May not see any DRAGGED events if it's a native drag;
0348: // 1.3 posts MOUSE_EXITED after MOUSE_PRESSED, no drag events
0349: if (clickCount == 1) {
0350: setRecordingType(SE_DRAG);
0351: consumed = dragStarted(target, x, y, modifiers,
0352: (MouseEvent) event);
0353: }
0354: }
0355: } else if (id == MouseEvent.MOUSE_ENTERED) {
0356: if (event.getSource() == target && !released) {
0357: // nothing
0358: } else if (event.getSource() != forwardedTarget) {
0359: consumed = false;
0360: setFinished(true);
0361: }
0362: } else if (id == MouseEvent.MOUSE_DRAGGED && canDrag()) {
0363: Log.debug("Changing click to drag start");
0364: MouseEvent me = (MouseEvent) event;
0365: Point where = SwingUtilities.convertPoint(
0366: me.getComponent(), me.getX(), me.getY(), target);
0367: int threshold = Integer.getInteger(
0368: "abbot.capture.drag_threshold", 5).intValue();
0369:
0370: if (Math.abs(where.x - x) >= threshold
0371: || Math.abs(where.y - y) >= threshold) {
0372: // Was actually a drag; pass off to drag handler
0373: setRecordingType(SE_DRAG);
0374: consumed = dragStarted(target, x, y, modifiers, me);
0375: } else {
0376: Log.debug("Drag too small: " + me + " (from "
0377: + dragSource + " (" + x + ", " + y + "))");
0378: }
0379: }
0380: // These events will not prevent a multi-click from being registered.
0381: else if ((id >= ComponentEvent.COMPONENT_FIRST && id <= ComponentEvent.COMPONENT_LAST)
0382: || (event instanceof ContainerEvent)
0383: || (event instanceof FocusEvent)
0384: || (id == HierarchyEvent.HIERARCHY_CHANGED && (((HierarchyEvent) event)
0385: .getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) == 0)) {
0386: // Ignore most hierarchy change and component events between
0387: // clicks.
0388: // The focus event is sporadic on w32 1.4.1_02
0389: } else {
0390: // All other events should cause the click to finish,
0391: // but don't register a click unless we've received the release
0392: // event.
0393: if (released) {
0394: consumed = false;
0395: setFinished(true);
0396: }
0397: }
0398: return consumed;
0399: }
0400:
0401: protected boolean parseWindowEvent(AWTEvent event) {
0402: boolean consumed = true;
0403: isClose = isClose(event);
0404: // Keep track of window open/close state so we don't parse the same
0405: // semantic event twice (e.g. COMPONENT_SHOWN + WINDOW_OPENED or
0406: // multiple WINDOW_CLOSED events).
0407: if (isClose) {
0408: closeEventWindows.put(event.getSource(), Boolean.TRUE);
0409: openEventWindows.remove(event.getSource());
0410: } else {
0411: openEventWindows.put(event.getSource(), Boolean.TRUE);
0412: closeEventWindows.remove(event.getSource());
0413: }
0414: Log
0415: .log("close=" + isClose + " (" + Robot.toString(event)
0416: + ")");
0417: window = (Window) event.getSource();
0418: setFinished(true);
0419: return consumed;
0420: }
0421:
0422: protected boolean parseKeyEvent(AWTEvent e) {
0423: int id = e.getID();
0424: boolean consumed = true;
0425: if (id == KeyEvent.KEY_TYPED) {
0426: KeyEvent typed = (KeyEvent) e;
0427: target = typed.getComponent();
0428: keychar = typed.getKeyChar();
0429: modifiers = typed.getModifiers();
0430: if ((modifiers & KeyEvent.ALT_MASK) == KeyEvent.ALT_MASK) {
0431: Log
0432: .debug("Waiting for potential focus accelerator on '"
0433: + keychar + "'");
0434: } else {
0435: // Ignore KEY_TYPED input for control and alt modifiers, since
0436: // the generated characters are not accepted as text input.
0437: // Add others if you encounter them, but err on the side of
0438: // accepting input that can later be removed.
0439: if ((modifiers & InputEvent.CTRL_MASK) == InputEvent.CTRL_MASK
0440: || (modifiers & InputEvent.ALT_MASK) == InputEvent.ALT_MASK) {
0441: Log.debug("Ignoring modifiers: " + modifiers);
0442: setRecordingType(SE_NONE);
0443: }
0444: setFinished(true);
0445: }
0446: } else if (id == FocusEvent.FOCUS_LOST) {
0447: // Ignore and wait for FOCUS_GAINED
0448: } else if (id == FocusEvent.FOCUS_GAINED) {
0449: // Looks like a focus accelerator focus change. Ignore the
0450: // KEY_TYPED event.
0451: Object o = e.getSource();
0452: char ch = KeyEvent.CHAR_UNDEFINED;
0453: if (o instanceof JTextComponent) {
0454: ch = ((JTextComponent) o).getFocusAccelerator();
0455: Log.debug("focus accelerator is '" + ch + "'");
0456: }
0457: if (Character.toUpperCase(ch) == Character
0458: .toUpperCase(keychar)) {
0459: setRecordingType(SE_NONE);
0460: setFinished(true);
0461: } else {
0462: setFinished(true);
0463: consumed = false;
0464: }
0465: } else {
0466: setRecordingType(SE_NONE);
0467: setFinished(true);
0468: consumed = false;
0469: }
0470: return consumed;
0471: }
0472:
0473: /** Base implementation handles context (popup) menus. */
0474: protected boolean parseMenuSelection(AWTEvent event) {
0475: int id = event.getID();
0476: boolean consumed = true;
0477: // press, release, show, [move, show,] press, release
0478: // press, [drag, show,] release (FIXME not done)
0479: // ACTION_PERFORMED and ITEM_STATE_CHANGED are only
0480: // produced by AWT components (wxp/1.4.2)
0481: if (id == ActionEvent.ACTION_PERFORMED
0482: || id == ItemEvent.ITEM_STATE_CHANGED) {
0483: awtMenuTarget = (MenuItem) event.getSource();
0484: invoker = AWT.getInvoker(awtMenuTarget);
0485: // If there is no invoker, the selection came from the MenuBar
0486: if (invoker != null) {
0487: isPopup = true;
0488: }
0489: Log.debug("AWT menu selection, invoker="
0490: + Robot.toString(invoker));
0491: if (event instanceof ActionEvent) {
0492: modifiers = ((ActionEvent) event).getModifiers();
0493: } else {
0494: // ItemEvent doesn't report modifiers, so ask use internal
0495: // tracking to see if any modifiers are active.
0496: modifiers = Robot.getState().getModifiers();
0497: }
0498: setFinished(true);
0499: } else if (id == MouseEvent.MOUSE_PRESSED) {
0500: MouseEvent me = (MouseEvent) event;
0501: // On the first press, we haven't yet set the invoker, which
0502: // is either a JMenu or the component holding the popup.
0503: if (invoker == null) {
0504: invoker = me.getComponent();
0505: menux = me.getX();
0506: menuy = me.getY();
0507: modifiers = me.getModifiers();
0508: isPopup = me.isPopupTrigger();
0509: // Must add the listener now, b/c on w32 release/click events
0510: // are not generated until *after* the awt popup selection.
0511: if (isPopup
0512: || (modifiers & AWTConstants.POPUP_MASK) != 0) {
0513: hasAWTPopup = addMenuListener(invoker);
0514: }
0515: // It's possible for a popup menu to be triggered by some
0516: // other event (e.g. a button click). Assume that action is
0517: // already recorded and simply make note of the appropriate
0518: // menu selection.
0519: if (invoker instanceof JMenuItem
0520: && !(invoker instanceof JMenu)) {
0521: menuTarget = invoker;
0522: invoker = null;
0523: menux = menuy = -1;
0524: modifiers = 0;
0525: isPopup = true;
0526: }
0527: } else if (event.getSource() instanceof JMenu) {
0528: // ignore
0529: } else if (event.getSource() instanceof JMenuItem) {
0530: // Click to select the menu item; this will be the second
0531: // press event received
0532: menuTarget = (Component) event.getSource();
0533: } else {
0534: // Mouse press in something other than the menu, assume it was
0535: // canceled.
0536: // Popup was canceled. Discard subsequent release/click.
0537: menuCanceled = true;
0538: setStatus("Popup menu selection canceled");
0539: }
0540: Log.log("Menu mouse press");
0541: } else if (id == MouseEvent.MOUSE_RELEASED) {
0542: MouseEvent me = (MouseEvent) event;
0543: // The menu target won't be set until the second mouse press
0544: if (menuCanceled) {
0545: setRecordingType(SE_NONE);
0546: setFinished(true);
0547: } else if (menuTarget == null) {
0548: // This is the first mouse release
0549: if (!isPopup) {
0550: isPopup = me.isPopupTrigger();
0551: }
0552: } else {
0553: if (menuTarget != null)
0554: setFinished(true);
0555: }
0556: Log.log("Menu mouse release");
0557: } else if (id == MouseEvent.MOUSE_CLICKED && isPopup) {
0558: // If it was a popup trigger, make sure there was a popup,
0559: // otherwise record it as a click.
0560: // Note that we won't likely get any events with an AWT popup, so
0561: // just assume it was invoked if there is one.
0562: if (!hasAWTPopup && AWT.findActivePopupMenu() == null) {
0563: setRecordingType(SE_CLICK);
0564: target = invoker;
0565: x = menux;
0566: y = menuy;
0567: setFinished(true);
0568: }
0569: } else {
0570: Log.debug("Ignoring " + ComponentTester.toString(event));
0571: }
0572: return consumed;
0573: }
0574:
0575: protected boolean parseDrop(AWTEvent event) {
0576: int id = event.getID();
0577:
0578: switch (id) {
0579: case MouseEvent.MOUSE_DRAGGED:
0580: case MouseEvent.MOUSE_ENTERED:
0581: updateDropTarget((MouseEvent) event);
0582: break;
0583: case MouseEvent.MOUSE_RELEASED:
0584: updateDropTarget((MouseEvent) event);
0585: setFinished(true);
0586: break;
0587: case MouseEvent.MOUSE_MOVED:
0588: setFinished(true);
0589: break;
0590: case MouseEvent.MOUSE_EXITED:
0591: // ignore this; wait for the next event to get
0592: // a new drop target
0593: break;
0594: default:
0595: if (Log.isClassDebugEnabled(ComponentRecorder.class))
0596: Log
0597: .debug("Ignoring "
0598: + ComponentTester.toString(event));
0599: }
0600: return true;
0601: }
0602:
0603: /** Non-native drags use the drag source as the source for all mouse events
0604: * during the drag except for enter/exit events. Native drags use the
0605: * parent window as the source for all SunDropTargetEvents. In both cases,
0606: * extract the actual target and location relative to the target.
0607: */
0608: private void updateDropTarget(MouseEvent me) {
0609: dropTarget = SwingUtilities.getDeepestComponentAt(me
0610: .getComponent(), me.getX(), me.getY());
0611: Point where = SwingUtilities.convertPoint(me.getComponent(), me
0612: .getX(), me.getY(), dropTarget);
0613: dropx = where.x;
0614: dropy = where.y;
0615: }
0616:
0617: protected boolean parseInputMethod(AWTEvent event) {
0618: boolean consumed = true;
0619: int id = event.getID();
0620: if (id == KeyEvent.KEY_RELEASED) {
0621: KeyEvent ke = (KeyEvent) event;
0622: int code = ke.getKeyCode();
0623: switch (code) {
0624: case KeyEvent.VK_HALF_WIDTH:
0625: // This indicates the input method start (for kanji, anyway)
0626: break;
0627: case KeyEvent.VK_FULL_WIDTH:
0628: // This indicates the input method end (for kanji, anyway)
0629: Log.log("Captured " + imText);
0630: setFinished(true);
0631: break;
0632: case KeyEvent.VK_ALT_GRAPH:
0633: case KeyEvent.VK_CONTROL:
0634: case KeyEvent.VK_SHIFT:
0635: case KeyEvent.VK_META:
0636: case KeyEvent.VK_ALT:
0637: Log.debug("Modifier indicates end of InputMethod");
0638: consumed = true;
0639: setFinished(true);
0640: break;
0641: default:
0642: // Consume other key release events, assuming there was no
0643: // corresponding key press event.
0644: imKeyCodes.add(new Integer(code));
0645: break;
0646: }
0647: } else if (event instanceof InputMethodEvent) {
0648: InputMethodEvent ime = (InputMethodEvent) event;
0649: if (id == InputMethodEvent.INPUT_METHOD_TEXT_CHANGED) {
0650: if (ime.getCommittedCharacterCount() > 0) {
0651: AttributedCharacterIterator iter = ime.getText();
0652: StringBuffer sb = new StringBuffer();
0653: for (char ch = iter.first(); ch != CharacterIterator.DONE; ch = iter
0654: .next()) {
0655: sb.append(ch);
0656: }
0657: target = (Component) ime.getSource();
0658: imText.append(sb.toString());
0659: Log.debug("Partial capture " + sb.toString());
0660: }
0661: setFinished(true);
0662: }
0663: } else {
0664: consumed = false;
0665: setFinished(true);
0666: }
0667: return consumed;
0668: }
0669:
0670: /** Handle an event. Return whether the event was consumed. */
0671: public boolean parse(AWTEvent event) {
0672: if (Log.isClassDebugEnabled(ComponentRecorder.class))
0673: Log.debug("Parsing " + ComponentTester.toString(event)
0674: + " as " + TYPES[getRecordingType()]);
0675:
0676: // Default handling is event consumed, and assume not finished
0677: boolean consumed = true;
0678:
0679: switch (getRecordingType()) {
0680: case SE_WINDOW:
0681: consumed = parseWindowEvent(event);
0682: break;
0683: case SE_KEY:
0684: consumed = parseKeyEvent(event);
0685: break;
0686: case SE_CLICK:
0687: consumed = parseClick(event);
0688: break;
0689: case SE_MENU:
0690: consumed = parseMenuSelection(event);
0691: break;
0692: case SE_DROP:
0693: consumed = parseDrop(event);
0694: break;
0695: case SE_IM:
0696: consumed = parseInputMethod(event);
0697: break;
0698: default:
0699: Log.warn("Unknown input type: " + getRecordingType());
0700: // error
0701: break;
0702: }
0703: if (isFinished()) {
0704: try {
0705: Step step = createStep();
0706: setStep(step);
0707: Log.log("Semantic event recorded: " + step);
0708: } catch (Throwable thr) {
0709: String msg = Strings.get("editor.recording.error");
0710: BugReport br = new BugReport(msg, thr);
0711: Log.log("Semantic recorder error: " + br.toString());
0712: setStatus(Strings.get("editor.see_console"));
0713: setRecordingType(SE_NONE);
0714: throw br;
0715: }
0716: }
0717:
0718: return consumed;
0719: }
0720:
0721: /** Returns whether the first drag motion event should be consumed.
0722: * Derived classes may override this to provide custom drag behavior.
0723: * Default behavior saves the drag initiation event by itself.
0724: */
0725: protected boolean dragStarted(Component target, int x, int y,
0726: int modifiers, MouseEvent dragEvent) {
0727: dragSource = target;
0728: dragx = x;
0729: dragy = y;
0730: setFinished(true);
0731: return false;
0732: }
0733:
0734: /** Returns the script step generated from the events recorded so far. */
0735: protected Step createStep() {
0736: Step step = null;
0737: int type = getRecordingType();
0738: Log
0739: .debug("Creating step for semantic recorder, type: "
0740: + (type >= 0 && type < TYPES.length ? TYPES[getRecordingType()]
0741: : String.valueOf(type)));
0742: switch (type) {
0743: case SE_WINDOW:
0744: step = createWindowEvent(window, isClose);
0745: break;
0746: case SE_MENU:
0747: if (awtMenuTarget != null) {
0748: if (invoker == null) {
0749: MenuContainer mc = awtMenuTarget.getParent();
0750: while (mc instanceof MenuComponent
0751: && !(mc instanceof Component)) {
0752: mc = ((MenuComponent) mc).getParent();
0753: }
0754: if (mc == null) {
0755: throw new Error("AWT MenuItem " + awtMenuTarget
0756: + " has no Component ancestor");
0757: }
0758: invoker = (Component) mc;
0759: }
0760: step = createAWTMenuSelection(invoker, awtMenuTarget,
0761: isPopup);
0762: } else if (isPopup) {
0763: step = createPopupMenuSelection(invoker, menux, menuy,
0764: menuTarget);
0765: } else if (menuTarget != null) {
0766: step = createMenuSelection(menuTarget);
0767: }
0768: break;
0769: case SE_KEY: {
0770: if (keychar != KeyEvent.CHAR_UNDEFINED) {
0771: step = createKey(target, keychar, modifiers);
0772: } else {
0773: step = null;
0774: }
0775: break;
0776: }
0777: case SE_CLICK: {
0778: step = createClick(target, x, y, modifiers,
0779: canMultipleClick() ? clickCount : 1);
0780: break;
0781: }
0782: case SE_DRAG: {
0783: step = createDrag(dragSource, dragx, dragy);
0784: break;
0785: }
0786: case SE_DROP:
0787: step = createDrop(dropTarget, dropx, dropy);
0788: break;
0789: case SE_IM:
0790: if (imText.length() > 0)
0791: step = createInputMethod(target, imKeyCodes, imText
0792: .toString());
0793: else {
0794: Log.debug("Input method resulted in no text");
0795: step = null;
0796: }
0797: break;
0798: default:
0799: step = null;
0800: break;
0801: }
0802:
0803: return step;
0804: }
0805:
0806: /** Create a wait for the window show/hide. Use an appropriate identifier
0807: string, which might be the name, title, or component reference.
0808: */
0809: protected Step createWindowEvent(Window window, boolean isClose) {
0810: ComponentReference ref = getResolver().addComponent(window);
0811: String method = "assertComponentShowing";
0812: Assert step = new Assert(getResolver(), null,
0813: ComponentTester.class.getName(), method,
0814: new String[] { ref.getID() }, "true", isClose);
0815: step.setWait(true);
0816: return step;
0817: }
0818:
0819: protected Step createMenuSelection(Component menuItem) {
0820: ComponentReference cr = getResolver().addComponent(menuItem);
0821: Step step = new Action(getResolver(), null,
0822: "actionSelectMenuItem", new String[] { cr.getID() });
0823: return step;
0824: }
0825:
0826: protected Step createAWTMenuSelection(Component parent,
0827: MenuItem menuItem, boolean isPopup) {
0828: ComponentReference ref = getResolver().addComponent(parent);
0829: String method = "actionSelectAWTMenuItem";
0830: if (isPopup)
0831: method = "actionSelectAWTPopupMenuItem";
0832: // Get a unique path for the MenuItem
0833: String path = AWT.getPath(menuItem);
0834: // Do a quick search on the invoker for other popups. If there are
0835: // duplicates, include the menu item name
0836: Step step = new Action(getResolver(), null, method,
0837: new String[] { ref.getID(), path });
0838: return step;
0839: }
0840:
0841: protected Step createPopupMenuSelection(Component invoker, int x,
0842: int y, Component menuItem) {
0843: Step step;
0844: if (invoker != null) {
0845: ComponentReference inv = getResolver()
0846: .addComponent(invoker);
0847: JMenuItem mi = (JMenuItem) menuItem;
0848: String where = getLocationArgument(invoker, x, y);
0849: step = new Action(getResolver(), null,
0850: "actionSelectPopupMenuItem", new String[] {
0851: inv.getID(), where,
0852: JMenuItemMatcher.getPath(mi) }, invoker
0853: .getClass());
0854: } else {
0855: ComponentReference ref = getResolver().addComponent(
0856: menuItem);
0857: step = new Action(getResolver(), null,
0858: "actionSelectMenuItem",
0859: new String[] { ref.getID() });
0860: }
0861: return step;
0862: }
0863:
0864: protected Step createKey(Component comp, char keychar, int mods) {
0865: ComponentReference cr = getResolver().addComponent(comp);
0866: // NOTE: Any keys which might have effects as key press/release should
0867: // be encoded as a keystroke, rather than a keystring.
0868: // NOTE: We encode strings rather than integer values, since the
0869: // names are more useful.
0870: String code = (String) specialMap.get(String.valueOf(keychar));
0871: if (code != null) {
0872: String[] args = mods != 0 ? new String[] { cr.getID(),
0873: code, AWT.getKeyModifiers(mods) } : new String[] {
0874: cr.getID(), code };
0875: return new Action(getResolver(), null, "actionKeyStroke",
0876: args);
0877: }
0878: return new Action(getResolver(), null, "actionKeyString",
0879: new String[] { cr.getID(), String.valueOf(keychar) });
0880: }
0881:
0882: protected Step createDrop(Component comp, int x, int y) {
0883: Step step = null;
0884: if (comp != null) {
0885: ComponentReference cr = getResolver().addComponent(comp);
0886: String where = getLocationArgument(comp, x, y);
0887: step = new Action(getResolver(), null, "actionDrop",
0888: new String[] { cr.getID(), where }, comp.getClass());
0889: }
0890: return step;
0891: }
0892:
0893: protected Step createDrag(Component comp, int x, int y) {
0894: ComponentReference ref = getResolver().addComponent(comp);
0895: String where = getLocationArgument(comp, x, y);
0896: Step step = new Action(getResolver(), null, "actionDrag",
0897: new String[] { ref.getID(), where, }, comp.getClass());
0898: return step;
0899: }
0900:
0901: /** Create a click event with the given event information. */
0902: protected Step createClick(Component target, int x, int y,
0903: int mods, int count) {
0904: Log.debug("creating click");
0905: ComponentReference cr = getResolver().addComponent(target);
0906: ArrayList args = new ArrayList();
0907: args.add(cr.getID());
0908: args.add(getLocationArgument(target, x, y));
0909: if ((mods != 0 && mods != MouseEvent.BUTTON1_MASK) || count > 1) {
0910: // NOTE: this currently saves POPUP or TERTIARY, rather than
0911: // an explicit button 2 or 3. I figure that makes more sense
0912: // than a hard coded button number.
0913: args.add(AWT.getMouseModifiers(mods));
0914: if (count > 1) {
0915: args.add(String.valueOf(count));
0916: }
0917: }
0918: return new Action(getResolver(), null, "actionClick",
0919: (String[]) args.toArray(new String[args.size()]),
0920: target.getClass());
0921: }
0922:
0923: protected Step createInputMethod(Component comp, ArrayList codes,
0924: String text) {
0925: Log.debug("Text length is " + text.length());
0926: ComponentReference ref = getResolver().addComponent(comp);
0927: return new Action(getResolver(), null, "actionKeyString",
0928: new String[] { ref.getID(), text });
0929: }
0930:
0931: protected void init(int recordingType) {
0932: super .init(recordingType);
0933: target = null;
0934: forwardedTarget = null;
0935: released = false;
0936: clickCount = 0;
0937: keychar = KeyEvent.CHAR_UNDEFINED;
0938: invoker = null;
0939: awtMenuTarget = null;
0940: isPopup = false;
0941: hasAWTPopup = false;
0942: menuListener = null;
0943: menuTarget = null;
0944: menuCanceled = false;
0945: dragSource = dropTarget = null;
0946: window = null;
0947: isClose = false;
0948: imKeyCodes.clear();
0949: imText.delete(0, imText.length());
0950: }
0951:
0952: /** Invoke when end of the semantic event has been seen. */
0953: protected void setFinished(boolean state) {
0954: MenuListener listener = null;
0955: synchronized (this ) {
0956: super .setFinished(state);
0957: listener = menuListener;
0958: menuListener = null;
0959: }
0960: if (listener != null)
0961: listener.dispose();
0962: }
0963:
0964: private boolean addMenuListener(Component invoker) {
0965: PopupMenu[] popups = AWT.getPopupMenus(invoker);
0966: if (popups.length > 0) {
0967: menuListener = new MenuListener(popups);
0968: return true;
0969: }
0970: return false;
0971: }
0972:
0973: private class MenuListener implements ItemListener {
0974: private ArrayList items = new ArrayList();
0975:
0976: public MenuListener(PopupMenu[] popups) {
0977: for (int i = 0; i < popups.length; i++) {
0978: addRecursive(popups[i]);
0979: }
0980: }
0981:
0982: private void addRecursive(Menu menu) {
0983: for (int i = 0; i < menu.getItemCount(); i++) {
0984: MenuItem item = menu.getItem(i);
0985: if (item instanceof Menu)
0986: addRecursive((Menu) item);
0987: else if (item instanceof CheckboxMenuItem) {
0988: ((CheckboxMenuItem) item).addItemListener(this );
0989: items.add(item);
0990: }
0991: }
0992: }
0993:
0994: public void itemStateChanged(ItemEvent e) {
0995: dispose();
0996: parse(e);
0997: }
0998:
0999: public void dispose() {
1000: while (items.size() > 0) {
1001: ((CheckboxMenuItem) items.get(0))
1002: .removeItemListener(this );
1003: items.remove(0);
1004: }
1005: }
1006: }
1007:
1008: /** Obtain the String representation of the Component-specific location. */
1009: protected String getLocationArgument(Component c, int x, int y) {
1010: return getLocation(c, x, y).toString();
1011: }
1012:
1013: /** Obtain a more precise location than the given coordinate, if
1014: * possible.
1015: */
1016: protected ComponentLocation getLocation(Component c, int x, int y) {
1017: ComponentTester tester = ComponentTester.getTester(c);
1018: return tester.getLocation(c, new Point(x, y));
1019: }
1020: }
|