|
|||||||||||||||||||
| Source file | Conditionals | Statements | Methods | TOTAL | |||||||||||||||
| ComponentRecorder.java | 78.9% | 80.6% | 92.7% | 80.8% |
|
||||||||||||||
| 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 |
|