mirror of
https://github.com/openjdk/jdk.git
synced 2026-04-27 15:20:53 +00:00
1004 lines
41 KiB
Java
1004 lines
41 KiB
Java
/*
|
|
* Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package java.awt;
|
|
|
|
import java.awt.event.InputEvent;
|
|
import java.awt.event.KeyEvent;
|
|
import java.awt.geom.AffineTransform;
|
|
import java.awt.image.BaseMultiResolutionImage;
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.DataBufferInt;
|
|
import java.awt.image.DirectColorModel;
|
|
import java.awt.image.MultiResolutionImage;
|
|
import java.awt.image.Raster;
|
|
import java.awt.image.WritableRaster;
|
|
import java.awt.peer.RobotPeer;
|
|
|
|
import sun.awt.ComponentFactory;
|
|
import sun.awt.SunToolkit;
|
|
import sun.awt.image.SunWritableRaster;
|
|
import sun.java2d.SunGraphicsEnvironment;
|
|
|
|
import static sun.java2d.SunGraphicsEnvironment.toDeviceSpace;
|
|
import static sun.java2d.SunGraphicsEnvironment.toDeviceSpaceAbs;
|
|
|
|
/**
|
|
* This class is used to generate native system input events
|
|
* for the purposes of test automation, self-running demos, and
|
|
* other applications where control of the mouse and keyboard
|
|
* is needed. The primary purpose of Robot is to facilitate
|
|
* automated testing of Java platform implementations.
|
|
* <p>
|
|
* Using the class to generate input events differs from posting
|
|
* events to the AWT event queue or AWT components in that the
|
|
* events are generated in the platform's native input
|
|
* queue. For example, {@code Robot.mouseMove} will actually move
|
|
* the mouse cursor instead of just generating mouse move events.
|
|
*
|
|
* @apiNote When {@code autoWaitForIdle()} is enabled, mouse and key related methods
|
|
* cannot be called on the AWT EDT. This is because when {@code autoWaitForIdle()}
|
|
* is enabled, the mouse and key methods implicitly call {@code waitForIdle()}
|
|
* which will throw {@code IllegalThreadStateException} when called on the AWT EDT.
|
|
* In addition, screen capture operations can be lengthy
|
|
* and {@code delay(long ms)} clearly inserts a delay, so these also
|
|
* should not be called on the EDT. Taken together, this means that as much as possible,
|
|
* methods on this class should not be called on the EDT.
|
|
* <p>
|
|
* Note that some platforms require special privileges or extensions
|
|
* to access low-level input control. If the current platform configuration
|
|
* does not allow input control, an {@code AWTException} will be thrown
|
|
* when trying to construct Robot objects. For example, X-Window systems
|
|
* will throw the exception if the XTEST 2.2 standard extension is not supported
|
|
* (or not enabled) by the X server.
|
|
* <p>
|
|
* Applications that use Robot for purposes other than self-testing should
|
|
* handle these error conditions gracefully.
|
|
* <p>
|
|
* Platforms and desktop environments may impose restrictions or limitations
|
|
* on the access required to implement all functionality in the Robot class.
|
|
* For example:
|
|
* <ul>
|
|
* <li> preventing access to the contents of any part of a desktop
|
|
* or Window on the desktop that is not owned by the running application.</li>
|
|
* <li> treating window decorations as non-owned content.</li>
|
|
* <li> ignoring or limiting specific requests to manipulate windows.</li>
|
|
* <li> ignoring or limiting specific requests for Robot generated (synthesized)
|
|
* events related to keyboard and mouse etc.</li>
|
|
* <li> requiring specific or global permissions to any access to window
|
|
* contents, even application owned content,or to perform even limited
|
|
* synthesizing of events.</li>
|
|
* </ul>
|
|
*
|
|
* The Robot API specification requires that approvals for these be granted
|
|
* for full operation.
|
|
* If they are not granted, the API will be degraded as discussed here.
|
|
* Relevant specific API methods may document more specific limitations
|
|
* and requirements.
|
|
* Depending on the policies of the desktop environment,
|
|
* the approvals mentioned above may:
|
|
* <ul>
|
|
* <li>be required every time</li>
|
|
* <li>or persistent for the lifetime of an application,</li>
|
|
* <li>or persistent across multiple user desktop sessions</li>
|
|
* <li>be fine-grained permissions</li>
|
|
* <li>be associated with a specific binary application,
|
|
* or a class of binary applications.</li>
|
|
* </ul>
|
|
*
|
|
* When such approvals need to given interactively, it may impede the normal
|
|
* operation of the application until approved, and if approval is denied
|
|
* or not possible, or cannot be made persistent then it will degrade
|
|
* the functionality of this class and in turn any part of the operation
|
|
* of the application which is dependent on it.
|
|
*
|
|
* @author Robi Khan
|
|
* @since 1.3
|
|
*/
|
|
public class Robot {
|
|
private static final int MAX_DELAY = 60000;
|
|
private RobotPeer peer;
|
|
private boolean isAutoWaitForIdle = false;
|
|
private int autoDelay = 0;
|
|
private static int LEGAL_BUTTON_MASK = 0;
|
|
|
|
private DirectColorModel screenCapCM = null;
|
|
|
|
/**
|
|
* Default delay in milliseconds for mouse
|
|
* {@link #glide(int, int, int, int) glide},
|
|
* {@link #type(int) type}, and
|
|
* {@link #click(int) click}.
|
|
*/
|
|
public static final int DEFAULT_DELAY = 20;
|
|
|
|
/**
|
|
* Default pixel step-length for mouse
|
|
* {@link #glide(int, int, int, int) glide}.
|
|
*/
|
|
public static final int DEFAULT_STEP_LENGTH = 2;
|
|
|
|
/**
|
|
* Constructs a Robot object in the coordinate system of the primary screen.
|
|
*
|
|
* @throws AWTException if the platform configuration does not allow
|
|
* low-level input control. This exception is always thrown when
|
|
* GraphicsEnvironment.isHeadless() returns true
|
|
* @see java.awt.GraphicsEnvironment#isHeadless
|
|
*/
|
|
public Robot() throws AWTException {
|
|
checkHeadless();
|
|
init(GraphicsEnvironment.getLocalGraphicsEnvironment()
|
|
.getDefaultScreenDevice());
|
|
}
|
|
|
|
/**
|
|
* Creates a Robot for the given screen device. Coordinates passed
|
|
* to Robot method calls like mouseMove, getPixelColor and
|
|
* createScreenCapture will be interpreted as being in the same coordinate
|
|
* system as the specified screen. Note that depending on the platform
|
|
* configuration, multiple screens may either:
|
|
* <ul>
|
|
* <li>share the same coordinate system to form a combined virtual screen</li>
|
|
* <li>use different coordinate systems to act as independent screens</li>
|
|
* </ul>
|
|
* <p>
|
|
* If screen devices are reconfigured such that the coordinate system is
|
|
* affected, the behavior of existing Robot objects is undefined.
|
|
*
|
|
* @param screen A screen GraphicsDevice indicating the coordinate
|
|
* system the Robot will operate in.
|
|
* @throws AWTException if the platform configuration does not allow
|
|
* low-level input control. This exception is always thrown when
|
|
* GraphicsEnvironment.isHeadless() returns true.
|
|
* @throws IllegalArgumentException if {@code screen} is not a screen
|
|
* GraphicsDevice.
|
|
* @see java.awt.GraphicsEnvironment#isHeadless
|
|
* @see GraphicsDevice
|
|
*/
|
|
public Robot(GraphicsDevice screen) throws AWTException {
|
|
checkHeadless();
|
|
checkIsScreenDevice(screen);
|
|
init(screen);
|
|
}
|
|
|
|
private void init(GraphicsDevice screen) throws AWTException {
|
|
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
|
if (toolkit instanceof ComponentFactory) {
|
|
peer = ((ComponentFactory)toolkit).createRobot(screen);
|
|
}
|
|
initLegalButtonMask();
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
private static synchronized void initLegalButtonMask() {
|
|
if (LEGAL_BUTTON_MASK != 0) return;
|
|
|
|
int tmpMask = 0;
|
|
if (Toolkit.getDefaultToolkit().areExtraMouseButtonsEnabled()){
|
|
if (Toolkit.getDefaultToolkit() instanceof SunToolkit) {
|
|
final int buttonsNumber = ((SunToolkit)(Toolkit.getDefaultToolkit())).getNumberOfButtons();
|
|
for (int i = 0; i < buttonsNumber; i++){
|
|
tmpMask |= InputEvent.getMaskForButton(i+1);
|
|
}
|
|
}
|
|
}
|
|
tmpMask |= InputEvent.BUTTON1_MASK|
|
|
InputEvent.BUTTON2_MASK|
|
|
InputEvent.BUTTON3_MASK|
|
|
InputEvent.BUTTON1_DOWN_MASK|
|
|
InputEvent.BUTTON2_DOWN_MASK|
|
|
InputEvent.BUTTON3_DOWN_MASK;
|
|
LEGAL_BUTTON_MASK = tmpMask;
|
|
}
|
|
|
|
/**
|
|
* Check for headless state and throw {@code AWTException} if headless.
|
|
*/
|
|
private static void checkHeadless() throws AWTException {
|
|
if (GraphicsEnvironment.isHeadless()) {
|
|
throw new AWTException("headless environment");
|
|
}
|
|
}
|
|
|
|
/* check if the given device is a screen device */
|
|
private static void checkIsScreenDevice(GraphicsDevice device) {
|
|
if (device == null || device.getType() != GraphicsDevice.TYPE_RASTER_SCREEN) {
|
|
throw new IllegalArgumentException("not a valid screen device");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves mouse pointer to given screen coordinates.
|
|
* <p>
|
|
* The mouse pointer may not visually move on some platforms,
|
|
* while the subsequent mousePress and mouseRelease can be
|
|
* delivered to the correct location
|
|
*
|
|
* @param x X position
|
|
* @param y Y position
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching
|
|
* thread and {@code isAutoWaitForIdle} would return true
|
|
*/
|
|
public synchronized void mouseMove(int x, int y) {
|
|
peer.mouseMove(x, y);
|
|
afterEvent();
|
|
}
|
|
|
|
/**
|
|
* Presses one or more mouse buttons. The mouse buttons should
|
|
* be released using the {@link #mouseRelease(int)} method.
|
|
*
|
|
* @param buttons the Button mask; a combination of one or more
|
|
* mouse button masks.
|
|
* <p>
|
|
* It is allowed to use only a combination of valid values as a {@code buttons} parameter.
|
|
* A valid combination consists of {@code InputEvent.BUTTON1_DOWN_MASK},
|
|
* {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK}
|
|
* and values returned by the
|
|
* {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method.
|
|
*
|
|
* The valid combination also depends on a
|
|
* {@link Toolkit#areExtraMouseButtonsEnabled() Toolkit.areExtraMouseButtonsEnabled()} value as follows:
|
|
* <ul>
|
|
* <li> If support for extended mouse buttons is
|
|
* {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java
|
|
* then it is allowed to use only the following standard button masks:
|
|
* {@code InputEvent.BUTTON1_DOWN_MASK}, {@code InputEvent.BUTTON2_DOWN_MASK},
|
|
* {@code InputEvent.BUTTON3_DOWN_MASK}.
|
|
* <li> If support for extended mouse buttons is
|
|
* {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java
|
|
* then it is allowed to use the standard button masks
|
|
* and masks for existing extended mouse buttons, if the mouse has more then three buttons.
|
|
* In that way, it is allowed to use the button masks corresponding to the buttons
|
|
* in the range from 1 to {@link java.awt.MouseInfo#getNumberOfButtons() MouseInfo.getNumberOfButtons()}.
|
|
* <br>
|
|
* It is recommended to use the {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)}
|
|
* method to obtain the mask for any mouse button by its number.
|
|
* </ul>
|
|
* <p>
|
|
* The following standard button masks are also accepted:
|
|
* <ul>
|
|
* <li>{@code InputEvent.BUTTON1_MASK}
|
|
* <li>{@code InputEvent.BUTTON2_MASK}
|
|
* <li>{@code InputEvent.BUTTON3_MASK}
|
|
* </ul>
|
|
* However, it is recommended to use {@code InputEvent.BUTTON1_DOWN_MASK},
|
|
* {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} instead.
|
|
* Either extended {@code _DOWN_MASK} or old {@code _MASK} values
|
|
* should be used, but both those models should not be mixed.
|
|
* @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button
|
|
* and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java
|
|
* @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button
|
|
* that does not exist on the mouse and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching thread and {@code isAutoWaitForIdle} would return true
|
|
* @see #mouseRelease(int)
|
|
* @see InputEvent#getMaskForButton(int)
|
|
* @see Toolkit#areExtraMouseButtonsEnabled()
|
|
* @see java.awt.MouseInfo#getNumberOfButtons()
|
|
* @see java.awt.event.MouseEvent
|
|
*/
|
|
public synchronized void mousePress(int buttons) {
|
|
checkButtonsArgument(buttons);
|
|
peer.mousePress(buttons);
|
|
afterEvent();
|
|
}
|
|
|
|
/**
|
|
* Releases one or more mouse buttons.
|
|
*
|
|
* @param buttons the Button mask; a combination of one or more
|
|
* mouse button masks.
|
|
* <p>
|
|
* It is allowed to use only a combination of valid values as a {@code buttons} parameter.
|
|
* A valid combination consists of {@code InputEvent.BUTTON1_DOWN_MASK},
|
|
* {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK}
|
|
* and values returned by the
|
|
* {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method.
|
|
*
|
|
* The valid combination also depends on a
|
|
* {@link Toolkit#areExtraMouseButtonsEnabled() Toolkit.areExtraMouseButtonsEnabled()} value as follows:
|
|
* <ul>
|
|
* <li> If the support for extended mouse buttons is
|
|
* {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java
|
|
* then it is allowed to use only the following standard button masks:
|
|
* {@code InputEvent.BUTTON1_DOWN_MASK}, {@code InputEvent.BUTTON2_DOWN_MASK},
|
|
* {@code InputEvent.BUTTON3_DOWN_MASK}.
|
|
* <li> If the support for extended mouse buttons is
|
|
* {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java
|
|
* then it is allowed to use the standard button masks
|
|
* and masks for existing extended mouse buttons, if the mouse has more then three buttons.
|
|
* In that way, it is allowed to use the button masks corresponding to the buttons
|
|
* in the range from 1 to {@link java.awt.MouseInfo#getNumberOfButtons() MouseInfo.getNumberOfButtons()}.
|
|
* <br>
|
|
* It is recommended to use the {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)}
|
|
* method to obtain the mask for any mouse button by its number.
|
|
* </ul>
|
|
* <p>
|
|
* The following standard button masks are also accepted:
|
|
* <ul>
|
|
* <li>{@code InputEvent.BUTTON1_MASK}
|
|
* <li>{@code InputEvent.BUTTON2_MASK}
|
|
* <li>{@code InputEvent.BUTTON3_MASK}
|
|
* </ul>
|
|
* However, it is recommended to use {@code InputEvent.BUTTON1_DOWN_MASK},
|
|
* {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} instead.
|
|
* Either extended {@code _DOWN_MASK} or old {@code _MASK} values
|
|
* should be used, but both those models should not be mixed.
|
|
* @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button
|
|
* and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java
|
|
* @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse button
|
|
* that does not exist on the mouse and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching thread and {@code isAutoWaitForIdle} would return true
|
|
* @see #mousePress(int)
|
|
* @see InputEvent#getMaskForButton(int)
|
|
* @see Toolkit#areExtraMouseButtonsEnabled()
|
|
* @see java.awt.MouseInfo#getNumberOfButtons()
|
|
* @see java.awt.event.MouseEvent
|
|
*/
|
|
public synchronized void mouseRelease(int buttons) {
|
|
checkButtonsArgument(buttons);
|
|
peer.mouseRelease(buttons);
|
|
afterEvent();
|
|
}
|
|
|
|
private static void checkButtonsArgument(int buttons) {
|
|
if ( (buttons|LEGAL_BUTTON_MASK) != LEGAL_BUTTON_MASK ) {
|
|
throw new IllegalArgumentException("Invalid combination of button flags");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rotates the scroll wheel on wheel-equipped mice.
|
|
*
|
|
* @param wheelAmt number of "notches" to move the mouse wheel
|
|
* Negative values indicate movement up/away from the user,
|
|
* positive values indicate movement down/towards the user.
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching
|
|
* thread and {@code isAutoWaitForIdle} would return true
|
|
*
|
|
* @since 1.4
|
|
*/
|
|
public synchronized void mouseWheel(int wheelAmt) {
|
|
peer.mouseWheel(wheelAmt);
|
|
afterEvent();
|
|
}
|
|
|
|
/**
|
|
* Presses a given key. The key should be released using the
|
|
* {@code keyRelease} method.
|
|
* <p>
|
|
* Key codes that have more than one physical key associated with them
|
|
* (e.g. {@code KeyEvent.VK_SHIFT} could mean either the
|
|
* left or right shift key) will map to the left key.
|
|
*
|
|
* @param keycode Key to press (e.g. {@code KeyEvent.VK_A})
|
|
* @throws IllegalArgumentException if {@code keycode} is not
|
|
* a valid key
|
|
* @throws IllegalThreadStateException if called on the AWT event
|
|
* dispatching thread and {@code isAutoWaitForIdle} would return true
|
|
* @see #keyRelease(int)
|
|
* @see java.awt.event.KeyEvent
|
|
*/
|
|
public synchronized void keyPress(int keycode) {
|
|
checkKeycodeArgument(keycode);
|
|
peer.keyPress(keycode);
|
|
afterEvent();
|
|
}
|
|
|
|
/**
|
|
* Releases a given key.
|
|
* <p>
|
|
* Key codes that have more than one physical key associated with them
|
|
* (e.g. {@code KeyEvent.VK_SHIFT} could mean either the
|
|
* left or right shift key) will map to the left key.
|
|
*
|
|
* @param keycode Key to release (e.g. {@code KeyEvent.VK_A})
|
|
* @throws IllegalArgumentException if {@code keycode} is not a
|
|
* valid key
|
|
* @throws IllegalThreadStateException if called on the AWT event
|
|
* dispatching thread and {@code isAutoWaitForIdle} would return true
|
|
* @see #keyPress(int)
|
|
* @see java.awt.event.KeyEvent
|
|
*/
|
|
public synchronized void keyRelease(int keycode) {
|
|
checkKeycodeArgument(keycode);
|
|
peer.keyRelease(keycode);
|
|
afterEvent();
|
|
}
|
|
|
|
private static void checkKeycodeArgument(int keycode) {
|
|
// rather than build a big table or switch statement here, we'll
|
|
// just check that the key isn't VK_UNDEFINED and assume that the
|
|
// peer implementations will throw an exception for other bogus
|
|
// values e.g. -1, 999999
|
|
if (keycode == KeyEvent.VK_UNDEFINED) {
|
|
throw new IllegalArgumentException("Invalid key code");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the color of a pixel at the given screen coordinates.
|
|
* <p>
|
|
* If the desktop environment requires that permissions be granted
|
|
* to capture screen content, and the required permissions are not granted,
|
|
* then a {@code SecurityException} may be thrown,
|
|
* or the content of the returned {@code Color} is undefined.
|
|
* </p>
|
|
* @apiNote It is recommended to avoid calling this method on
|
|
* the AWT Event Dispatch Thread since screen capture may be a lengthy
|
|
* operation, particularly if acquiring permissions is needed and involves
|
|
* user interaction.
|
|
*
|
|
* @param x X position of pixel
|
|
* @param y Y position of pixel
|
|
* @throws SecurityException if access to the screen is denied
|
|
* by the desktop environment
|
|
* @return Color of the pixel
|
|
*/
|
|
public synchronized Color getPixelColor(int x, int y) {
|
|
Point point = peer.useAbsoluteCoordinates() ? toDeviceSpaceAbs(x, y)
|
|
: toDeviceSpace(x, y);
|
|
return new Color(peer.getRGBPixel(point.x, point.y));
|
|
}
|
|
|
|
/**
|
|
* Creates an image containing pixels read from the screen.
|
|
* <p>
|
|
* If the desktop environment requires that permissions be granted
|
|
* to capture screen content, and the required permissions are not granted,
|
|
* then a {@code SecurityException} may be thrown,
|
|
* or the contents of the returned {@code BufferedImage} are undefined.
|
|
* </p>
|
|
* @apiNote It is recommended to avoid calling this method on
|
|
* the AWT Event Dispatch Thread since screen capture may be a lengthy
|
|
* operation, particularly if acquiring permissions is needed and involves
|
|
* user interaction.
|
|
*
|
|
* @param screenRect Rect to capture in screen coordinates
|
|
* @return The captured image
|
|
* @throws IllegalArgumentException if {@code screenRect} width and height
|
|
* are not greater than zero
|
|
* @throws SecurityException if access to the screen is denied
|
|
* by the desktop environment
|
|
*/
|
|
public synchronized BufferedImage createScreenCapture(Rectangle screenRect) {
|
|
return createCompatibleImage(screenRect, false)[0];
|
|
}
|
|
|
|
/**
|
|
* Creates an image containing pixels read from the screen.
|
|
* This method can be used in case there is a scaling transform
|
|
* from user space to screen (device) space.
|
|
* Typically this means that the display is a high resolution screen,
|
|
* although strictly it means any case in which there is such a transform.
|
|
* Returns a {@link java.awt.image.MultiResolutionImage}.
|
|
* <p>
|
|
* For a non-scaled display, the {@code MultiResolutionImage}
|
|
* will have one image variant:
|
|
* <ul>
|
|
* <li> Base Image with user specified size.
|
|
* </ul>
|
|
* <p>
|
|
* For a high resolution display where there is a scaling transform,
|
|
* the {@code MultiResolutionImage} will have two image variants:
|
|
* <ul>
|
|
* <li> Base Image with user specified size. This is scaled from the screen.
|
|
* <li> Native device resolution image with device size pixels.
|
|
* </ul>
|
|
* <p>
|
|
* Example:
|
|
* <pre>{@code
|
|
* Image nativeResImage;
|
|
* MultiResolutionImage mrImage = robot.createMultiResolutionScreenCapture(frame.getBounds());
|
|
* List<Image> resolutionVariants = mrImage.getResolutionVariants();
|
|
* if (resolutionVariants.size() > 1) {
|
|
* nativeResImage = resolutionVariants.get(1);
|
|
* } else {
|
|
* nativeResImage = resolutionVariants.get(0);
|
|
* }
|
|
* }</pre>
|
|
*
|
|
* @apiNote It is recommended to avoid calling this method on
|
|
* the AWT Event Dispatch Thread since screen capture may be a lengthy
|
|
* operation, particularly if acquiring permissions is needed and involves
|
|
* user interaction.
|
|
*
|
|
* @param screenRect Rect to capture in screen coordinates
|
|
* @return The captured image
|
|
* @throws IllegalArgumentException if {@code screenRect} width and height
|
|
* are not greater than zero
|
|
* @throws SecurityException if access to the screen is denied
|
|
* by the desktop environment
|
|
*
|
|
* @since 9
|
|
*/
|
|
public synchronized MultiResolutionImage
|
|
createMultiResolutionScreenCapture(Rectangle screenRect) {
|
|
|
|
return new BaseMultiResolutionImage(
|
|
createCompatibleImage(screenRect, true));
|
|
}
|
|
|
|
private synchronized BufferedImage[]
|
|
createCompatibleImage(Rectangle screenRect, boolean isHiDPI) {
|
|
|
|
checkValidRect(screenRect);
|
|
|
|
BufferedImage lowResolutionImage;
|
|
BufferedImage highResolutionImage;
|
|
DataBufferInt buffer;
|
|
WritableRaster raster;
|
|
BufferedImage[] imageArray;
|
|
|
|
if (screenCapCM == null) {
|
|
/*
|
|
* Fix for 4285201
|
|
* Create a DirectColorModel equivalent to the default RGB ColorModel,
|
|
* except with no Alpha component.
|
|
*/
|
|
|
|
screenCapCM = new DirectColorModel(24,
|
|
/* red mask */ 0x00FF0000,
|
|
/* green mask */ 0x0000FF00,
|
|
/* blue mask */ 0x000000FF);
|
|
}
|
|
|
|
int[] bandmasks = new int[3];
|
|
bandmasks[0] = screenCapCM.getRedMask();
|
|
bandmasks[1] = screenCapCM.getGreenMask();
|
|
bandmasks[2] = screenCapCM.getBlueMask();
|
|
|
|
// need to sync the toolkit prior to grabbing the pixels since in some
|
|
// cases rendering to the screen may be delayed
|
|
Toolkit.getDefaultToolkit().sync();
|
|
|
|
GraphicsConfiguration gc = GraphicsEnvironment
|
|
.getLocalGraphicsEnvironment()
|
|
.getDefaultScreenDevice().
|
|
getDefaultConfiguration();
|
|
gc = SunGraphicsEnvironment.getGraphicsConfigurationAtPoint(
|
|
gc, screenRect.getCenterX(), screenRect.getCenterY());
|
|
|
|
AffineTransform tx = gc.getDefaultTransform();
|
|
double uiScaleX = tx.getScaleX();
|
|
double uiScaleY = tx.getScaleY();
|
|
int[] pixels;
|
|
|
|
if (uiScaleX == 1 && uiScaleY == 1) {
|
|
|
|
pixels = peer.getRGBPixels(screenRect);
|
|
buffer = new DataBufferInt(pixels, pixels.length);
|
|
|
|
bandmasks[0] = screenCapCM.getRedMask();
|
|
bandmasks[1] = screenCapCM.getGreenMask();
|
|
bandmasks[2] = screenCapCM.getBlueMask();
|
|
|
|
raster = Raster.createPackedRaster(buffer, screenRect.width,
|
|
screenRect.height, screenRect.width, bandmasks, null);
|
|
SunWritableRaster.makeTrackable(buffer);
|
|
|
|
highResolutionImage = new BufferedImage(screenCapCM, raster,
|
|
false, null);
|
|
imageArray = new BufferedImage[1];
|
|
imageArray[0] = highResolutionImage;
|
|
|
|
} else {
|
|
Rectangle scaledRect;
|
|
if (peer.useAbsoluteCoordinates()) {
|
|
scaledRect = toDeviceSpaceAbs(gc, screenRect.x,
|
|
screenRect.y, screenRect.width, screenRect.height);
|
|
} else {
|
|
scaledRect = toDeviceSpace(gc, screenRect.x,
|
|
screenRect.y, screenRect.width, screenRect.height);
|
|
}
|
|
// HighResolutionImage
|
|
pixels = peer.getRGBPixels(scaledRect);
|
|
buffer = new DataBufferInt(pixels, pixels.length);
|
|
raster = Raster.createPackedRaster(buffer, scaledRect.width,
|
|
scaledRect.height, scaledRect.width, bandmasks, null);
|
|
SunWritableRaster.makeTrackable(buffer);
|
|
|
|
highResolutionImage = new BufferedImage(screenCapCM, raster,
|
|
false, null);
|
|
|
|
|
|
// LowResolutionImage
|
|
lowResolutionImage = new BufferedImage(screenRect.width,
|
|
screenRect.height, highResolutionImage.getType());
|
|
Graphics2D g = lowResolutionImage.createGraphics();
|
|
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
|
|
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
|
g.setRenderingHint(RenderingHints.KEY_RENDERING,
|
|
RenderingHints.VALUE_RENDER_QUALITY);
|
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
|
RenderingHints.VALUE_ANTIALIAS_ON);
|
|
g.drawImage(highResolutionImage, 0, 0,
|
|
screenRect.width, screenRect.height,
|
|
0, 0, scaledRect.width, scaledRect.height, null);
|
|
g.dispose();
|
|
|
|
if(!isHiDPI) {
|
|
imageArray = new BufferedImage[1];
|
|
imageArray[0] = lowResolutionImage;
|
|
} else {
|
|
imageArray = new BufferedImage[2];
|
|
imageArray[0] = lowResolutionImage;
|
|
imageArray[1] = highResolutionImage;
|
|
}
|
|
|
|
}
|
|
|
|
return imageArray;
|
|
}
|
|
|
|
private static void checkValidRect(Rectangle rect) {
|
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
throw new IllegalArgumentException("Rectangle width and height must be > 0");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Called after an event is generated
|
|
*/
|
|
private void afterEvent() {
|
|
autoWaitForIdle();
|
|
autoDelay();
|
|
}
|
|
|
|
/**
|
|
* Returns whether this Robot automatically invokes {@code waitForIdle}
|
|
* after generating an event.
|
|
* @return Whether {@code waitForIdle} is automatically called
|
|
*/
|
|
public synchronized boolean isAutoWaitForIdle() {
|
|
return isAutoWaitForIdle;
|
|
}
|
|
|
|
/**
|
|
* Sets whether this Robot automatically invokes {@code waitForIdle}
|
|
* after generating an event.
|
|
*
|
|
* @apiNote Setting this to true means you cannot call mouse and key-controlling events
|
|
* on the AWT Event Dispatching Thread
|
|
*
|
|
* @param isOn Whether {@code waitForIdle} is automatically invoked
|
|
*/
|
|
public synchronized void setAutoWaitForIdle(boolean isOn) {
|
|
isAutoWaitForIdle = isOn;
|
|
}
|
|
|
|
/*
|
|
* Calls waitForIdle after every event if so desired.
|
|
*/
|
|
private void autoWaitForIdle() {
|
|
if (isAutoWaitForIdle) {
|
|
waitForIdle();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the number of milliseconds this Robot sleeps after generating an event.
|
|
*
|
|
* @return the delay duration in milliseconds
|
|
*/
|
|
public synchronized int getAutoDelay() {
|
|
return autoDelay;
|
|
}
|
|
|
|
/**
|
|
* Sets the number of milliseconds this Robot sleeps after generating an event.
|
|
*
|
|
* @param ms the delay duration in milliseconds
|
|
* @throws IllegalArgumentException If {@code ms}
|
|
* is not between 0 and 60,000 milliseconds inclusive
|
|
*/
|
|
public synchronized void setAutoDelay(int ms) {
|
|
checkDelayArgument(ms);
|
|
autoDelay = ms;
|
|
}
|
|
|
|
/*
|
|
* Automatically sleeps for the specified interval after event generated.
|
|
*/
|
|
private void autoDelay() {
|
|
delay(autoDelay);
|
|
}
|
|
|
|
/**
|
|
* Sleeps for the specified time.
|
|
* <p>
|
|
* If the invoking thread is interrupted while waiting, then it will return
|
|
* immediately with the interrupted status set. If the interrupted status is
|
|
* already set, this method returns immediately with the interrupted status
|
|
* set.
|
|
*
|
|
* @apiNote It is recommended to avoid calling this method on
|
|
* the AWT Event Dispatch Thread since delay may be a lengthy
|
|
* operation.
|
|
*
|
|
* @param ms time to sleep in milliseconds
|
|
* @throws IllegalArgumentException if {@code ms} is not between {@code 0}
|
|
* and {@code 60,000} milliseconds inclusive
|
|
*/
|
|
public void delay(int ms) {
|
|
checkDelayArgument(ms);
|
|
Thread thread = Thread.currentThread();
|
|
if (!thread.isInterrupted()) {
|
|
try {
|
|
Thread.sleep(ms);
|
|
} catch (final InterruptedException ignored) {
|
|
thread.interrupt(); // Preserve interrupted status
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void checkDelayArgument(int ms) {
|
|
if (ms < 0 || ms > MAX_DELAY) {
|
|
throw new IllegalArgumentException("Delay must be to 0 to 60,000ms");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Waits until all events currently on the event queue have been processed.
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching thread
|
|
*/
|
|
public synchronized void waitForIdle() {
|
|
checkNotDispatchThread();
|
|
SunToolkit.flushPendingEvents();
|
|
((SunToolkit) Toolkit.getDefaultToolkit()).realSync();
|
|
}
|
|
|
|
private static void checkNotDispatchThread() {
|
|
if (EventQueue.isDispatchThread()) {
|
|
throw new IllegalThreadStateException("Cannot call method from the event dispatcher thread");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a string representation of this Robot.
|
|
*
|
|
* @return the string representation.
|
|
*/
|
|
@Override
|
|
public synchronized String toString() {
|
|
String params = "autoDelay = "+getAutoDelay()+", "+"autoWaitForIdle = "+isAutoWaitForIdle();
|
|
return getClass().getName() + "[ " + params + " ]";
|
|
}
|
|
|
|
/**
|
|
* A convenience method that simulates clicking a mouse button by calling {@code mousePress},
|
|
* {@code mouseRelease} and {@code waitForIdle}. Invokes {@code waitForIdle} with a default
|
|
* delay of {@value #DEFAULT_DELAY} milliseconds after {@code mousePress} and {@code mouseRelease}
|
|
* calls. For specifics on valid inputs see {@link java.awt.Robot#mousePress(int)}.
|
|
*
|
|
* @param buttons The button mask; a combination of one or more mouse button masks.
|
|
* @throws IllegalArgumentException if the {@code buttons} mask contains the mask for
|
|
* extra mouse button and support for extended mouse buttons is
|
|
* {@linkplain Toolkit#areExtraMouseButtonsEnabled() disabled} by Java
|
|
* @throws IllegalArgumentException if the {@code buttons} mask contains the mask for
|
|
* extra mouse button that does not exist on the mouse and support for extended
|
|
* mouse buttons is {@linkplain Toolkit#areExtraMouseButtonsEnabled() enabled}
|
|
* by Java
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching thread
|
|
* @see #DEFAULT_DELAY
|
|
* @see #mousePress(int)
|
|
* @see #mouseRelease(int)
|
|
* @see InputEvent#getMaskForButton(int)
|
|
* @see Toolkit#areExtraMouseButtonsEnabled()
|
|
* @see java.awt.event.MouseEvent
|
|
* @since 26
|
|
*/
|
|
public void click(int buttons) {
|
|
try {
|
|
mousePress(buttons);
|
|
waitForIdle(DEFAULT_DELAY);
|
|
} finally {
|
|
mouseRelease(buttons);
|
|
waitForIdle(DEFAULT_DELAY);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A convenience method that clicks mouse button 1.
|
|
*
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching thread
|
|
* @see #click(int)
|
|
* @since 26
|
|
*/
|
|
public void click() {
|
|
click(InputEvent.BUTTON1_DOWN_MASK);
|
|
}
|
|
|
|
/**
|
|
* A convenience method that calls {@code waitForIdle} then waits an additional specified
|
|
* {@code delayValue} time in milliseconds.
|
|
*
|
|
* @param delayValue Additional delay length in milliseconds to wait until thread
|
|
* sync been completed
|
|
* @throws IllegalThreadStateException if called on the AWT event
|
|
* dispatching thread
|
|
* @throws IllegalArgumentException if {@code delayValue} is not between {@code 0}
|
|
* and {@code 60,000} milliseconds inclusive
|
|
* @since 26
|
|
*/
|
|
public void waitForIdle(int delayValue) {
|
|
waitForIdle();
|
|
delay(delayValue);
|
|
}
|
|
|
|
/**
|
|
* A convenience method that moves the mouse in multiple
|
|
* steps from its current location to the destination coordinates.
|
|
*
|
|
* @implSpec Invokes {@link #mouseMove(int, int) mouseMove} with a step-length
|
|
* of {@value #DEFAULT_STEP_LENGTH} and a step-delay of {@value #DEFAULT_DELAY}.
|
|
*
|
|
* @param x Destination point x coordinate
|
|
* @param y Destination point y coordinate
|
|
*
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching
|
|
* thread and {@code isAutoWaitForIdle} would return true
|
|
* @see #DEFAULT_STEP_LENGTH
|
|
* @see #DEFAULT_DELAY
|
|
* @see #glide(int, int, int, int, int, int)
|
|
* @since 26
|
|
*/
|
|
public void glide(int x, int y) {
|
|
Point p = MouseInfo.getPointerInfo().getLocation();
|
|
glide(p.x, p.y, x, y);
|
|
}
|
|
|
|
/**
|
|
* A convenience method that moves the mouse in multiple steps
|
|
* from source coordinates to the destination coordinates.
|
|
*
|
|
* @implSpec Invokes {@link #mouseMove(int, int) mouseMove} with a step-length
|
|
* of {@value #DEFAULT_STEP_LENGTH} and a step-delay of {@value #DEFAULT_DELAY}.
|
|
*
|
|
* @param srcX Source point x coordinate
|
|
* @param srcY Source point y coordinate
|
|
* @param dstX Destination point x coordinate
|
|
* @param dstY Destination point y coordinate
|
|
*
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching
|
|
* thread and {@code isAutoWaitForIdle} would return true
|
|
* @see #DEFAULT_STEP_LENGTH
|
|
* @see #DEFAULT_DELAY
|
|
* @see #glide(int, int, int, int, int, int)
|
|
* @since 26
|
|
*/
|
|
public void glide(int srcX, int srcY, int dstX, int dstY) {
|
|
glide(srcX, srcY, dstX, dstY, DEFAULT_STEP_LENGTH, DEFAULT_DELAY);
|
|
}
|
|
|
|
/**
|
|
* A convenience method that moves the mouse in multiple
|
|
* steps from source point to the destination point with a
|
|
* given {@code stepLength} and {@code stepDelay}.
|
|
*
|
|
* @param srcX Source point x coordinate
|
|
* @param srcY Source point y coordinate
|
|
* @param destX Destination point x coordinate
|
|
* @param destY Destination point y coordinate
|
|
* @param stepLength Preferred length of one step in pixels
|
|
* @param stepDelay Delay between steps in milliseconds
|
|
*
|
|
* @throws IllegalArgumentException if {@code stepLength} is not greater than zero
|
|
* @throws IllegalArgumentException if {@code stepDelay} is not between {@code 0}
|
|
* and {@code 60,000} milliseconds inclusive
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching
|
|
* thread and {@code isAutoWaitForIdle} would return true
|
|
* @see #mouseMove(int, int)
|
|
* @see #delay(int)
|
|
* @since 26
|
|
*/
|
|
public void glide(int srcX, int srcY, int destX, int destY, int stepLength, int stepDelay) {
|
|
if (stepLength <= 0) {
|
|
throw new IllegalArgumentException("Step length must be greater than zero");
|
|
}
|
|
if (stepDelay <= 0 || stepDelay > 60000) {
|
|
throw new IllegalArgumentException("Step delay must be between 0 and 60,000 milliseconds");
|
|
}
|
|
|
|
int stepNum;
|
|
double tDx, tDy;
|
|
double dx, dy, ds;
|
|
double x, y;
|
|
|
|
dx = (destX - srcX);
|
|
dy = (destY - srcY);
|
|
ds = Math.sqrt(dx*dx + dy*dy);
|
|
|
|
tDx = dx / ds * stepLength;
|
|
tDy = dy / ds * stepLength;
|
|
|
|
int stepsCount = (int) ds / stepLength;
|
|
|
|
// Walk the mouse to the destination one step at a time
|
|
mouseMove(srcX, srcY);
|
|
|
|
for (x = srcX, y = srcY, stepNum = 0;
|
|
stepNum < stepsCount;
|
|
stepNum++) {
|
|
x += tDx;
|
|
y += tDy;
|
|
mouseMove((int)x, (int)y);
|
|
delay(stepDelay);
|
|
}
|
|
|
|
// Ensure the mouse moves to the right destination.
|
|
// The steps may have led the mouse to a slightly wrong place.
|
|
if (x != destX || y != destY) {
|
|
mouseMove(destX, destY);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A convenience method that simulates typing a key by calling {@code keyPress}
|
|
* and {@code keyRelease}. Invokes {@code waitForIdle} with a delay of {@value #DEFAULT_DELAY}
|
|
* milliseconds after {@code keyPress} and {@code keyRelease} calls.
|
|
* <p>
|
|
* Key codes that have more than one physical key associated with them
|
|
* (e.g. {@code KeyEvent.VK_SHIFT} could mean either the
|
|
* left or right shift key) will map to the left key.
|
|
*
|
|
* @param keycode Key to type (e.g. {@code KeyEvent.VK_A})
|
|
* @throws IllegalArgumentException if {@code keycode} is not
|
|
* a valid key
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching thread
|
|
* @see #DEFAULT_DELAY
|
|
* @see #keyPress(int)
|
|
* @see #keyRelease(int)
|
|
* @see java.awt.event.KeyEvent
|
|
* @since 26
|
|
*/
|
|
public synchronized void type(int keycode) {
|
|
keyPress(keycode);
|
|
waitForIdle(DEFAULT_DELAY);
|
|
keyRelease(keycode);
|
|
waitForIdle(DEFAULT_DELAY);
|
|
}
|
|
|
|
/**
|
|
* A convenience method that simulates typing a char by calling {@code keyPress}
|
|
* and {@code keyRelease}. Gets the ExtendedKeyCode for the char and calls
|
|
* {@code type(int keycode)}.
|
|
*
|
|
* @param c Character to be typed (e.g. {@code 'a'})
|
|
* @throws IllegalArgumentException if {@code keycode} is not
|
|
* a valid key
|
|
* @throws IllegalThreadStateException if called on the AWT event dispatching thread
|
|
* @see #type(int)
|
|
* @see #keyPress(int)
|
|
* @see #keyRelease(int)
|
|
* @see java.awt.event.KeyEvent
|
|
* @since 26
|
|
*/
|
|
public synchronized void type(char c) {
|
|
type(KeyEvent.getExtendedKeyCodeForChar(c));
|
|
}
|
|
}
|