1 /* 2 * Copyright (c) 2009, 2013, Oracle and/or its affiliates. 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package org.jemmy.fx.control; 26 27 import java.util.function.Function; 28 import javafx.scene.Node; 29 import javafx.scene.control.Control; 30 import javafx.scene.control.IndexedCell; 31 import javafx.scene.control.TableCell; 32 import javafx.scene.control.TreeTableCell; 33 import javafx.util.Callback; 34 import org.jemmy.Point; 35 import org.jemmy.Rectangle; 36 import org.jemmy.action.FutureAction; 37 import org.jemmy.control.Wrap; 38 import org.jemmy.env.Environment; 39 import org.jemmy.fx.FXClickFocus; 40 import org.jemmy.fx.NodeWrap; 41 import org.jemmy.input.AbstractScroll; 42 import org.jemmy.interfaces.Caret; 43 import org.jemmy.interfaces.Focus; 44 import org.jemmy.interfaces.Parent; 45 import org.jemmy.lookup.Lookup; 46 47 /** 48 * @author Alexander Kirov 49 */ 50 public class TableUtils { 51 52 public Focus focuser(Wrap<? extends Node> wrap) { 53 final Rectangle bounds = wrap.getScreenBounds(); 54 return new FXClickFocus(wrap, new Point( 55 Math.max(bounds.getWidth() / 2, bounds.getWidth() - 3), 56 Math.max(bounds.getHeight() / 2, bounds.getHeight() - 3))); 57 } 58 59 public static <T extends Node> void scrollToInSingleDimension(final Wrap<? extends Control> controlWrap, 60 final Class<T> itemClass, 61 final Callback<T, Integer> indexCallback, 62 final Integer targetControlIndex, 63 final Caret caret, 64 final boolean isVertical) { 65 caret.to(new Caret.Direction() { 66 /** 67 * @return < 0 to scroll toward decreasing value, > 0 - vice versa 0 68 * to stop scrolling NOTE - see implementation 69 * KnobDragScrollerImpl.to(Direction) which is used in ScrollBarWrap 70 * better to return constant values (-1 || 0 || +1) to get smooth 71 * dragging 72 */ 73 final javafx.scene.Parent clippedContainer = getClippedContainerControl(controlWrap); 74 75 @Override 76 public int to() { 77 Bounds visibleBounds = shown1dImpl(controlWrap, clippedContainer, indexCallback, itemClass, isVertical); 78 if (targetControlIndex < visibleBounds.getBegin()) { 79 return -1; 80 } else if (targetControlIndex > visibleBounds.getEnd()) { 81 return 1; 82 } else { 83 return 0; 84 } 85 } 86 87 @Override 88 public String toString() { 89 return "'" + controlWrap.getControl() + "' state at index " + targetControlIndex; 90 } 91 }); 92 } 93 94 public static <CellClass extends Node> Bounds shown1d( 95 final Wrap<? extends Control> controlWrap, 96 final Callback<CellClass, Integer> indexCallback, 97 final Class<CellClass> itemClass, 98 final boolean isVertical) { 99 100 return shown1dImpl(controlWrap, getClippedContainerControl(controlWrap), indexCallback, itemClass, isVertical); 101 } 102 103 /** 104 * Extracted for optimization purpose - we don't need to find clipped 105 * container each time. 106 * 107 * @parameter clippedContainerWrap - precomputed wrap of clipped container. 108 */ 109 private static <CellClass extends Node> Bounds shown1dImpl( 110 final Wrap<? extends Control> controlWrap, 111 final javafx.scene.Parent clippedContainer, 112 final Callback<CellClass, Integer> indexCallback, 113 final Class<CellClass> itemClass, 114 final boolean isVertical) { 115 final int[] minmax = new int[]{Integer.MAX_VALUE, -1}; 116 final Parent<Node> parent = controlWrap.as(Parent.class, Node.class); 117 parent.lookup(itemClass, cntrl -> { 118 boolean checkIndices; 119 checkIndices = NodeWrap.isInBounds(clippedContainer, cntrl, controlWrap.getEnvironment(), isVertical); 120 if (checkIndices) { 121 int index = indexCallback.call(cntrl); 122 if (index >= 0) { 123 if (index < minmax[0]) { 124 minmax[0] = index; 125 } 126 if (index > minmax[1]) { 127 minmax[1] = index; 128 } 129 } 130 } 131 132 return true; 133 }).size(); 134 135 return new Bounds(minmax[0], minmax[1]); 136 } 137 138 /** 139 * checkScrolls(); must be called before this method call. 140 * 141 * @param row 142 * @param column 143 * @return 144 */ 145 static <CellClass extends IndexedCell> Wrap<? extends IndexedCell> scrollTo(final Environment env, 146 final Wrap<? extends Control> wrap, 147 AbstractScroll hScroll, 148 AbstractScroll vScroll, 149 final int row, 150 final int column, 151 final Function<CellClass, Point> infoProvider, 152 final Class<CellClass> cellType) { 153 154 final Rectangle actuallyVisibleArea = getActuallyVisibleArea(wrap); 155 if (vScroll != null) { 156 vScroll.caret().to(() -> { 157 int[] shown = shown(env, wrap, infoProvider, cellType, actuallyVisibleArea); 158 if (shown[1] > row) { 159 return -1; 160 } 161 if (shown[3] < row) { 162 return 1; 163 } 164 return 0; 165 }); 166 } 167 168 if (hScroll != null) { 169 hScroll.caret().to(() -> { 170 int[] shown = shown(env, wrap, infoProvider, cellType, actuallyVisibleArea); 171 if (shown[0] > column) { 172 return -1; 173 } 174 if (shown[2] < column) { 175 return 1; 176 } 177 return 0; 178 }); 179 } 180 return null; 181 } 182 183 /** 184 * Identifies which elements are shown in the TableView currently. 185 * 186 * @return {minColumn, minRow, maxColumn, maxRow} of cells that are fully 187 * visible in the list. 188 */ 189 public static <CellClass extends IndexedCell> int[] shown( 190 Environment env, 191 final Wrap<? extends Control> wrap, 192 final Function<CellClass, Point> infoProvider, 193 final Class<CellClass> cellType) { 194 195 final Rectangle actuallyVisibleArea = getActuallyVisibleArea(wrap); 196 197 return shown(env, wrap, infoProvider, cellType, actuallyVisibleArea); 198 } 199 200 private static <CellClass extends IndexedCell> int[] shown( 201 Environment env, 202 final Wrap<? extends Control> wrap, 203 final Function<CellClass, Point> infoProvider, 204 final Class<CellClass> cellType, 205 final Rectangle actuallyVisibleArea) { 206 return new FutureAction<>(env, () -> { 207 final int[] res = new int[]{Integer.MAX_VALUE, Integer.MAX_VALUE, -1, -1}; 208 209 final Parent<Node> parent = wrap.as(Parent.class, Node.class); 210 parent.lookup(cellType, control -> { 211 if (control.isVisible() && control.getParent().isVisible() && control.getOpacity() == 1.0) { 212 Rectangle bounds = NodeWrap.getScreenBounds(wrap.getEnvironment(), control); 213 Point cellCoord = infoProvider.apply(control); 214 int column = cellCoord.x; 215 int row = cellCoord.y; 216 if (actuallyVisibleArea.contains(bounds) && row >= 0 && column >= 0) { 217 res[0] = Math.min(res[0], column); 218 res[1] = Math.min(res[1], row); 219 res[2] = Math.max(res[2], column); 220 res[3] = Math.max(res[3], row); 221 } 222 } 223 return false; 224 }).size(); 225 226 return res; 227 }).get(); 228 } 229 230 /** 231 * @param controlWrap wrap of control like listView, tableView, treeView, or 232 * treeTableView. 233 * @param treeRowWrap wrap of row : treeItem, treeTableRow or tableRow, 234 * listItem. 235 * @return center of area, which is visible part of row/item. 236 */ 237 public static Point getClickPoint(Wrap controlWrap, Wrap rowWrap) { 238 Rectangle visibleAreaBounds = rowWrap.getScreenBounds(); 239 return getClickPointCommon(controlWrap, visibleAreaBounds); 240 } 241 242 /** 243 * Method, specific for TableView and TreeTableView, which counts only 244 * actual cells width, without space of the last empty column. 245 */ 246 public static Point getTableRowClickPoint(Wrap controlWrap, Wrap rowWrap) { 247 Rectangle clickableArea = TableUtils.constructClickableArea(rowWrap); 248 return getClickPointCommon(controlWrap, clickableArea); 249 } 250 251 /** 252 * Used for ListView, TreeView, TableView, TreeTableView. 253 * 254 * @param wrap of control. 255 * @return 256 */ 257 static Rectangle getActuallyVisibleArea(final Wrap<? extends Control> wrap) { 258 final Rectangle viewArea = getContainerWrap((Parent <Node>)wrap.as(Parent.class, Node.class)).getScreenBounds(); 259 final Rectangle clippedContainerArea = getClippedContainerWrap((Parent <Node>)wrap.as(Parent.class, Node.class)).getScreenBounds(); 260 return new Rectangle(viewArea.x, viewArea.y, clippedContainerArea.width, clippedContainerArea.height); 261 } 262 263 /** 264 * @return wrap of parent container that contains Cells 265 */ 266 static Wrap<? extends javafx.scene.Parent> getContainerWrap(Parent<Node> parent) { 267 return getParentWrap(parent, VIRTIAL_FLOW_CLASS_NAME); 268 } 269 270 static Wrap<? extends javafx.scene.Parent> getClippedContainerWrap(Parent<Node> parent) { 271 return getParentWrap(parent, CLIPPED_CONTAINER_CLASS_NAME); 272 } 273 274 static private Wrap<? extends javafx.scene.Parent> getParentWrap(Parent<Node> parent, final String className) { 275 return parent.lookup(javafx.scene.Parent.class, control -> control.getClass().getName().endsWith(className)).wrap(); 276 } 277 278 static private javafx.scene.Parent getClippedContainerControl(Wrap<? extends Control> controlWrap) { 279 final Parent<Node> parent = controlWrap.as(Parent.class, Node.class); 280 final Wrap<? extends javafx.scene.Parent> clippedContainerWrap = getClippedContainerWrap(parent); 281 return clippedContainerWrap.getControl(); 282 } 283 284 /** 285 * Common algorithm 286 */ 287 private static Point getClickPointCommon(Wrap controlWrap, Rectangle clickableArea) { 288 Rectangle visibleAreaBounds = TableUtils.getActuallyVisibleArea(controlWrap); 289 Rectangle intersection = clickableArea.intersection(visibleAreaBounds); 290 int dx = intersection.x - clickableArea.x; 291 int dy = intersection.y - clickableArea.y; 292 return new Point(dx + intersection.width / 2, dy + intersection.height / 2); 293 } 294 295 /** 296 * Special for TableView, and TreeTableView, as the last column contains 297 * empty not clickable space. 298 */ 299 private static Rectangle constructClickableArea(Wrap<?> wrap) { 300 Rectangle rec = null; 301 final Lookup lookup = wrap.as(Parent.class, Node.class).lookup(IndexedCell.class); 302 for (int i = 0; i < lookup.size(); i++) { 303 Wrap cell = lookup.wrap(i); 304 if (rec == null) { 305 rec = cell.getScreenBounds(); 306 } else { 307 rec = rec.union(cell.getScreenBounds()); 308 } 309 } 310 return rec; 311 } 312 private static final String VIRTIAL_FLOW_CLASS_NAME = "VirtualFlow"; 313 private static final String CLIPPED_CONTAINER_CLASS_NAME = "VirtualFlow$ClippedContainer"; 314 315 public final static class TableViewIndexInfoProvider implements Function<TableCell, Point> { 316 317 TableViewWrap theWrap; 318 319 public TableViewIndexInfoProvider(TableViewWrap wrap) { 320 this.theWrap = wrap; 321 } 322 323 @Override 324 public Point apply(TableCell t) { 325 return new Point(theWrap.getColumnIndex(t), theWrap.getRowIndex(t)); 326 } 327 } 328 329 public final static class TreeTableViewIndexInfoProvider implements Function<TreeTableCell, Point> { 330 331 TreeTableViewWrap theWrap; 332 333 public TreeTableViewIndexInfoProvider(TreeTableViewWrap wrap) { 334 this.theWrap = wrap; 335 } 336 337 @Override 338 public Point apply(TreeTableCell t) { 339 return new Point(theWrap.getColumnIndex(t), theWrap.getRowIndex(t)); 340 } 341 } 342 343 public static class Bounds { 344 345 private final int begin; 346 private final int end; 347 348 public Bounds(int begin, int end) { 349 this.begin = begin; 350 this.end = end; 351 } 352 353 public int getBegin() { 354 return begin; 355 } 356 357 public int getEnd() { 358 return end; 359 } 360 361 @Override 362 public String toString() { 363 return "Bounds : begin <" + begin + ">, end <" + end + ">."; 364 } 365 } 366 }