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