package abbot.editor.recorder; import java.awt.AWTEvent; import java.awt.Button; import java.awt.Component; import java.awt.Container; import java.awt.Dialog; import java.awt.Frame; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import javax.swing.AbstractButton; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JInternalFrame; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.JTree; import javax.swing.text.JTextComponent; import abbot.BugReport; import abbot.Log; import abbot.Platform; import abbot.i18n.Strings; import abbot.script.Action; import abbot.script.Assert; import abbot.script.Event; import abbot.script.Resolver; import abbot.script.Sequence; import abbot.script.Step; import abbot.script.XMLConstants; import abbot.tester.Robot; import abbot.util.AWT; /** * Provides recording of raw AWT events and high-level semantic events. * This is the main controller for any SemanticRecorder objects. */ // TODO: Are there other instances (cf JInternalFrame) where we'd like this // recorder to be the listener to other events? // TODO: add internal frame listener, re-post events when heard // TODO: discard events on LAF pieces of internal frames // TODO: extract filters as plugins // TODO: run all semantic recorder tests through this class, using canned // event streams instead of robot-generated events; keep the robot-generated // event streams, though, to test whether the stream has changed public class EventRecorder extends Recorder implements SemanticEvents { private static final String ANY_KEY = null; private static final int EITHER = 0; private static final int PRESS = 1; private static final int RELEASE = 2; private boolean captureMotion; private long lastStepTime; ArrayList steps = new ArrayList(); protected AWTEvent capturedEvent; /** Put all built-in recorder classes here. Don't worry though, 'cause if * it doesn't get added here it'll get found dynamically. */ protected static final Class[] recorderClasses = { AbstractButton.class, Button.class, Component.class, Container.class, Dialog.class, Frame.class, JComboBox.class, JComponent.class, JInternalFrame.class, JList.class, JMenuItem.class, JTabbedPane.class, JTable.class, JTextComponent.class, JTree.class, Window.class, }; /** Create a Recorder for use in capturing raw AWTEvents. Indicate * whether mouse motion should be captured, and what semantic event type * to capture. */ public EventRecorder(Resolver resolver, boolean captureMotion) { super(resolver); this.captureMotion = captureMotion; // Install existing semantic recorders for (int i=0;i < recorderClasses.length;i++) { getSemanticRecorder(recorderClasses[i]); } } /** Return the name of the type of GUI action to be recorded. */ public String toString() { return captureMotion ? Strings.get("actions.capture-all") : Strings.get("actions.capture"); } public void start() { super.start(); steps.clear(); MessageFormat mf = new MessageFormat(Strings.get("RecordingX")); setStatus(mf.format(new Object[] { toString() })); lastStepTime = getLastEventTime(); } private boolean isKey(Step step, String code, int type) { boolean match = false; if (step instanceof Event) { Event se = (Event)step; match = "KeyEvent".equals(se.getType()) && (type == EITHER || (type == PRESS && "KEY_PRESSED".equals(se.getKind())) || (type == RELEASE && "KEY_RELEASED".equals(se.getKind()))) && (code == ANY_KEY || code.equals(se.getAttribute(XMLConstants.TAG_KEYCODE))); } return match; } private boolean isKeyString(Step step) { return (step instanceof Action) && ((Action)step).getMethodName().equals("actionKeyString"); } private boolean isKeyStroke(Step step, String keycode) { if (step instanceof Action) { Action action = (Action)step; if (action.getMethodName().equals("actionKeyStroke")) { String[] args = action.getArguments(); return (keycode == ANY_KEY || (args.length > 1 && args[1].equals(keycode)) || (keycode.startsWith("VK_NUMPAD") && args[1].equals("VK_" + keycode.substring(9)))); } } return false; } private void removeTerminalShift() { // Remove the terminal SHIFT keypress if (steps.size() > 0) { Step step = (Step)steps.get(steps.size()-1); while (isKey(step, "VK_SHIFT", PRESS)) { steps.remove(step); if (steps.size() == 0) break; step = (Step)steps.get(steps.size()-1); } } } /** Eliminate redundant modifier keys surrounding keystrokes or * keystrings. */ private void removeExtraModifiers() { setStatus("Removing extra modifiers"); for (int i=0;i < steps.size();i++) { Step step = (Step)steps.get(i); if (isKey(step, ANY_KEY, PRESS)) { Event se = (Event)step; String cs = se.getAttribute(XMLConstants.TAG_KEYCODE); int code = AWT.getKeyCode(cs); boolean remove = false; boolean foundKeyStroke = false; if (AWT.isModifier(code)) { for (int j=i+1;j < steps.size();j++) { Step next = (Step)steps.get(j); if (isKey(next, cs, RELEASE)) { if (foundKeyStroke) { steps.remove(j); remove = true; } break; } else if (isKeyStroke(next, ANY_KEY) || isKeyString(next)) { foundKeyStroke = true; remove = true; } else if (!isKey(next, ANY_KEY, EITHER)) { break; } } } if (remove) { steps.remove(i--); } } } } /** Combine multiple keystroke actions into keystring actions. */ private void coalesceKeyStrings() { setStatus("Coalescing key strings"); for (int i=0;i < steps.size();i++) { Step step = (Step)steps.get(i); if (isKeyString(step)) { int j = i; while (++j < steps.size()) { Step next = (Step)steps.get(j); if (isKeyString(next)) { Action action = (Action)step; String[] args1 = action.getArguments(); String[] args2 = ((Action)next).getArguments(); action.setArguments(new String[] { args1[0], args1[1] + args2[1] }); setStatus("Joining '" + args1[1] + "' and '" + args2[1] + "'"); steps.remove(j--); } else { setStatus("Next step is not a key string: " + next); break; } } } } } /** Eliminate redundant key press/release events surrounding a keytyped * event. */ private void coalesceKeyEvents() { setStatus("Coalescing key events"); for (int i=0;i < steps.size();i++) { Step step = (Step)steps.get(i); if (isKey(step, ANY_KEY, PRESS)) { // In the case of modifiers, remove only if the presence of // the key down/up is redundant. Event se = (Event)step; String cs = se.getAttribute(XMLConstants.TAG_KEYCODE); int code = AWT.getKeyCode(cs); // OSX option modifier should be ignored, since it is used to // generate input method events. boolean isOSXOption = Platform.isOSX() && code == KeyEvent.VK_ALT; if (AWT.isModifier(code) && !isOSXOption) continue; // In the case of non-modifier keys, walk the steps until we // find the key release, then optionally replace the key press // with a keystroke, or remove it if the keystroke was already // recorded. This sorts out jumbled key press/release events. boolean foundKeyStroke = false; boolean foundRelease = false; for (int j=i+1;j < steps.size();j++) { Step next = (Step)steps.get(j); // If we find the release, remove it and this if (isKey(next, cs, RELEASE)) { foundRelease = true; String target = ((Event)next).getComponentID(); steps.remove(j); steps.remove(i); // Add a keystroke only if we didn't find any key // input between press and release (except on OSX, // where the option key generates input method events // which aren't recorded). if (!foundKeyStroke && !isOSXOption) { String mods = se.getAttribute(XMLConstants.TAG_MODIFIERS); String[] args = (mods == null || "0".equals(mods) ? new String[] { target, cs } : new String[] { target, cs, mods}); Step typed = new Action(getResolver(), null, "actionKeyStroke", args); steps.add(i, typed); setStatus("Insert artifical " + typed); } else { setStatus("Removed redundant key events (" + cs + ")"); --i; } break; } else if (isKeyStroke(next, ANY_KEY) || isKeyString(next)) { foundKeyStroke = true; // If it's a numpad keycode, use the numpad // keycode instead of the resulting numeric character // keystroke. if (cs.startsWith("VK_NUMPAD")) { foundKeyStroke = false; steps.remove(j--); } } } // We don't like standalone key presses if (!foundRelease) { setStatus("Removed extraneous key press (" + cs + ")"); steps.remove(i--); } } } } // Required for OS X, remove modifier keys when they're only used to // invoke MB2/3 private boolean pruneButtonModifier = false; private int lastButton = 0; /** Used only on Mac OS, to remove key modifiers that are used to simulate * mouse buttons 2 and 3. Returns whether the event should be ignored. */ private boolean pruneClickModifiers(AWTEvent event) { lastButton = 0; boolean ignoreEvent = false; if (event.getID() == MouseEvent.MOUSE_PRESSED) { MouseEvent me = (MouseEvent)event; int buttons = me.getModifiers() & (MouseEvent.BUTTON2_MASK|MouseEvent.BUTTON3_MASK); pruneButtonModifier = buttons != 0; lastButton = buttons; } else if (event.getID() == KeyEvent.KEY_RELEASED && pruneButtonModifier) { pruneButtonModifier = false; KeyEvent ke = (KeyEvent)event; int code = ke.getKeyCode(); if ((code == KeyEvent.VK_CONTROL || code == KeyEvent.VK_ALT && (lastButton & MouseEvent.BUTTON2_MASK) != 0) || (code == KeyEvent.VK_META && (lastButton & MouseEvent.BUTTON3_MASK) != 0)) { if (steps.size() > 1) { Step step = (Step)steps.get(steps.size()-2); if ((code == KeyEvent.VK_CONTROL && isKey(step, "VK_CONTROL", PRESS) || (code == KeyEvent.VK_ALT && isKey(step, "VK_ALT", PRESS))) || (code == KeyEvent.VK_META && isKey(step, "VK_META", PRESS))) { // might be another one steps.remove(steps.size()-2); pruneButtonModifier = true; ignoreEvent = true; } } } } return ignoreEvent; } /** Ignore any key presses at the end of the recording. */ private void removeTrailingKeyPresses() { while (steps.size() > 0 && isKey((Step)steps.get(steps.size()-1), ANY_KEY, PRESS)) { steps.remove(steps.size()-1); } } /** Remove keypress events preceding and following ActionMap actions. */ private void removeShortcutModifierKeyPresses() { int current = 0; int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); String modifier = AWT.getKeyCode(AWT.maskToKeyCode(mask)); while (current < steps.size()) { Step step = (Step)steps.get(current); if (isKey(step, modifier, PRESS)) { Log.debug("Found possible extraneous modifier"); int keyDown = current; Action action = null; while (++current < steps.size()) { step = (Step)steps.get(current); if (step instanceof Action) { if ("actionActionMap". equals(((Action)step).getMethodName())) { action = (Action)step; continue; } } else if (isKey(step, modifier, RELEASE)) { if (action != null) { Log.debug("Removing extraneous shortcut modifier"); steps.remove(current); steps.remove(keyDown); current = keyDown - 1; } } break; } } ++current; } } /** Insert an arbitrary script step into the currently recorded stream. */ public void insertStep(Step step) { steps.add(step); if ((step instanceof Assert) && ((Assert)step).getMethodName().equals("assertFrameShowing")) { long timeout = ((Assert)step).getTimeout(); long delta = System.currentTimeMillis() - lastStepTime; if (delta > timeout) timeout += delta; ((Assert)step).setTimeout(timeout); } lastStepTime = getLastEventTime(); } /** * Return a sequence containing all the semantic and basic events captured * thus far. */ protected Step createStep() { removeTerminalShift(); coalesceKeyEvents(); removeExtraModifiers(); coalesceKeyStrings(); removeShortcutModifierKeyPresses(); removeTrailingKeyPresses(); return new Sequence(getResolver(), null, steps); } /** The current semantic recorder, if any. */ private SemanticRecorder semanticRecorder = null; /** Return whether an event was generated. Assumes a SemanticRecorder is active. @throws RecordingFailedException if an error was encountered. */ private boolean saveSemanticEvent() throws RecordingFailedException { Log.log("Storing event from current semantic recorder"); try { Step step = semanticRecorder.getStep(); if (step != null) { insertStep(step); setStatus("Added " + step); } else { setStatus("No semantic event found, events skipped"); } semanticRecorder = null; return step != null; } catch(BugReport bug) { // changed to windowtester exception // throw new RecordingFailedException(bug); throw new com.windowtester.swing.recorder.RecordingFailedException(bug); } catch(Exception e) { Log.log("Recording failed when saving action: " + e); // 1/3/07 kp: change message to windowtester message //String msg = Strings.get("editor.recording.exception"); String msg = "Windowtester recording exception"; // throw new RecordingFailedException(new BugReport(msg, e)); throw new com.windowtester.swing.recorder.RecordingFailedException(new BugReport(msg, e)); } } public void terminate() throws RecordingFailedException { Log.log("EventRecorder terminated"); if (semanticRecorder != null) { saveSemanticEvent(); } } /** Handle an event. This can either be ignored or contribute to the * recording. * For a given event, if no current semantic recorder is active, * select one based on the event's component. If the semantic recorder * accepts the event, then it is used to consume each subsequent event, * until its recordEvent method returns true, indicating that the semantic * event has completed. */ protected void recordEvent(java.awt.AWTEvent event) throws RecordingFailedException { // Discard any key/button release events at the start of the recording. if (steps.size() == 0 && event.getID() == KeyEvent.KEY_RELEASED) { Log.log("Ignoring initial release event"); return; } SemanticRecorder newRecorder = null; // Process extraneous key modifiers used to simulate mouse buttons // Only check events while we have no semantic recorder, though, // because we wish to ignore everything between the modifiers if (Platform.isMacintosh() && semanticRecorder == null) { if (pruneClickModifiers(event)) return; } if (semanticRecorder == null) { SemanticRecorder sr = (event.getSource() instanceof Component) ? getSemanticRecorder((Component)event.getSource()) // Use ComponentRecorder for MenuComponents : getSemanticRecorder(Component.class); if (sr.accept(event)) { semanticRecorder = newRecorder = sr; setStatus("Recording semantic event with " + sr); if (event.getSource() instanceof JInternalFrame) { // Ideally, adding an extra listener would be done by the // JInternalFrameRecorder, but the object needs more state // than is available to the recorder (notably to be able // to send events to the primary recorder). If something // else turns up similar to this, then the EventRecorder // should be made available to the semantic recorders. // // Must add a listener, since COMPONENT_HIDDEN is not sent // on JInternalFrame close (1.4.1). JInternalFrame f = (JInternalFrame)event.getSource(); new InternalFrameWatcher(f); } } } // If we're currently recording a semantic event, continue to do so if (semanticRecorder != null) { boolean consumed = semanticRecorder.record(event); boolean finished = semanticRecorder.isFinished(); if (finished) { Log.debug("Semantic recorder is finished"); saveSemanticEvent(); } // If not consumed, need to check for semantic recorder (again) // (but avoid recursing indefinitely) if (!consumed && newRecorder == null) { Log.debug("Event was not consumed, parse it again"); recordEvent(event); } } else { captureRawEvent(event); } } /** Capture the given event as a raw event. */ private void captureRawEvent(AWTEvent event) { // FIXME maybe measure time delay between events and insert delay // events? int id = event.getID(); boolean capture = false; switch(id) { case MouseEvent.MOUSE_PRESSED: case MouseEvent.MOUSE_RELEASED: capture = true; break; case KeyEvent.KEY_PRESSED: case KeyEvent.KEY_RELEASED: KeyEvent e = (KeyEvent)event; capture = e.getKeyCode() != KeyEvent.VK_UNDEFINED; if (!capture) { Log.warn("VM bug: no valid keycode on key " + (id == KeyEvent.KEY_PRESSED ? "press" : "release")); } break; case MouseEvent.MOUSE_ENTERED: case MouseEvent.MOUSE_EXITED: // case MouseEvent.MOUSE_MOVED: case MouseEvent.MOUSE_DRAGGED: capture = captureMotion; break; default: break; } if (capture) { Event step = new Event(getResolver(), null, event); capturedEvent = event; insertStep(step); setStatus("Added event " + step); } } /** Events of interest when recording all actions. */ public static final long RECORDING_EVENT_MASK = AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK | AWTEvent.WINDOW_EVENT_MASK /*| AWTEvent.PAINT_EVENT_MASK*/ /*| AWTEvent.HIERARCHY_EVENT_MASK | AWTEvent.HIERARCHY_BOUNDS_EVENT_MASK*/ | AWTEvent.COMPONENT_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK // required for non-standard input | AWTEvent.INPUT_METHOD_EVENT_MASK // For java.awt.Choice selections | AWTEvent.ITEM_EVENT_MASK // required to capture MenuItem actions | AWTEvent.ACTION_EVENT_MASK; /** Return the events of interest to this Recorder. */ public long getEventMask() { return RECORDING_EVENT_MASK; } /** Maps component classes to corresponding semantic recorders. */ private HashMap semanticRecorders = new HashMap(); /** Return the semantic recorder for the given component. */ private SemanticRecorder getSemanticRecorder(Component comp) { // FIXME extract into AWT.getLAFParent? // Account for LAF implementations that use a JButton on top // of the combo box if ((comp instanceof JButton) && (comp.getParent() instanceof JComboBox)) { comp = comp.getParent(); } // Account for LAF components of JInternalFrame else if (AWT.isInternalFrameDecoration(comp)) { while (!(comp instanceof JInternalFrame)) comp = comp.getParent(); } return getSemanticRecorder(comp.getClass()); } /** Return the semantic recorder for the given component class. */ protected SemanticRecorder getSemanticRecorder(Class cls) { // System.out.println("getting recorder for: " + cls); if (!(Component.class.isAssignableFrom(cls))) { throw new IllegalArgumentException("Class (" + cls + ") must derive from " + "Component"); } SemanticRecorder sr = (SemanticRecorder)semanticRecorders.get(cls); if (sr == null) { Class ccls = Robot.getCanonicalClass(cls); if (ccls != cls) { sr = getSemanticRecorder(ccls); // Additionally cache the mapping from the non-canonical class semanticRecorders.put(cls, sr); return sr; } String cname = Robot.simpleClassName(cls); try { //cname = "abbot.editor.recorder." + cname + "Recorder"; cname = getRecoderName(cname); Class recorderClass = Class.forName(cname); Constructor ctor = recorderClass.getConstructor(new Class[] { Resolver.class, }); sr = (SemanticRecorder)ctor.newInstance(new Object[] { getResolver() }); sr.addActionListener(getListener()); } catch(InvocationTargetException e) { Log.warn(e); } catch(NoSuchMethodException e) { sr = getSemanticRecorder(cls.getSuperclass()); } catch(InstantiationException e) { sr = getSemanticRecorder(cls.getSuperclass()); } catch(IllegalAccessException iae) { sr = getSemanticRecorder(cls.getSuperclass()); } catch(ClassNotFoundException cnf) { sr = getSemanticRecorder(cls.getSuperclass()); } // Cache the results for future reference semanticRecorders.put(cls, sr); } return sr; } /** Special adapter to catch events on JInternalFrame instances. */ private class InternalFrameWatcher extends AbstractInternalFrameWatcher { public InternalFrameWatcher(JInternalFrame f) { super(f); } protected void dispatch(AWTEvent e) { record(e); } } /** * Extracted the generation of recorder name, so as to override * @author keertip * 10/11/06 */ protected String getRecoderName(String cname){ return "abbot.editor.recorder." + cname + "Recorder"; } }