001: package abbot.editor.recorder;
002:
003: import java.awt.*;
004: import java.awt.event.*;
005: import java.lang.reflect.*;
006: import java.text.MessageFormat;
007: import java.util.*;
008:
009: import javax.swing.*;
010:
011: import abbot.*;
012: import abbot.i18n.Strings;
013: import abbot.script.*;
014: import abbot.script.Action;
015: import abbot.script.Event;
016: import abbot.script.Resolver;
017: import abbot.tester.Robot;
018: import abbot.util.AWT;
019:
020: /**
021: * Provides recording of raw AWT events and high-level semantic events.
022: * This is the main controller for any SemanticRecorder objects.
023: */
024:
025: // TODO: Are there other instances (cf JInternalFrame) where we'd like this
026: // recorder to be the listener to other events?
027: // TODO: add internal frame listener, re-post events when heard
028: // TODO: discard events on LAF pieces of internal frames
029: // TODO: extract filters as plugins
030: // TODO: run all semantic recorder tests through this class, using canned
031: // event streams instead of robot-generated events; keep the robot-generated
032: // event streams, though, to test whether the stream has changed
033: public class EventRecorder extends Recorder implements SemanticEvents {
034:
035: private static final String ANY_KEY = null;
036: private static final int EITHER = 0;
037: private static final int PRESS = 1;
038: private static final int RELEASE = 2;
039: private boolean captureMotion;
040: private long lastStepTime;
041: ArrayList steps = new ArrayList();
042:
043: /** Put all built-in recorder classes here. Don't worry though, 'cause if
044: * it doesn't get added here it'll get found dynamically.
045: */
046: private static final Class[] recorderClasses = {
047: AbstractButton.class, Component.class, Container.class,
048: Dialog.class, Frame.class, JComboBox.class,
049: JComponent.class, JInternalFrame.class, JList.class,
050: JMenuItem.class, JTabbedPane.class, JTable.class,
051: JTree.class, Window.class, };
052:
053: /** Create a Recorder for use in capturing raw AWTEvents. Indicate
054: * whether mouse motion should be captured, and what semantic event type
055: * to capture.
056: */
057: public EventRecorder(Resolver resolver, boolean captureMotion) {
058: super (resolver);
059: this .captureMotion = captureMotion;
060: // Install existing semantic recorders
061: for (int i = 0; i < recorderClasses.length; i++) {
062: getSemanticRecorder(recorderClasses[i]);
063: }
064: }
065:
066: /** Return the name of the type of GUI action to be recorded. */
067: public String toString() {
068: return captureMotion ? Strings.get("actions.capture-all")
069: : Strings.get("actions.capture");
070: }
071:
072: public void start() {
073: super .start();
074: steps.clear();
075: MessageFormat mf = new MessageFormat(Strings.get("RecordingX"));
076: setStatus(mf.format(new Object[] { toString() }));
077: lastStepTime = getLastEventTime();
078: }
079:
080: private boolean isKey(Step step, String code, int type) {
081: boolean match = false;
082: if (step instanceof Event) {
083: Event se = (Event) step;
084: match = "KeyEvent".equals(se.getType())
085: && (type == EITHER
086: || (type == PRESS && "KEY_PRESSED"
087: .equals(se.getKind())) || (type == RELEASE && "KEY_RELEASED"
088: .equals(se.getKind())))
089: && (code == ANY_KEY || code.equals(se
090: .getAttribute(XMLConstants.TAG_KEYCODE)));
091: }
092: return match;
093: }
094:
095: private boolean isKeyString(Step step) {
096: return (step instanceof Action)
097: && ((Action) step).getMethodName().equals(
098: "actionKeyString");
099: }
100:
101: private boolean isKeyStroke(Step step, String keycode) {
102: if (step instanceof Action) {
103: Action action = (Action) step;
104: if (action.getMethodName().equals("actionKeyStroke")) {
105: String[] args = action.getArguments();
106: return (keycode == ANY_KEY
107: || (args.length > 1 && args[1].equals(keycode)) || (keycode
108: .startsWith("VK_NUMPAD") && args[1]
109: .equals("VK_" + keycode.substring(9))));
110: }
111: }
112: return false;
113: }
114:
115: private void removeTerminalShift() {
116: // Remove the terminal SHIFT keypress
117: if (steps.size() > 0) {
118: Step step = (Step) steps.get(steps.size() - 1);
119: while (isKey(step, "VK_SHIFT", PRESS)) {
120: steps.remove(step);
121: if (steps.size() == 0)
122: break;
123: step = (Step) steps.get(steps.size() - 1);
124: }
125: }
126: }
127:
128: /** Eliminate redundant modifier keys surrounding keystrokes or
129: * keystrings.
130: */
131: private void removeExtraModifiers() {
132: setStatus("Removing extra modifiers");
133: for (int i = 0; i < steps.size(); i++) {
134: Step step = (Step) steps.get(i);
135: if (isKey(step, ANY_KEY, PRESS)) {
136: Event se = (Event) step;
137: String cs = se.getAttribute(XMLConstants.TAG_KEYCODE);
138: int code = AWT.getKeyCode(cs);
139: boolean remove = false;
140: boolean foundKeyStroke = false;
141: if (AWT.isModifier(code)) {
142: for (int j = i + 1; j < steps.size(); j++) {
143: Step next = (Step) steps.get(j);
144: if (isKey(next, cs, RELEASE)) {
145: if (foundKeyStroke) {
146: steps.remove(j);
147: remove = true;
148: }
149: break;
150: } else if (isKeyStroke(next, ANY_KEY)
151: || isKeyString(next)) {
152: foundKeyStroke = true;
153: remove = true;
154: } else if (!isKey(next, ANY_KEY, EITHER)) {
155: break;
156: }
157: }
158: }
159: if (remove) {
160: steps.remove(i--);
161: }
162: }
163: }
164: }
165:
166: /** Combine multiple keystroke actions into keystring actions. */
167: private void coalesceKeyStrings() {
168: setStatus("Coalescing key strings");
169: for (int i = 0; i < steps.size(); i++) {
170: Step step = (Step) steps.get(i);
171: if (isKeyString(step)) {
172: int j = i;
173: while (++j < steps.size()) {
174: Step next = (Step) steps.get(j);
175: if (isKeyString(next)) {
176: Action action = (Action) step;
177: String[] args1 = action.getArguments();
178: String[] args2 = ((Action) next).getArguments();
179: action.setArguments(new String[] { args1[0],
180: args1[1] + args2[1] });
181: setStatus("Joining '" + args1[1] + "' and '"
182: + args2[1] + "'");
183: steps.remove(j--);
184: } else {
185: setStatus("Next step is not a key string: "
186: + next);
187: break;
188: }
189: }
190: }
191: }
192: }
193:
194: /** Eliminate redundant key press/release events surrounding a keytyped
195: * event.
196: */
197: private void coalesceKeyEvents() {
198: setStatus("Coalescing key events");
199: for (int i = 0; i < steps.size(); i++) {
200: Step step = (Step) steps.get(i);
201: if (isKey(step, ANY_KEY, PRESS)) {
202: // In the case of modifiers, remove only if the presence of
203: // the key down/up is redundant.
204: Event se = (Event) step;
205: String cs = se.getAttribute(XMLConstants.TAG_KEYCODE);
206: int code = AWT.getKeyCode(cs);
207: // OSX option modifier should be ignored, since it is used to
208: // generate input method events.
209: boolean isOSXOption = Platform.isOSX()
210: && code == KeyEvent.VK_ALT;
211: if (AWT.isModifier(code) && !isOSXOption)
212: continue;
213:
214: // In the case of non-modifier keys, walk the steps until we
215: // find the key release, then optionally replace the key press
216: // with a keystroke, or remove it if the keystroke was already
217: // recorded. This sorts out jumbled key press/release events.
218: boolean foundKeyStroke = false;
219: boolean foundRelease = false;
220: for (int j = i + 1; j < steps.size(); j++) {
221: Step next = (Step) steps.get(j);
222: // If we find the release, remove it and this
223: if (isKey(next, cs, RELEASE)) {
224: foundRelease = true;
225: String target = ((Event) next).getComponentID();
226: steps.remove(j);
227: steps.remove(i);
228: // Add a keystroke only if we didn't find any key
229: // input between press and release (except on OSX,
230: // where the option key generates input method events
231: // which aren't recorded).
232: if (!foundKeyStroke && !isOSXOption) {
233: String mods = se
234: .getAttribute(XMLConstants.TAG_MODIFIERS);
235: String[] args = (mods == null
236: || "0".equals(mods) ? new String[] {
237: target, cs } : new String[] {
238: target, cs, mods });
239: Step typed = new Action(getResolver(),
240: null, "actionKeyStroke", args);
241: steps.add(i, typed);
242: setStatus("Insert artifical " + typed);
243: } else {
244: setStatus("Removed redundant key events ("
245: + cs + ")");
246: --i;
247: }
248: break;
249: } else if (isKeyStroke(next, ANY_KEY)
250: || isKeyString(next)) {
251: foundKeyStroke = true;
252: // If it's a numpad keycode, use the numpad
253: // keycode instead of the resulting numeric character
254: // keystroke.
255: if (cs.startsWith("VK_NUMPAD")) {
256: foundKeyStroke = false;
257: steps.remove(j--);
258: }
259: }
260: }
261: // We don't like standalone key presses
262: if (!foundRelease) {
263: setStatus("Removed extraneous key press (" + cs
264: + ")");
265: steps.remove(i--);
266: }
267: }
268: }
269: }
270:
271: // Required for OS X, remove modifier keys when they're only used to
272: // invoke MB2/3
273: private boolean pruneButtonModifier = false;
274: private int lastButton = 0;
275:
276: /** Used only on Mac OS, to remove key modifiers that are used to simulate
277: * mouse buttons 2 and 3. Returns whether the event should be ignored.
278: */
279: private boolean pruneClickModifiers(AWTEvent event) {
280: lastButton = 0;
281: boolean ignoreEvent = false;
282: if (event.getID() == MouseEvent.MOUSE_PRESSED) {
283: MouseEvent me = (MouseEvent) event;
284: int buttons = me.getModifiers()
285: & (MouseEvent.BUTTON2_MASK | MouseEvent.BUTTON3_MASK);
286: pruneButtonModifier = buttons != 0;
287: lastButton = buttons;
288: } else if (event.getID() == KeyEvent.KEY_RELEASED
289: && pruneButtonModifier) {
290: pruneButtonModifier = false;
291: KeyEvent ke = (KeyEvent) event;
292: int code = ke.getKeyCode();
293: if ((code == KeyEvent.VK_CONTROL || code == KeyEvent.VK_ALT
294: && (lastButton & MouseEvent.BUTTON2_MASK) != 0)
295: || (code == KeyEvent.VK_META && (lastButton & MouseEvent.BUTTON3_MASK) != 0)) {
296: if (steps.size() > 1) {
297: Step step = (Step) steps.get(steps.size() - 2);
298: if ((code == KeyEvent.VK_CONTROL
299: && isKey(step, "VK_CONTROL", PRESS) || (code == KeyEvent.VK_ALT && isKey(
300: step, "VK_ALT", PRESS)))
301: || (code == KeyEvent.VK_META && isKey(step,
302: "VK_META", PRESS))) {
303: // might be another one
304: steps.remove(steps.size() - 2);
305: pruneButtonModifier = true;
306: ignoreEvent = true;
307: }
308: }
309: }
310: }
311: return ignoreEvent;
312: }
313:
314: /** Ignore any key presses at the end of the recording. */
315: private void removeTrailingKeyPresses() {
316: while (steps.size() > 0
317: && isKey((Step) steps.get(steps.size() - 1), ANY_KEY,
318: PRESS)) {
319: steps.remove(steps.size() - 1);
320: }
321: }
322:
323: /** Remove keypress events preceding and following ActionMap actions. */
324: private void removeShortcutModifierKeyPresses() {
325: int current = 0;
326: int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
327: String modifier = AWT.getKeyCode(AWT.maskToKeyCode(mask));
328: while (current < steps.size()) {
329: Step step = (Step) steps.get(current);
330: if (isKey(step, modifier, PRESS)) {
331: Log.debug("Found possible extraneous modifier");
332: int keyDown = current;
333: Action action = null;
334: while (++current < steps.size()) {
335: step = (Step) steps.get(current);
336: if (step instanceof Action) {
337: if ("actionActionMap".equals(((Action) step)
338: .getMethodName())) {
339: action = (Action) step;
340: continue;
341: }
342: } else if (isKey(step, modifier, RELEASE)) {
343: if (action != null) {
344: Log
345: .debug("Removing extraneous shortcut modifier");
346: steps.remove(current);
347: steps.remove(keyDown);
348: current = keyDown - 1;
349: }
350: }
351: break;
352: }
353: }
354: ++current;
355: }
356: }
357:
358: /** Insert an arbitrary script step into the currently recorded stream. */
359: public void insertStep(Step step) {
360: steps.add(step);
361: if ((step instanceof Assert)
362: && ((Assert) step).getMethodName().equals(
363: "assertFrameShowing")) {
364: long timeout = ((Assert) step).getTimeout();
365: long delta = System.currentTimeMillis() - lastStepTime;
366: if (delta > timeout)
367: timeout += delta;
368: ((Assert) step).setTimeout(timeout);
369: }
370: lastStepTime = getLastEventTime();
371: }
372:
373: /**
374: * Return a sequence containing all the semantic and basic events captured
375: * thus far.
376: */
377: protected Step createStep() {
378: removeTerminalShift();
379: coalesceKeyEvents();
380: removeExtraModifiers();
381: coalesceKeyStrings();
382: removeShortcutModifierKeyPresses();
383: removeTrailingKeyPresses();
384:
385: return new Sequence(getResolver(), null, steps);
386: }
387:
388: /** The current semantic recorder, if any. */
389: private SemanticRecorder semanticRecorder = null;
390:
391: /** Return whether an event was generated. Assumes a SemanticRecorder is
392: active.
393: @throws RecordingFailedException if an error was encountered.
394: */
395: private boolean saveSemanticEvent() throws RecordingFailedException {
396: Log.log("Storing event from current semantic recorder");
397: try {
398: Step step = semanticRecorder.getStep();
399: if (step != null) {
400: insertStep(step);
401: setStatus("Added " + step);
402: } else {
403: setStatus("No semantic event found, events skipped");
404: }
405: semanticRecorder = null;
406: return step != null;
407: } catch (BugReport bug) {
408: throw new RecordingFailedException(bug);
409: } catch (Exception e) {
410: Log.log("Recording failed when saving action: " + e);
411: String msg = Strings.get("editor.recording.exception");
412: throw new RecordingFailedException(new BugReport(msg, e));
413: }
414: }
415:
416: public void terminate() throws RecordingFailedException {
417: Log.log("EventRecorder terminated");
418: if (semanticRecorder != null) {
419: saveSemanticEvent();
420: }
421: }
422:
423: /** Handle an event. This can either be ignored or contribute to the
424: * recording.
425: * For a given event, if no current semantic recorder is active,
426: * select one based on the event's component. If the semantic recorder
427: * accepts the event, then it is used to consume each subsequent event,
428: * until its recordEvent method returns true, indicating that the semantic
429: * event has completed.
430: */
431: protected void recordEvent(AWTEvent event)
432: throws RecordingFailedException {
433:
434: // Discard any key/button release events at the start of the recording.
435: if (steps.size() == 0 && event.getID() == KeyEvent.KEY_RELEASED) {
436: Log.log("Ignoring initial release event");
437: return;
438: }
439:
440: SemanticRecorder newRecorder = null;
441:
442: // Process extraneous key modifiers used to simulate mouse buttons
443: // Only check events while we have no semantic recorder, though,
444: // because we wish to ignore everything between the modifiers
445: if (Platform.isMacintosh() && semanticRecorder == null) {
446: if (pruneClickModifiers(event))
447: return;
448: }
449:
450: if (semanticRecorder == null) {
451: SemanticRecorder sr = (event.getSource() instanceof Component) ? getSemanticRecorder((Component) event
452: .getSource())
453: // Use ComponentRecorder for MenuComponents
454: : getSemanticRecorder(Component.class);
455: if (sr.accept(event)) {
456: semanticRecorder = newRecorder = sr;
457: setStatus("Recording semantic event with " + sr);
458: if (event.getSource() instanceof JInternalFrame) {
459: // Ideally, adding an extra listener would be done by the
460: // JInternalFrameRecorder, but the object needs more state
461: // than is available to the recorder (notably to be able
462: // to send events to the primary recorder). If something
463: // else turns up similar to this, then the EventRecorder
464: // should be made available to the semantic recorders.
465: //
466: // Must add a listener, since COMPONENT_HIDDEN is not sent
467: // on JInternalFrame close (1.4.1).
468: JInternalFrame f = (JInternalFrame) event
469: .getSource();
470: new InternalFrameWatcher(f);
471: }
472: }
473: }
474:
475: // If we're currently recording a semantic event, continue to do so
476: if (semanticRecorder != null) {
477: boolean consumed = semanticRecorder.record(event);
478: boolean finished = semanticRecorder.isFinished();
479: if (finished) {
480: Log.debug("Semantic recorder is finished");
481: saveSemanticEvent();
482: }
483: // If not consumed, need to check for semantic recorder (again)
484: // (but avoid recursing indefinitely)
485: if (!consumed && newRecorder == null) {
486: Log.debug("Event was not consumed, parse it again");
487: recordEvent(event);
488: }
489: } else {
490: captureRawEvent(event);
491: }
492: }
493:
494: /** Capture the given event as a raw event. */
495: private void captureRawEvent(AWTEvent event) {
496:
497: // FIXME maybe measure time delay between events and insert delay
498: // events?
499: int id = event.getID();
500: boolean capture = false;
501: switch (id) {
502: case MouseEvent.MOUSE_PRESSED:
503: case MouseEvent.MOUSE_RELEASED:
504: capture = true;
505: break;
506: case KeyEvent.KEY_PRESSED:
507: case KeyEvent.KEY_RELEASED:
508: KeyEvent e = (KeyEvent) event;
509: capture = e.getKeyCode() != KeyEvent.VK_UNDEFINED;
510: if (!capture) {
511: Log.warn("VM bug: no valid keycode on key "
512: + (id == KeyEvent.KEY_PRESSED ? "press"
513: : "release"));
514: }
515: break;
516: case MouseEvent.MOUSE_ENTERED:
517: case MouseEvent.MOUSE_EXITED:
518: case MouseEvent.MOUSE_MOVED:
519: case MouseEvent.MOUSE_DRAGGED:
520: capture = captureMotion;
521: break;
522: default:
523: break;
524: }
525: if (capture) {
526: Event step = new Event(getResolver(), null, event);
527: insertStep(step);
528: setStatus("Added event " + step);
529: }
530: }
531:
532: /** Events of interest when recording all actions. */
533: public static final long RECORDING_EVENT_MASK = AWTEvent.MOUSE_EVENT_MASK
534: | AWTEvent.MOUSE_MOTION_EVENT_MASK
535: | AWTEvent.KEY_EVENT_MASK | AWTEvent.WINDOW_EVENT_MASK
536: /*| AWTEvent.PAINT_EVENT_MASK*/
537: /*| AWTEvent.HIERARCHY_EVENT_MASK
538: | AWTEvent.HIERARCHY_BOUNDS_EVENT_MASK*/
539: | AWTEvent.COMPONENT_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK
540: // required for non-standard input
541: | AWTEvent.INPUT_METHOD_EVENT_MASK
542: // For java.awt.Choice selections
543: | AWTEvent.ITEM_EVENT_MASK
544: // required to capture MenuItem actions
545: | AWTEvent.ACTION_EVENT_MASK;
546:
547: /** Return the events of interest to this Recorder. */
548: public long getEventMask() {
549: return RECORDING_EVENT_MASK;
550: }
551:
552: /** Maps component classes to corresponding semantic recorders. */
553: private HashMap semanticRecorders = new HashMap();
554:
555: /** Return the semantic recorder for the given component. */
556: private SemanticRecorder getSemanticRecorder(Component comp) {
557: // FIXME extract into AWT.getLAFParent?
558: // Account for LAF implementations that use a JButton on top
559: // of the combo box
560: if ((comp instanceof JButton)
561: && (comp.getParent() instanceof JComboBox)) {
562: comp = comp.getParent();
563: }
564: // Account for LAF components of JInternalFrame
565: else if (AWT.isInternalFrameDecoration(comp)) {
566: while (!(comp instanceof JInternalFrame))
567: comp = comp.getParent();
568: }
569: return getSemanticRecorder(comp.getClass());
570: }
571:
572: /** Return the semantic recorder for the given component class. */
573: private SemanticRecorder getSemanticRecorder(Class cls) {
574: if (!(Component.class.isAssignableFrom(cls))) {
575: throw new IllegalArgumentException(
576: "Class must derive from " + "Component");
577: }
578: SemanticRecorder sr = (SemanticRecorder) semanticRecorders
579: .get(cls);
580: if (sr == null) {
581: Class ccls = Robot.getCanonicalClass(cls);
582: if (ccls != cls) {
583: sr = getSemanticRecorder(ccls);
584: // Additionally cache the mapping from the non-canonical class
585: semanticRecorders.put(cls, sr);
586: return sr;
587: }
588: String cname = Robot.simpleClassName(cls);
589: try {
590: cname = "abbot.editor.recorder." + cname + "Recorder";
591: Class recorderClass = Class.forName(cname);
592: Constructor ctor = recorderClass
593: .getConstructor(new Class[] { Resolver.class, });
594: sr = (SemanticRecorder) ctor
595: .newInstance(new Object[] { getResolver() });
596: sr.addActionListener(getListener());
597: } catch (InvocationTargetException e) {
598: Log.warn(e);
599: } catch (NoSuchMethodException e) {
600: sr = getSemanticRecorder(cls.getSuperclass());
601: } catch (InstantiationException e) {
602: sr = getSemanticRecorder(cls.getSuperclass());
603: } catch (IllegalAccessException iae) {
604: sr = getSemanticRecorder(cls.getSuperclass());
605: } catch (ClassNotFoundException cnf) {
606: sr = getSemanticRecorder(cls.getSuperclass());
607: }
608: // Cache the results for future reference
609: semanticRecorders.put(cls, sr);
610: }
611: return sr;
612: }
613:
614: /** Special adapter to catch events on JInternalFrame instances. */
615: private class InternalFrameWatcher extends
616: AbstractInternalFrameWatcher {
617: public InternalFrameWatcher(JInternalFrame f) {
618: super (f);
619: }
620:
621: protected void dispatch(AWTEvent e) {
622: record(e);
623: }
624: }
625: }
|