1 /*
2 * Copyright 1999-2007 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.peer;
29 import java.awt.image;
30 import java.awt.event;
31 import java.lang.reflect.InvocationTargetException;
32 import sun.awt.ComponentFactory;
33 import sun.awt.SunToolkit;
34 import sun.security.util.SecurityConstants;
35
36 /**
37 * This class is used to generate native system input events
38 * for the purposes of test automation, self-running demos, and
39 * other applications where control of the mouse and keyboard
40 * is needed. The primary purpose of Robot is to facilitate
41 * automated testing of Java platform implementations.
42 * <p>
43 * Using the class to generate input events differs from posting
44 * events to the AWT event queue or AWT components in that the
45 * events are generated in the platform's native input
46 * queue. For example, <code>Robot.mouseMove</code> will actually move
47 * the mouse cursor instead of just generating mouse move events.
48 * <p>
49 * Note that some platforms require special privileges or extensions
50 * to access low-level input control. If the current platform configuration
51 * does not allow input control, an <code>AWTException</code> will be thrown
52 * when trying to construct Robot objects. For example, X-Window systems
53 * will throw the exception if the XTEST 2.2 standard extension is not supported
54 * (or not enabled) by the X server.
55 * <p>
56 * Applications that use Robot for purposes other than self-testing should
57 * handle these error conditions gracefully.
58 *
59 * @author Robi Khan
60 * @since 1.3
61 */
62 public class Robot {
63 private static final int MAX_DELAY = 60000;
64 private RobotPeer peer;
65 private boolean isAutoWaitForIdle = false;
66 private int autoDelay = 0;
67 private static final int LEGAL_BUTTON_MASK =
68 InputEvent.BUTTON1_MASK|
69 InputEvent.BUTTON2_MASK|
70 InputEvent.BUTTON3_MASK;
71
72 // location of robot's GC, used in mouseMove(), getPixelColor() and captureScreenImage()
73 private Point gdLoc;
74
75 private DirectColorModel screenCapCM = null;
76
77 /**
78 * Constructs a Robot object in the coordinate system of the primary screen.
79 * <p>
80 *
81 * @throws AWTException if the platform configuration does not allow
82 * low-level input control. This exception is always thrown when
83 * GraphicsEnvironment.isHeadless() returns true
84 * @throws SecurityException if <code>createRobot</code> permission is not granted
85 * @see java.awt.GraphicsEnvironment#isHeadless
86 * @see SecurityManager#checkPermission
87 * @see AWTPermission
88 */
89 public Robot() throws AWTException {
90 if (GraphicsEnvironment.isHeadless()) {
91 throw new AWTException("headless environment");
92 }
93 init(GraphicsEnvironment.getLocalGraphicsEnvironment()
94 .getDefaultScreenDevice());
95 }
96
97 /**
98 * Creates a Robot for the given screen device. Coordinates passed
99 * to Robot method calls like mouseMove and createScreenCapture will
100 * be interpreted as being in the same coordinate system as the
101 * specified screen. Note that depending on the platform configuration,
102 * multiple screens may either:
103 * <ul>
104 * <li>share the same coordinate system to form a combined virtual screen</li>
105 * <li>use different coordinate systems to act as independent screens</li>
106 * </ul>
107 * This constructor is meant for the latter case.
108 * <p>
109 * If screen devices are reconfigured such that the coordinate system is
110 * affected, the behavior of existing Robot objects is undefined.
111 *
112 * @param screen A screen GraphicsDevice indicating the coordinate
113 * system the Robot will operate in.
114 * @throws AWTException if the platform configuration does not allow
115 * low-level input control. This exception is always thrown when
116 * GraphicsEnvironment.isHeadless() returns true.
117 * @throws IllegalArgumentException if <code>screen</code> is not a screen
118 * GraphicsDevice.
119 * @throws SecurityException if <code>createRobot</code> permission is not granted
120 * @see java.awt.GraphicsEnvironment#isHeadless
121 * @see GraphicsDevice
122 * @see SecurityManager#checkPermission
123 * @see AWTPermission
124 */
125 public Robot(GraphicsDevice screen) throws AWTException {
126 checkIsScreenDevice(screen);
127 init(screen);
128 }
129
130 private void init(GraphicsDevice screen) throws AWTException {
131 checkRobotAllowed();
132 gdLoc = screen.getDefaultConfiguration().getBounds().getLocation();
133 Toolkit toolkit = Toolkit.getDefaultToolkit();
134 if (toolkit instanceof ComponentFactory) {
135 peer = ((ComponentFactory)toolkit).createRobot(this, screen);
136 disposer = new RobotDisposer(peer);
137 sun.java2d.Disposer.addRecord(anchor, disposer);
138 }
139 }
140
141 /* determine if the security policy allows Robot's to be created */
142 private void checkRobotAllowed() {
143 SecurityManager security = System.getSecurityManager();
144 if (security != null) {
145 security.checkPermission(SecurityConstants.CREATE_ROBOT_PERMISSION);
146 }
147 }
148
149 /* check if the given device is a screen device */
150 private void checkIsScreenDevice(GraphicsDevice device) {
151 if (device == null || device.getType() != GraphicsDevice.TYPE_RASTER_SCREEN) {
152 throw new IllegalArgumentException("not a valid screen device");
153 }
154 }
155
156 private transient Object anchor = new Object();
157
158 static class RobotDisposer implements sun.java2d.DisposerRecord {
159 private final RobotPeer peer;
160 public RobotDisposer(RobotPeer peer) {
161 this.peer = peer;
162 }
163 public void dispose() {
164 if (peer != null) {
165 peer.dispose();
166 }
167 }
168 }
169
170 private transient RobotDisposer disposer;
171
172 /**
173 * Moves mouse pointer to given screen coordinates.
174 * @param x X position
175 * @param y Y position
176 */
177 public synchronized void mouseMove(int x, int y) {
178 peer.mouseMove(gdLoc.x + x, gdLoc.y + y);
179 afterEvent();
180 }
181
182 /**
183 * Presses one or more mouse buttons. The mouse buttons should
184 * be released using the <code>mouseRelease</code> method.
185 *
186 * @param buttons the Button mask; a combination of one or more
187 * of these flags:
188 * <ul>
189 * <li><code>InputEvent.BUTTON1_MASK</code>
190 * <li><code>InputEvent.BUTTON2_MASK</code>
191 * <li><code>InputEvent.BUTTON3_MASK</code>
192 * </ul>
193 * @throws IllegalArgumentException if the button mask is not a
194 * valid combination
195 * @see #mouseRelease(int)
196 */
197 public synchronized void mousePress(int buttons) {
198 checkButtonsArgument(buttons);
199 peer.mousePress(buttons);
200 afterEvent();
201 }
202
203 /**
204 * Releases one or more mouse buttons.
205 *
206 * @param buttons the Button mask; a combination of one or more
207 * of these flags:
208 * <ul>
209 * <li><code>InputEvent.BUTTON1_MASK</code>
210 * <li><code>InputEvent.BUTTON2_MASK</code>
211 * <li><code>InputEvent.BUTTON3_MASK</code>
212 * </ul>
213 * @see #mousePress(int)
214 * @throws IllegalArgumentException if the button mask is not a valid
215 * combination
216 */
217 public synchronized void mouseRelease(int buttons) {
218 checkButtonsArgument(buttons);
219 peer.mouseRelease(buttons);
220 afterEvent();
221 }
222
223 private void checkButtonsArgument(int buttons) {
224 if ( (buttons|LEGAL_BUTTON_MASK) != LEGAL_BUTTON_MASK ) {
225 throw new IllegalArgumentException("Invalid combination of button flags");
226 }
227 }
228
229 /**
230 * Rotates the scroll wheel on wheel-equipped mice.
231 *
232 * @param wheelAmt number of "notches" to move the mouse wheel
233 * Negative values indicate movement up/away from the user,
234 * positive values indicate movement down/towards the user.
235 *
236 * @since 1.4
237 */
238 public synchronized void mouseWheel(int wheelAmt) {
239 peer.mouseWheel(wheelAmt);
240 afterEvent();
241 }
242
243 /**
244 * Presses a given key. The key should be released using the
245 * <code>keyRelease</code> method.
246 * <p>
247 * Key codes that have more than one physical key associated with them
248 * (e.g. <code>KeyEvent.VK_SHIFT</code> could mean either the
249 * left or right shift key) will map to the left key.
250 *
251 * @param keycode Key to press (e.g. <code>KeyEvent.VK_A</code>)
252 * @throws IllegalArgumentException if <code>keycode</code> is not
253 * a valid key
254 * @see #keyRelease(int)
255 * @see java.awt.event.KeyEvent
256 */
257 public synchronized void keyPress(int keycode) {
258 checkKeycodeArgument(keycode);
259 peer.keyPress(keycode);
260 afterEvent();
261 }
262
263 /**
264 * Releases a given key.
265 * <p>
266 * Key codes that have more than one physical key associated with them
267 * (e.g. <code>KeyEvent.VK_SHIFT</code> could mean either the
268 * left or right shift key) will map to the left key.
269 *
270 * @param keycode Key to release (e.g. <code>KeyEvent.VK_A</code>)
271 * @throws IllegalArgumentException if <code>keycode</code> is not a
272 * valid key
273 * @see #keyPress(int)
274 * @see java.awt.event.KeyEvent
275 */
276 public synchronized void keyRelease(int keycode) {
277 checkKeycodeArgument(keycode);
278 peer.keyRelease(keycode);
279 afterEvent();
280 }
281
282 private void checkKeycodeArgument(int keycode) {
283 // rather than build a big table or switch statement here, we'll
284 // just check that the key isn't VK_UNDEFINED and assume that the
285 // peer implementations will throw an exception for other bogus
286 // values e.g. -1, 999999
287 if (keycode == KeyEvent.VK_UNDEFINED) {
288 throw new IllegalArgumentException("Invalid key code");
289 }
290 }
291
292 /**
293 * Returns the color of a pixel at the given screen coordinates.
294 * @param x X position of pixel
295 * @param y Y position of pixel
296 * @return Color of the pixel
297 */
298 public synchronized Color getPixelColor(int x, int y) {
299 Color color = new Color(peer.getRGBPixel(gdLoc.x + x, gdLoc.y + y));
300 return color;
301 }
302
303 /**
304 * Creates an image containing pixels read from the screen. This image does
305 * not include the mouse cursor.
306 * @param screenRect Rect to capture in screen coordinates
307 * @return The captured image
308 * @throws IllegalArgumentException if <code>screenRect</code> width and height are not greater than zero
309 * @throws SecurityException if <code>readDisplayPixels</code> permission is not granted
310 * @see SecurityManager#checkPermission
311 * @see AWTPermission
312 */
313 public synchronized BufferedImage createScreenCapture(Rectangle screenRect) {
314 checkScreenCaptureAllowed();
315
316 // according to the spec, screenRect is relative to robot's GD
317 Rectangle translatedRect = new Rectangle(screenRect);
318 translatedRect.translate(gdLoc.x, gdLoc.y);
319 checkValidRect(translatedRect);
320
321 BufferedImage image;
322 DataBufferInt buffer;
323 WritableRaster raster;
324
325 if (screenCapCM == null) {
326 /*
327 * Fix for 4285201
328 * Create a DirectColorModel equivalent to the default RGB ColorModel,
329 * except with no Alpha component.
330 */
331
332 screenCapCM = new DirectColorModel(24,
333 /* red mask */ 0x00FF0000,
334 /* green mask */ 0x0000FF00,
335 /* blue mask */ 0x000000FF);
336 }
337
338 int pixels[];
339 int[] bandmasks = new int[3];
340
341 pixels = peer.getRGBPixels(translatedRect);
342 buffer = new DataBufferInt(pixels, pixels.length);
343
344 bandmasks[0] = screenCapCM.getRedMask();
345 bandmasks[1] = screenCapCM.getGreenMask();
346 bandmasks[2] = screenCapCM.getBlueMask();
347
348 raster = Raster.createPackedRaster(buffer, translatedRect.width, translatedRect.height, translatedRect.width, bandmasks, null);
349
350 image = new BufferedImage(screenCapCM, raster, false, null);
351
352 return image;
353 }
354
355 private static void checkValidRect(Rectangle rect) {
356 if (rect.width <= 0 || rect.height <= 0) {
357 throw new IllegalArgumentException("Rectangle width and height must be > 0");
358 }
359 }
360
361 private static void checkScreenCaptureAllowed() {
362 SecurityManager security = System.getSecurityManager();
363 if (security != null) {
364 security.checkPermission(
365 SecurityConstants.READ_DISPLAY_PIXELS_PERMISSION);
366 }
367 }
368
369 /*
370 * Called after an event is generated
371 */
372 private void afterEvent() {
373 autoWaitForIdle();
374 autoDelay();
375 }
376
377 /**
378 * Returns whether this Robot automatically invokes <code>waitForIdle</code>
379 * after generating an event.
380 * @return Whether <code>waitForIdle</code> is automatically called
381 */
382 public synchronized boolean isAutoWaitForIdle() {
383 return isAutoWaitForIdle;
384 }
385
386 /**
387 * Sets whether this Robot automatically invokes <code>waitForIdle</code>
388 * after generating an event.
389 * @param isOn Whether <code>waitForIdle</code> is automatically invoked
390 */
391 public synchronized void setAutoWaitForIdle(boolean isOn) {
392 isAutoWaitForIdle = isOn;
393 }
394
395 /*
396 * Calls waitForIdle after every event if so desired.
397 */
398 private void autoWaitForIdle() {
399 if (isAutoWaitForIdle) {
400 waitForIdle();
401 }
402 }
403
404 /**
405 * Returns the number of milliseconds this Robot sleeps after generating an event.
406 */
407 public synchronized int getAutoDelay() {
408 return autoDelay;
409 }
410
411 /**
412 * Sets the number of milliseconds this Robot sleeps after generating an event.
413 * @throws IllegalArgumentException If <code>ms</code> is not between 0 and 60,000 milliseconds inclusive
414 */
415 public synchronized void setAutoDelay(int ms) {
416 checkDelayArgument(ms);
417 autoDelay = ms;
418 }
419
420 /*
421 * Automatically sleeps for the specified interval after event generated.
422 */
423 private void autoDelay() {
424 delay(autoDelay);
425 }
426
427 /**
428 * Sleeps for the specified time.
429 * To catch any <code>InterruptedException</code>s that occur,
430 * <code>Thread.sleep()</code> may be used instead.
431 * @param ms time to sleep in milliseconds
432 * @throws IllegalArgumentException if <code>ms</code> is not between 0 and 60,000 milliseconds inclusive
433 * @see java.lang.Thread#sleep
434 */
435 public synchronized void delay(int ms) {
436 checkDelayArgument(ms);
437 try {
438 Thread.sleep(ms);
439 } catch(InterruptedException ite) {
440 ite.printStackTrace();
441 }
442 }
443
444 private void checkDelayArgument(int ms) {
445 if (ms < 0 || ms > MAX_DELAY) {
446 throw new IllegalArgumentException("Delay must be to 0 to 60,000ms");
447 }
448 }
449
450 /**
451 * Waits until all events currently on the event queue have been processed.
452 * @throws IllegalThreadStateException if called on the AWT event dispatching thread
453 */
454 public synchronized void waitForIdle() {
455 checkNotDispatchThread();
456 // post a dummy event to the queue so we know when
457 // all the events before it have been processed
458 try {
459 SunToolkit.flushPendingEvents();
460 EventQueue.invokeAndWait( new Runnable() {
461 public void run() {
462 // dummy implementation
463 }
464 } );
465 } catch(InterruptedException ite) {
466 System.err.println("Robot.waitForIdle, non-fatal exception caught:");
467 ite.printStackTrace();
468 } catch(InvocationTargetException ine) {
469 System.err.println("Robot.waitForIdle, non-fatal exception caught:");
470 ine.printStackTrace();
471 }
472 }
473
474 private void checkNotDispatchThread() {
475 if (EventQueue.isDispatchThread()) {
476 throw new IllegalThreadStateException("Cannot call method from the event dispatcher thread");
477 }
478 }
479
480 /**
481 * Returns a string representation of this Robot.
482 *
483 * @return the string representation.
484 */
485 public synchronized String toString() {
486 String params = "autoDelay = "+getAutoDelay()+", "+"autoWaitForIdle = "+isAutoWaitForIdle();
487 return getClass().getName() + "[ " + params + " ]";
488 }
489 }