1 /* 2 * Copyright 1999-2009 Sun Microsystems, Inc. All Rights Reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Sun designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Sun in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, 22 * CA 95054 USA or visit www.sun.com if you need additional information or 23 * have any questions. 24 */ 25 26 package java.awt; 27 28 import java.awt.event.InputEvent; 29 import java.awt.event.KeyEvent; 30 import java.awt.image.BufferedImage; 31 import java.awt.image.DataBufferInt; 32 import java.awt.image.DirectColorModel; 33 import java.awt.image.Raster; 34 import java.awt.image.WritableRaster; 35 import java.awt.peer.RobotPeer; 36 import java.lang.reflect.InvocationTargetException; 37 import sun.awt.ComponentFactory; 38 import sun.awt.SunToolkit; 39 import sun.awt.image.SunWritableRaster; 40 import sun.security.util.SecurityConstants; 41 42 /** 43 * This class is used to generate native system input events 44 * for the purposes of test automation, self-running demos, and 45 * other applications where control of the mouse and keyboard 46 * is needed. The primary purpose of Robot is to facilitate 47 * automated testing of Java platform implementations. 48 * <p> 49 * Using the class to generate input events differs from posting 50 * events to the AWT event queue or AWT components in that the 51 * events are generated in the platform's native input 52 * queue. For example, <code>Robot.mouseMove</code> will actually move 53 * the mouse cursor instead of just generating mouse move events. 54 * <p> 55 * Note that some platforms require special privileges or extensions 56 * to access low-level input control. If the current platform configuration 57 * does not allow input control, an <code>AWTException</code> will be thrown 58 * when trying to construct Robot objects. For example, X-Window systems 59 * will throw the exception if the XTEST 2.2 standard extension is not supported 60 * (or not enabled) by the X server. 61 * <p> 62 * Applications that use Robot for purposes other than self-testing should 63 * handle these error conditions gracefully. 64 * 65 * @author Robi Khan 66 * @since 1.3 67 */ 68 public class Robot { 69 private static final int MAX_DELAY = 60000; 70 private RobotPeer peer; 71 private boolean isAutoWaitForIdle = false; 72 private int autoDelay = 0; 73 private static int LEGAL_BUTTON_MASK = 0; 74 75 // location of robot's GC, used in mouseMove(), getPixelColor() and captureScreenImage() 76 private Point gdLoc; 77 78 private DirectColorModel screenCapCM = null; 79 80 /** 81 * Constructs a Robot object in the coordinate system of the primary screen. 82 * <p> 83 * 84 * @throws AWTException if the platform configuration does not allow 85 * low-level input control. This exception is always thrown when 86 * GraphicsEnvironment.isHeadless() returns true 87 * @throws SecurityException if <code>createRobot</code> permission is not granted 88 * @see java.awt.GraphicsEnvironment#isHeadless 89 * @see SecurityManager#checkPermission 90 * @see AWTPermission 91 */ 92 public Robot() throws AWTException { 93 if (GraphicsEnvironment.isHeadless()) { 94 throw new AWTException("headless environment"); 95 } 96 init(GraphicsEnvironment.getLocalGraphicsEnvironment() 97 .getDefaultScreenDevice()); 98 } 99 100 /** 101 * Creates a Robot for the given screen device. Coordinates passed 102 * to Robot method calls like mouseMove and createScreenCapture will 103 * be interpreted as being in the same coordinate system as the 104 * specified screen. Note that depending on the platform configuration, 105 * multiple screens may either: 106 * <ul> 107 * <li>share the same coordinate system to form a combined virtual screen</li> 108 * <li>use different coordinate systems to act as independent screens</li> 109 * </ul> 110 * This constructor is meant for the latter case. 111 * <p> 112 * If screen devices are reconfigured such that the coordinate system is 113 * affected, the behavior of existing Robot objects is undefined. 114 * 115 * @param screen A screen GraphicsDevice indicating the coordinate 116 * system the Robot will operate in. 117 * @throws AWTException if the platform configuration does not allow 118 * low-level input control. This exception is always thrown when 119 * GraphicsEnvironment.isHeadless() returns true. 120 * @throws IllegalArgumentException if <code>screen</code> is not a screen 121 * GraphicsDevice. 122 * @throws SecurityException if <code>createRobot</code> permission is not granted 123 * @see java.awt.GraphicsEnvironment#isHeadless 124 * @see GraphicsDevice 125 * @see SecurityManager#checkPermission 126 * @see AWTPermission 127 */ 128 public Robot(GraphicsDevice screen) throws AWTException { 129 checkIsScreenDevice(screen); 130 init(screen); 131 } 132 133 private void init(GraphicsDevice screen) throws AWTException { 134 checkRobotAllowed(); 135 gdLoc = screen.getDefaultConfiguration().getBounds().getLocation(); 136 Toolkit toolkit = Toolkit.getDefaultToolkit(); 137 if (toolkit instanceof ComponentFactory) { 138 peer = ((ComponentFactory)toolkit).createRobot(this, screen); 139 disposer = new RobotDisposer(peer); 140 sun.java2d.Disposer.addRecord(anchor, disposer); 141 } 142 initLegalButtonMask(); 143 } 144 145 private static synchronized void initLegalButtonMask() { 146 if (LEGAL_BUTTON_MASK != 0) return; 147 148 int tmpMask = 0; 149 if (Toolkit.getDefaultToolkit().areExtraMouseButtonsEnabled()){ 150 if (Toolkit.getDefaultToolkit() instanceof SunToolkit) { 151 final int buttonsNumber = ((SunToolkit)(Toolkit.getDefaultToolkit())).getNumberOfButtons(); 152 for (int i = 0; i < buttonsNumber; i++){ 153 tmpMask |= InputEvent.getMaskForButton(i+1); 154 } 155 } 156 } 157 tmpMask |= InputEvent.BUTTON1_MASK| 158 InputEvent.BUTTON2_MASK| 159 InputEvent.BUTTON3_MASK| 160 InputEvent.BUTTON1_DOWN_MASK| 161 InputEvent.BUTTON2_DOWN_MASK| 162 InputEvent.BUTTON3_DOWN_MASK; 163 LEGAL_BUTTON_MASK = tmpMask; 164 } 165 166 /* determine if the security policy allows Robot's to be created */ 167 private void checkRobotAllowed() { 168 SecurityManager security = System.getSecurityManager(); 169 if (security != null) { 170 security.checkPermission(SecurityConstants.CREATE_ROBOT_PERMISSION); 171 } 172 } 173 174 /* check if the given device is a screen device */ 175 private void checkIsScreenDevice(GraphicsDevice device) { 176 if (device == null || device.getType() != GraphicsDevice.TYPE_RASTER_SCREEN) { 177 throw new IllegalArgumentException("not a valid screen device"); 178 } 179 } 180 181 private transient Object anchor = new Object(); 182 183 static class RobotDisposer implements sun.java2d.DisposerRecord { 184 private final RobotPeer peer; 185 public RobotDisposer(RobotPeer peer) { 186 this.peer = peer; 187 } 188 public void dispose() { 189 if (peer != null) { 190 peer.dispose(); 191 } 192 } 193 } 194 195 private transient RobotDisposer disposer; 196 197 /** 198 * Moves mouse pointer to given screen coordinates. 199 * @param x X position 200 * @param y Y position 201 */ 202 public synchronized void mouseMove(int x, int y) { 203 peer.mouseMove(gdLoc.x + x, gdLoc.y + y); 204 afterEvent(); 205 } 206 207 /** 208 * Presses one or more mouse buttons. The mouse buttons should 209 * be released using the {@link #mouseRelease(int)} method. 210 * 211 * @param buttons the Button mask; a combination of one or more 212 * mouse button masks. 213 * <p> 214 * It is allowed to use only a combination of valid values as a {@code buttons} parameter. 215 * A valid combination consists of {@code InputEvent.BUTTON1_DOWN_MASK}, 216 * {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} 217 * and values returned by the 218 * {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method. 219 * 220 * The valid combination also depends on a 221 * {@link Toolkit#areExtraMouseButtonsEnabled() Toolkit.areExtraMouseButtonsEnabled()} value as follows: 222 * <ul> 223 * <li> If support for extended mouse buttons is 224 * {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java 225 * then it is allowed to use only the following standard button masks: 226 * {@code InputEvent.BUTTON1_DOWN_MASK}, {@code InputEvent.BUTTON2_DOWN_MASK}, 227 * {@code InputEvent.BUTTON3_DOWN_MASK}. 228 * <li> If support for extended mouse buttons is 229 * {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java 230 * then it is allowed to use the standard button masks 231 * and masks for existing extended mouse buttons, if the mouse has more then three buttons. 232 * In that way, it is allowed to use the button masks corresponding to the buttons 233 * in the range from 1 to {@link java.awt.MouseInfo#getNumberOfButtons() MouseInfo.getNumberOfButtons()}. 234 * <br> 235 * It is recommended to use the {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} 236 * method to obtain the mask for any mouse button by its number. 237 * </ul> 238 * <p> 239 * The following standard button masks are also accepted: 240 * <ul> 241 * <li>{@code InputEvent.BUTTON1_MASK} 242 * <li>{@code InputEvent.BUTTON2_MASK} 243 * <li>{@code InputEvent.BUTTON3_MASK} 244 * </ul> 245 * However, it is recommended to use {@code InputEvent.BUTTON1_DOWN_MASK}, 246 * {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} instead. 247 * Either extended {@code _DOWN_MASK} or old {@code _MASK} values 248 * should be used, but both those models should not be mixed. 249 * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button 250 * and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java 251 * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button 252 * that does not exist on the mouse and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java 253 * @see #mouseRelease(int) 254 * @see InputEvent#getMaskForButton(int) 255 * @see Toolkit#areExtraMouseButtonsEnabled() 256 * @see java.awt.MouseInfo#getNumberOfButtons() 257 * @see java.awt.event.MouseEvent 258 */ 259 public synchronized void mousePress(int buttons) { 260 checkButtonsArgument(buttons); 261 peer.mousePress(buttons); 262 afterEvent(); 263 } 264 265 /** 266 * Releases one or more mouse buttons. 267 * 268 * @param buttons the Button mask; a combination of one or more 269 * mouse button masks. 270 * <p> 271 * It is allowed to use only a combination of valid values as a {@code buttons} parameter. 272 * A valid combination consists of {@code InputEvent.BUTTON1_DOWN_MASK}, 273 * {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} 274 * and values returned by the 275 * {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method. 276 * 277 * The valid combination also depends on a 278 * {@link Toolkit#areExtraMouseButtonsEnabled() Toolkit.areExtraMouseButtonsEnabled()} value as follows: 279 * <ul> 280 * <li> If the support for extended mouse buttons is 281 * {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java 282 * then it is allowed to use only the following standard button masks: 283 * {@code InputEvent.BUTTON1_DOWN_MASK}, {@code InputEvent.BUTTON2_DOWN_MASK}, 284 * {@code InputEvent.BUTTON3_DOWN_MASK}. 285 * <li> If the support for extended mouse buttons is 286 * {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java 287 * then it is allowed to use the standard button masks 288 * and masks for existing extended mouse buttons, if the mouse has more then three buttons. 289 * In that way, it is allowed to use the button masks corresponding to the buttons 290 * in the range from 1 to {@link java.awt.MouseInfo#getNumberOfButtons() MouseInfo.getNumberOfButtons()}. 291 * <br> 292 * It is recommended to use the {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} 293 * method to obtain the mask for any mouse button by its number. 294 * </ul> 295 * <p> 296 * The following standard button masks are also accepted: 297 * <ul> 298 * <li>{@code InputEvent.BUTTON1_MASK} 299 * <li>{@code InputEvent.BUTTON2_MASK} 300 * <li>{@code InputEvent.BUTTON3_MASK} 301 * </ul> 302 * However, it is recommended to use {@code InputEvent.BUTTON1_DOWN_MASK}, 303 * {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} instead. 304 * Either extended {@code _DOWN_MASK} or old {@code _MASK} values 305 * should be used, but both those models should not be mixed. 306 * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button 307 * and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java 308 * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button 309 * that does not exist on the mouse and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java 310 * @see #mousePress(int) 311 * @see InputEvent#getMaskForButton(int) 312 * @see Toolkit#areExtraMouseButtonsEnabled() 313 * @see java.awt.MouseInfo#getNumberOfButtons() 314 * @see java.awt.event.MouseEvent 315 */ 316 public synchronized void mouseRelease(int buttons) { 317 checkButtonsArgument(buttons); 318 peer.mouseRelease(buttons); 319 afterEvent(); 320 } 321 322 private void checkButtonsArgument(int buttons) { 323 if ( (buttons|LEGAL_BUTTON_MASK) != LEGAL_BUTTON_MASK ) { 324 throw new IllegalArgumentException("Invalid combination of button flags"); 325 } 326 } 327 328 /** 329 * Rotates the scroll wheel on wheel-equipped mice. 330 * 331 * @param wheelAmt number of "notches" to move the mouse wheel 332 * Negative values indicate movement up/away from the user, 333 * positive values indicate movement down/towards the user. 334 * 335 * @since 1.4 336 */ 337 public synchronized void mouseWheel(int wheelAmt) { 338 peer.mouseWheel(wheelAmt); 339 afterEvent(); 340 } 341 342 /** 343 * Presses a given key. The key should be released using the 344 * <code>keyRelease</code> method. 345 * <p> 346 * Key codes that have more than one physical key associated with them 347 * (e.g. <code>KeyEvent.VK_SHIFT</code> could mean either the 348 * left or right shift key) will map to the left key. 349 * 350 * @param keycode Key to press (e.g. <code>KeyEvent.VK_A</code>) 351 * @throws IllegalArgumentException if <code>keycode</code> is not 352 * a valid key 353 * @see #keyRelease(int) 354 * @see java.awt.event.KeyEvent 355 */ 356 public synchronized void keyPress(int keycode) { 357 checkKeycodeArgument(keycode); 358 peer.keyPress(keycode); 359 afterEvent(); 360 } 361 362 /** 363 * Releases a given key. 364 * <p> 365 * Key codes that have more than one physical key associated with them 366 * (e.g. <code>KeyEvent.VK_SHIFT</code> could mean either the 367 * left or right shift key) will map to the left key. 368 * 369 * @param keycode Key to release (e.g. <code>KeyEvent.VK_A</code>) 370 * @throws IllegalArgumentException if <code>keycode</code> is not a 371 * valid key 372 * @see #keyPress(int) 373 * @see java.awt.event.KeyEvent 374 */ 375 public synchronized void keyRelease(int keycode) { 376 checkKeycodeArgument(keycode); 377 peer.keyRelease(keycode); 378 afterEvent(); 379 } 380 381 private void checkKeycodeArgument(int keycode) { 382 // rather than build a big table or switch statement here, we'll 383 // just check that the key isn't VK_UNDEFINED and assume that the 384 // peer implementations will throw an exception for other bogus 385 // values e.g. -1, 999999 386 if (keycode == KeyEvent.VK_UNDEFINED) { 387 throw new IllegalArgumentException("Invalid key code"); 388 } 389 } 390 391 /** 392 * Returns the color of a pixel at the given screen coordinates. 393 * @param x X position of pixel 394 * @param y Y position of pixel 395 * @return Color of the pixel 396 */ 397 public synchronized Color getPixelColor(int x, int y) { 398 Color color = new Color(peer.getRGBPixel(gdLoc.x + x, gdLoc.y + y)); 399 return color; 400 } 401 402 /** 403 * Creates an image containing pixels read from the screen. This image does 404 * not include the mouse cursor. 405 * @param screenRect Rect to capture in screen coordinates 406 * @return The captured image 407 * @throws IllegalArgumentException if <code>screenRect</code> width and height are not greater than zero 408 * @throws SecurityException if <code>readDisplayPixels</code> permission is not granted 409 * @see SecurityManager#checkPermission 410 * @see AWTPermission 411 */ 412 public synchronized BufferedImage createScreenCapture(Rectangle screenRect) { 413 checkScreenCaptureAllowed(); 414 415 // according to the spec, screenRect is relative to robot's GD 416 Rectangle translatedRect = new Rectangle(screenRect); 417 translatedRect.translate(gdLoc.x, gdLoc.y); 418 checkValidRect(translatedRect); 419 420 BufferedImage image; 421 DataBufferInt buffer; 422 WritableRaster raster; 423 424 if (screenCapCM == null) { 425 /* 426 * Fix for 4285201 427 * Create a DirectColorModel equivalent to the default RGB ColorModel, 428 * except with no Alpha component. 429 */ 430 431 screenCapCM = new DirectColorModel(24, 432 /* red mask */ 0x00FF0000, 433 /* green mask */ 0x0000FF00, 434 /* blue mask */ 0x000000FF); 435 } 436 437 // need to sync the toolkit prior to grabbing the pixels since in some 438 // cases rendering to the screen may be delayed 439 Toolkit.getDefaultToolkit().sync(); 440 441 int pixels[]; 442 int[] bandmasks = new int[3]; 443 444 pixels = peer.getRGBPixels(translatedRect); 445 buffer = new DataBufferInt(pixels, pixels.length); 446 447 bandmasks[0] = screenCapCM.getRedMask(); 448 bandmasks[1] = screenCapCM.getGreenMask(); 449 bandmasks[2] = screenCapCM.getBlueMask(); 450 451 raster = Raster.createPackedRaster(buffer, translatedRect.width, translatedRect.height, translatedRect.width, bandmasks, null); 452 SunWritableRaster.makeTrackable(buffer); 453 454 image = new BufferedImage(screenCapCM, raster, false, null); 455 456 return image; 457 } 458 459 private static void checkValidRect(Rectangle rect) { 460 if (rect.width <= 0 || rect.height <= 0) { 461 throw new IllegalArgumentException("Rectangle width and height must be > 0"); 462 } 463 } 464 465 private static void checkScreenCaptureAllowed() { 466 SecurityManager security = System.getSecurityManager(); 467 if (security != null) { 468 security.checkPermission( 469 SecurityConstants.READ_DISPLAY_PIXELS_PERMISSION); 470 } 471 } 472 473 /* 474 * Called after an event is generated 475 */ 476 private void afterEvent() { 477 autoWaitForIdle(); 478 autoDelay(); 479 } 480 481 /** 482 * Returns whether this Robot automatically invokes <code>waitForIdle</code> 483 * after generating an event. 484 * @return Whether <code>waitForIdle</code> is automatically called 485 */ 486 public synchronized boolean isAutoWaitForIdle() { 487 return isAutoWaitForIdle; 488 } 489 490 /** 491 * Sets whether this Robot automatically invokes <code>waitForIdle</code> 492 * after generating an event. 493 * @param isOn Whether <code>waitForIdle</code> is automatically invoked 494 */ 495 public synchronized void setAutoWaitForIdle(boolean isOn) { 496 isAutoWaitForIdle = isOn; 497 } 498 499 /* 500 * Calls waitForIdle after every event if so desired. 501 */ 502 private void autoWaitForIdle() { 503 if (isAutoWaitForIdle) { 504 waitForIdle(); 505 } 506 } 507 508 /** 509 * Returns the number of milliseconds this Robot sleeps after generating an event. 510 */ 511 public synchronized int getAutoDelay() { 512 return autoDelay; 513 } 514 515 /** 516 * Sets the number of milliseconds this Robot sleeps after generating an event. 517 * @throws IllegalArgumentException If <code>ms</code> is not between 0 and 60,000 milliseconds inclusive 518 */ 519 public synchronized void setAutoDelay(int ms) { 520 checkDelayArgument(ms); 521 autoDelay = ms; 522 } 523 524 /* 525 * Automatically sleeps for the specified interval after event generated. 526 */ 527 private void autoDelay() { 528 delay(autoDelay); 529 } 530 531 /** 532 * Sleeps for the specified time. 533 * To catch any <code>InterruptedException</code>s that occur, 534 * <code>Thread.sleep()</code> may be used instead. 535 * @param ms time to sleep in milliseconds 536 * @throws IllegalArgumentException if <code>ms</code> is not between 0 and 60,000 milliseconds inclusive 537 * @see java.lang.Thread#sleep 538 */ 539 public synchronized void delay(int ms) { 540 checkDelayArgument(ms); 541 try { 542 Thread.sleep(ms); 543 } catch(InterruptedException ite) { 544 ite.printStackTrace(); 545 } 546 } 547 548 private void checkDelayArgument(int ms) { 549 if (ms < 0 || ms > MAX_DELAY) { 550 throw new IllegalArgumentException("Delay must be to 0 to 60,000ms"); 551 } 552 } 553 554 /** 555 * Waits until all events currently on the event queue have been processed. 556 * @throws IllegalThreadStateException if called on the AWT event dispatching thread 557 */ 558 public synchronized void waitForIdle() { 559 checkNotDispatchThread(); 560 // post a dummy event to the queue so we know when 561 // all the events before it have been processed 562 try { 563 SunToolkit.flushPendingEvents(); 564 EventQueue.invokeAndWait( new Runnable() { 565 public void run() { 566 // dummy implementation 567 } 568 } ); 569 } catch(InterruptedException ite) { 570 System.err.println("Robot.waitForIdle, non-fatal exception caught:"); 571 ite.printStackTrace(); 572 } catch(InvocationTargetException ine) { 573 System.err.println("Robot.waitForIdle, non-fatal exception caught:"); 574 ine.printStackTrace(); 575 } 576 } 577 578 private void checkNotDispatchThread() { 579 if (EventQueue.isDispatchThread()) { 580 throw new IllegalThreadStateException("Cannot call method from the event dispatcher thread"); 581 } 582 } 583 584 /** 585 * Returns a string representation of this Robot. 586 * 587 * @return the string representation. 588 */ 589 public synchronized String toString() { 590 String params = "autoDelay = "+getAutoDelay()+", "+"autoWaitForIdle = "+isAutoWaitForIdle(); 591 return getClass().getName() + "[ " + params + " ]"; 592 } 593 }