/******************************************************************************* * Copyright (c) 2016 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 *******************************************************************************/ package org.eclipse.jubula.rc.swt.tester; import static org.eclipse.jubula.rc.common.driver.CheckWithTimeoutQueuer.invokeAndWait; import java.util.StringTokenizer; import org.eclipse.jubula.rc.common.driver.DragAndDropHelper; import org.eclipse.jubula.rc.common.driver.IEventThreadQueuer; import org.eclipse.jubula.rc.common.driver.IRobot; import org.eclipse.jubula.rc.common.driver.IRunnable; import org.eclipse.jubula.rc.common.exception.StepExecutionException; import org.eclipse.jubula.rc.common.implclasses.table.Cell; import org.eclipse.jubula.rc.common.implclasses.tree.AbstractTreeNodeOperation; import org.eclipse.jubula.rc.common.implclasses.tree.AbstractTreeOperationContext; import org.eclipse.jubula.rc.common.implclasses.tree.ExpandCollapseTreeNodeOperation; import org.eclipse.jubula.rc.common.implclasses.tree.INodePath; import org.eclipse.jubula.rc.common.implclasses.tree.StandardDepthFirstTraverser; import org.eclipse.jubula.rc.common.implclasses.tree.TreeNodeOperation; import org.eclipse.jubula.rc.common.tester.AbstractTreeTableTester; import org.eclipse.jubula.rc.common.tester.adapter.interfaces.ITreeComponent; import org.eclipse.jubula.rc.common.util.KeyStrokeUtil; import org.eclipse.jubula.rc.common.util.Verifier; import org.eclipse.jubula.rc.swt.components.SWTCell; import org.eclipse.jubula.rc.swt.driver.KeyCodeConverter; import org.eclipse.jubula.rc.swt.tester.util.CAPUtil; import org.eclipse.jubula.rc.swt.tester.util.ToggleCheckboxOperation; import org.eclipse.jubula.rc.swt.tester.util.TreeOperationContext; import org.eclipse.jubula.rc.swt.tester.util.VerifyCheckboxOperation; import org.eclipse.jubula.rc.swt.utils.SwtUtils; import org.eclipse.jubula.toolkit.enums.ValueSets; import org.eclipse.jubula.tools.internal.constants.SwtToolkitConstants; import org.eclipse.jubula.tools.internal.objects.event.EventFactory; import org.eclipse.jubula.tools.internal.objects.event.TestErrorEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.FontMetrics; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; /** * Toolkit specific commands for the <code>Tree</code> * * @author BREDEX GmbH */ public class TreeTester extends AbstractTreeTableTester { /** * Finds the item at a given position in the tree. * * @author BREDEX GmbH * @created Jul 28, 2010 */ private static final class ItemAtPointTreeNodeOperation extends AbstractTreeNodeOperation { /** the item that was found at the given position */ private TreeItem m_itemAtPoint; /** the position (in absolute coordinates) at which to find the item */ private Point m_absPoint; /** * the bounds (in absolute coordinates) of the tree in which the * search should take place */ private Rectangle m_absTreeBounds; /** * Constructor * * @param absPoint The position (in absolute coordinates) at which to * find the item. * @param absTreeBounds The bounds (in absolute coordinates) of the * tree in which the search should take place. */ public ItemAtPointTreeNodeOperation(Point absPoint, Rectangle absTreeBounds) { m_absPoint = absPoint; m_absTreeBounds = absTreeBounds; } /** * {@inheritDoc} */ public boolean operate(Object node) throws StepExecutionException { if (getContext().isVisible(node) && node instanceof TreeItem) { TreeItem currentItem = (TreeItem)node; final Rectangle absItemBounds = SwtUtils.getBounds(currentItem); absItemBounds.x = m_absTreeBounds.x; absItemBounds.width = m_absTreeBounds.width; if (SwtUtils.containsInclusive( absItemBounds, m_absPoint)) { m_itemAtPoint = currentItem; return false; } } return true; } /** * * @return the item found at the given position, or <code>null</code> if * no item was found. Note that this method will always return * <code>null</code> if called before or during execution of * {@link #operate(Object)}. */ public TreeItem getItemAtPoint() { return m_itemAtPoint; } } /** * * @return the Tree */ private Tree getTreeTable() { return (Tree) getComponent().getRealComponent(); } /** {@inheritDoc} */ @Override public void rcCheckPropertyAtMousePosition(final String name, final String value, final String operator, int timeout) { invokeAndWait("rcCheckPropertyAtMousePosition", timeout, //$NON-NLS-1$ new Runnable() { public void run() { Object cell = null; int numColumns = getEventThreadQueuer().invokeAndWait( "checkColumnIndex", //$NON-NLS-1$ new IRunnable<Integer>() { public Integer run() { return getTreeTable().getColumnCount(); } }); if (numColumns > 0) { cell = getCellAtMousePosition(); } else { cell = getNodeAtMousePosition(); } final ITreeComponent bean = getTreeAdapter(); final String propToStr = bean.getPropertyValueOfCell(name, cell); Verifier.match(propToStr, value, operator); } }); } /** * {@inheritDoc} */ public void rcDragByTextPath(int mouseButton, String modifier, String pathType, int preAscend, String treePath, String operator) { final DragAndDropHelper dndHelper = DragAndDropHelper.getInstance(); dndHelper.setMouseButton(mouseButton); dndHelper.setModifier(modifier); dndHelper.setDragComponent(null); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); rcSelect(pathType, preAscend, treePath, operator, 0, 1, ValueSets.BinaryChoice.no.rcValue()); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); } /** * {@inheritDoc} */ public void rcDropByTextPath(final String pathType, final int preAscend, final String treePath, final String operator, int delayBeforeDrop) { final DragAndDropHelper dndHelper = DragAndDropHelper.getInstance(); final IRobot robot = getRobot(); try { pressOrReleaseModifiers(dndHelper.getModifier(), true); getEventThreadQueuer().invokeAndWait( "rcDropByTextPath - perform drag", new IRunnable<Void>() { //$NON-NLS-1$ public Void run() throws StepExecutionException { // drag robot.mousePress(dndHelper.getDragComponent(), null, dndHelper.getMouseButton()); CAPUtil.shakeMouse(); return null; } }); postMouseMovementEvent(); // drop rcSelect(pathType, preAscend, treePath, operator, 0, 1, ValueSets.BinaryChoice.no.rcValue()); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); waitBeforeDrop(delayBeforeDrop); } finally { robot.mouseRelease(null, null, dndHelper.getMouseButton()); pressOrReleaseModifiers(dndHelper.getModifier(), false); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); } } /** * {@inheritDoc} */ public void rcDragByIndexPath(int mouseButton, String modifier, String pathType, int preAscend, String indexPath) { final DragAndDropHelper dndHelper = DragAndDropHelper.getInstance(); dndHelper.setMouseButton(mouseButton); dndHelper.setModifier(modifier); dndHelper.setDragComponent(null); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); rcSelectByIndices(pathType, preAscend, indexPath, 0, 1, ValueSets.BinaryChoice.no.rcValue()); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); } /** * {@inheritDoc} */ public void rcDropByIndexPath(final String pathType, final int preAscend, final String indexPath, int delayBeforeDrop) { final DragAndDropHelper dndHelper = DragAndDropHelper.getInstance(); final IRobot robot = getRobot(); try { pressOrReleaseModifiers(dndHelper.getModifier(), true); getEventThreadQueuer().invokeAndWait( "rcDropByIndexPath - perform drag", new IRunnable<Void>() { //$NON-NLS-1$ public Void run() throws StepExecutionException { // drag robot.mousePress(dndHelper.getDragComponent(), null, dndHelper.getMouseButton()); CAPUtil.shakeMouse(); return null; } }); postMouseMovementEvent(); // drop rcSelectByIndices(pathType, preAscend, indexPath, 0, 1, ValueSets.BinaryChoice.no.rcValue()); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); waitBeforeDrop(delayBeforeDrop); } finally { robot.mouseRelease(null, null, dndHelper.getMouseButton()); pressOrReleaseModifiers(dndHelper.getModifier(), false); SwtUtils.waitForDisplayIdle(getTreeTable().getDisplay()); } } /** * Post a MouseMove event in order to break the Display out of its post-drag * "freeze". It appears as though the mouse position change needs to be * extreme in order to nudge the Display back into action (i.e. * (<mouse-location> + 1) was insufficient), so the default Event values (x, * y = 0) are used. */ private void postMouseMovementEvent() { Event wakeEvent = new Event(); wakeEvent.type = SWT.MouseMove; getTreeTable().getDisplay().post(wakeEvent); waitForDisplayUpdate(); } /** * {@inheritDoc} */ @Override protected Object getNodeAtMousePosition() throws StepExecutionException { return getEventThreadQueuer().invokeAndWait("getItemAtMousePosition", new IRunnable<TreeItem>() { //$NON-NLS-1$ public TreeItem run() throws StepExecutionException { Point mousePos = SwtUtils.convertToSwtPoint( getRobot().getCurrentMousePosition()); ItemAtPointTreeNodeOperation op = new ItemAtPointTreeNodeOperation( mousePos, SwtUtils.getWidgetBounds(getTreeTable())); TreeItem topItem = getTreeTable().getTopItem(); if (topItem != null) { // FIXME zeb This may be slow for very large trees, as the // search may continue long past the // visible client area of the tree. // It may also cause problems with regard to // lazy/virtual nodes. StandardDepthFirstTraverser traverser = new StandardDepthFirstTraverser( new TreeOperationContext( getEventThreadQueuer(), getRobot(), getTreeTable())); traverser.traversePath(op, topItem); if (op.getItemAtPoint() != null) { return op.getItemAtPoint(); } } throw new StepExecutionException("No tree node found at mouse position.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.NOT_FOUND)); } }); } /** * * @return the table cell at the current mouse position. * @throws StepExecutionException If no table cell can be found at the * current mouse position. */ private Cell getCellAtMousePosition() throws StepExecutionException { final Tree tree = getTreeTable(); final java.awt.Point awtMousePos = getRobot().getCurrentMousePosition(); Cell returnvalue = getEventThreadQueuer().invokeAndWait( "getCellAtMousePosition", //$NON-NLS-1$ new IRunnable<Cell>() { private int m_rowCount = 0; public Cell run() throws StepExecutionException { Cell cell = null; for (TreeItem item : tree.getItems()) { cell = findCell(item); if (cell != null) { break; } } if (cell == null) { throw new StepExecutionException( "No cell under mouse position found!", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.NOT_FOUND)); } return cell; } /** * This method tries to find the cell which is under the current mouse position * belonging to a given tree item or its sub items * @param item the tree item * @return the cell if found, <code>null</code> if not */ private Cell findCell(TreeItem item) { Cell cell = null; for (int col = 0; col < tree.getColumnCount(); col++) { final Rectangle itemBounds = getCellBounds( getEventThreadQueuer(), tree, m_rowCount, col, item); final org.eclipse.swt.graphics.Point absItemBounds = tree.toDisplay(itemBounds.x, itemBounds.y); final java.awt.Rectangle absRect = new java.awt.Rectangle(absItemBounds.x, absItemBounds.y, itemBounds.width, itemBounds.height); if (absRect.contains(awtMousePos)) { cell = new SWTCell(m_rowCount, col, item); } } m_rowCount++; if (cell == null && item.getExpanded()) { for (TreeItem subItem : item.getItems()) { cell = findCell(subItem); if (cell != null) { break; } } } return cell; } }); return returnvalue; } /** * @param etq * the EventThreadQueuer to use * @param table * the table to use * @param row * The row of the cell * @param col * The column of the cell * @param ti * The tree item * @return The bounding rectangle for the cell, relative to the table's * location. */ private static Rectangle getCellBounds(IEventThreadQueuer etq, final Tree table, final int row, final int col, final TreeItem ti) { Rectangle cellBounds = etq.invokeAndWait( "getCellBounds", //$NON-NLS-1$ new IRunnable<Rectangle>() { public Rectangle run() { int column = (table.getColumnCount() > 0 || col > 0) ? col : 0; org.eclipse.swt.graphics.Rectangle r = ti.getBounds(column); String text = CAPUtil.getWidgetText(ti, SwtToolkitConstants.WIDGET_TEXT_KEY_PREFIX + column, ti.getText(column)); Image image = ti.getImage(column); if (text != null && text.length() != 0) { GC gc = new GC(table); int charWidth = 0; try { FontMetrics fm = gc.getFontMetrics(); charWidth = fm.getAverageCharWidth(); } finally { gc.dispose(); } r.width = text.length() * charWidth; if (image != null) { r.width += image.getBounds().width; } } else if (image != null) { r.width = image.getBounds().width; } if (column > 0) { TreeColumn tc = table.getColumn(column); int alignment = tc.getAlignment(); if (alignment == SWT.CENTER) { r.x += ((double)tc.getWidth() / 2) - ((double)r.width / 2); } if (alignment == SWT.RIGHT) { r.x += tc.getWidth() - r.width; } } return new Rectangle(r.x, r.y, r.width, r.height); } }); return cellBounds; } /** * Selects Checkbox of last node of the path given by <code>treepath</code>. * * @param pathType whether the path is relative or absolute * @param preAscend * Relative traversals will start this many parent nodes * above the current node. Absolute traversals ignore this * parameter. * @param treePath The tree path. * @param operator * If regular expressions are used to match the tree path * @throws StepExecutionException If the tree path is invalid, if the * double-click to expand the node fails, or if the selection is invalid. */ public void rcToggleCheckbox(String pathType, int preAscend, String treePath, String operator) throws StepExecutionException { toggleCheckBoxByPath(pathType, preAscend, createStringNodePath(splitTextTreePath(treePath), operator)); } /** * Selects Checkbox of last node of the path given by <code>indexPath</code> * @param pathType whether the path is relative or absolute * @param preAscend * Relative traversals will start this many parent nodes * above the current node. Absolute traversals ignore this * parameter. * @param indexPath the index path * @throws StepExecutionException if <code>indexPath</code> is not a valid * path */ public void rcToggleCheckboxByIndices(String pathType, int preAscend, String indexPath) throws StepExecutionException { toggleCheckBoxByPath(pathType, preAscend, createIndexNodePath(splitIndexTreePath(indexPath))); } /** * Verify Selection of checkbox of the node at the end of the <code>treepath</code>. * * @param pathType whether the path is relative or absolute * @param preAscend * Relative traversals will start this many parent nodes * above the current node. Absolute traversals ignore this * parameter. * @param treePath The tree path. * @param operator * If regular expressions are used to match the tree path * @param checked true if checkbox of tree node is selected, false otherwise * @param timeout the maximum amount of time to wait for the state to occur * @throws StepExecutionException If the tree path is invalid, if the * double-click to expand the node fails, or if the selection is invalid. */ public void rcVerifyCheckbox(final String pathType, final int preAscend, final String treePath, final String operator, final boolean checked, int timeout) throws StepExecutionException { invokeAndWait("rcVerifyCheckBox", timeout, new Runnable() { //$NON-NLS-1$ public void run() { verifyCheckBoxByPath(pathType, preAscend, createStringNodePath( splitTextTreePath(treePath), operator), checked); } }); } /** * Verify Selection of checkbox of last node of the path given by <code>indexPath</code> * @param pathType whether the path is relative or absolute * @param preAscend * Relative traversals will start this many parent nodes * above the current node. Absolute traversals ignore this * parameter. * @param indexPath the index path * @param checked true if checkbox of tree node is selected, false otherwise * @param timeout the maximum amount of time to wait for the state to occur * @throws StepExecutionException if <code>indexPath</code> is not a valid * path */ public void rcVerifyCheckboxByIndices(final String pathType, final int preAscend, final String indexPath, final boolean checked, int timeout) throws StepExecutionException { invokeAndWait("rcVerifyChecktboxByIndices", timeout, new Runnable() { //$NON-NLS-1$ public void run() { verifyCheckBoxByPath(pathType, preAscend, createIndexNodePath(splitIndexTreePath(indexPath)), checked); } }); } /** * Verifies whether the checkbox of the first selection in the tree is checked * * @param checked true if checkbox of node is selected, false otherwise * @param timeout the maximum amount of time to wait for the state to occur * @throws StepExecutionException If no node is selected or the verification fails. */ public void rcVerifySelectedCheckbox(final boolean checked, int timeout) throws StepExecutionException { invokeAndWait("rcVerifySelectedCheckbox", timeout, new Runnable() { //$NON-NLS-1$ public void run() { Boolean checkSelected = getEventThreadQueuer().invokeAndWait( "rcVerifyTreeCheckbox", new IRunnable<Boolean>() { //$NON-NLS-1$ public Boolean run() { AbstractTreeOperationContext context = ((ITreeComponent)getComponent()) .getContext(); TreeItem node = (TreeItem) getSelectedNode(context); return node.getChecked(); } }); Verifier.equals(checked, checkSelected.booleanValue()); } }); } /** * @param pathType pathType * @param preAscend * Relative traversals will start this many parent nodes * above the current node. Absolute traversals ignore this * parameter. * @param objectPath objectPath * @param checked true if Checkbox should be enabled, false otherwise */ private void verifyCheckBoxByPath(String pathType, int preAscend, INodePath objectPath, final boolean checked) { TreeNodeOperation expOp = new ExpandCollapseTreeNodeOperation(false); TreeOperationContext context = new TreeOperationContext( getEventThreadQueuer(), getRobot(), getTreeTable()); TreeNodeOperation checkboxOp = new VerifyCheckboxOperation( checked, context); INodePath subPath = objectPath.subPath(0, objectPath.getLength() - 1); traverseTreeByPath(subPath, pathType, preAscend, expOp); traverseLastElementByPath(objectPath, pathType, preAscend, checkboxOp); } /** * @param pathType pathType * @param preAscend * Relative traversals will start this many parent nodes * above the current node. Absolute traversals ignore this * parameter. * @param objectPath objectPath */ private void toggleCheckBoxByPath(String pathType, int preAscend, INodePath objectPath) { TreeNodeOperation expOp = new ExpandCollapseTreeNodeOperation(false); TreeOperationContext context = new TreeOperationContext( getEventThreadQueuer(), getRobot(), getTreeTable()); TreeNodeOperation selCheckboxOp = new ToggleCheckboxOperation(context); INodePath subPath = objectPath.subPath(0, objectPath.getLength() - 1); traverseTreeByPath(subPath, pathType, preAscend, expOp); traverseLastElementByPath(objectPath, pathType, preAscend, selCheckboxOp); } /** * Forces all outstanding paint requests for the receiver's component's * display to be processed before this method returns. * * @see Display#update() */ private void waitForDisplayUpdate() { ((Control)getComponent().getRealComponent()) .getDisplay().syncExec(new Runnable() { public void run() { ((Control) getComponent().getRealComponent()) .getDisplay().update(); } }); } /** * {@inheritDoc} */ @Override protected void pressOrReleaseModifiers(String modifier, boolean press) { final IRobot robot = getRobot(); final StringTokenizer modTok = new StringTokenizer( KeyStrokeUtil.getModifierString(modifier), " "); //$NON-NLS-1$ while (modTok.hasMoreTokens()) { final String mod = modTok.nextToken(); final int keyCode = KeyCodeConverter.getKeyCode(mod); if (press) { robot.keyPress(null, keyCode); } else { robot.keyRelease(null, keyCode); } } } }