1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.gridpane;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.tring.Quad;
  35 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.util.CardinalPoint;
  36 import com.oracle.javafx.scenebuilder.kit.metadata.util.ColorEncoder;
  37 import com.oracle.javafx.scenebuilder.kit.util.Deprecation;
  38 import java.util.ArrayList;
  39 import java.util.HashSet;
  40 import java.util.List;
  41 import java.util.Set;
  42 import javafx.geometry.BoundingBox;
  43 import javafx.geometry.Bounds;
  44 import javafx.scene.Cursor;
  45 import javafx.scene.Group;
  46 import javafx.scene.Node;
  47 import javafx.scene.control.Label;
  48 import javafx.scene.layout.GridPane;
  49 import javafx.scene.layout.Region;
  50 import javafx.scene.paint.Color;
  51 import javafx.scene.shape.Line;
  52 import javafx.scene.shape.Path;
  53 import javafx.scene.shape.Rectangle;
  54 
  55 /**
  56  *
  57  */
  58 class GridPaneMosaic {
  59 
  60     public final static double NORTH_TRAY_SIZE = 22;
  61     public final static double SOUTH_TRAY_SIZE = NORTH_TRAY_SIZE;
  62     public final static double WEST_TRAY_SIZE = 24;
  63     public final static double EAST_TRAY_SIZE = WEST_TRAY_SIZE;
  64 
  65     private final Group topGroup = new Group();
  66     private final Path gridPath = new Path();
  67     private final Group hgapLinesGroup = new Group();
  68     private final Group vgapLinesGroup = new Group();
  69     private final Group northTrayGroup = new Group();
  70     private final Group southTrayGroup = new Group();
  71     private final Group westTrayGroup = new Group();
  72     private final Group eastTrayGroup = new Group();
  73     private final Rectangle targetCellShadow = new Rectangle();
  74     private final Line targetGapShadowV = new Line();
  75     private final Line targetGapShadowH= new Line();
  76     private final Group hgapSensorsGroup = new Group();
  77     private final Group vgapSensorsGroup = new Group();
  78 
  79     private final Quad gridAreaQuad = new Quad();
  80     private final List<Quad> gridHoleQuads = new ArrayList<>();
  81 
  82     private final String baseStyleClass;
  83     private final boolean shouldShowTrays;
  84     private final boolean shouldCreateSensors;
  85 
  86     private GridPane gridPane;
  87     private int columnCount;
  88     private int rowCount;
  89     private List<Bounds> cellBounds = new ArrayList<>();
  90     private final Set<Integer> selectedColumnIndexes = new HashSet<>();
  91     private final Set<Integer> selectedRowIndexes = new HashSet<>();
  92     private int targetColumnIndex = -1;
  93     private int targetRowIndex = -1;
  94     private int targetGapColumnIndex = -1;
  95     private int targetGapRowIndex = -1;
  96     private Color trayColor;
  97 
  98     public GridPaneMosaic(String baseStyleClass, boolean shouldShowTrays, boolean shouldCreateSensors) {
  99         assert baseStyleClass != null;
 100 
 101         this.baseStyleClass = baseStyleClass;
 102         this.shouldShowTrays = shouldShowTrays;
 103         this.shouldCreateSensors = shouldCreateSensors;
 104 
 105         final List<Node> topChildren = topGroup.getChildren();
 106         topChildren.add(gridPath);              // Mouse transparent
 107         topChildren.add(hgapLinesGroup);        // Mouse transparent
 108         topChildren.add(vgapLinesGroup);        // Mouse transparent
 109         topChildren.add(northTrayGroup);
 110         topChildren.add(southTrayGroup);
 111         topChildren.add(westTrayGroup);
 112         topChildren.add(eastTrayGroup);
 113         topChildren.add(targetCellShadow);      // Mouse transparent
 114         topChildren.add(targetGapShadowV);      // Mouse transparent
 115         topChildren.add(targetGapShadowH);      // Mouse transparent
 116         topChildren.add(hgapSensorsGroup);
 117         topChildren.add(vgapSensorsGroup);
 118         gridAreaQuad.addToPath(gridPath);
 119 
 120         gridPath.setMouseTransparent(true);
 121         gridPath.getStyleClass().add("gap");
 122         gridPath.getStyleClass().add(baseStyleClass);
 123 
 124         hgapLinesGroup.setMouseTransparent(true);
 125         vgapLinesGroup.setMouseTransparent(true);
 126 
 127         targetCellShadow.setMouseTransparent(true);
 128         targetCellShadow.getStyleClass().add("gap");
 129         targetCellShadow.getStyleClass().add("selected");
 130         targetCellShadow.getStyleClass().add(baseStyleClass);
 131 
 132         targetGapShadowV.setMouseTransparent(true);
 133         targetGapShadowV.getStyleClass().add("gap");
 134         targetGapShadowV.getStyleClass().add("hilit");
 135         targetGapShadowV.getStyleClass().add(baseStyleClass);
 136 
 137         targetGapShadowH.setMouseTransparent(true);
 138         targetGapShadowH.getStyleClass().add("gap");
 139         targetGapShadowH.getStyleClass().add("hilit");
 140         targetGapShadowH.getStyleClass().add(baseStyleClass);
 141     }
 142 
 143     public Group getTopGroup() {
 144         return topGroup;
 145     }
 146 
 147     public GridPane getGridPane() {
 148         return gridPane;
 149     }
 150 
 151     public void setGridPane(GridPane gridPane) {
 152         this.gridPane = gridPane;
 153         update();
 154     }
 155 
 156     public void setTrayColor(Color trayColor) {
 157         this.trayColor = trayColor;
 158         if (shouldShowTrays) {
 159             updateTrayColor();
 160         }
 161     }
 162 
 163     public void setSelectedColumnIndexes(Set<Integer> indexes) {
 164         selectedColumnIndexes.clear();
 165         selectedColumnIndexes.addAll(indexes);
 166         update();
 167     }
 168 
 169     public void setSelectedRowIndexes(Set<Integer> indexes) {
 170         selectedRowIndexes.clear();
 171         selectedRowIndexes.addAll(indexes);
 172         update();
 173     }
 174 
 175     public void setTargetCell(int targetColumnIndex, int targetRowIndex) {
 176         assert (targetColumnIndex == -1) == (targetRowIndex == -1);
 177         this.targetColumnIndex = targetColumnIndex;
 178         this.targetRowIndex = targetRowIndex;
 179         this.targetGapColumnIndex = -1;
 180         this.targetGapRowIndex = -1;
 181         update();
 182     }
 183 
 184     public void setTargetGap(int targetGapColumnIndex, int targetGapRowIndex) {
 185         assert (-1 <= targetGapColumnIndex) && (targetGapColumnIndex <= columnCount);
 186         assert (-1 <= targetGapRowIndex) && (targetGapRowIndex <= rowCount);
 187 
 188         this.targetGapColumnIndex = targetGapColumnIndex;
 189         this.targetGapRowIndex = targetGapRowIndex;
 190         this.targetColumnIndex = -1;
 191         this.targetRowIndex = -1;
 192         update();
 193     }
 194 
 195     public void update() {
 196 
 197         columnCount = Deprecation.getGridPaneColumnCount(gridPane);
 198         rowCount = Deprecation.getGridPaneRowCount(gridPane);
 199         if ((columnCount == 0) || (rowCount == 0)) {
 200             columnCount = rowCount = 0;
 201         }
 202         this.cellBounds.clear();
 203         for (int c = 0; c < columnCount; c++) {
 204             for (int r = 0; r < rowCount; r++) {
 205                 this.cellBounds.add(Deprecation.getGridPaneCellBounds(gridPane, c, r));
 206             }
 207         }
 208 
 209         gridAreaQuad.setBounds(gridPane.getLayoutBounds());
 210         adjustHoleItems();
 211         adjustHGapLines();
 212         adjustVGapLines();
 213         if (shouldShowTrays) {
 214             adjustTrayItems(northTrayGroup.getChildren(), "north", columnCount);
 215             adjustTrayItems(southTrayGroup.getChildren(), "south", columnCount);
 216             adjustTrayItems(westTrayGroup.getChildren(), "west", rowCount);
 217             adjustTrayItems(eastTrayGroup.getChildren(), "east", rowCount);
 218         }
 219         if (shouldCreateSensors) {
 220             final int hgapSensorCount = Math.max(0, columnCount-1);
 221             final int vgapSensorCount = Math.max(0, rowCount-1);
 222             adjustGapSensors(hgapSensorsGroup.getChildren(), Cursor.H_RESIZE, hgapSensorCount);
 223             adjustGapSensors(vgapSensorsGroup.getChildren(), Cursor.V_RESIZE, vgapSensorCount);
 224         }
 225 
 226         if (columnCount >= 1) {
 227             assert rowCount >= 1;
 228 
 229             updateHoleBounds();
 230             updateHGapLines();
 231             updateVGapLines();
 232 
 233             if (shouldShowTrays) {
 234                 updateNorthTrayBounds();
 235                 updateSouthTrayBounds();
 236                 updateWestTrayBounds();
 237                 updateEastTrayBounds();
 238 
 239                 updateSelection(northTrayGroup.getChildren(), selectedColumnIndexes);
 240                 updateSelection(southTrayGroup.getChildren(), selectedColumnIndexes);
 241                 updateSelection(westTrayGroup.getChildren(), selectedRowIndexes);
 242                 updateSelection(eastTrayGroup.getChildren(), selectedRowIndexes);
 243 
 244                 updateTrayColor();
 245             }
 246 
 247             if (shouldCreateSensors) {
 248                 updateHGapSensors();
 249                 updateVGapSensors();
 250             }
 251 
 252 
 253             updateTargetCell();
 254             updateTargetGap();
 255         }
 256     }
 257 
 258 
 259     public List<Node> getNorthTrayNodes() {
 260         return northTrayGroup.getChildren();
 261     }
 262 
 263     public List<Node> getSouthTrayNodes() {
 264         return southTrayGroup.getChildren();
 265     }
 266 
 267     public List<Node> getWestTrayNodes() {
 268         return westTrayGroup.getChildren();
 269     }
 270 
 271     public List<Node> getEastTrayNodes() {
 272         return eastTrayGroup.getChildren();
 273     }
 274 
 275     public List<Node> getHgapSensorNodes() {
 276         return hgapSensorsGroup.getChildren();
 277     }
 278 
 279     public List<Node> getVgapSensorNodes() {
 280         return vgapSensorsGroup.getChildren();
 281     }
 282 
 283 
 284     /*
 285      * Private
 286      */
 287 
 288 
 289     private void adjustHoleItems() {
 290         final int holeCount = columnCount * rowCount;
 291 
 292         while (gridHoleQuads.size() < holeCount) {
 293             final Quad holeQuad = new Quad(false /* clockwise */); // Counterclockwise !!
 294             holeQuad.addToPath(gridPath);
 295             gridHoleQuads.add(holeQuad);
 296         }
 297         while (holeCount < gridHoleQuads.size()) {
 298             final int cellIndex = gridHoleQuads.size()-1;
 299             gridHoleQuads.get(cellIndex).removeFromPath(gridPath);
 300             gridHoleQuads.remove(cellIndex);
 301         }
 302     }
 303 
 304 
 305     private void adjustHGapLines() {
 306         final int hgapLineCount;
 307         if (gridPane.getHgap() == 0) {
 308             hgapLineCount = Math.max(0, columnCount-1);
 309         } else {
 310             hgapLineCount = 0;
 311         }
 312         final List<Node> children = hgapLinesGroup.getChildren();
 313         while (children.size() < hgapLineCount) {
 314             children.add(makeGapLine());
 315         }
 316         while (children.size() > hgapLineCount) {
 317             children.remove(0);
 318         }
 319     }
 320 
 321     private void adjustVGapLines() {
 322         final int vgapLineCount;
 323         if (gridPane.getVgap() == 0) {
 324             vgapLineCount = Math.max(0, rowCount-1);
 325         } else {
 326             vgapLineCount = 0;
 327         }
 328         final List<Node> children = vgapLinesGroup.getChildren();
 329         while (children.size() < vgapLineCount) {
 330             children.add(makeGapLine());
 331         }
 332         while (children.size() > vgapLineCount) {
 333             children.remove(0);
 334         }
 335     }
 336 
 337     private Line makeGapLine() {
 338         final Line result = new Line();
 339         result.getStyleClass().add("gap");
 340         result.getStyleClass().add("empty");
 341         result.getStyleClass().add(baseStyleClass);
 342         return result;
 343     }
 344 
 345 
 346     private void adjustTrayItems(List<Node> trayChildren, String direction, int targetCount) {
 347 
 348         while (trayChildren.size() < targetCount) {
 349             final int trayIndex = trayChildren.size();
 350             trayChildren.add(makeTrayLabel(trayIndex, direction));
 351         }
 352         while (targetCount < trayChildren.size()) {
 353             final int trayIndex = trayChildren.size()-1;
 354             trayChildren.remove(trayIndex);
 355         }
 356     }
 357 
 358     private Label makeTrayLabel(int num, String direction) {
 359         final Label result = new Label();
 360         result.getStyleClass().add("tray");
 361         result.getStyleClass().add(direction);
 362         result.getStyleClass().add(baseStyleClass);
 363         result.setText(String.valueOf(num));
 364         result.setMinWidth(Region.USE_PREF_SIZE);
 365         result.setMaxWidth(Region.USE_PREF_SIZE);
 366         result.setMinHeight(Region.USE_PREF_SIZE);
 367         result.setMaxHeight(Region.USE_PREF_SIZE);
 368 
 369         if (trayColor != null) {
 370             final String webColor = ColorEncoder.encodeColorToRGBA(trayColor);
 371             final String style = "-fx-background-color:"+ webColor +";";//NOI18N
 372             result.setStyle(style);
 373         }
 374 
 375         return result;
 376     }
 377 
 378     private void updateTrayColor() {
 379         final String style;
 380 
 381         if (trayColor == null) {
 382             style = "";//NOI18N
 383         } else {
 384             final String webColor = ColorEncoder.encodeColorToRGBA(trayColor);
 385             style = "-fx-background-color:"+ webColor +";";//NOI18N
 386         }
 387 
 388         adjustTrayStyle(northTrayGroup.getChildren(), style);
 389         adjustTrayStyle(southTrayGroup.getChildren(), style);
 390         adjustTrayStyle(westTrayGroup.getChildren(), style);
 391         adjustTrayStyle(eastTrayGroup.getChildren(), style);
 392     }
 393 
 394 
 395     private void adjustTrayStyle(List<Node> trayChildren, String style) {
 396 
 397         for (Node tray : trayChildren) {
 398             assert tray instanceof Label;
 399             final Label trayLabel = (Label) tray;
 400             trayLabel.setStyle(style);
 401         }
 402     }
 403 
 404 
 405     private void adjustGapSensors(List<Node> gapSensors, Cursor cursor, int targetCount) {
 406         while (gapSensors.size() < targetCount) {
 407             gapSensors.add(makeGapSensor(cursor));
 408         }
 409         while (targetCount < gapSensors.size()) {
 410             final int gapIndex = gapSensors.size()-1;
 411             gapSensors.remove(gapIndex);
 412         }
 413     }
 414 
 415     private Line makeGapSensor(Cursor cursor) {
 416         final Line result = new Line();
 417         result.setCursor(cursor);
 418         result.setStroke(Color.TRANSPARENT);
 419 
 420         return result;
 421     }
 422 
 423 
 424 
 425     private void updateHGapSensors() {
 426         final List<Node> children = hgapSensorsGroup.getChildren();
 427         final int sensorCount = children.size();
 428         assert (sensorCount == 0) || (sensorCount == columnCount-1);
 429         for (int i = 0; i < sensorCount; i++) {
 430             /*
 431              *                       x0  xm   x1
 432              *   y0  +----------------+       +-----------------+
 433              *       |   topLeftCell  |       |   topRightCell  |
 434              *       +----------------+       +-----------------+
 435              *
 436              *       ...
 437              *
 438              *       +----------------+       +-----------------+
 439              *       | bottomLeftCell |       |                 |
 440              *   y1  +----------------+       +-----------------+
 441              */
 442 
 443             final Bounds topLeftCellBounds = getCellBounds(i, 0);
 444             final Bounds topRightCellBounds = getCellBounds(i+1, 0);
 445             final Bounds bottomLeftCellBounds = getCellBounds(i, rowCount-1);
 446             final double x0 = topLeftCellBounds.getMaxX();
 447             final double x1 = topRightCellBounds.getMinX();
 448             final double xm = (x0 + x1) / 2.0;
 449             final double y0 = topLeftCellBounds.getMinY();
 450             final double y1 = bottomLeftCellBounds.getMaxY();
 451             final double strokeWidth = Math.max(8.0, x1 - x0);
 452             final Line line = (Line) children.get(i);
 453             line.setStartX(xm);
 454             line.setStartY(y0);
 455             line.setEndX(xm);
 456             line.setEndY(y1);
 457             line.setStrokeWidth(strokeWidth);
 458         }
 459     }
 460 
 461     private void updateVGapSensors() {
 462         final List<Node> children = vgapSensorsGroup.getChildren();
 463         final int sensorCount = children.size();
 464         assert (sensorCount == 0) || (sensorCount == rowCount-1);
 465         for (int i = 0; i < sensorCount; i++) {
 466 
 467             /*
 468              *       x0                                        x1
 469              *       +----------------+       +-----------------+
 470              *       |   topLeftCell  |  ...  |   topRightCell  |
 471              *   y0  +----------------+       +-----------------+
 472              *   ym
 473              *   y1  +----------------+       +-----------------+
 474              *       | bottomLeftCell |  ...  |                 |
 475              *       +----------------+       +-----------------+
 476              */
 477 
 478             final Bounds topLeftCellBounds = getCellBounds(0, i);
 479             final Bounds bottomLeftCellBounds = getCellBounds(0, i+1);
 480             final Bounds topRightCellBounds = getCellBounds(columnCount-1, i);
 481             final double x0 = topLeftCellBounds.getMinX();
 482             final double x1 = topRightCellBounds.getMaxX();
 483             final double y0 = topLeftCellBounds.getMaxY();
 484             final double y1 = bottomLeftCellBounds.getMinY();
 485             final double ym = (y0 + y1) / 2.0;
 486             final double strokeWidth = Math.max(8.0, y1 - y0);
 487             final Line line = (Line) children.get(i);
 488             line.setStartX(x0);
 489             line.setStartY(ym);
 490             line.setEndX(x1);
 491             line.setEndY(ym);
 492             line.setStrokeWidth(strokeWidth);
 493         }
 494     }
 495 
 496     private void updateHoleBounds() {
 497         for (int c = 0; c < columnCount; c++) {
 498             for (int r = 0; r < rowCount; r++) {
 499                 final Bounds cb = getCellBounds(c, r);
 500                 gridHoleQuads.get(getCellIndex(c, r)).setBounds(cb);
 501             }
 502         }
 503     }
 504 
 505     private void updateHGapLines() {
 506         final List<Node> children = hgapLinesGroup.getChildren();
 507         final int lineCount = children.size();
 508         assert (lineCount == 0) || (lineCount == columnCount-1);
 509         for (int i = 0; i < lineCount; i++) {
 510             final Bounds topLeftCellBounds = getCellBounds(i, 0);
 511             final Bounds topRightCellBounds = getCellBounds(i+1, 0);
 512             final Bounds bottomLeftCellBounds = getCellBounds(i, rowCount-1);
 513             final double startX = (topLeftCellBounds.getMaxX() + topRightCellBounds.getMinX()) / 2.0;
 514             final double startY = topLeftCellBounds.getMinY();
 515             final double endY = bottomLeftCellBounds.getMaxY();
 516             final double snappedX = Math.round(startX) + 0.5;
 517             final Line line = (Line) children.get(i);
 518             line.setStartX(snappedX);
 519             line.setStartY(startY);
 520             line.setEndX(snappedX);
 521             line.setEndY(endY);
 522         }
 523     }
 524 
 525     private void updateVGapLines() {
 526         final List<Node> children = vgapLinesGroup.getChildren();
 527         final int lineCount = children.size();
 528         assert (lineCount == 0) || (lineCount == rowCount-1);
 529         for (int i = 0; i < lineCount; i++) {
 530             final Bounds topLeftCellBounds = getCellBounds(0, i);
 531             final Bounds bottomLeftCellBounds = getCellBounds(0, i+1);
 532             final Bounds topRightCellBounds = getCellBounds(columnCount-1, i);
 533             final double startX = topLeftCellBounds.getMinX();
 534             final double startY = (topLeftCellBounds.getMaxY() + bottomLeftCellBounds.getMinY()) / 2.0;
 535             final double endX = topRightCellBounds.getMaxX();
 536             final double snappedY = Math.round(startY) + 0.5;
 537             final Line line = (Line) children.get(i);
 538             line.setStartX(startX);
 539             line.setStartY(snappedY);
 540             line.setEndX(endX);
 541             line.setEndY(snappedY);
 542         }
 543     }
 544 
 545 
 546     private void updateNorthTrayBounds() {
 547         final List<Node> northTrayChildren = northTrayGroup.getChildren();
 548         assert northTrayChildren.size() == columnCount;
 549 
 550         for (int c = 0; c < columnCount; c++) {
 551             updateNorthTrayBounds(c, (Label)northTrayChildren.get(c));
 552         }
 553     }
 554 
 555 
 556     private void updateNorthTrayBounds(int column, Label label) {
 557         final Bounds gb = gridPane.getLayoutBounds();
 558         final Bounds cb = getCellBounds(column, 0);
 559 
 560 
 561         /*
 562          *            x0                x1
 563          *            +-----------------+
 564          *            |     north(c)    |
 565          * y0  ....---+-----------------+---...
 566          *            |    padding.top  |
 567          *     ....---+-----------------+---...
 568          *            |                 |
 569          *            |    cell(c, 0)   |
 570          *            |                 |
 571          * y1  ....---+-----------------+---...
 572          */
 573 
 574         final double x0 = cb.getMinX();
 575         final double x1 = cb.getMaxX();
 576         final double y0 = gb.getMinY();
 577         final double y1 = cb.getMaxY();
 578         assert x0 <= x1;
 579         assert y0 <= y1;
 580 
 581         label.setPrefWidth(x1 - x0);
 582         label.setPrefHeight(NORTH_TRAY_SIZE);
 583 
 584         final Bounds area = new BoundingBox(x0, y0, x1-x0, y1-y0);
 585         relocateNode(label, area, CardinalPoint.N);
 586     }
 587 
 588 
 589     private void updateSouthTrayBounds() {
 590         final List<Node> trayChildren = southTrayGroup.getChildren();
 591         assert trayChildren.size() == columnCount;
 592 
 593         for (int c = 0; c < columnCount; c++) {
 594             updateSouthTrayBounds(c, (Label)trayChildren.get(c));
 595         }
 596     }
 597 
 598 
 599     private void updateSouthTrayBounds(int column, Label label) {
 600         final Bounds gb = gridPane.getLayoutBounds();
 601         final Bounds cb = getCellBounds(column, 0);
 602 
 603 
 604         /*
 605          *            x0                x1
 606          * y0  ....---+-----------------+---...
 607          *            |                 |
 608          *            |   cell(c, n-1)  |
 609          *            |                 |
 610          *     ....---+-----------------+---...
 611          *            |  padding.bottom |
 612          * y1  ....---+-----------------+---...
 613          *            |     south(c)    |
 614          *            +-----------------+
 615          */
 616 
 617         final double x0 = cb.getMinX();
 618         final double x1 = cb.getMaxX();
 619         final double y0 = cb.getMinY();
 620         final double y1 = gb.getMaxY();
 621         assert x0 <= x1;
 622         assert y0 <= y1;
 623 
 624         label.setPrefWidth(x1 - x0);
 625         label.setPrefHeight(SOUTH_TRAY_SIZE);
 626 
 627         final Bounds area = new BoundingBox(x0, y0, x1-x0, y1-y0);
 628         relocateNode(label, area, CardinalPoint.S);
 629     }
 630 
 631 
 632     private void updateWestTrayBounds() {
 633         final List<Node> trayChildren = westTrayGroup.getChildren();
 634         assert trayChildren.size() == rowCount;
 635 
 636         for (int r = 0; r < rowCount; r++) {
 637             updateWestTrayBounds(r, (Label)trayChildren.get(r));
 638         }
 639     }
 640 
 641 
 642     private void updateWestTrayBounds(int row, Label label) {
 643         final Bounds gb = gridPane.getLayoutBounds();
 644         final Bounds cb = getCellBounds(0,row);
 645 
 646 
 647         /*
 648          *         x0                    x1
 649          *         .   .                 .
 650          *         .   .                 .
 651          *         .   .                 .
 652          *         |   |                 |
 653          * y0 +----+---+-----------------+...
 654          *    |    |   |                 |
 655          *    |    |   |                 |
 656          *    |    |   |                 |
 657          *    |    |   |   cell(0, row)  |
 658          *    |    |   |                 |
 659          *    |    |   |                 |
 660          *    |    |   |                 |
 661          * y1 +----+---+-----------------+...
 662          *         |   |                 |
 663          *         .   .                 .
 664          *         .   .                 .
 665          *         .   .                 .
 666          *      ^    ^
 667          *      |    |
 668          *      |    padding.left
 669          *      |
 670          *      west(r)
 671          */
 672 
 673         final double x0 = gb.getMinX();
 674         final double x1 = cb.getMaxX();
 675         final double y0 = cb.getMinY();
 676         final double y1 = cb.getMaxY();
 677         assert x0 <= x1;
 678         assert y0 <= y1;
 679 
 680         label.setPrefWidth(y1 - y0);
 681         label.setPrefHeight(WEST_TRAY_SIZE);
 682 
 683         final Bounds area = new BoundingBox(x0, y0, x1-x0, y1-y0);
 684         relocateNode(label, area, CardinalPoint.W);
 685     }
 686 
 687 
 688 
 689 
 690     private void updateEastTrayBounds() {
 691         final List<Node> trayChildren = eastTrayGroup.getChildren();
 692         assert trayChildren.size() == rowCount;
 693 
 694         for (int r = 0; r < rowCount; r++) {
 695             updateEastTrayBounds(r, (Label)trayChildren.get(r));
 696         }
 697     }
 698 
 699 
 700     private void updateEastTrayBounds(int row, Label label) {
 701         final Bounds gb = gridPane.getLayoutBounds();
 702         final Bounds cb = getCellBounds(0,row);
 703 
 704 
 705         /*
 706          *             x0                    x1
 707          *             .                 .   .
 708          *             .                 .   .
 709          *             .                 .   .
 710          *             |                 |   |
 711          * y0          +-----------------+---+----+...
 712          *             |                 |   |    |
 713          *             |                 |   |    |
 714          *             |                 |   |    |
 715          *             |   cell(0, row)  |   |    |
 716          *             |                 |   |    |
 717          *             |                 |   |    |
 718          *             |                 |   |    |
 719          * y1          +-----------------+---+----+...
 720          *             |                 |
 721          *             .                 .
 722          *             .                 .
 723          *             .                 .
 724          *                                  ^    ^
 725          *                                  |    |
 726          *                                  |    west(r)
 727          *                                  |
 728          *                                  padding.right
 729          */
 730 
 731         final double x0 = cb.getMinX();
 732         final double x1 = gb.getMaxX();
 733         final double y0 = cb.getMinY();
 734         final double y1 = cb.getMaxY();
 735         assert x0 <= x1;
 736         assert y0 <= y1;
 737 
 738         label.setPrefWidth(y1 - y0);
 739         label.setPrefHeight(EAST_TRAY_SIZE);
 740 
 741         final Bounds area = new BoundingBox(x0, y0, x1-x0, y1-y0);
 742         relocateNode(label, area, CardinalPoint.E);
 743     }
 744 
 745 
 746     private void relocateNode(Label node, Bounds area, CardinalPoint cp) {
 747         assert node != null;
 748 
 749         final double nodeW = node.getPrefWidth();
 750         final double nodeH = node.getPrefHeight();
 751         final double areaW = area.getWidth();
 752         final double areaH = area.getHeight();
 753 
 754         /*
 755          * From
 756          *
 757          *      +----------+
 758          *      |   node   |--------------------+
 759          *      +----------+                    |
 760          *           |                          |
 761          *           |                          |
 762          *           |           area           |
 763          *           |                          |
 764          *           |                          |
 765          *           |                          |
 766          *           +--------------------------+
 767          *
 768          *
 769          * to North
 770          *                   +----------+
 771          *                   |   node   |
 772          *           +-------+----------+-------+
 773          *           |                          |
 774          *           |                          |
 775          *           |                          |   rotation   = 0°
 776          *           |           area           |   translateX = +areaW/2
 777          *           |                          |   translateY = -nodeH/2
 778          *           |                          |
 779          *           |                          |
 780          *           +--------------------------+
 781          *
 782          * to South
 783          *           +--------------------------+
 784          *           |                          |
 785          *           |                          |
 786          *           |                          |   rotation   = 0°
 787          *           |           area           |   translateX = +areaW/2
 788          *           |                          |   translateY = +areaW+nodeH/2
 789          *           |                          |
 790          *           |                          |
 791          *           +-------+----------+-------+
 792          *                   |   node   |
 793          *                   +----------+
 794          *
 795          * to West
 796          *           +--------------------------+
 797          *           |                          |
 798          *      +----+                          |
 799          *      |    |                          |   rotation   = -90°
 800          *      |node|           area           |   translateX = -nodeH/2
 801          *      |    |                          |   translateY = +areaH/2
 802          *      +----+                          |
 803          *           |                          |
 804          *           +--------------------------+
 805          *
 806          * to East
 807          *           +--------------------------+
 808          *           |                          |
 809          *           |                          |----+
 810          *           |                          |    |   rotation   = +90°
 811          *           |           area           |node|   translateX = +areaW+nodeH/2
 812          *           |                          |    |   translateY = +areaH/2
 813          *           |                          |----+
 814          *           |                          |
 815          *           +--------------------------+
 816          */
 817 
 818         final double rotation, translateX, translateY;
 819         switch(cp) {
 820             case N:
 821                 rotation = 0.0;
 822                 translateX = +areaW/2.0;
 823                 translateY = -nodeH/2.0;
 824                 break;
 825             case S:
 826                 rotation = 0.0;
 827                 translateX = +areaW/2.0;
 828                 translateY = +areaH + nodeH/2.0;
 829                 break;
 830             case W:
 831                 rotation = -90.0;
 832                 translateX = -nodeH/2.0;
 833                 translateY = +areaH/2.0;
 834                 break;
 835             case E:
 836                 rotation = +90.0;
 837                 translateX = +areaW + nodeH/2.0;
 838                 translateY = +areaH/2.0;
 839                 break;
 840             default:
 841                 assert false;
 842                 rotation = translateX = translateY = 0;
 843                 break;
 844         }
 845 
 846         final double nodeCenterX = nodeW / 2.0;
 847         final double nodeCenterY = nodeH / 2.0;
 848         final double layoutDX = area.getMinX() - nodeCenterX + translateX;
 849         final double layoutDY = area.getMinY() - nodeCenterY + translateY;
 850 
 851         node.setLayoutX(layoutDX);
 852         node.setLayoutY(layoutDY);
 853         node.setRotate(rotation);
 854     }
 855 
 856 
 857 
 858     private void updateSelection(List<Node> trayChildren, Set<Integer> selectedIndexes) {
 859         final String selectedClass = "selected";
 860 
 861         for (int i = 0, count = trayChildren.size(); i < count; i++) {
 862             final List<String> trayStyleClasses = trayChildren.get(i).getStyleClass();
 863             if (selectedIndexes.contains(i)) {
 864                 if (trayStyleClasses.contains(selectedClass) == false) {
 865                     trayStyleClasses.add(selectedClass);
 866                 }
 867             } else {
 868                 if (trayStyleClasses.contains(selectedClass)) {
 869                     trayStyleClasses.remove(selectedClass);
 870                 }
 871             }
 872         }
 873     }
 874 
 875 
 876     private void updateTargetCell() {
 877         if (targetColumnIndex == -1) {
 878             assert targetRowIndex == -1;
 879             targetCellShadow.setVisible(false);
 880         } else {
 881             targetCellShadow.setVisible(true);
 882             final Bounds tb = getCellBounds(targetColumnIndex, targetRowIndex);
 883             targetCellShadow.setX(tb.getMinX());
 884             targetCellShadow.setY(tb.getMinY());
 885             targetCellShadow.setWidth(tb.getWidth());
 886             targetCellShadow.setHeight(tb.getHeight());
 887         }
 888     }
 889 
 890 
 891     private Bounds getCellBounds(int c, int r) {
 892         final int cellIndex = getCellIndex(c, r);
 893         assert cellIndex < cellBounds.size();
 894         return cellBounds.get(cellIndex);
 895     }
 896 
 897     private int getCellIndex(int c, int r) {
 898         return c * rowCount + r;
 899     }
 900 
 901 
 902     private static final double MIN_STROKE_WIDTH = 8;
 903 
 904     private void updateTargetGap() {
 905 
 906         /*
 907          * targetGapShadowV
 908          */
 909         if (targetGapColumnIndex == -1) {
 910             targetGapShadowV.setVisible(false);
 911         } else {
 912             targetGapShadowV.setVisible(true);
 913 
 914             final double startX, startY, endY, strokeWidth;
 915             if (targetGapColumnIndex < columnCount) {
 916                 final Bounds topCellBounds = getCellBounds(targetGapColumnIndex, 0);
 917                 final Bounds bottomCellBounds = getCellBounds(targetGapColumnIndex, rowCount-1);
 918                 startY = topCellBounds.getMinY();
 919                 endY = bottomCellBounds.getMaxY();
 920                 if (targetGapColumnIndex == 0) {
 921                     startX = topCellBounds.getMinX();
 922                     strokeWidth = MIN_STROKE_WIDTH;
 923                 } else {
 924                     assert targetGapColumnIndex >= 1;
 925                     final Bounds leftTopCellBounds = getCellBounds(targetGapColumnIndex-1, 0);
 926                     startX = (leftTopCellBounds.getMaxX() + topCellBounds.getMinX()) / 2.0;
 927                     strokeWidth = Math.abs(leftTopCellBounds.getMaxX() - topCellBounds.getMinX());
 928                 }
 929             } else {
 930                 final Bounds topCellBounds = getCellBounds(columnCount-1, 0);
 931                 final Bounds bottomCellBounds = getCellBounds(columnCount-1, rowCount-1);
 932                 startX = topCellBounds.getMaxX();
 933                 startY = topCellBounds.getMinY();
 934                 endY = bottomCellBounds.getMaxY();
 935                 strokeWidth = MIN_STROKE_WIDTH;
 936             }
 937             targetGapShadowV.setStartX(startX);
 938             targetGapShadowV.setStartY(startY);
 939             targetGapShadowV.setEndX(startX);
 940             targetGapShadowV.setEndY(endY);
 941             targetGapShadowV.setStrokeWidth(Math.max(strokeWidth, MIN_STROKE_WIDTH));
 942         }
 943 
 944         /*
 945          * targetGapShadowH
 946          */
 947         if (targetGapRowIndex == -1) {
 948             targetGapShadowH.setVisible(false);
 949         } else {
 950             targetGapShadowH.setVisible(true);
 951 
 952             final double startX, endX, startY, strokeWidth;
 953             if (targetGapRowIndex < rowCount) {
 954                 final Bounds leftCellBounds = getCellBounds(0, targetGapRowIndex);
 955                 final Bounds rightCellBounds = getCellBounds(columnCount-1, targetGapRowIndex);
 956                 startX = leftCellBounds.getMinX();
 957                 endX = rightCellBounds.getMaxX();
 958                 if (targetGapRowIndex == 0) {
 959                     startY = leftCellBounds.getMinY();
 960                     strokeWidth = MIN_STROKE_WIDTH;
 961                 } else {
 962                     assert targetGapRowIndex >= 1;
 963                     final Bounds aboveLeftCellBounds = getCellBounds(0, targetGapRowIndex-1);
 964                     startY = (aboveLeftCellBounds.getMaxY() + leftCellBounds.getMinY()) / 2.0;
 965                     strokeWidth = Math.abs(aboveLeftCellBounds.getMaxY() - leftCellBounds.getMinY());
 966                 }
 967             } else {
 968                 final Bounds leftCellBounds = getCellBounds(0, rowCount-1);
 969                 final Bounds rightCellBounds = getCellBounds(columnCount-1, rowCount-1);
 970                 startX = leftCellBounds.getMinX();
 971                 endX = rightCellBounds.getMaxX();
 972                 startY = leftCellBounds.getMaxY();
 973                 strokeWidth = MIN_STROKE_WIDTH;
 974             }
 975             targetGapShadowH.setStartX(startX);
 976             targetGapShadowH.setStartY(startY);
 977             targetGapShadowH.setEndX(endX);
 978             targetGapShadowH.setEndY(startY);
 979             targetGapShadowH.setStrokeWidth(Math.max(strokeWidth, MIN_STROKE_WIDTH));
 980         }
 981 
 982     }
 983 }