/*
 * Copyright (c) 2010, 2014, 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 com.sun.javafx.scene.traversal;

import java.util.List;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;

import static com.sun.javafx.scene.traversal.Direction.*;

First search for on-axis nodes. A node is on-axis if any part of its closest edge lies between the pair of rays cast in the traversal direction from the corners of the originating node. If there are multiple on-axis nodes, the closest is chosen. If there is a tie for the closest on-axis node, the tie is broken by choosing the one whose center is closest to the center of the originating node. TODO: investigate alternative tiebreaker algorithms (filed as RT-19470). If there are no on-axis nodes, compute the distance OUT (in the traversal direction) and the distance to the SIDE (perpendicular to the traversal direction) and compute a distance metric OUT + WEIGHT * SIDE. Choose the node with the smallest distance metric. TODO: presumably WEIGHT is greater than one, so that nodes farther OUT but close (and not on) axis are chosen in preference to nodes that are physically closer but are farther off to the side. Determine WEIGHT somehow, possibly empirically (filed as RT-19471).
/** * First search for on-axis nodes. A node is on-axis if any part of its * closest edge lies between the pair of rays cast in the traversal direction * from the corners of the originating node. * * If there are multiple on-axis nodes, the closest is chosen. * * If there is a tie for the closest on-axis node, the tie is broken * by choosing the one whose center is closest to the center of the * originating node. TODO: investigate alternative tiebreaker algorithms * (filed as RT-19470). * * If there are no on-axis nodes, compute the distance OUT (in the traversal * direction) and the distance to the SIDE (perpendicular to the traversal * direction) and compute a distance metric OUT + WEIGHT * SIDE. Choose the * node with the smallest distance metric. * * TODO: presumably WEIGHT is greater than one, so that nodes farther OUT * but close (and not on) axis are chosen in preference to nodes that are * physically closer but are farther off to the side. Determine WEIGHT * somehow, possibly empirically (filed as RT-19471). */
public class WeightedClosestCorner implements Algorithm { WeightedClosestCorner() { } private boolean isOnAxis(Direction dir, Bounds cur, Bounds tgt) { final double cmin, cmax, tmin, tmax; if (dir == UP || dir == DOWN) { cmin = cur.getMinX(); cmax = cur.getMaxX(); tmin = tgt.getMinX(); tmax = tgt.getMaxX(); } else { // dir == LEFT || dir == RIGHT cmin = cur.getMinY(); cmax = cur.getMaxY(); tmin = tgt.getMinY(); tmax = tgt.getMaxY(); } return tmin <= cmax && tmax >= cmin; }
Compute the out-distance to the near edge of the target in the traversal direction. Negative means the near edge is "behind".
/** * Compute the out-distance to the near edge of the target in the * traversal direction. Negative means the near edge is "behind". */
private double outDistance(Direction dir, Bounds cur, Bounds tgt) { final double distance; if (dir == UP) { distance = cur.getMinY() - tgt.getMaxY(); } else if (dir == DOWN) { distance = tgt.getMinY() - cur.getMaxY(); } else if (dir == LEFT) { distance = cur.getMinX() - tgt.getMaxX(); } else { // dir == RIGHT distance = tgt.getMinX() - cur.getMaxX(); } return distance; }
Computes the side distance from current center to target center. Always positive. This is only used for on-axis nodes.
/** * Computes the side distance from current center to target center. * Always positive. This is only used for on-axis nodes. */
private double centerSideDistance(Direction dir, Bounds cur, Bounds tgt) { final double cc; // current center final double tc; // target center if (dir == UP || dir == DOWN) { cc = cur.getMinX() + cur.getWidth() / 2.0f; tc = tgt.getMinX() + tgt.getWidth() / 2.0f; } else { // dir == LEFT || dir == RIGHT cc = cur.getMinY() + cur.getHeight() / 2.0f; tc = tgt.getMinY() + tgt.getHeight() / 2.0f; } return Math.abs(tc - cc); //return (tc > cc) ? tc - cc : cc - tc; }
Computes the side distance between the closest corners of the current and target. Always positive. This is only used for off-axis nodes.
/** * Computes the side distance between the closest corners of the current * and target. Always positive. This is only used for off-axis nodes. */
private double cornerSideDistance(Direction dir, Bounds cur, Bounds tgt) { final double distance; if (dir == UP || dir == DOWN) { if (tgt.getMinX() > cur.getMaxX()) { // on the right distance = tgt.getMinX() - cur.getMaxX(); } else { // on the left distance = cur.getMinX() - tgt.getMaxX(); } } else { // dir == LEFT or dir == RIGHT if (tgt.getMinY() > cur.getMaxY()) { // below distance = tgt.getMinY() - cur.getMaxY(); } else { // above distance = cur.getMinY() - tgt.getMaxY(); } } return distance; } public Node select(Node node, Direction dir, TraversalContext context) { Node newNode = null; List<Node> nodes = context.getAllTargetNodes(); int target = traverse(context.getSceneLayoutBounds(node), dir, nodes, context); if (target != -1) { newNode = nodes.get(target); } return newNode; } @Override public Node selectFirst(TraversalContext context) { List<Node> nodes = context.getAllTargetNodes(); Point2D zeroZero = new Point2D(0,0); if (nodes.size() > 0) { int nodeIndex; Node nearestNode = nodes.get(0); double nearestDistance = zeroZero.distance(context.getSceneLayoutBounds(nodes.get(0)).getMinX(), context.getSceneLayoutBounds(nodes.get(0)).getMinY()); double distance; for (nodeIndex = 1; nodeIndex < nodes.size(); nodeIndex++) { distance = zeroZero.distance(context.getSceneLayoutBounds(nodes.get(nodeIndex)).getMinX(), context.getSceneLayoutBounds(nodes.get(nodeIndex)).getMinY()); if (nearestDistance > distance) { nearestDistance = distance; nearestNode = nodes.get(nodeIndex); } } return nearestNode; } return null; } @Override public Node selectLast(TraversalContext context) { return null; } public int traverse(Bounds origin, Direction dir, List<Node> targets, TraversalContext context) { final int target; if (dir == NEXT || dir == NEXT_IN_LINE || dir == PREVIOUS) { target = trav1D(origin, dir, targets, context); } else { target = trav2D(origin, dir, targets, context); } return target; } private int trav2D(Bounds origin, Direction dir, List<Node> targets, TraversalContext context) { Bounds bestBounds = null; double bestMetric = 0.0; int bestIndex = -1; for (int i = 0; i < targets.size(); i++) { final Bounds targetBounds = context.getSceneLayoutBounds(targets.get(i)); final double outd = outDistance(dir, origin, targetBounds); final double metric; if (isOnAxis(dir, origin, targetBounds)) { metric = outd + centerSideDistance(dir, origin, targetBounds) / 100; } else { final double cosd = cornerSideDistance(dir, origin, targetBounds); metric = 100000 + outd*outd + 9*cosd*cosd; } if (outd < 0.0) { continue; } if (bestBounds == null || metric < bestMetric) { bestBounds = targetBounds; bestMetric = metric; bestIndex = i; } } return bestIndex; } /* * Consider focus targets to have a total order using values * (minY, minX, hashCode). */ private int compare1D(Bounds a, Bounds b) { int res = 0; // the following use the node's center final double metric1a = (a.getMinY() + a.getMaxY()) / 2; final double metric1b = (b.getMinY() + b.getMaxY()) / 2; final double metric2a = (a.getMinX() + a.getMaxX()) / 2; final double metric2b = (b.getMinX() + b.getMaxX()) / 2; final double metric3a = a.hashCode(); final double metric3b = b.hashCode(); if (metric1a < metric1b) { res = -1; } else if (metric1a > metric1b) { res = 1; } else if (metric2a < metric2b) { res = -1; } else if (metric2a > metric2b) { res = 1; } else if (metric3a < metric3b) { res = -1; } else if (metric3a > metric3b) { res = 1; } return res; } private int compare1D(Bounds a, Bounds b, Direction dir) { return (dir != PREVIOUS) ? -compare1D(a, b) : compare1D(a, b); } private int trav1D(Bounds origin, Direction dir, List<Node> targets, TraversalContext context) { int bestSoFar = -1; int leastSoFar = -1; for (int i = 0; i < targets.size(); i++) { if (leastSoFar == -1 || compare1D(context.getSceneLayoutBounds(targets.get(i)), context.getSceneLayoutBounds(targets.get(leastSoFar)), dir) < 0) { leastSoFar = i; } if (compare1D(context.getSceneLayoutBounds(targets.get(i)), origin, dir) < 0) { continue; } if (bestSoFar == -1 || compare1D(context.getSceneLayoutBounds(targets.get(i)), context.getSceneLayoutBounds(targets.get(bestSoFar)), dir) < 0) { bestSoFar = i; } } return (bestSoFar == -1) ? leastSoFar : bestSoFar; } }