/*******************************************************************************
* Copyright (c) 2013 BREDEX GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BREDEX GmbH - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.jubula.rc.javafx.driver;
import java.awt.AWTEvent;
import java.awt.AWTException;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import javax.swing.UIManager;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.eclipse.jubula.rc.common.driver.ClickOptions;
import org.eclipse.jubula.rc.common.driver.ClickOptions.ClickModifier;
import org.eclipse.jubula.rc.common.driver.DragAndDropHelper;
import org.eclipse.jubula.rc.common.driver.IEventThreadQueuer;
import org.eclipse.jubula.rc.common.driver.IMouseMotionTracker;
import org.eclipse.jubula.rc.common.driver.IRobot;
import org.eclipse.jubula.rc.common.driver.IRobotEventConfirmer;
import org.eclipse.jubula.rc.common.driver.IRunnable;
import org.eclipse.jubula.rc.common.driver.InterceptorOptions;
import org.eclipse.jubula.rc.common.driver.KeyTyper;
import org.eclipse.jubula.rc.common.driver.MouseMovementStrategy;
import org.eclipse.jubula.rc.common.driver.RobotTiming;
import org.eclipse.jubula.rc.common.exception.RobotException;
import org.eclipse.jubula.rc.common.exception.StepExecutionException;
import org.eclipse.jubula.rc.common.logger.AutServerLogger;
import org.eclipse.jubula.rc.common.tester.adapter.interfaces.IComponent;
import org.eclipse.jubula.rc.common.util.LocalScreenshotUtil;
import org.eclipse.jubula.rc.common.util.PointUtil;
import org.eclipse.jubula.rc.common.util.PropertyUtil;
import org.eclipse.jubula.rc.javafx.components.CurrentStages;
import org.eclipse.jubula.rc.javafx.listener.ComponentHandler;
import org.eclipse.jubula.rc.javafx.tester.util.KeyCodeUtil;
import org.eclipse.jubula.rc.javafx.tester.util.NodeBounds;
import org.eclipse.jubula.rc.javafx.tester.util.Rounding;
import org.eclipse.jubula.toolkit.enums.ValueSets;
import org.eclipse.jubula.toolkit.enums.ValueSets.InteractionMode;
import org.eclipse.jubula.tools.internal.i18n.I18n;
import org.eclipse.jubula.tools.internal.objects.event.EventFactory;
import org.eclipse.jubula.tools.internal.objects.event.TestErrorEvent;
/**
* <p>
* JavaFX implementation but similar to the AWT/Swing implementation of the
* <code>IRobot</code> interface. It uses the {@link java.awt.Robot}to move the
* mouse and perform clicks. Any mouse move or click is intercepted and
* confirmed using the appropriate AWT/Swing implementations of
* {@link org.eclipse.jubula.rc.swing.driver.IRobotEventInterceptor}and
* {@link org.eclipse.jubula.rc.swing.driver.IRobotEventConfirmer}.
* </p>
*
* <p>
* The <code>click()</code> and <code>move()</code> implementations expect that
* the graphics component is of type {@link java.awt.Component}and the
* constraints object is <code>null</code> or of type {@link java.awt.Rectangle}
* .
* </p>
*
* @author BREDEX GmbH
* @created 31.10.2013
*/
public class RobotJavaFXImpl implements IRobot<Rectangle> {
/** the logger */
private static AutServerLogger log = new AutServerLogger(
RobotJavaFXImpl.class);
/** ID of Metal Look and Feel */
private static final String METAL_LAF_ID = "Metal"; //$NON-NLS-1$
/** The AWT Robot instance. */
private Robot m_robot;
/** The event interceptor. */
private RobotEventInterceptorJavaFXImpl m_interceptor;
/** The mouse motion tracker. */
private IMouseMotionTracker m_mouseMotionTracker;
/** The event thread queuer. */
private IEventThreadQueuer m_queuer;
/**
* Scrolls to a component, to make it visible. Currently only ListView and
* TableView are supported.
*/
private class Scroller {
/** The component to scroll to visible. */
private Node m_component;
/**
* @param component
* The component to scroll to visible.
*/
public Scroller(Node component) {
m_component = component;
}
/**
* Scrolls the component to visible.
*
* @param component
* the component to scroll to
*/
private void scrollObjectToVisible(final Node component) {
// scroll all parent scroll panes
List<ScrollPane> panes2Scroll = new ArrayList<ScrollPane>();
Parent p = component.getParent();
while (p != null) {
if (p instanceof ScrollPane) {
panes2Scroll.add((ScrollPane) p);
}
p = p.getParent();
}
// scroll inner panes before outer
for (int i = 0; i < panes2Scroll.size(); i++) {
ScrollPane nextPane = panes2Scroll.get(i);
scrollToNode(nextPane, component);
}
Parent parent = component.getParent();
Node scrollNode = component;
for (; (parent != null)
&& !(parent instanceof ListView)
&& !(parent instanceof TableView)
&& !(parent instanceof TreeView)
&& !(parent instanceof ScrollPane);
parent = parent.getParent()) {
if (parent instanceof TreeCell) {
scrollNode = parent;
}
}
if (parent instanceof ListView) {
((ListView<Node>) parent).scrollTo(scrollNode);
} else if (parent instanceof TableView) {
((TableView<Node>) parent).scrollTo(scrollNode);
} else if (parent instanceof TreeView) {
if (scrollNode instanceof TreeCell) {
final TreeView<?> treeView = (TreeView<?>) parent;
treeView.scrollTo(treeView.getRow(
((TreeCell) scrollNode).getTreeItem()));
}
}
}
/**
* @param sPane
* the scroll pane to scroll within
* @param scrollNode
* the node to scroll to
*/
private void scrollToNode(final ScrollPane sPane,
final Node scrollNode) {
final Node sPaneContent = sPane.getContent();
if (scrollNode == sPaneContent) {
return;
}
Bounds nodeInScrollPaneContent = scrollNode.getBoundsInLocal();
// translate local node bounds to ScrollPane content node relative bounds
Node currentNode = scrollNode;
do {
Parent nextParent = currentNode.getParent();
boolean cornerCase = false;
if (nextParent instanceof Group) {
// BEGIN: skip skins in between
Parent parentLookup = nextParent.getParent();
while (parentLookup != null
&& !(parentLookup instanceof ScrollPane)) {
parentLookup = parentLookup.getParent();
}
// END: skip skins in between
// there is a corner case if:
// - a group is the direct content node of a scroll pane
if (parentLookup != null) {
ScrollPane potentialGroupParent =
(ScrollPane) parentLookup;
if (potentialGroupParent.getContent() == nextParent) {
cornerCase = true;
}
}
}
if (!cornerCase) {
nodeInScrollPaneContent = currentNode
.localToParent(nodeInScrollPaneContent);
}
currentNode = nextParent;
} while (currentNode != sPaneContent);
// determine left upper corner of node to scroll to
final double nodeX = nodeInScrollPaneContent.getMinX();
final double nodeY = nodeInScrollPaneContent.getMinY();
// determine scrolling scaling factor - defaults to 1.0
double hmin = sPane.getHmin();
final double scaleH = sPane.getHmax() - hmin;
double vmin = sPane.getVmin();
final double scaleV = sPane.getVmax() - vmin;
// determine the actually scrollable distance in x and y direction
final Bounds viewPortBounds = sPane.getViewportBounds();
final Bounds contentBounds = sPaneContent.getBoundsInLocal();
final double actuallyScrollableHDistance =
contentBounds.getWidth() - viewPortBounds.getWidth();
final double actuallyScrollableVDistance =
contentBounds.getHeight() - viewPortBounds.getHeight();
// scroll ScrollPane programmatically
sPane.setHvalue(hmin
+ (nodeX / actuallyScrollableHDistance) * scaleH);
sPane.setVvalue(vmin
+ (nodeY / actuallyScrollableVDistance) * scaleV);
}
/**
* Scrolls the component passed to the constructor to visible.
*
*/
public void scrollToVisible() {
scrollObjectToVisible(m_component);
}
}
/**
* Creates a new instance.
*
* @param factory
* The Robot factory instance.
* @throws RobotException
* If the AWT-Robot cannot be created.
*/
public RobotJavaFXImpl(RobotFactoryJavaFXImpl factory)
throws RobotException {
try {
m_robot = new Robot();
m_robot.setAutoWaitForIdle(true);
m_robot.setAutoDelay(0);
} catch (AWTException awte) {
log.error(awte);
m_robot = null;
throw new RobotException(awte);
} catch (SecurityException se) {
log.error(se);
m_robot = null;
throw new RobotException(se);
}
m_interceptor = factory.getRobotEventInterceptor();
m_mouseMotionTracker = factory.getMouseMotionTracker();
m_queuer = factory.getEventThreadQueuer();
}
/**
* Gets a location inside the component. If <code>offset</code> is
* <code>null</code>, it returns the middle of the component otherwise it
* adds the offset to the upper left corner.
*
* @param comp
* the component to get the location for
* @param offset
* the offset
* @throws IllegalArgumentException
* if <code>component</code> is null
* @return the <b>global </b> coordinates of <code>component</code>
*/
private Point getLocation(Node comp, final Point offset)
throws IllegalArgumentException {
Validate.notNull(comp, "component must not be null"); //$NON-NLS-1$
Scene s = comp.getScene();
s.getRoot().layout();
Point2D pos = comp.localToScreen(0, 0);
double x = pos.getX();
double y = pos.getY();
if (offset == null) {
final Bounds boundsInParent = comp.getBoundsInParent();
x += boundsInParent.getWidth() / 2;
y += boundsInParent.getHeight() / 2;
} else {
x += offset.x;
y += offset.y;
}
return new Point(Rounding.round(x), Rounding.round(y));
}
/**
* Implementation of the mouse click. The mouse is moved into the graphics
* component by calling <code>moveImpl()</code> before performing the click.
*
* @param graphicsComponent
* The graphics component to click on
* @param constraints
* The constraints, must be a <code>java.awt.Rectangle</code> or
* <code>null</code>. The constraints are <em>relative</em> to
* the location/origin of the <code>graphicsComponent</code>.
* @param clickOptions
* The click options
* @param xPos
* xPos in component
* @param yPos
* yPos in component
* @param yAbsolute
* true if y-position should be absolute
* @param xAbsolute
* true if x-position should be absolute
* @throws RobotException
* If the click delay is interrupted or the event confirmation
* receives a timeout.
*/
private void clickImpl(Object graphicsComponent, Rectangle constraints,
ClickOptions clickOptions, int xPos, boolean xAbsolute, int yPos,
boolean yAbsolute) throws RobotException {
moveImpl(graphicsComponent, constraints, xPos, xAbsolute,
yPos, yAbsolute, clickOptions);
clickImpl(graphicsComponent, clickOptions);
}
/**
* Clicks at the current mouse position.
*
* @param graphicsComp
* The component used for confirming the click.
* @param clickOp
* Configuration for the click.
*/
private void clickImpl(Object graphicsComp, ClickOptions clickOp) {
int buttonMask = getButtonMask(clickOp.getMouseButton());
int clickCount = clickOp.getClickCount();
int[] modifierMask = getModifierMask(clickOp.getClickModifier());
if (clickCount > 0) {
IRobotEventConfirmer confirmer = null;
if (clickOp.isConfirmClick()) {
InterceptorOptions options = new InterceptorOptions(
new long[] { AWTEvent.MOUSE_EVENT_MASK });
confirmer = m_interceptor.intercept(options);
}
try {
pressModifier(modifierMask);
RobotTiming.sleepPreClickDelay();
for (int i = 0; i < clickCount; i++) {
m_robot.mousePress(buttonMask);
RobotTiming.sleepPostMouseDownDelay();
m_robot.mouseRelease(buttonMask);
RobotTiming.sleepPostMouseUpDelay();
}
if (confirmer != null) {
confirmer.waitToConfirm(null,
new ClickJavaFXEventMatcher(clickOp));
}
} finally {
releaseModifier(modifierMask);
}
}
}
/**
* @param modifierMask
* array of modifiers to press before click
*/
private void pressModifier(int[] modifierMask) {
for (int i = 0; i < modifierMask.length; i++) {
keyPress(null, modifierMask[i]);
}
}
/**
* @param modifierMask
* array of modifiers release after click
*/
private void releaseModifier(int[] modifierMask) {
for (int i = 0; i < modifierMask.length; i++) {
keyRelease(null, modifierMask[i]);
}
}
/**
* @param clickModifier
* the click modifier to use for this click
* @return an array of modifiers to press before click and release after
* click
*/
private int[] getModifierMask(ClickModifier clickModifier) {
int[] modifier = new int[0];
if (clickModifier.hasModifiers(ClickModifier.M1)) {
modifier = ArrayUtils.add(modifier,
KeyCodeUtil.getKeyCode(KeyCode.CONTROL));
}
if (clickModifier.hasModifiers(ClickModifier.M2)) {
modifier = ArrayUtils.add(modifier,
KeyCodeUtil.getKeyCode(KeyCode.SHIFT));
}
if (clickModifier.hasModifiers(ClickModifier.M3)) {
modifier = ArrayUtils.add(modifier,
KeyCodeUtil.getKeyCode(KeyCode.ALT));
}
if (clickModifier.hasModifiers(ClickModifier.M4)) {
modifier = ArrayUtils.add(modifier,
KeyCodeUtil.getKeyCode(KeyCode.META));
}
return modifier;
}
/**
* Checks if the mouse has to be moved on <code>p</code> or if the mouse
* pointer already resides on this location.
*
* @param p
* The point to move to
* @return <code>true</code> if the mouse pointer resides on a different
* point, otherwise <code>false</code>.
*/
private boolean isMouseMoveRequired(Point p) {
boolean result = true;
Point point = getCurrentMousePosition();
if (point != null) {
result = !point.equals(p);
if (log.isDebugEnabled()) {
log.debug("Last converted screen point : " + point); //$NON-NLS-1$
log.debug("Required screen point : " + p); //$NON-NLS-1$
log.debug("Mouse move required? : " + result); //$NON-NLS-1$
}
}
return result;
}
/**
* Implementation of the mouse move. The mouse is moved into the graphics
* component.
*
* @param graphicsComponent
* The component to move to
* @param constraints
* The more specific constraints. Use this, for example when you
* want the click point to be relative to a part of the component
* (e.g. tree node, table cell, etc) rather than the overall
* component itself. May be <code>null</code>.
* @param xPos
* xPos in component
* @param yPos
* yPos in component
* @param xAbsolute
* true if x-position should be absolute
* @param yAbsolute
* true if y-position should be absolute
* @param clickOptions
* The click options
* @throws StepExecutionException
* If the click delay is interrupted or the event confirmation
* receives a timeout.
*/
private void moveImpl(final Object graphicsComponent,
final Rectangle constraints, final int xPos,
final boolean xAbsolute, final int yPos, final boolean yAbsolute,
final ClickOptions clickOptions) throws StepExecutionException {
Rectangle bounds = getComponentBounds(graphicsComponent, clickOptions);
if (constraints != null) {
bounds.x += constraints.x;
bounds.y += constraints.y;
bounds.height = constraints.height;
bounds.width = constraints.width;
}
Point p = PointUtil.calculateAwtPointToGo(xPos, xAbsolute, yPos,
yAbsolute, bounds);
boolean isInside = true;
if (graphicsComponent instanceof Node) {
isInside = EventThreadQueuerJavaFXImpl.invokeAndWait(
"CheckIfContains", new Callable<Boolean>() { //$NON-NLS-1$
@Override
public Boolean call() throws Exception {
return NodeBounds.checkIfContains(new Point2D(
p.x, p.y), (Node) graphicsComponent);
}
});
}
if (!isInside) {
throw new StepExecutionException(
TestErrorEvent.CLICKPOINT_INVALID,
EventFactory.createActionError(
TestErrorEvent.CLICKPOINT_INVALID));
}
// Move if necessary
if (isMouseMoveRequired(p)) {
if (log.isDebugEnabled()) {
log.debug("Moving mouse to: " + p); //$NON-NLS-1$
}
Point startpoint = m_mouseMotionTracker.getLastMousePointOnScreen();
if (startpoint == null) {
// If there is no starting point the center of the root
// component is used
if (graphicsComponent instanceof Stage) {
Stage s = (Stage) graphicsComponent;
Node root = s.getScene().getRoot();
startpoint = (root != null) ? getLocation(root, null)
: new Point(Rounding.round(s.getWidth() / 2),
Rounding.round(s.getHeight() / 2));
} else {
Node node = (Node) graphicsComponent;
Node root = node.getScene().getRoot();
Node c = (root != null) ? root : node;
startpoint = getLocation(c, null);
}
}
IRobotEventConfirmer confirmer = null;
InterceptorOptions options = new InterceptorOptions(
new long[] { AWTEvent.MOUSE_MOTION_EVENT_MASK });
//For drag Events we have to register the confirmer earlier
//because the drag event is thrown when the movement starts
if (DragAndDropHelper.getInstance().isDragMode()) {
confirmer = m_interceptor.intercept(options);
}
final Point[] mouseMove = MouseMovementStrategy.getMovementPath(
startpoint, p, clickOptions.getStepMovement(),
clickOptions.getFirstHorizontal());
for (int i = 0; i < mouseMove.length - 1; i++) {
m_robot.mouseMove(mouseMove[i].x, mouseMove[i].y);
m_robot.waitForIdle();
}
if (!DragAndDropHelper.getInstance().isDragMode()) {
confirmer = m_interceptor.intercept(options);
}
Point endPoint = mouseMove[mouseMove.length - 1];
m_robot.mouseMove(endPoint.x, endPoint.y);
m_robot.waitForIdle();
if (confirmer != null) {
confirmMove(confirmer, graphicsComponent);
}
}
}
/**
* Confirms a move, either a normal move or a drag move.
*
* @param confirmer
* the confirmer
* @param comp
* the component to confirm for
*/
private void confirmMove(IRobotEventConfirmer confirmer, Object comp) {
if (DragAndDropHelper.getInstance().isDragMode()) {
confirmer.waitToConfirm(null, new MouseMovedEventMatcher(
MouseEvent.MOUSE_DRAGGED));
} else {
confirmer.waitToConfirm(null, new MouseMovedEventMatcher(
MouseEvent.MOUSE_MOVED));
}
}
/**
* Refreshes the complete layout and returns the bounds of the given
* Component.
*
* @param comp
* the Component
* @param clickOp
* not used
* @return Rectangle with the Bounds
*/
private Rectangle getComponentBounds(final Object comp,
ClickOptions clickOp) {
ComponentHandler.syncStageResize();
Rectangle bounds = null;
if (comp instanceof Stage) {
Stage s = (Stage) comp;
bounds = new Rectangle(new Point(Rounding.round(s.getX()),
Rounding.round(s.getY())));
// This is not multi display compatible
Screen screen = Screen.getPrimary();
final Rectangle2D screenBounds = screen.getBounds();
int displayWidth = Rounding.round(screenBounds.getWidth());
int displayHeight = Rounding.round(screenBounds.getHeight());
if (s.isFullScreen()) {
bounds.width = Rounding.round(displayWidth);
bounds.height = Rounding.round(displayHeight);
} else if (s.isMaximized()) {
int x = Rounding.round(s.getX());
int y = Rounding.round(s.getY());
// trimming the bounds to the display if necessary
bounds.width = Rounding.round(s.getWidth());
bounds.height = Rounding.round(s.getHeight());
if (x < 0 || y < 0) {
bounds.x = 0;
bounds.y = 0;
if (bounds.width > displayWidth) {
bounds.width = displayWidth;
}
if (bounds.height > displayHeight) {
bounds.height = displayHeight;
}
}
} else {
bounds.width = Rounding.round(s.getWidth());
bounds.height = Rounding.round(s.getHeight());
}
} else {
final Node node = (Node) comp;
if (clickOp != null
&& clickOp.isScrollToVisible()) {
ensureComponentVisible(node);
}
bounds = EventThreadQueuerJavaFXImpl.invokeAndWait(
"Robot get node bounds", new Callable<Rectangle>() { //$NON-NLS-1$
@Override
public Rectangle call() throws Exception {
Parent parent = node.getParent();
if (parent != null) {
parent.requestLayout();
parent.layout();
}
return NodeBounds.getAbsoluteBounds(node);
}
});
}
return bounds;
}
/**
* {@inheritDoc}
*/
public void click(Object graphicsComponent, Rectangle constraints)
throws RobotException {
click(graphicsComponent, constraints, ClickOptions.create());
}
/**
* {@inheritDoc}
*/
public void click(Object graphicsComponent, Rectangle constraints,
ClickOptions clickOptions) throws RobotException {
clickImpl(graphicsComponent, constraints, clickOptions, 50, false, 50,
false);
}
/**
* Gets the InputEvent-ButtonMask of the given mouse button number
*
* @param button
* the button number
* @return the InputEvent button mask
*/
private int getButtonMask(int button) {
if (button == InteractionMode.primary.rcIntValue()) {
return java.awt.event.InputEvent.BUTTON1_MASK;
}
if (button == InteractionMode.tertiary.rcIntValue()) {
return java.awt.event.InputEvent.BUTTON2_MASK;
}
if (button == InteractionMode.secondary.rcIntValue()) {
return java.awt.event.InputEvent.BUTTON3_MASK;
}
throw new RobotException("unsupported mouse button", null); //$NON-NLS-1$
}
/**
* {@inheritDoc}
*/
public void clickAtCurrentPosition(Object graphicsComponent,
int clickCount, int button) {
ClickOptions clickOptions = new ClickOptions();
clickOptions.setClickCount(clickCount);
clickOptions.setMouseButton(button);
clickImpl(graphicsComponent, clickOptions);
}
/**
* {@inheritDoc}
*
*/
public void move(Object graphicsComponent, Rectangle constraints)
throws RobotException {
moveImpl(graphicsComponent, constraints, 50, false, 50,
false, ClickOptions.create());
}
/**
* {@inheritDoc} <br>
* <b>* Currently delegates the key type to the Robot </b>
*/
public void type(final Object graphicsComponent, char c)
throws RobotException {
Validate.notNull(graphicsComponent,
"The graphic component must not be null"); //$NON-NLS-1$
final KeyEvent event = new KeyEvent(
KeyEvent.KEY_TYPED, String.valueOf(c),
StringUtils.EMPTY, null, false, false, false, false);
InterceptorOptions options = new InterceptorOptions(
new long[] { AWTEvent.KEY_EVENT_MASK });
IRobotEventConfirmer confirmer = m_interceptor.intercept(options);
m_queuer.invokeLater("Type character", new Runnable() { //$NON-NLS-1$
@Override
public void run() {
final Scene scene;
if (graphicsComponent instanceof Stage) {
scene = ((Stage)graphicsComponent).getScene();
} else {
scene = ((Node)graphicsComponent).getScene();
}
Node focusOwner = scene.getFocusOwner();
EventTarget eventTarget =
focusOwner != null ? focusOwner : scene;
Event.fireEvent(eventTarget, event);
}
});
confirmer.waitToConfirm(graphicsComponent,
new KeyJavaFXEventMatcher(KeyEvent.KEY_TYPED));
}
/**
* {@inheritDoc}
*/
public void type(Object graphicsComponent, String text)
throws RobotException {
if (text != null) {
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
type(graphicsComponent, ch);
}
}
}
/**
* {@inheritDoc}
*/
public void keyType(Object graphicsComponent, int keycode) {
keyType(graphicsComponent, keycode, false);
}
/**
* @param graphicsComponent The graphics component the key code is typed in, may be null
* @param keycode The key code.
* @param isUpperCase Boolean whether character is upper case.
*/
public void keyType(final Object graphicsComponent, final int keycode,
final boolean isUpperCase)
throws RobotException {
try {
InterceptorOptions options = new InterceptorOptions(
new long[] { AWTEvent.KEY_EVENT_MASK });
IRobotEventConfirmer confirmer = m_interceptor.intercept(options);
try {
if (isUpperCase) {
m_robot.keyPress(java.awt.event.KeyEvent.VK_SHIFT);
}
m_robot.keyPress(keycode);
confirmer.waitToConfirm(graphicsComponent,
new KeyJavaFXEventMatcher(KeyEvent.KEY_PRESSED));
} finally {
m_robot.keyRelease(keycode);
if (isUpperCase) {
m_robot.keyRelease(java.awt.event.KeyEvent.VK_SHIFT);
}
}
confirmer.waitToConfirm(graphicsComponent,
new KeyJavaFXEventMatcher(KeyEvent.KEY_RELEASED));
} catch (IllegalArgumentException e) {
throw new RobotException(e);
}
}
/**
* {@inheritDoc}
*/
public String getSystemModifierSpec() {
String keyStrokeSpec = ValueSets.Modifier.control.rcValue();
if (!(UIManager.getLookAndFeel().getID().equals(METAL_LAF_ID))) {
if (Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
== java.awt.Event.META_MASK) {
keyStrokeSpec = ValueSets.Modifier.meta.rcValue();
} else if (Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
== java.awt.Event.ALT_MASK) {
keyStrokeSpec = ValueSets.Modifier.alt.rcValue();
}
}
return keyStrokeSpec;
}
/**
* Implements the key press or release.
*
* @param graphicsComponent
* The component, may be <code>null</code>
* @param keyCode
* The key code
* @param press
* If <code>true</code>, the key is pressed, otherwise released
*/
private void keyPressReleaseImpl(Object graphicsComponent, int keyCode,
boolean press) {
InterceptorOptions options = new InterceptorOptions(
new long[] { AWTEvent.KEY_EVENT_MASK });
IRobotEventConfirmer confirmer = m_interceptor.intercept(options);
if (press) {
m_robot.keyPress(keyCode);
} else {
m_robot.keyRelease(keyCode);
}
confirmer.waitToConfirm(graphicsComponent, new KeyJavaFXEventMatcher(
press ? KeyEvent.KEY_PRESSED : KeyEvent.KEY_RELEASED));
}
/**
* {@inheritDoc}
*/
public void keyPress(Object graphicsComponent, int keycode)
throws RobotException {
keyPressReleaseImpl(graphicsComponent, keycode, true);
}
/**
* {@inheritDoc}
*/
public void keyRelease(Object graphicsComponent, int keycode)
throws RobotException {
keyPressReleaseImpl(graphicsComponent, keycode, false);
}
/**
* a method to turn the toggle keys caps-lock, num-lock and scroll-lock on
* and off. If the given key code is one of these buttons otherwise this is
* a normal button press.
*
* @param obj
* Component
* @param key
* to set key Event
* @param activated
* boolean
*/
public void keyToggle(Object obj, int key, boolean activated) {
keyPressReleaseImpl(null, key, true);
keyPressReleaseImpl(null, key, false);
}
/**
* {@inheritDoc}
*/
public void keyStroke(String keyStrokeSpec) throws RobotException {
try {
KeyTyper.getInstance().type(keyStrokeSpec, m_interceptor,
new KeyJavaFXEventMatcher(KeyEvent.KEY_PRESSED),
new KeyJavaFXEventMatcher(KeyEvent.KEY_RELEASED));
} catch (AWTException e) {
throw new RobotException(e);
}
}
/**
* Ensures that the passed component is visible.
*
* @param component
* The component.
* @throws RobotException
* If the component's screen location cannot be calculated.
*/
private void ensureComponentVisible(final Node component)
throws RobotException {
m_queuer.invokeAndWait("ensureVisible", new IRunnable<Void>() { //$NON-NLS-1$
public Void run() {
Scroller scroller = new Scroller(component);
scroller.scrollToVisible();
return null;
}
});
}
/**
* {@inheritDoc}
*/
public void scrollToVisible(Object graphicsComponent, Rectangle constraints)
throws RobotException {
ensureComponentVisible((Node) graphicsComponent);
}
/**
* {@inheritDoc}
*/
public void activateApplication(String method) throws RobotException {
try {
final Window window = getActiveWindow();
if (window == null) {
return;
}
WindowActivationMethod wam = WindowActivationMethod
.createWindowActivationMethod(method, m_robot, m_queuer);
wam.activate(window);
// Verify that window was successfully activated
Window activeWindow = m_queuer.invokeAndWait("getActiveWindow", //$NON-NLS-1$
new IRunnable<Window>() {
public Window run() throws StepExecutionException {
if (window.isFocused()) {
return window;
}
return null;
}
});
if (activeWindow != window) {
throw new StepExecutionException(
I18n.getString(TestErrorEvent.WINDOW_ACTIVATION_FAILED,
true),
EventFactory
.createActionError(
TestErrorEvent.
WINDOW_ACTIVATION_FAILED));
}
} catch (Exception exc) {
throw new RobotException(exc);
}
}
/**
* @return The current mouse position as a Point {@inheritDoc}
*/
public Point getCurrentMousePosition() {
return MouseInfo.getPointerInfo().getLocation();
}
/**
* Guesses the active window. Returns null if no active window is found.
*
* @return the active window
*/
private Window getActiveWindow() {
return m_queuer.invokeAndWait("getActiveWindow", //$NON-NLS-1$
new IRunnable<Window>() {
public Window run() throws StepExecutionException {
Window w = CurrentStages.getfocusStage();
if (w == null) {
w = CurrentStages.getfirstStage();
((Stage) w).toFront();
}
return w;
}
});
}
/**
*
* {@inheritDoc}
*/
public boolean isMouseInComponent(final Object graphicsComponent) {
final Point currMousePos = getCurrentMousePosition();
return EventThreadQueuerJavaFXImpl.invokeAndWait("isMouseInComponent", //$NON-NLS-1$
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
if (graphicsComponent instanceof Node) {
Node comp = (Node) graphicsComponent;
comp.getScene().getRoot().layout();
if (currMousePos == null) {
return false;
}
return NodeBounds.checkIfContains(new Point2D(
currMousePos.x, currMousePos.y), comp);
}
Stage comp = (Stage) graphicsComponent;
comp.getScene().getRoot().layout();
Bounds stageBounds = new BoundingBox(comp.getX(),
comp.getY(), comp.getWidth(), comp
.getHeight());
return stageBounds.contains(new Point2D(
currMousePos.x, currMousePos.y));
}
});
}
/**
* Presses the given mouse button on the given component in the given
* constraints. <br>
* <b>Note:</b> Use only for Drag and Drop! To click with the mouse, use
* click-methods!
*
* @param graphicsComponent
* the component where to press the mouse button. If null, the
* mouse is pressed at the current location.
* @param constraints
* A constraints object used by the Robot implementation, may be
* <code>null</code>.
* @param button
* the mouse button which is to be pressed.
*/
public void mousePress(Object graphicsComponent, Rectangle constraints,
int button) {
DragAndDropHelper.getInstance().setDragMode(true);
if (graphicsComponent != null) {
move(graphicsComponent, constraints);
}
RobotTiming.sleepPreClickDelay();
m_robot.mousePress(getButtonMask(button));
}
/**
* Releases the given mouse button on the given component in the given
* constraints. <br>
* <b>Note:</b> Use only for Drag and Drop! To click with the mouse, use
* click-methods!
*
* @param graphicsComponent
* The graphics component. If null, the mouse button is released
* at the current location.
* @param constraints
* A constraints object used by the Robot implementation, may be
* <code>null</code>.
* @param button
* the mouse button.
*/
public void mouseRelease(Object graphicsComponent, Rectangle constraints,
int button) throws RobotException {
if (graphicsComponent != null) {
move(graphicsComponent, constraints);
}
RobotTiming.sleepPreClickDelay();
m_robot.mouseRelease(getButtonMask(button));
DragAndDropHelper.getInstance().setDragMode(false);
}
/**
* {@inheritDoc}
*/
public void click(Object graphicsComponent, Rectangle constraints,
ClickOptions clickOptions, int xPos, boolean xAbsolute, int yPos,
boolean yAbsolute) throws RobotException {
clickImpl(graphicsComponent, constraints, clickOptions, xPos,
xAbsolute, yPos, yAbsolute);
}
/**
* {@inheritDoc}
*/
public String getPropertyValue(Object graphicsComp, String propertyName)
throws RobotException {
return PropertyUtil.getPropertyValue(graphicsComp, propertyName);
}
/** {@inheritDoc} */
public BufferedImage createFullScreenCapture() {
return LocalScreenshotUtil.createFullScreenCapture();
}
/**
* Return the currently used EventInterceptor
* @return the Interceptor
*/
public RobotEventInterceptorJavaFXImpl getInterceptor() {
return m_interceptor;
}
/**
* {@inheritDoc}
*/
public void shakeMouse() {
/** number of pixels by which a "mouse shake" offsets the mouse cursor */
final int mouseShakeOffset = 10;
Point origin = getCurrentMousePosition();
try {
m_robot.mouseMove(
origin.x + mouseShakeOffset,
origin.y + mouseShakeOffset);
m_robot.mouseMove(
origin.x - mouseShakeOffset,
origin.y - mouseShakeOffset);
} finally {
m_robot.mouseMove(origin.x, origin.y);
}
}
@Override
public Rectangle getComponentBounds(IComponent component) {
return getComponentBounds(component.getRealComponent(), null);
}
}