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 }