Clover coverage report - clover
Coverage timestamp: Sat Oct 8 2005 22:54:17 EDT
file stats: LOC: 1,102   Methods: 41
NCLOC: 825   Classes: 2
 
 Source file Conditionals Statements Methods TOTAL
ComponentRecorder.java 78.9% 80.6% 92.7% 80.8%
coverage coverage
 1   
 package abbot.editor.recorder;
 2   
 
 3   
 import java.awt.*;
 4   
 import java.awt.event.*;
 5   
 import java.text.*;
 6   
 import java.util.*;
 7   
 
 8   
 import javax.swing.*;
 9   
 import javax.swing.text.JTextComponent;
 10   
 
 11   
 import abbot.BugReport;
 12   
 import abbot.Log;
 13   
 import abbot.Platform;
 14   
 import abbot.finder.*;
 15   
 import abbot.finder.matchers.WindowMatcher;
 16   
 import abbot.i18n.Strings;
 17   
 import abbot.script.*;
 18   
 import abbot.script.Action;
 19   
 import abbot.script.Resolver;
 20   
 import abbot.tester.*;
 21   
 import abbot.tester.Robot;
 22   
 import abbot.util.AWT;
 23   
 
 24   
 /**
 25   
  * Record basic semantic events you might find on any component.  This class
 26   
  * handles the following actions:<p>
 27   
  * <ul>
 28   
  * <li>window actions
 29   
  * <li>popup menus
 30   
  * <li>click 
 31   
  * <li>typed keys
 32   
  * <li>basic drag and drop
 33   
  * <li>InputMethod events (extended character input)
 34   
  * </ul>
 35   
  * Clicks, popup menus, and drag/drop actions may be based on coordinates or
 36   
  * component substructure (cell, row, tab, etc) locations.
 37   
  *
 38   
  * <h3>Window Actions</h3>
 39   
  * While these nominally might be handled in a WindowRecorder, they are so
 40   
  * common that it's easier to handle here instead.  Currently supports
 41   
  * tracking show/hide/activate.  TODO: move/resize/iconfify/deiconify.
 42   
  * <h3>Popup Menus</h3>
 43   
  * Currently only the click/select/click sequence is supported.  The
 44   
  * press/drag/release version shouldn't be hard to implement, though.
 45   
  * <h3>Click</h3>
 46   
  * Simple press/release on a component, storing the exact coordinate of the
 47   
  * click.  Most things with selectability will want to override this.  Culling
 48   
  * accidental intervening drags would be nice but probably not worth the
 49   
  * effort or complexity (better just to be less sloppy with your mouse).
 50   
  * <h3>Key Type</h3>
 51   
  * Capture only events that result in actual output.  No plain modifiers,
 52   
  * shortcuts, or mnemonics.
 53   
  * <h3>Drag/Drop</h3>
 54   
  * Basic drag from one component and drop on another, storing exact
 55   
  * coordinates of the press/release actions.  Should definitely override this
 56   
  * to represent your component's internal objects (e.g. cells in a table).
 57   
  * Note that these are two distinct actions, even though they always appear
 58   
  * together.  The source is responsible for identifying the drag, and the
 59   
  * target is responsible for identifying the drop.
 60   
  * <h3>InputMethod</h3>
 61   
  * Catch extended character input.
 62   
  */
 63   
 // NOTE: Mac OSX robot will actually generate key modifiers prior
 64   
 // to  button2/3 
 65   
 // NOTE: Mac OSX CTRL/ALT+MB1 invokes MB2
 66   
 // CTRL+MB1->CTRL+MB2
 67   
 // ALT+MB1->MB2
 68   
     // TODO: test recorders by sending an event stream; test platform stream
 69   
     // by generating robot events and verifying stream seen; this splits the
 70   
     // tests into separate concerns.
 71   
 public class ComponentRecorder extends SemanticRecorder {
 72   
 
 73   
     private static final String[] TYPES = {
 74   
         "any", "window", "menu", "click", "key",
 75   
         "drag", "drop", "text", "input method"
 76   
     };
 77   
 
 78   
     /** Mappings for special keys. */
 79   
     private static java.util.HashMap specialMap;
 80   
 
 81   
     static {
 82   
         // Make explicit some special key mappings which we DON'T want to save
 83   
         // as the resulting characters (b/c they may not actually be
 84   
         // characters, or they're not particularly good to save as
 85   
         // characters. 
 86  17
         int[][] mappings = {
 87   
             { '\t', KeyEvent.VK_TAB },
 88   
             { '', KeyEvent.VK_ESCAPE }, // No escape sequence exists
 89   
             { '\b', KeyEvent.VK_BACK_SPACE },
 90   
             { '', KeyEvent.VK_DELETE }, // No escape sequence exists
 91   
             { '\n', KeyEvent.VK_ENTER },
 92   
             { '\r', KeyEvent.VK_ENTER },
 93   
         };
 94  17
         specialMap = new java.util.HashMap();
 95  17
         for (int i=0;i < mappings.length;i++) {
 96  102
             specialMap.put(String.valueOf((char)mappings[i][0]),
 97   
                            AWT.getKeyCode(mappings[i][1]));
 98   
         }
 99   
     }
 100   
 
 101   
     // For windows
 102   
     private Window window = null;
 103   
     private boolean isClose = false;
 104   
     // For key events
 105   
     private char keychar = KeyEvent.CHAR_UNDEFINED;
 106   
     private int modifiers;
 107   
     // For clicks
 108   
     private Component target;
 109   
     private Component forwardedTarget;
 110   
     private int x, y;
 111   
     private boolean released;
 112   
     private int clickCount;
 113   
     // For menu events
 114   
     private Component invoker;
 115   
     private int menux, menuy;
 116   
     private MenuItem awtMenuTarget;
 117   
     private Component menuTarget;
 118   
     private boolean isPopup;
 119   
     private boolean hasAWTPopup;
 120   
     private MenuListener menuListener;
 121   
     private boolean menuCanceled;
 122   
     // For drag events
 123   
     // This class is responsible for handling drag/drop once the action has
 124   
     // been recognized by a derived class
 125   
     private Component dragSource;
 126   
     private int dragx, dragy;
 127   
     // For drop events
 128   
     private Component dropTarget;
 129   
     private int dropx, dropy;
 130   
     private boolean nativeDrag;
 131   
     // InputMethod
 132   
     private ArrayList imKeyCodes = new ArrayList();
 133   
     private StringBuffer imText = new StringBuffer();
 134   
     /** Keep a short-term memory of windows we've seen open/close already. */
 135   
     private static WeakHashMap closeEventWindows = new WeakHashMap();
 136   
     private static WeakHashMap openEventWindows = new WeakHashMap();
 137   
     
 138   
     /** Create a ComponentRecorder for use in capturing the semantics of a GUI
 139   
      * action.
 140   
      */ 
 141  854
     public ComponentRecorder(Resolver resolver) {
 142  854
         super(resolver);
 143   
     }
 144   
 
 145   
     /** Does the given event indicate a window was shown? */
 146  198
     protected boolean isOpen(AWTEvent event) {
 147  198
         int id = event.getID();
 148   
         // 1.3 VMs may generate a WINDOW_OPEN without a COMPONENT_SHOWN
 149   
         // (see EventRecorderTest.testClickWithDialog)
 150   
         // NOTE: COMPONENT_SHOWN precedes WINDOW_OPENED, but we don't really
 151   
         // care in this case, since we're just recording the event, not
 152   
         // watching for the component's validity.
 153  198
         if (((id == WindowEvent.WINDOW_OPENED
 154   
               && !openEventWindows.containsKey(event.getSource()))
 155   
              || id == ComponentEvent.COMPONENT_SHOWN)) {
 156  20
             return true;
 157   
         }
 158  178
         return false;
 159   
     }
 160   
 
 161   
     /** Does the given event indicate a window was closed? */
 162  222
     protected boolean isClose(AWTEvent event) {
 163  222
         int id = event.getID();
 164   
         // Window.dispose doesn't generate a HIDDEN event, but it does
 165   
         // generate a WINDOW_CLOSED event (1.3/1.4)
 166  222
         if (((id == WindowEvent.WINDOW_CLOSED
 167   
               && !closeEventWindows.containsKey(event.getSource()))
 168   
              || id == ComponentEvent.COMPONENT_HIDDEN)) {
 169  19
             return true;
 170   
         }
 171  203
         return false;
 172   
     }
 173   
 
 174   
     /** Returns whether this ComponentRecorder wishes to accept the given
 175   
      * event.  If the event is accepted, the recorder must invoke init() with
 176   
      * the appropriate semantic event type.
 177   
      */
 178  698
     public boolean accept(AWTEvent event) {
 179  698
         int rtype = SE_NONE;
 180   
 
 181  698
         if (isWindowEvent(event)) {
 182  26
             rtype = SE_WINDOW;
 183   
         }
 184  672
         else if (isMenuEvent(event)) {
 185  11
             rtype = SE_MENU;
 186   
         }
 187  661
         else if (isKeyTyped(event)) {
 188  43
             rtype = SE_KEY;
 189   
         }
 190  618
         else if (isClick(event)) {
 191  71
             rtype = SE_CLICK;
 192   
         }
 193  547
         else if (isDragDrop(event)) {
 194  4
             rtype = SE_DROP;
 195   
         }
 196  543
         else if (isInputMethod(event)) {
 197  0
             rtype = SE_IM;
 198   
         }
 199   
         else {
 200  543
             if (Log.isClassDebugEnabled(ComponentRecorder.class))
 201  0
                 Log.debug("Ignoring " + Robot.toString(event));
 202   
         }
 203   
 
 204  698
         init(rtype);
 205  698
         boolean accepted = rtype != SE_NONE;
 206  698
         if (accepted && Log.isClassDebugEnabled(ComponentRecorder.class))
 207  0
             Log.debug("Accepted " + ComponentTester.toString(event));
 208  698
         return accepted;
 209   
     }
 210   
 
 211   
     /** Test whether the given event is a trigger for a window event. 
 212   
      * Allow derived classes to change definition of a click.
 213   
      */
 214  694
     protected boolean isWindowEvent(AWTEvent event) {
 215   
         // Ignore activate and deactivate.  They are unreliable.
 216   
         // We only want open/close events on non-tooltip and non-popup windows
 217  694
         return (event.getSource() instanceof Window)
 218   
             && !AWT.isHeavyweightPopup((Window)event.getSource())
 219   
             && !isToolTip(event.getSource())
 220   
             && (isClose(event) || isOpen(event));
 221   
     }
 222   
 
 223   
     /**
 224   
      * Return true if the given event source is a tooltip.
 225   
      * Such events look like window events, but we check for them before other
 226   
      * kinds of window events so as to be able to filter them out.
 227   
      * <P>
 228   
      * TODO: emit steps to confirm value of tooltip?
 229   
      * <P>
 230   
      * @param source the object to examine
 231   
      * @return true if this event source is a tooltip
 232   
      */
 233  191
     protected boolean isToolTip(Object source){
 234   
         // Tooltips appear to be a direct subclass of JWindow and
 235   
         // have a single component of class JToolTip
 236  191
         if (source instanceof JWindow && !(source instanceof JFrame)){
 237  8
             Container pane = ((JWindow)source).getContentPane();
 238  8
             while (pane.getComponentCount() == 1){
 239  8
                 Component child = pane.getComponent(0);
 240  8
                 if (child instanceof JToolTip)
 241  0
                     return true;
 242  8
                 if (!(child instanceof Container))
 243  0
                     break;
 244  8
                 pane = (Container)child;
 245   
             }
 246   
         }
 247  191
         return false;
 248   
     }
 249   
 
 250  662
     protected boolean isMenuEvent(AWTEvent event) {
 251  662
         if (event.getID() == ActionEvent.ACTION_PERFORMED
 252   
             && event.getSource() instanceof java.awt.MenuItem) {
 253  4
             return true;
 254   
         }
 255  658
         else if (event.getID() == MouseEvent.MOUSE_PRESSED) {
 256  74
             MouseEvent me = (MouseEvent)event;
 257  74
             return me.isPopupTrigger()
 258   
                 || ((me.getModifiers() & AWTConstants.POPUP_MASK) != 0)
 259   
                 || me.getSource() instanceof JMenu;
 260   
         }
 261  584
         return false;
 262   
     }
 263   
 
 264  661
     protected boolean isKeyTyped(AWTEvent event) {
 265  661
         return event.getID() == KeyEvent.KEY_TYPED;
 266   
     }
 267   
 
 268   
     /** Test whether the given event is a trigger for a mouse button click.
 269   
      * Allow derived classes to change definition of a click.
 270   
      */ 
 271  665
     protected boolean isClick(AWTEvent event) {
 272  665
         if (event.getID() == MouseEvent.MOUSE_PRESSED) {
 273  87
             MouseEvent me = (MouseEvent)event;
 274  87
             return (me.getModifiers() & MouseEvent.BUTTON1_MASK) != 0;
 275   
         }
 276  578
         return false;
 277   
     }
 278   
 
 279   
     /** Test whether the given event precurses a drop. */
 280  547
     protected boolean isDragDrop(AWTEvent event) {
 281  547
         return event.getID() == MouseEvent.MOUSE_DRAGGED;
 282   
     }
 283   
 
 284   
     /** Default to recording a drag if it looks like one. */
 285   
     // FIXME may be some better detection, like checking for DND interfaces. */
 286  27
     protected boolean canDrag() {
 287  27
         return true;
 288   
     }
 289   
 
 290   
     /** Default to waiting for multiple clicks. */
 291  154
     protected boolean canMultipleClick() {
 292  154
         return true;
 293   
     }
 294   
 
 295   
     /** Is this the start of an input method event? */
 296  543
     private boolean isInputMethod(AWTEvent event) {
 297   
         // NOTE: HALF_WIDTH signals start of kanji input
 298   
         // NOTE: Mac uses input method for some dual-keystroke chars (option-e)
 299  543
         return (event.getID() == KeyEvent.KEY_RELEASED
 300   
                 && ((KeyEvent)event).getKeyCode() == KeyEvent.VK_HALF_WIDTH)
 301   
             || event.getID() == InputMethodEvent.INPUT_METHOD_TEXT_CHANGED;
 302   
     }
 303   
 
 304   
     /** Provide standard parsing of mouse button events. */
 305  311
     protected boolean parseClick(AWTEvent event) {
 306  311
         boolean consumed = true;
 307  311
         int id = event.getID();
 308  311
         if (id == MouseEvent.MOUSE_PRESSED) {
 309  74
             Log.debug("Parsing mouse down");
 310  74
             MouseEvent me = (MouseEvent)event;
 311  74
             if (clickCount == 0) {
 312  69
                 target = me.getComponent();
 313  69
                 x = me.getX();
 314  69
                 y = me.getY();
 315  69
                 modifiers = me.getModifiers();
 316  69
                 clickCount = 1;
 317   
                 // Add the component immediately, just in case it gets removed
 318   
                 // from the hierarchy as a result of the click.
 319  69
                 getResolver().addComponent(target);
 320   
             }
 321   
             else {
 322  5
                 if (target == me.getComponent()) {
 323  4
                     clickCount = me.getClickCount();
 324   
                 }
 325  1
                 else if (!released) {
 326   
                     // It's possible to get two consecutive MOUSE_PRESSED
 327   
                     // events for different targets (e.g. double click on a
 328   
                     // table cell to get the default editor) (OSX 1.3.1, XP
 329   
                     // 1.4.1_01). Ignore the second click, since it is
 330   
                     // artificial, and wait for the original click to finish.
 331   
                     // i.e. w32 1.3.1
 332   
                     // MOUSE_PRESSED  JTable
 333   
                     // MOUSE_PRESSED  JTextField
 334   
                     // FOCUS_LOST     JTable
 335   
                     // FOCUS_GAINED   JTextField
 336   
                     // MOUSE_EXITED   JTable
 337   
                     // MOUSE_ENTERED  JTextField
 338   
                     // MOUSE_RELEASED JTable
 339   
                     // MOUSE_RELEASED JTextField
 340  1
                     forwardedTarget = me.getComponent();
 341   
                 }
 342   
             }
 343  74
             released = false;
 344   
         }
 345  237
         else if (id == MouseEvent.MOUSE_RELEASED) {
 346  60
             Log.debug("Parsing mouse up");
 347  60
             released = true;
 348   
             // Optionally disallow multiple clicks
 349  60
             if (!canMultipleClick())
 350  4
                 setFinished(true);
 351   
         }
 352  177
         else if (id == MouseEvent.MOUSE_CLICKED) {
 353   
             // optionally wait for multiple clicks
 354  51
             if (!canMultipleClick())
 355  0
                 setFinished(true);
 356   
         }
 357  126
         else if (id == MouseEvent.MOUSE_EXITED) {
 358  2
             Log.debug("exit event, released=" + released);
 359  2
             if (event.getSource() != target || released) {
 360  1
                 consumed = false;
 361  1
                 setFinished(true);
 362   
             }
 363  1
             else if (!released) {
 364   
                 // May not see any DRAGGED events if it's a native drag;
 365   
                 // 1.3 posts MOUSE_EXITED after MOUSE_PRESSED, no drag events
 366  1
                 if (clickCount == 1) {
 367  0
                     setRecordingType(SE_DRAG);
 368  0
                     consumed = dragStarted(target, x, y, modifiers,
 369   
                                            (MouseEvent)event);
 370   
                 }
 371   
             }
 372   
         }
 373  124
         else if (id == MouseEvent.MOUSE_ENTERED) {
 374  1
             if (event.getSource() == target && !released) {
 375   
                 // nothing
 376   
             }
 377  1
             else if (event.getSource() != forwardedTarget) {
 378  0
                 consumed = false;
 379  0
                 setFinished(true);
 380   
             }
 381   
         }
 382  123
         else if (id == MouseEvent.MOUSE_DRAGGED && canDrag()) {
 383  27
             Log.debug("Changing click to drag start");
 384  27
             MouseEvent me = (MouseEvent)event;
 385  27
             if (Math.abs(me.getX() - x) >= AWTConstants.DRAG_THRESHOLD
 386   
                 || Math.abs(me.getY() - y) >= AWTConstants.DRAG_THRESHOLD) {
 387   
                 // Was actually a drag; pass off to drag handler
 388  14
                 setRecordingType(SE_DRAG);
 389  14
                 consumed = dragStarted(target, x, y, modifiers, me);
 390   
             }
 391   
             else {
 392  13
                 Log.debug("Drag too small");
 393   
             }
 394   
         }
 395   
         // These events will not prevent a multi-click from being registered.
 396  96
         else if ((id >= ComponentEvent.COMPONENT_FIRST
 397   
                    && id <= ComponentEvent.COMPONENT_LAST)
 398   
                  || (event instanceof ContainerEvent)
 399   
                  || (event instanceof FocusEvent)  
 400   
                  || (id == HierarchyEvent.HIERARCHY_CHANGED 
 401   
                      && (((HierarchyEvent)event).getChangeFlags()
 402   
                          & HierarchyEvent.SHOWING_CHANGED) == 0)) {
 403   
             // Ignore most hierarchy change and component events between
 404   
             // clicks. 
 405   
             // The focus event is sporadic on w32 1.4.1_02
 406   
         }
 407   
         else {
 408   
             // All other events should cause the click to finish,
 409   
             // but don't register a click unless we've received the release
 410   
             // event. 
 411  2
             if (released) {
 412  2
                 consumed = false;
 413  2
                 setFinished(true);
 414   
             }
 415   
         }
 416  311
         return consumed;
 417   
     }
 418   
 
 419  19
     protected boolean parseWindowEvent(AWTEvent event) {
 420  19
         boolean consumed = true;
 421  19
         isClose = isClose(event);
 422   
         // Keep track of window open/close state so we don't parse the same
 423   
         // semantic event twice (e.g. COMPONENT_SHOWN + WINDOW_OPENED or
 424   
         // multiple WINDOW_CLOSED events).
 425  19
         if (isClose) {
 426  8
             closeEventWindows.put(event.getSource(), Boolean.TRUE);
 427  8
             openEventWindows.remove(event.getSource());
 428   
         }
 429   
         else {
 430  11
             openEventWindows.put(event.getSource(), Boolean.TRUE);
 431  11
             closeEventWindows.remove(event.getSource());
 432   
         }
 433  19
         Log.log("close=" + isClose + " (" + Robot.toString(event) + ")");
 434  19
         window = (Window)event.getSource();
 435  19
         setFinished(true);
 436  19
         return consumed;
 437   
     }
 438   
 
 439  50
     protected boolean parseKeyEvent(AWTEvent e) {
 440  50
         int id = e.getID();
 441  50
         boolean consumed = true;
 442  50
         if (id == KeyEvent.KEY_TYPED) {
 443  44
             KeyEvent typed = (KeyEvent)e;
 444  44
             target = typed.getComponent();
 445  44
             keychar = typed.getKeyChar();
 446  44
             modifiers = typed.getModifiers();
 447  44
             if ((modifiers & KeyEvent.ALT_MASK) == KeyEvent.ALT_MASK) {
 448  4
                 Log.debug("Waiting for potential focus accelerator on '"
 449   
                           + keychar + "'");
 450   
             }
 451   
             else {
 452   
                 // Ignore KEY_TYPED input for control and alt modifiers, since
 453   
                 // the generated characters are not accepted as text input.
 454   
                 // Add others if you encounter them, but err on the side of
 455   
                 // accepting input that can later be removed.
 456  40
                 if ((modifiers & InputEvent.CTRL_MASK) == InputEvent.CTRL_MASK
 457   
                     || (modifiers & InputEvent.ALT_MASK) == InputEvent.ALT_MASK) {
 458  0
                     Log.debug("Ignoring modifiers: " + modifiers);
 459  0
                     setRecordingType(SE_NONE);
 460   
                 }
 461  40
                 setFinished(true);
 462   
             }
 463   
         }
 464  6
         else if (id == FocusEvent.FOCUS_LOST) {
 465   
             // Ignore and wait for FOCUS_GAINED
 466   
         }
 467  3
         else if (id == FocusEvent.FOCUS_GAINED) {
 468   
             // Looks like a focus accelerator focus change.  Ignore the
 469   
             // KEY_TYPED event.
 470  2
             Object o = e.getSource();
 471  2
             char ch = KeyEvent.CHAR_UNDEFINED;
 472  2
             if (o instanceof JTextComponent) {
 473  2
                 ch = ((JTextComponent)o).getFocusAccelerator();
 474  2
                 Log.debug("focus accelerator is '" + ch + "'");
 475   
             }
 476  2
             if (Character.toUpperCase(ch) == Character.toUpperCase(keychar)) {
 477  2
                 setRecordingType(SE_NONE);
 478  2
                 setFinished(true);
 479   
             }
 480   
             else {
 481  0
                 setFinished(true);
 482  0
                 consumed = false;
 483   
             }
 484   
         }
 485   
         else {
 486  1
             setRecordingType(SE_NONE);
 487  1
             setFinished(true);
 488  1
             consumed = false;
 489   
         }
 490  50
         return consumed;
 491   
     }
 492   
 
 493   
     /** Base implementation handles context (popup) menus. */
 494  174
     protected boolean parseMenuSelection(AWTEvent event) {
 495  174
         int id = event.getID();
 496  174
         boolean consumed = true;
 497   
         // press, release, show, [move, show,] press, release
 498   
         // press, [drag, show,] release (FIXME not done)
 499   
         // ACTION_PERFORMED and ITEM_STATE_CHANGED are only 
 500   
         // produced by AWT components (wxp/1.4.2)
 501  174
         if (id == ActionEvent.ACTION_PERFORMED
 502   
             || id == ItemEvent.ITEM_STATE_CHANGED) {
 503  5
             awtMenuTarget = (MenuItem)event.getSource();
 504  5
             invoker = AWT.getInvoker(awtMenuTarget);
 505   
             // If there is no invoker, the selection came from the MenuBar
 506  5
             if (invoker != null) {
 507  4
                 isPopup = true;
 508   
             }
 509  5
             Log.debug("AWT menu selection, invoker="
 510   
                       + Robot.toString(invoker));
 511  5
             if (event instanceof ActionEvent) {
 512  5
                 modifiers = ((ActionEvent)event).getModifiers();
 513   
             }
 514   
             else {
 515   
                 // ItemEvent doesn't report modifiers, so ask use internal
 516   
                 // tracking to see if any modifiers are active.
 517  0
                 modifiers = Robot.getState().getModifiers();
 518   
             }
 519  5
             setFinished(true);
 520   
         }
 521  169
         else if (id == MouseEvent.MOUSE_PRESSED) {
 522  12
             MouseEvent me = (MouseEvent)event;
 523   
             // On the first press, we haven't yet set the invoker, which
 524   
             // is either a JMenu or the component holding the popup.
 525  12
             if (invoker == null) {
 526  7
                 invoker = me.getComponent();
 527  7
                 menux = me.getX();
 528  7
                 menuy = me.getY();
 529  7
                 modifiers = me.getModifiers();
 530  7
                 isPopup = me.isPopupTrigger();
 531   
                 // Must add the listener now, b/c on w32 release/click events
 532   
                 // are not generated until *after* the awt popup selection.
 533  7
                 if (isPopup || (modifiers & AWTConstants.POPUP_MASK) != 0) {
 534  5
                     hasAWTPopup = addMenuListener(invoker);
 535   
                 }
 536   
                 // It's possible for a popup menu to be triggered by some
 537   
                 // other event (e.g. a button click).  Assume that action is
 538   
                 // already recorded and simply make note of the appropriate
 539   
                 // menu selection.
 540  7
                 if (invoker instanceof JMenuItem
 541   
                     && !(invoker instanceof JMenu)) {
 542  1
                     menuTarget = invoker;
 543  1
                     invoker = null;
 544  1
                     menux = menuy = -1;
 545  1
                     modifiers = 0;
 546  1
                     isPopup = true;
 547   
                 }
 548   
             }
 549  5
             else if (event.getSource() instanceof JMenu) {
 550   
                 // ignore
 551   
             }
 552  4
             else if (event.getSource() instanceof JMenuItem) {
 553   
                 // Click to select the menu item; this will be the second
 554   
                 // press event received
 555  4
                 menuTarget = (Component)event.getSource();
 556   
             }
 557   
             else {
 558   
                 // Mouse press in something other than the menu, assume it was
 559   
                 // canceled. 
 560   
                 // Popup was canceled.  Discard subsequent release/click.
 561  0
                 menuCanceled = true;
 562  0
                 setStatus("Popup menu selection canceled");
 563   
             }
 564  12
             Log.log("Menu mouse press");
 565   
         }
 566  157
         else if (id == MouseEvent.MOUSE_RELEASED) {
 567  12
             MouseEvent me = (MouseEvent)event;
 568   
             // The menu target won't be set until the second mouse press
 569  12
             if (menuCanceled) {
 570  0
                 setRecordingType(SE_NONE);
 571  0
                 setFinished(true);
 572   
             }
 573  12
             else if (menuTarget == null) {
 574   
                 // This is the first mouse release
 575  7
                 if (!isPopup) {
 576  6
                     isPopup = me.isPopupTrigger();
 577   
                 }
 578   
             }
 579   
             else {
 580  5
                 if (menuTarget != null)
 581  5
                     setFinished(true);
 582   
             }
 583  12
             Log.log("Menu mouse release");
 584   
         }
 585  145
         else if (id == MouseEvent.MOUSE_CLICKED && isPopup) {
 586   
             // If it was a popup trigger, make sure there was a popup,
 587   
             // otherwise record it as a click.
 588   
             // Note that we won't likely get any events with an AWT popup, so
 589   
             // just assume it was invoked if there is one.
 590  5
             if (!hasAWTPopup
 591   
                 && AWT.findActivePopupMenu() == null) {
 592  1
                 setRecordingType(SE_CLICK);
 593  1
                 target = invoker;
 594  1
                 x = menux;
 595  1
                 y = menuy;
 596  1
                 setFinished(true);
 597   
             }
 598   
         }
 599   
         else {
 600  140
             Log.debug("Ignoring " + ComponentTester.toString(event));
 601   
         }
 602  174
         return consumed;
 603   
     }
 604   
 
 605   
     // TODO: set up test to generate several different drag types, then sample
 606   
     // the event streams on different platforms:
 607   
     // drag from one component to another
 608   
     // drag within a component
 609  31
     protected boolean parseDrop(AWTEvent event) {
 610  31
         int id = event.getID();
 611  31
         boolean consumed = true;
 612   
 
 613   
         // Use enter/exit events to determine what the final destination
 614   
         // is, since drag events always use the drag source for the component. 
 615  31
         if (id == MouseEvent.MOUSE_DRAGGED) {
 616   
             // If we don't have a target yet, default to the drag source
 617  15
             MouseEvent me = (MouseEvent)event;
 618  15
             if (dropTarget == null) {
 619  6
                 Log.debug("No target yet, using drag source as target");
 620  6
                 dropTarget = me.getComponent();
 621  6
                 dropx = me.getX();
 622  6
                 dropy = me.getY();
 623   
             }
 624   
             // origin is always the drag source
 625  15
             Point p = Robot.getState().getDragOrigin();
 626  15
             if (Math.abs(p.x - me.getX()) > AWTConstants.DRAG_THRESHOLD
 627   
                 || Math.abs(p.y - me.getY()) > AWTConstants.DRAG_THRESHOLD) {
 628  11
                 setNativeDrag(false);
 629  11
                 Log.debug("Not a native drag");
 630   
             }
 631   
         }
 632  16
         else if (id == MouseEvent.MOUSE_MOVED) {
 633   
             // This seems to be the canonical exit from a drag/drop
 634   
             // Observed on: w32 1.4.2 JTree, JTable, JLabel drops
 635   
             // I'd much rather see a MOUSE_RELEASED when the drag completes. 
 636  2
             dropTarget = ((MouseEvent)event).getComponent();
 637  2
             dropx = ((MouseEvent)event).getX();
 638  2
             dropy = ((MouseEvent)event).getY();
 639  2
             setFinished(true);
 640   
         }
 641  14
         else if (id == MouseEvent.MOUSE_ENTERED) {
 642  0
             Log.debug("Drag enter");
 643  0
             dropTarget = ((MouseEvent)event).getComponent();
 644  0
             dropx = ((MouseEvent)event).getX();
 645  0
             dropy = ((MouseEvent)event).getY();
 646  0
             Log.debug("Not a native drag");
 647  0
             setNativeDrag(false);
 648   
         }
 649  14
         else if (id == MouseEvent.MOUSE_EXITED) {
 650   
             // If a true drag is in effect (as of 1.4+), we will see no java
 651   
             // event queue events until this one (on w32, anyway).
 652   
             // If it's 1.3, we won't get any events, even after the drag
 653   
             // completes. 
 654  3
             MouseEvent me = (MouseEvent)event;
 655  3
             if (nativeDrag
 656   
                 && Platform.JAVA_VERSION >= Platform.JAVA_1_4) {
 657  1
                 Log.debug("Inferring drop");
 658  1
                 dropTarget = me.getComponent();
 659  1
                 dropx = me.getX();
 660  1
                 dropy = me.getY();
 661  1
                 setFinished(true);
 662   
             }
 663   
         }
 664  11
         else if (id == MouseEvent.MOUSE_RELEASED) {
 665  11
             MouseEvent me = (MouseEvent)event;
 666  11
             Log.debug("Dropped");
 667   
             // FIXME verify that the component is always the original press
 668   
             // source 
 669  11
             dropTarget = me.getComponent();
 670  11
             dropx = me.getX();
 671  11
             dropy = me.getY();
 672  11
             setFinished(true);
 673   
         }
 674   
         else {
 675  0
             if (Log.isClassDebugEnabled(ComponentRecorder.class))
 676  0
                 Log.debug("Ignoring " + ComponentTester.toString(event));
 677   
         }
 678  31
         return consumed;
 679   
     }
 680   
 
 681  0
     protected boolean parseInputMethod(AWTEvent event) {
 682  0
         boolean consumed = true;
 683  0
         int id = event.getID();
 684  0
         if (id == KeyEvent.KEY_RELEASED) {
 685  0
             KeyEvent ke = (KeyEvent)event;
 686  0
             int code = ke.getKeyCode();
 687  0
             switch (code) {
 688  0
             case KeyEvent.VK_HALF_WIDTH:
 689   
                 // This indicates the input method start (for kanji, anyway)
 690  0
                 break;
 691  0
             case KeyEvent.VK_FULL_WIDTH:
 692   
                 // This indicates the input method end (for kanji, anyway)
 693  0
                 Log.log("Captured " + imText);
 694  0
                 setFinished(true);
 695  0
                 break;
 696  0
             case KeyEvent.VK_ALT_GRAPH:
 697  0
             case KeyEvent.VK_CONTROL:
 698  0
             case KeyEvent.VK_SHIFT:
 699  0
             case KeyEvent.VK_META:
 700  0
             case KeyEvent.VK_ALT:
 701  0
                 Log.debug("Modifier indicates end of InputMethod");
 702  0
                 consumed = false;
 703  0
                 setFinished(true);
 704  0
                 break;
 705  0
             default:
 706   
                 // Consume other key release events, assuming there was no
 707   
                 // corresponding key press event.
 708  0
                 imKeyCodes.add(new Integer(code));
 709  0
                 break;
 710   
             }
 711   
         }
 712  0
         else if (event instanceof InputMethodEvent) {
 713  0
             InputMethodEvent ime = (InputMethodEvent)event;
 714  0
             if (id == InputMethodEvent.INPUT_METHOD_TEXT_CHANGED) {
 715  0
                 if (ime.getCommittedCharacterCount() > 0) {
 716  0
                     AttributedCharacterIterator iter = ime.getText();
 717  0
                     StringBuffer sb = new StringBuffer();
 718  0
                     for (char ch = iter.first();
 719  0
                          ch != CharacterIterator.DONE;
 720   
                          ch = iter.next()) {
 721  0
                         sb.append(ch);
 722   
                     }
 723  0
                     imText.append(sb.toString());
 724  0
                     Log.debug("Partial capture " + sb.toString());
 725   
                 }
 726  0
                 if (!Platform.isOSX())
 727  0
                     setFinished(true);
 728   
             }
 729   
         }
 730   
         else {
 731  0
             consumed = false;
 732  0
             setFinished(true);
 733   
         }
 734  0
         return consumed;
 735   
     }
 736   
 
 737   
     /** Handle an event.  Return whether the event was consumed. */
 738  1309
     public boolean parse(AWTEvent event) {
 739  1309
         if (Log.isClassDebugEnabled(ComponentRecorder.class))
 740  0
             Log.debug("Parsing " + ComponentTester.toString(event)
 741   
                       + " as " + TYPES[getRecordingType()]);
 742   
 
 743   
         // Default handling is event consumed, and assume not finished
 744  1309
         boolean consumed = true;
 745   
 
 746  1309
         switch(getRecordingType()) {
 747  55
         case SE_WINDOW:
 748  55
             consumed = parseWindowEvent(event);
 749  55
             break;
 750  50
         case SE_KEY:
 751  50
             consumed = parseKeyEvent(event);
 752  50
             break;
 753  999
         case SE_CLICK: 
 754  999
             consumed = parseClick(event);
 755  999
             break;
 756  174
         case SE_MENU:
 757  174
             consumed = parseMenuSelection(event);
 758  174
             break;
 759  31
         case SE_DROP:
 760  31
             consumed = parseDrop(event);
 761  31
             break;
 762  0
         case SE_IM:
 763  0
             consumed = parseInputMethod(event);
 764  0
             break;
 765  0
         default:
 766  0
             Log.warn("Unknown input type: " + getRecordingType());
 767   
             // error
 768  0
             break;
 769   
         }
 770  1309
         if (isFinished()) {
 771  111
             try {
 772  111
                 Step step = createStep();
 773  111
                 setStep(step);
 774  111
                 Log.log("Semantic event recorded: " + step);
 775   
             }
 776   
             catch(Throwable thr) {
 777  0
                 String msg = Strings.get("editor.recording.error");
 778  0
                 BugReport br = new BugReport(msg, thr);
 779  0
                 Log.log("Semantic recorder error: " + br.toString());
 780  0
                 setStatus(Strings.get("editor.see_console"));
 781  0
                 setRecordingType(SE_NONE);
 782  0
                 throw br;
 783   
             }
 784   
         }
 785   
 
 786  1309
         return consumed;
 787   
     }
 788   
 
 789   
     /** Provide a hint about the type of the drag. */
 790  11
     protected void setNativeDrag(boolean n) {
 791  11
         nativeDrag = n;
 792   
     }
 793   
 
 794   
     /** Returns whether the first drag motion event should be consumed.
 795   
      * Derived classes may override this to provide custom drag behavior.
 796   
      * Default behavior saves the drag initiation event by itself.
 797   
      */
 798  4
     protected boolean dragStarted(Component target,
 799   
                                   int x, int y,
 800   
                                   int modifiers,
 801   
                                   MouseEvent dragEvent) {
 802  4
         dragSource = target;
 803  4
         dragx = x;
 804  4
         dragy = y;
 805  4
         setFinished(true);
 806  4
         return false;
 807   
     }
 808   
 
 809   
     /** Returns the script step generated from the events recorded so far. */
 810  134
     protected Step createStep() {
 811  134
         Step step = null;
 812  134
         int type = getRecordingType();
 813  134
         Log.debug("Creating step for semantic recorder, type: "
 814  134
                   + (type >= 0 && type < TYPES.length
 815   
                      ? TYPES[getRecordingType()] : String.valueOf(type)));
 816  134
         switch(type) {
 817  19
         case SE_WINDOW:
 818  19
             step = createWindowEvent(window, isClose);
 819  19
             break;
 820  10
         case SE_MENU: 
 821  10
             if (awtMenuTarget != null) {
 822  5
                 if (invoker == null) {
 823  1
                     MenuContainer mc = awtMenuTarget.getParent();
 824  1
                     while (mc instanceof MenuComponent
 825   
                            && !(mc instanceof Component)) {
 826  2
                         mc = ((MenuComponent)mc).getParent();
 827   
                     }
 828  1
                     if (mc == null) {
 829  0
                         throw new Error("AWT MenuItem " + awtMenuTarget
 830   
                                         + " has no Component ancestor");
 831   
                     }
 832  1
                     invoker = (Component)mc;
 833   
                 }
 834  5
                 step = createAWTMenuSelection(invoker, awtMenuTarget, isPopup);
 835   
             }
 836  5
             else if (isPopup) {
 837  4
                 step = createPopupMenuSelection(invoker, menux, menuy,
 838   
                                                 menuTarget);
 839   
             }
 840  1
             else if (menuTarget != null) {
 841  1
                 step = createMenuSelection(menuTarget);
 842   
             }
 843  10
             break;
 844  40
         case SE_KEY: {
 845  40
             if (keychar != KeyEvent.CHAR_UNDEFINED) {
 846  40
                 step = createKey(target, keychar, modifiers);
 847   
             }
 848   
             else {
 849  0
                 step = null;
 850   
             }
 851  40
             break;
 852   
         }
 853  51
         case SE_CLICK: {
 854  51
             step = createClick(target, x, y, modifiers,
 855  51
                                canMultipleClick() ? clickCount : 1);
 856  51
             break;
 857   
         }
 858  4
         case SE_DRAG: {
 859  4
             step = createDrag(dragSource, dragx, dragy);
 860  4
             break;
 861   
         }
 862  4
         case SE_DROP:
 863  4
             step = createDrop(dropTarget, dropx, dropy);
 864  4
             break;
 865  0
         case SE_IM:
 866  0
             if (imText.length() > 0)
 867  0
                 step = createInputMethod(imKeyCodes, imText.toString());
 868   
             else {
 869  0
                 Log.debug("Input method resulted in no text");
 870  0
                 step = null;
 871   
             }
 872  0
             break;
 873  6
         default:
 874  6
             step = null;
 875  6
             break;
 876   
         }
 877   
 
 878  134
         return step;
 879   
     }
 880   
 
 881   
     /** Create a wait for the window show/hide.  Use an appropriate identifier
 882   
         string, which might be the name, title, or component reference.
 883   
     */
 884  19
     protected Step createWindowEvent(Window window, boolean isClose) {
 885  19
         ComponentReference ref = getResolver().addComponent(window);
 886  19
         String method = "assertComponentShowing";
 887  19
         Assert step = new Assert(getResolver(), null,
 888   
                                  ComponentTester.class.getName(),
 889   
                                  method, 
 890   
                                  new String[] { ref.getID() },
 891   
                                  "true", isClose);
 892  19
         step.setWait(true);
 893  19
         return step;
 894   
     }
 895   
 
 896  1
     protected Step createMenuSelection(Component menuItem) {
 897  1
         ComponentReference cr = getResolver().addComponent(menuItem);
 898  1
         Step step = new Action(getResolver(), 
 899   
                                null, "actionSelectMenuItem",
 900   
                                new String[] { cr.getID() });
 901  1
         return step;
 902   
     }
 903   
 
 904  5
     protected Step createAWTMenuSelection(Component parent, MenuItem menuItem,
 905   
                                           boolean isPopup) {
 906  5
         ComponentReference ref = getResolver().addComponent(parent);
 907  5
         String method = "actionSelectAWTMenuItem";
 908  5
         if (isPopup)
 909  4
             method = "actionSelectAWTPopupMenuItem";
 910   
         // Get a unique path for the MenuItem
 911  5
         String path = AWT.getPath(menuItem);
 912   
         // Do a quick search on the invoker for other popups.  If there are
 913   
         // duplicates, include the menu item name
 914  5
         Step step = new Action(getResolver(), 
 915   
                                null, method,
 916   
                                new String[] { ref.getID(), path });
 917  5
         return step;
 918   
     }
 919   
 
 920  4
     protected Step createPopupMenuSelection(Component invoker, int x, int y,
 921   
                                             Component menuItem) {
 922  4
         Step step;
 923  4
         if (invoker != null) {
 924  3
             ComponentReference inv = getResolver().addComponent(invoker);
 925  3
             JMenuItem mi = (JMenuItem)menuItem;
 926  3
             String where = getLocationArgument(invoker, x, y);
 927  3
             step = new Action(getResolver(), 
 928   
                               null, "actionSelectPopupMenuItem",
 929   
                               new String[] { inv.getID(), where,
 930   
                                              mi.getText()
 931   
                               }, invoker.getClass());
 932   
         }
 933   
         else {
 934  1
             ComponentReference ref = getResolver().addComponent(menuItem);
 935  1
             step = new Action(getResolver(),
 936   
                               null, "actionSelectMenuItem",
 937   
                               new String[] { ref.getID() });
 938   
         }
 939  4
         return step;
 940   
     }
 941   
 
 942  40
     protected Step createKey(Component comp, char keychar, int mods) {
 943  40
         ComponentReference cr = getResolver().addComponent(comp);
 944   
         // NOTE: Any keys which might have effects as key press/release should
 945   
         // be encoded as a keystroke, rather than a keystring.
 946   
         // NOTE: We encode strings rather than integer values, since the
 947   
         // names are more useful.
 948  40
         String code = (String)specialMap.get(String.valueOf(keychar));
 949  40
         if (code != null) {
 950  8
             String[] args = mods != 0
 951   
                 ? new String[] { cr.getID(), code,
 952   
                                  AWT.getKeyModifiers(mods) }
 953   
                 : new String[] { cr.getID(), code };
 954  8
             return new Action(getResolver(), null, "actionKeyStroke", args);
 955   
         }            
 956  32
         return new Action(getResolver(), null,
 957   
                           "actionKeyString",
 958   
                           new String[] { cr.getID(),
 959   
                                          String.valueOf(keychar) });
 960   
     }
 961   
 
 962  4
     protected Step createDrop(Component comp, int x, int y) {
 963  4
         Step step = null;
 964  4
         if (comp != null) {
 965  4
             ComponentReference cr = getResolver().addComponent(comp);
 966  4
             String where = getLocationArgument(comp, x, y);
 967  4
             step = new Action(getResolver(), 
 968   
                               null, "actionDrop", new String[] { 
 969   
                                   cr.getID(), where
 970   
                               }, comp.getClass());
 971   
         }
 972  4
         return step;
 973   
     }
 974   
 
 975  4
     protected Step createDrag(Component comp, int x, int y) {
 976  4
         ComponentReference ref = getResolver().addComponent(comp);
 977  4
         String where = getLocationArgument(comp, x, y);
 978  4
         Step step = new Action(getResolver(), 
 979   
                                null, "actionDrag", new String[] {
 980   
                                    ref.getID(), where,
 981   
                                }, comp.getClass());
 982  4
         return step;
 983   
     }
 984   
 
 985   
     /** Create a click event with the given event information. */
 986  8
     protected Step createClick(Component target, int x, int y,
 987   
                                int mods, int count) {
 988  8
         Log.debug("creating click");
 989  8
         ComponentReference cr = getResolver().addComponent(target);
 990  8
         ArrayList args = new ArrayList();
 991  8
         args.add(cr.getID());
 992  8
         args.add(getLocationArgument(target, x, y));
 993  8
         if ((mods != 0 && mods != MouseEvent.BUTTON1_MASK)
 994   
             || count > 1) {
 995   
             // NOTE: this currently saves POPUP or TERTIARY, rather than
 996   
             // an explicit button 2 or 3.  I figure that makes more sense
 997   
             // than a hard coded button number.
 998  3
             args.add(AWT.getMouseModifiers(mods));
 999  3
             if (count > 1) {
 1000  2
                 args.add(String.valueOf(count));
 1001   
             }
 1002   
         }
 1003  8
         return new Action(getResolver(), null, "actionClick",
 1004   
                           (String[])args.toArray(new String[args.size()]),
 1005   
                           target.getClass());
 1006   
     }
 1007   
 
 1008  0
     protected Step createInputMethod(ArrayList codes, String text) {
 1009  0
         Log.debug("Text length is " + text.length());
 1010  0
         return new Action(getResolver(), null,
 1011   
                           "actionKeyString", 
 1012   
                           new String[] { text });
 1013   
     }
 1014   
 
 1015  729
     protected void init(int recordingType) {
 1016  729
         super.init(recordingType);
 1017  729
         target = null;
 1018  729
         forwardedTarget = null;
 1019  729
         released = false;
 1020  729
         clickCount = 0;
 1021  729
         keychar = KeyEvent.CHAR_UNDEFINED;
 1022  729
         invoker = null;
 1023  729
         awtMenuTarget = null;
 1024  729
         isPopup = false;
 1025  729
         hasAWTPopup = false;
 1026  729
         menuListener = null;
 1027  729
         menuTarget = null;
 1028  729
         menuCanceled = false;
 1029  729
         dragSource = dropTarget = null;
 1030  729
         nativeDrag = true;
 1031  729
         window = null;
 1032  729
         isClose = false;
 1033  729
         imKeyCodes.clear();
 1034  729
         imText.delete(0, imText.length());
 1035   
     }
 1036   
 
 1037   
     /** Invoke when end of the semantic event has been seen. */
 1038  871
     protected void setFinished(boolean state) {
 1039  871
         MenuListener listener = null;
 1040  871
         synchronized(this) {
 1041  871
             super.setFinished(state);
 1042  871
             listener = menuListener;
 1043  871
             menuListener = null;
 1044   
         }
 1045  871
         if (listener != null)
 1046  1
             listener.dispose();
 1047   
     }
 1048   
 
 1049  5
     private boolean addMenuListener(Component invoker) {
 1050  5
         PopupMenu[] popups = AWT.getPopupMenus(invoker);
 1051  5
         if (popups.length > 0) {
 1052  1
             menuListener = new MenuListener(popups);
 1053  1
             return true;
 1054   
         }
 1055  4
         return false;
 1056   
     }
 1057   
 
 1058   
     private class MenuListener implements ItemListener {
 1059   
         private ArrayList items = new ArrayList();
 1060  1
         public MenuListener(PopupMenu[] popups) {
 1061  1
             for (int i=0;i < popups.length;i++) {
 1062  1
                 addRecursive(popups[i]);
 1063   
             }
 1064   
         }
 1065  1
         private void addRecursive(Menu menu) {
 1066  1
             for (int i=0;i < menu.getItemCount();i++) {
 1067  4
                 MenuItem item = menu.getItem(i);
 1068  4
                 if (item instanceof Menu)
 1069  0
                     addRecursive((Menu)item);
 1070  4
                 else if (item instanceof CheckboxMenuItem) {
 1071  1
                     ((CheckboxMenuItem)item).addItemListener(this);
 1072  1
                     items.add(item);
 1073   
                 }
 1074   
             }
 1075   
         }
 1076  0
         public void itemStateChanged(ItemEvent e) {
 1077  0
             dispose();
 1078  0
             parse(e);
 1079   
         }
 1080  1
         public void dispose() {
 1081  1
             while (items.size() > 0) {
 1082  1
                 ((CheckboxMenuItem)items.get(0)).removeItemListener(this);
 1083  1
                 items.remove(0);
 1084   
             }
 1085   
         }
 1086   
     }
 1087   
 
 1088   
     /** Obtain the String representation of the Component-specific location. */
 1089  55
     protected String getLocationArgument(Component c, int x, int y) {
 1090  55
         return getLocation(c, x, y).toString();
 1091   
     }
 1092   
 
 1093   
     /** Obtain a more precise location than the given coordinate, if
 1094   
      * possible.
 1095   
      */
 1096  55
     protected ComponentLocation getLocation(Component c, int x, int y) {
 1097  55
         ComponentTester tester = ComponentTester.getTester(c);
 1098  55
         return tester.getLocation(c, new Point(x, y));
 1099   
     }
 1100   
 }
 1101   
 
 1102