001package conexp.fx.gui.cellpane; 002 003/* 004 * #%L 005 * Concept Explorer FX 006 * %% 007 * Copyright (C) 2010 - 2023 Francesco Kriegel 008 * %% 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as 011 * published by the Free Software Foundation, either version 3 of the 012 * License, or (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public 020 * License along with this program. If not, see 021 * <http://www.gnu.org/licenses/gpl-3.0.html>. 022 * #L% 023 */ 024 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Comparator; 029import java.util.ConcurrentModificationException; 030import java.util.HashMap; 031import java.util.Map; 032import java.util.Set; 033import java.util.concurrent.ConcurrentHashMap; 034 035import com.google.common.base.Predicate; 036import com.google.common.collect.Collections2; 037import com.google.common.collect.HashMultimap; 038import com.google.common.collect.Iterables; 039import com.google.common.collect.Lists; 040import com.google.common.collect.Multimaps; 041import com.google.common.collect.SetMultimap; 042import com.google.common.collect.Sets; 043import com.sun.javafx.geom.BaseBounds; 044import com.sun.javafx.geom.transform.BaseTransform; 045 046import conexp.fx.core.collections.IntPair; 047import conexp.fx.gui.properties.BoundedIntPairProperty; 048import conexp.fx.gui.properties.SimpleIntPairProperty; 049import conexp.fx.gui.util.ColorScheme; 050import javafx.application.Platform; 051import javafx.beans.binding.BooleanBinding; 052import javafx.beans.binding.DoubleBinding; 053import javafx.beans.binding.IntegerBinding; 054import javafx.beans.binding.ObjectBinding; 055import javafx.beans.property.BooleanProperty; 056import javafx.beans.property.DoubleProperty; 057import javafx.beans.property.IntegerProperty; 058import javafx.beans.property.ObjectProperty; 059import javafx.beans.property.ReadOnlyStringProperty; 060import javafx.beans.property.ReadOnlyStringWrapper; 061import javafx.beans.property.SimpleBooleanProperty; 062import javafx.beans.property.SimpleDoubleProperty; 063import javafx.beans.property.SimpleIntegerProperty; 064import javafx.beans.property.SimpleMapProperty; 065import javafx.beans.property.SimpleObjectProperty; 066import javafx.beans.value.ChangeListener; 067import javafx.beans.value.ObservableValue; 068import javafx.collections.FXCollections; 069import javafx.event.EventHandler; 070import javafx.geometry.Orientation; 071import javafx.scene.control.ScrollBar; 072import javafx.scene.input.MouseEvent; 073import javafx.scene.layout.ColumnConstraints; 074import javafx.scene.layout.GridPane; 075import javafx.scene.layout.RowConstraints; 076import javafx.scene.layout.StackPane; 077 078public abstract class CellPane<TCellPane extends CellPane<TCellPane, TCell>, TCell extends Cell<TCell, TCellPane>> 079 extends GridPane { 080 081 public final ReadOnlyStringProperty id; 082 protected final StackPane contentAndInteractionStackPane = new StackPane() { 083 084 @Deprecated 085 public final BaseBounds 086 impl_computeGeomBounds( 087 final BaseBounds baseBounds, 088 final BaseTransform baseTransform) { 089 synchronized (this) { 090 while (true) 091 try { 092 return super.impl_computeGeomBounds( 093 baseBounds, 094 baseTransform); 095 } catch (ConcurrentModificationException e) { 096 // System.err.println("ignore 097 // " + e.toString() + " in 098 // <CellPane>.contentAndInteractionStackPane.impl_computeGeomBounds(...)"); 099 } 100 } 101 }; 102 }; 103 protected final GridPane contentPane = new GridPane() { 104 105 @Deprecated 106 public final BaseBounds 107 impl_computeGeomBounds( 108 final BaseBounds baseBounds, 109 final BaseTransform baseTransform) { 110 synchronized (this) { 111 while (true) 112 try { 113 return super.impl_computeGeomBounds( 114 baseBounds, 115 baseTransform); 116 } catch (ConcurrentModificationException e) { 117 // System.err.println("ignore 118 // " + e.toString() + " in 119 // <CellPane>.contentPane.impl_computeGeomBounds(...)"); 120 } 121 } 122 }; 123 }; 124 protected final GridPane interactionPane = new GridPane() { 125 126 @Deprecated 127 public final BaseBounds 128 impl_computeGeomBounds( 129 final BaseBounds baseBounds, 130 final BaseTransform baseTransform) { 131 synchronized (this) { 132 while (true) 133 try { 134 return super.impl_computeGeomBounds( 135 baseBounds, 136 baseTransform); 137 } catch (ConcurrentModificationException e) { 138 // System.err.println("ignore 139 // " + e.toString() + " in 140 // <CellPane>.interactionPane.impl_computeGeomBounds(...)"); 141 } 142 } 143 }; 144 }; 145 private final EventHandler<MouseEvent> dehighlightEventHandler = new EventHandler<MouseEvent>() { 146 147 @Override 148 public void 149 handle(MouseEvent event) { 150 dehighlight(); 151 } 152 }; 153 public final IntegerProperty maxRows = new SimpleIntegerProperty(); 154 public final IntegerProperty maxColumns = new SimpleIntegerProperty(); 155 public final BooleanProperty autoSizeRows = new SimpleBooleanProperty(false); 156 public final BooleanProperty autoSizeColumns = new SimpleBooleanProperty(false); 157 public final IntegerProperty rowHeightDefault = new SimpleIntegerProperty(); 158 public final IntegerProperty columnWidthDefault = new SimpleIntegerProperty(); 159 public final IntegerProperty textSizeDefault = new SimpleIntegerProperty(); 160 public final DoubleProperty zoomFactor = new SimpleDoubleProperty(1); 161 public final DoubleProperty maximalTextWidth = new SimpleDoubleProperty(0); 162 public final IntegerBinding rowHeight = new IntegerBinding() { 163 164 { 165 super.bind( 166 zoomFactor, 167 rowHeightDefault, 168 maximalTextWidth); 169 } 170 171 protected int computeValue() { 172 if (autoSizeRows.get()) 173 return (int) maximalTextWidth 174 .get() + 10; 175 // return Math.min( 176 // 250, 177 // (int) maximalTextWidth.get() 178 // + 10); 179 return (int) (zoomFactor.get() 180 * (double) rowHeightDefault 181 .get()); 182 } 183 }; 184 public final IntegerBinding columnWidth = new IntegerBinding() { 185 186 { 187 super.bind( 188 zoomFactor, 189 columnWidthDefault, 190 maximalTextWidth); 191 } 192 193 protected int computeValue() { 194 if (autoSizeColumns.get()) 195 return (int) maximalTextWidth 196 .get() + 10; 197 // return Math.min( 198 // 250, 199 // (int) maximalTextWidth.get() 200 // + 10); 201 return (int) (zoomFactor.get() 202 * columnWidthDefault.get()); 203 }; 204 }; 205 public final IntegerBinding textSize = new IntegerBinding() { 206 207 { 208 super.bind( 209 zoomFactor, 210 textSizeDefault); 211 } 212 213 protected int computeValue() { 214 return (int) (zoomFactor.get() 215 * (double) textSizeDefault 216 .get()); 217 }; 218 }; 219 protected final DoubleBinding prefHeight = new DoubleBinding() { 220 221 { 222 super.bind(rowHeight, maxRows); 223 } 224 225 @Override 226 protected double computeValue() { 227 return maxRows.doubleValue() 228 * rowHeight.doubleValue(); 229 } 230 }; 231 protected final DoubleBinding prefWidth = new DoubleBinding() { 232 233 { 234 super.bind( 235 columnWidth, 236 maxColumns); 237 } 238 239 @Override 240 protected double computeValue() { 241 return maxColumns.doubleValue() 242 * columnWidth.doubleValue(); 243 } 244 }; 245 public final IntegerBinding visibleRows = new IntegerBinding() { 246 247 { 248 super.bind( 249 contentAndInteractionStackPane 250 .heightProperty(), 251 maxRows, 252 rowHeight); 253 } 254 255 protected int computeValue() { 256 if (rowHeight.intValue() == 0) 257 return 0; 258 return Math.min( 259 contentAndInteractionStackPane 260 .heightProperty() 261 .intValue() 262 / rowHeight.intValue(), 263 maxRows.intValue()); 264 }; 265 }; 266 public final IntegerBinding visibleColumns = new IntegerBinding() { 267 268 { 269 super.bind( 270 contentAndInteractionStackPane 271 .widthProperty(), 272 maxColumns, 273 columnWidth); 274 } 275 276 protected int computeValue() { 277 if (columnWidth.intValue() == 0) 278 return 0; 279 return Math.min( 280 contentAndInteractionStackPane 281 .widthProperty() 282 .intValue() 283 / columnWidth.intValue(), 284 maxColumns.intValue()); 285 }; 286 }; 287 public final ObjectProperty<InteractionMode> interactionMode = 288 new SimpleObjectProperty<InteractionMode>(); 289 public final ObjectProperty<ColorScheme> colorScheme = 290 new SimpleObjectProperty<ColorScheme>(ColorScheme.DEFAULT); 291 public final BooleanProperty animate = new SimpleBooleanProperty(); 292 public final BooleanProperty highlight = new SimpleBooleanProperty(); 293 protected final BooleanProperty isDragging = new SimpleBooleanProperty(false); 294 protected final BooleanProperty isDropping = new SimpleBooleanProperty(false); 295 protected final RowScrollBar rowScrollBar = new RowScrollBar(); 296 protected final ColumnScrollBar columnScrollBar = new ColumnScrollBar(); 297 protected int actualRows = 0; 298 protected int actualColumns = 0; 299 protected final SetMultimap<Integer, TCell> rows = 300 Multimaps.synchronizedSetMultimap(HashMultimap.<Integer, TCell> create()); 301 protected final SetMultimap<Integer, TCell> columns = 302 Multimaps.synchronizedSetMultimap(HashMultimap.<Integer, TCell> create()); 303 public final SimpleMapProperty<Integer, Integer> rowMap = 304 new SimpleMapProperty<Integer, Integer>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Integer>())); 305 public final SimpleMapProperty<Integer, Integer> columnMap = 306 new SimpleMapProperty<Integer, Integer>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Integer>())); 307 public final SimpleMapProperty<Integer, Double> rowOpacityMap = 308 new SimpleMapProperty<Integer, Double>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Double>())); 309 public final SimpleMapProperty<Integer, Double> columnOpacityMap = 310 new SimpleMapProperty<Integer, Double>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Double>())); 311 protected final BoundedIntPairProperty minCoordinates = 312 new BoundedIntPairProperty(IntPair.zero(), new ObjectBinding<IntPair>() { 313 314 // { 315 // super.bind(); 316 // } 317 @Override 318 protected IntPair computeValue() { 319 return IntPair.zero(); 320 } 321 }, new ObjectBinding<IntPair>() { 322 323 { 324 super.bind(rowScrollBar.maxProperty(), columnScrollBar.maxProperty()); 325 } 326 327 @Override 328 protected IntPair computeValue() { 329 return IntPair.valueOf((int) rowScrollBar.getMax(), (int) columnScrollBar.getMax()); 330 } 331 }); 332 public final IntegerBinding minRow = new IntegerBinding() { 333 334 { 335 super.bind( 336 rowScrollBar.valueProperty()); 337 } 338 339 protected int computeValue() { 340 return rowScrollBar 341 .valueProperty() 342 .intValue(); 343 }; 344 }; 345 public final IntegerBinding maxRow = new IntegerBinding() { 346 347 { 348 super.bind(minRow, visibleRows); 349 } 350 351 protected int computeValue() { 352 return minRow.intValue() 353 + visibleRows.intValue() - 1; 354 }; 355 }; 356 public final IntegerBinding minColumn = new IntegerBinding() { 357 358 { 359 super.bind( 360 columnScrollBar 361 .valueProperty()); 362 } 363 364 protected int computeValue() { 365 return columnScrollBar 366 .valueProperty() 367 .intValue(); 368 }; 369 }; 370 public final IntegerBinding maxColumn = new IntegerBinding() { 371 372 { 373 super.bind( 374 minColumn, 375 visibleColumns); 376 } 377 378 protected int computeValue() { 379 return minColumn.intValue() 380 + visibleColumns.intValue() 381 - 1; 382 }; 383 }; 384 protected final RowConstraints rowConstraints = new RowConstraints(); 385 protected final ColumnConstraints columnConstraints = new ColumnConstraints(); 386 387 protected final class RowScrollBar extends ScrollBar { 388 389 private RowScrollBar() { 390 super(); 391 this.setOrientation(Orientation.VERTICAL); 392 this.minHeightProperty().bind(rowHeight); 393 this.prefHeightProperty().bind(prefHeight); 394 this.maxHeightProperty().bind(prefHeight); 395 this.minWidthProperty().bind(columnWidth); 396 this.prefWidthProperty().bind(columnWidth); 397 this.maxWidthProperty().bind(columnWidth); 398 this.setMin(0); 399 this.visibleProperty().bind(new BooleanBinding() { 400 401 { 402 super.bind(visibleRows, maxRows); 403 } 404 405 @Override 406 protected boolean computeValue() { 407 return visibleRows.get() < maxRows.get(); 408 } 409 }); 410 this.maxProperty().bind(new DoubleBinding() { 411 412 { 413 super.bind(maxRows, visibleRows); 414 } 415 416 @Override 417 protected double computeValue() { 418 return maxRows.doubleValue() - visibleRows.doubleValue(); 419 } 420 }); 421 this.setUnitIncrement(1); 422 this.blockIncrementProperty().bind(new IntegerBinding() { 423 424 { 425 super.bind(visibleRows); 426 } 427 428 @Override 429 protected int computeValue() { 430 return (int) visibleRows.doubleValue() - 1; 431 } 432 }); 433 this.setValue(0); 434 this.visibleAmountProperty().bind(new DoubleBinding() { 435 436 { 437 super.bind(maxProperty(), visibleRows, maxRows); 438 } 439 440 @Override 441 protected double computeValue() { 442 if (maxRows.get() == 0) 443 return 1; 444 return (visibleRows.doubleValue() / maxRows.doubleValue()) * maxProperty().doubleValue(); 445 } 446 }); 447// this.valueProperty().addListener(new ChangeListener<Number>() { 448// 449// public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { 450// final int deltaValue = newValue.intValue() - oldValue.intValue(); 451// if (deltaValue != 0) 452// scrollRows(deltaValue); 453// } 454// }); 455 this.addEventHandler(MouseEvent.MOUSE_ENTERED, dehighlightEventHandler); 456 this.addEventHandler(MouseEvent.MOUSE_EXITED, dehighlightEventHandler); 457 } 458 } 459 460 protected final class ColumnScrollBar extends ScrollBar { 461 462 private ColumnScrollBar() { 463 super(); 464 this.setOrientation(Orientation.HORIZONTAL); 465 this.minWidthProperty().bind(columnWidth); 466 this.prefWidthProperty().bind(prefWidth); 467 this.maxWidthProperty().bind(prefWidth); 468 this.minHeightProperty().bind(rowHeight); 469 this.prefHeightProperty().bind(rowHeight); 470 this.maxHeightProperty().bind(rowHeight); 471 this.setMin(0); 472 this.visibleProperty().bind(new BooleanBinding() { 473 474 { 475 super.bind(visibleColumns, maxColumns); 476 } 477 478 @Override 479 protected boolean computeValue() { 480 return visibleColumns.get() < maxColumns.get(); 481 } 482 }); 483 this.maxProperty().bind(new DoubleBinding() { 484 485 { 486 super.bind(maxColumns, visibleColumns); 487 } 488 489 @Override 490 protected double computeValue() { 491 return maxColumns.doubleValue() - visibleColumns.doubleValue(); 492 } 493 }); 494 this.setUnitIncrement(1); 495 this.blockIncrementProperty().bind(new IntegerBinding() { 496 497 { 498 super.bind(visibleColumns); 499 } 500 501 @Override 502 protected int computeValue() { 503 return (int) visibleColumns.doubleValue() - 1; 504 } 505 }); 506 this.setValue(0); 507 this.visibleAmountProperty().bind(new DoubleBinding() { 508 509 { 510 super.bind(maxProperty(), visibleColumns, maxColumns); 511 } 512 513 @Override 514 protected double computeValue() { 515 if (maxColumns.get() == 0) 516 return 1; 517 return (visibleColumns.doubleValue() / maxColumns.doubleValue()) * maxProperty().doubleValue(); 518 } 519 }); 520// this.valueProperty().addListener(new ChangeListener<Number>() { 521// 522// public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { 523// final int deltaValue = newValue.intValue() - oldValue.intValue(); 524// if (deltaValue != 0) 525// scrollColumns(deltaValue); 526// } 527// }); 528 this.addEventHandler(MouseEvent.MOUSE_ENTERED, dehighlightEventHandler); 529 this.addEventHandler(MouseEvent.MOUSE_EXITED, dehighlightEventHandler); 530 } 531 } 532 533 public final boolean interactive; 534 535 protected CellPane(final String id, final InteractionMode interactionMode) { 536 this(id, interactionMode, true); 537 } 538 539 protected CellPane(final String id, final InteractionMode interactionMode, final boolean interactive) { 540 super(); 541 this.id = new ReadOnlyStringWrapper(id).getReadOnlyProperty(); 542 this.interactionMode.set(interactionMode); 543 this.interactive = interactive; 544 this.zoomFactor.addListener(new ChangeListener<Number>() { 545 546 @Override 547 public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { 548 maximalTextWidth.set(maximalTextWidth.get() * (newValue.doubleValue() / oldValue.doubleValue())); 549 } 550 }); 551 minCoordinates.addListener(new ChangeListener<IntPair>() { 552 553 @Override 554 public void changed(ObservableValue<? extends IntPair> observable, IntPair oldValue, IntPair newValue) { 555 if (rowScrollBar.getValue() != newValue.x()) 556 rowScrollBar.setValue(Math.min(newValue.x(), rowScrollBar.getMax())); 557 if (columnScrollBar.getValue() != newValue.y()) 558 columnScrollBar.setValue(Math.min(newValue.y(), columnScrollBar.getMax())); 559 } 560 }); 561 rowScrollBar.valueProperty().addListener(new ChangeListener<Number>() { 562 563 @Override 564 public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { 565 if (minCoordinates.get().x() != newValue.intValue()) 566 minCoordinates.set(newValue.intValue(), minCoordinates.get().y()); 567 } 568 }); 569 columnScrollBar.valueProperty().addListener(new ChangeListener<Number>() { 570 571 @Override 572 public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { 573 if (minCoordinates.get().y() != newValue.intValue()) 574 minCoordinates.set(minCoordinates.get().x(), newValue.intValue()); 575 } 576 }); 577 interactionPane.setOnScroll(event -> { 578 if (event.getDeltaX() > 0) 579 columnScrollBar.decrement(); 580 else if (event.getDeltaX() < 0) 581 columnScrollBar.increment(); 582 if (event.getDeltaY() > 0) 583 rowScrollBar.decrement(); 584 else if (event.getDeltaY() < 0) 585 rowScrollBar.increment(); 586 }); 587// interactionPane.setOnSwipeDown(rowScrollBar.getOnSwipeDown()); 588// interactionPane.setOnSwipeUp(rowScrollBar.getOnSwipeUp()); 589// interactionPane.setOnSwipeLeft(columnScrollBar.getOnSwipeLeft()); 590// interactionPane.setOnSwipeRight(columnScrollBar.getOnSwipeRight()); 591 minCoordinates.addListener(new ChangeListener<IntPair>() { 592 593 @Override 594 public void changed(ObservableValue<? extends IntPair> observable, IntPair oldValue, IntPair newValue) { 595 scroll(newValue.x() - oldValue.x(), newValue.y() - oldValue.y()); 596 } 597 }); 598 this.minHeightProperty().bind(rowHeight); 599 this.prefHeightProperty().bind(prefHeight); 600 this.maxHeightProperty().bind(prefHeight); 601 this.contentPane.minHeightProperty().bind(rowHeight); 602 this.contentPane.prefHeightProperty().bind(prefHeight); 603 this.contentPane.maxHeightProperty().bind(prefHeight); 604 this.interactionPane.minHeightProperty().bind(rowHeight); 605 this.interactionPane.prefHeightProperty().bind(prefHeight); 606 this.interactionPane.maxHeightProperty().bind(prefHeight); 607 this.contentAndInteractionStackPane.minHeightProperty().bind(rowHeight); 608 this.contentAndInteractionStackPane.prefHeightProperty().bind(prefHeight); 609 this.contentAndInteractionStackPane.maxHeightProperty().bind(prefHeight); 610 this.rowConstraints.minHeightProperty().bind(rowHeight); 611 this.rowConstraints.maxHeightProperty().bind(rowHeight); 612 this.minWidthProperty().bind(columnWidth); 613 this.prefWidthProperty().bind(prefWidth); 614 this.maxWidthProperty().bind(prefWidth); 615 this.contentPane.minWidthProperty().bind(columnWidth); 616 this.contentPane.prefWidthProperty().bind(prefWidth); 617 this.contentPane.maxWidthProperty().bind(prefWidth); 618 this.interactionPane.minWidthProperty().bind(columnWidth); 619 this.interactionPane.prefWidthProperty().bind(prefWidth); 620 this.interactionPane.maxWidthProperty().bind(prefWidth); 621 this.contentAndInteractionStackPane.minWidthProperty().bind(columnWidth); 622 this.contentAndInteractionStackPane.prefWidthProperty().bind(prefWidth); 623 this.contentAndInteractionStackPane.maxWidthProperty().bind(prefWidth); 624 this.columnConstraints.minWidthProperty().bind(columnWidth); 625 this.columnConstraints.maxWidthProperty().bind(columnWidth); 626 this.visibleColumns.addListener(new ChangeListener<Number>() { 627 628 public void 629 changed(final ObservableValue<? extends Number> observable, final Number oldValue, final Number newValue) { 630 final int deltaColumns = newValue.intValue() - oldValue.intValue(); 631 adjustColumns(deltaColumns); 632 } 633 }); 634 this.visibleRows.addListener(new ChangeListener<Number>() { 635 636 public void 637 changed(final ObservableValue<? extends Number> observable, final Number oldValue, final Number newValue) { 638 final int deltaRows = newValue.intValue() - oldValue.intValue(); 639 adjustRows(deltaRows); 640 } 641 }); 642 interactionPane.addEventHandler(MouseEvent.MOUSE_EXITED, dehighlightEventHandler); 643 this.contentAndInteractionStackPane.getChildren().addAll(contentPane, interactionPane); 644 this.add(contentAndInteractionStackPane, 0, 0); 645 this.add(rowScrollBar, 1, 0); 646 this.add(columnScrollBar, 0, 1); 647 contentAndInteractionStackPane.toFront(); 648 interactionPane.toFront(); 649 } 650 651 public final StackPane getContentAndInteractionStackPane() { 652 return contentAndInteractionStackPane; 653 } 654 655 public final GridPane getContentPane() { 656 return contentPane; 657 } 658 659 public final GridPane getInteractionPane() { 660 return interactionPane; 661 } 662 663 public RowScrollBar getRowScrollBar() { 664 return rowScrollBar; 665 } 666 667 public ColumnScrollBar getColumnScrollBar() { 668 return columnScrollBar; 669 } 670 671 public final Set<TCell> getCellsByGridRow(final int gridRow) { 672 return rows.get(gridRow); 673 } 674 675 public final Set<TCell> getCellsByGridColumn(final int gridColumn) { 676 return columns.get(gridColumn); 677 } 678 679 public final TCell getCellByGridCoordinates(final IntPair coordinates) { 680 return getCellByGridCoordinates(coordinates.x().intValue(), coordinates.y().intValue()); 681 } 682 683 public final TCell getCellByGridCoordinates(final int row, final int column) { 684 return Iterables.getOnlyElement(Sets.intersection(getCellsByGridRow(row), getCellsByGridColumn(column))); 685 } 686 687 private final class ContentRowPredicate implements Predicate<TCell> { 688 689 private final int contentRow; 690 691 private ContentRowPredicate(int contentRow) { 692 super(); 693 this.contentRow = contentRow; 694 } 695 696 @Override 697 public boolean apply(TCell cell) { 698 return cell.contentCoordinates.get().x().intValue() == contentRow; 699 } 700 } 701 702 private final class ContentColumnPredicate implements Predicate<TCell> { 703 704 private final int contentColumn; 705 706 private ContentColumnPredicate(int contentColumn) { 707 super(); 708 this.contentColumn = contentColumn; 709 } 710 711 @Override 712 public boolean apply(TCell cell) { 713 return cell.contentCoordinates.get().y().intValue() == contentColumn; 714 } 715 } 716 717 public final Collection<TCell> getCellsByContentRow(final int contentRow) { 718 return Collections2.filter(rows.values(), new ContentRowPredicate(contentRow)); 719 } 720 721 public final Collection<TCell> getCellsByContentColumn(final int contentColumn) { 722 return Collections2.filter(rows.values(), new ContentColumnPredicate(contentColumn)); 723 } 724 725 public final TCell getCellByContentCoordinates(final IntPair coordinates) { 726 return getCellByContentCoordinates(coordinates.x().intValue(), coordinates.y().intValue()); 727 } 728 729 public final TCell getCellByContentCoordinates(final int row, final int column) { 730 return Iterables.getOnlyElement(Iterables.filter(getCellsByContentRow(row), new Predicate<TCell>() { 731 732 @Override 733 public boolean apply(TCell cell) { 734 return getCellsByContentColumn(column).contains(cell); 735 } 736 })); 737 } 738 739 public final void bind(final CellPane<?, ?> anotherSuperPane, final InteractionMode interactionMode) { 740 if (interactionMode.isRowsEnabled()) { 741 this.maxRows.bind(anotherSuperPane.maxRows); 742 this.rowHeightDefault.bind(anotherSuperPane.rowHeightDefault); 743 this.animate.bind(anotherSuperPane.animate); 744 this.isDragging.bindBidirectional(anotherSuperPane.isDragging); 745 this.isDropping.bindBidirectional(anotherSuperPane.isDropping); 746 this.highlightRowMap.bindBidirectional(anotherSuperPane.highlightRowMap); 747 this.rowOpacityMap.bindBidirectional(anotherSuperPane.rowOpacityMap); 748 this.dragRowMap.bindBidirectional(anotherSuperPane.dragRowMap); 749 this.rowMap.bindBidirectional(anotherSuperPane.rowMap); 750 this.rowScrollBar.valueProperty().bindBidirectional(anotherSuperPane.rowScrollBar.valueProperty()); 751 } 752 if (interactionMode.isColumnsEnabled()) { 753 this.maxColumns.bind(anotherSuperPane.maxColumns); 754 this.columnWidthDefault.bind(anotherSuperPane.columnWidthDefault); 755 this.animate.bind(anotherSuperPane.animate); 756 this.isDragging.bindBidirectional(anotherSuperPane.isDragging); 757 this.isDropping.bindBidirectional(anotherSuperPane.isDropping); 758 this.highlightColumnMap.bindBidirectional(anotherSuperPane.highlightColumnMap); 759 this.columnOpacityMap.bindBidirectional(anotherSuperPane.columnOpacityMap); 760 this.dragColumnMap.bindBidirectional(anotherSuperPane.dragColumnMap); 761 this.columnMap.bindBidirectional(anotherSuperPane.columnMap); 762 this.columnScrollBar.valueProperty().bindBidirectional(anotherSuperPane.columnScrollBar.valueProperty()); 763 } 764 } 765 766 public final void updateContent() { 767 for (final TCell cell : rows.values()) 768 cell.updateContent(); 769 } 770 771 /** 772 * This method creates a new instance of a TCell. In a concrete implementation it suffices to simply return a new 773 * TCell, i.e. <code>return new TCell(...)</code>. 774 * 775 * @param gridRow 776 * the grid row 777 * @param gridColumn 778 * the grid column 779 * @return the t cell 780 */ 781 protected abstract TCell createCell(final int gridRow, final int gridColumn); 782 783 protected void addCell(final int gridRow, final int gridColumn) { 784 createCell(gridRow, gridColumn); 785 } 786 787 private final void removeCell(final TCell cell) { 788 cell.dispose(); 789 } 790 791 private final void adjustRows(final int deltaValue) { 792 if (deltaValue > 0) { 793 final int upShift = Math.max(0, maxRow.get() + deltaValue - maxRows.get()); 794 if (upShift > 0) 795 rowScrollBar.setValue(Math.max(0, rowScrollBar.getValue() - upShift)); 796 for (int i = 0; i < deltaValue; i++) 797 appendRow(); 798 } else if (deltaValue < 0) 799 for (int i = 0; i < -deltaValue; i++) 800 removeRow(); 801 } 802 803 private final void appendRow() { 804 this.contentPane.getRowConstraints().add(rowConstraints); 805 this.interactionPane.getRowConstraints().add(rowConstraints); 806 final int interactionRow = actualRows++; 807 for (int interactionColumn = 0; interactionColumn < actualColumns; interactionColumn++) 808 addCell(interactionRow, interactionColumn); 809 } 810 811 private final void removeRow() { 812 final int row = --actualRows; 813 if (row != -1) { 814 for (final TCell cell : getCellsByGridRow(row)) 815 removeCell(cell); 816 this.contentPane.getRowConstraints().remove(row); 817 this.interactionPane.getRowConstraints().remove(row); 818 } else 819 actualRows++; 820 } 821 822 private final void adjustColumns(int deltaValue) { 823 if (deltaValue > 0) { 824 final int leftShift = Math.max(0, maxColumn.get() + deltaValue - maxColumns.get()); 825 if (leftShift > 0) 826 columnScrollBar.setValue(Math.max(0, columnScrollBar.getValue() - leftShift)); 827 for (int i = 0; i < deltaValue; i++) 828 appendColumn(); 829 } else if (deltaValue < 0) 830 for (int i = 0; i < -deltaValue; i++) 831 removeColumn(); 832 } 833 834 private final void appendColumn() { 835 this.contentPane.getColumnConstraints().add(columnConstraints); 836 this.interactionPane.getColumnConstraints().add(columnConstraints); 837 final int interactionColumn = actualColumns++; 838 for (int interactionRow = 0; interactionRow < actualRows; interactionRow++) 839 addCell(interactionRow, interactionColumn); 840 } 841 842 private final void removeColumn() { 843 final int column = --actualColumns; 844 if (column != -1) { 845 for (final TCell cell : getCellsByGridColumn(column)) 846 removeCell(cell); 847 this.contentPane.getColumnConstraints().remove(column); 848 this.interactionPane.getColumnConstraints().remove(column); 849 } else 850 actualColumns++; 851 } 852 853 protected final SimpleIntPairProperty scrollDeltaCoordinates = new SimpleIntPairProperty(); 854 855 protected enum MovementDirection { 856 UP, 857 UP_LEFT, 858 LEFT, 859 DOWN_LEFT, 860 DOWN, 861 DOWN_RIGHT, 862 RIGHT, 863 UP_RIGHT; 864 865 protected static final MovementDirection valueOf(int rowDelta, int columnDelta) { 866 if (rowDelta > 0 && columnDelta == 0) 867 return UP; 868 if (rowDelta > 0 && columnDelta > 0) 869 return UP_LEFT; 870 if (rowDelta == 0 && columnDelta > 0) 871 return LEFT; 872 if (rowDelta < 0 && columnDelta > 0) 873 return DOWN_LEFT; 874 if (rowDelta < 0 && columnDelta == 0) 875 return DOWN; 876 if (rowDelta < 0 && columnDelta < 0) 877 return DOWN_RIGHT; 878 if (rowDelta == 0 && columnDelta < 0) 879 return RIGHT; 880 if (rowDelta > 0 && columnDelta < 0) 881 return UP_RIGHT; 882 return null; 883 } 884 } 885 886 private final IntPair flipRow(final IntPair gridCoordinates) { 887 return IntPair.valueOf(flipRow(gridCoordinates.x()), gridCoordinates.y()); 888 } 889 890 private final int flipRow(final int row) { 891 return maxRow.get() - row; 892 } 893 894 private final IntPair flipColumn(final IntPair gridCoordinates) { 895 return IntPair.valueOf(gridCoordinates.x(), flipColumn(gridCoordinates.y())); 896 } 897 898 private final int flipColumn(final int column) { 899 return maxColumn.get() - column; 900 } 901 902 private final IntPair flipBoth(final IntPair gridCoordinates) { 903 return IntPair.valueOf(flipRow(gridCoordinates.x()), flipColumn(gridCoordinates.y())); 904 } 905 906 protected final void scroll(int rowDelta, int columnDelta) { 907 switch (MovementDirection.valueOf(rowDelta, columnDelta)) { 908 case UP: 909 for (int row = 0; row < actualRows; row++) 910 for (TCell cell : getCellsByGridRow(row)) 911 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 912 break; 913 case UP_LEFT: 914 ArrayList<TCell> cantorianSortedCells = Lists.newArrayList(rows.values()); 915 Collections.sort(cantorianSortedCells, new Comparator<TCell>() { 916 917 @Override 918 public int compare(TCell o1, TCell o2) { 919 return IntPair.POSITIVE_CANTORIAN_COMPARATOR.compare(o1.gridCoordinates.get(), o2.gridCoordinates.get()); 920 } 921 }); 922 for (TCell cell : cantorianSortedCells) 923 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 924 cantorianSortedCells.clear(); 925 break; 926 case LEFT: 927 for (int column = 0; column < actualColumns; column++) 928 for (TCell cell : getCellsByGridColumn(column)) 929 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 930 break; 931 case DOWN_LEFT: 932 cantorianSortedCells = Lists.newArrayList(rows.values()); 933 Collections.sort(cantorianSortedCells, new Comparator<TCell>() { 934 935 @Override 936 public int compare(TCell o1, TCell o2) { 937 return IntPair.POSITIVE_CANTORIAN_COMPARATOR 938 .compare(flipRow(o1.gridCoordinates.get()), flipRow(o2.gridCoordinates.get())); 939 } 940 }); 941 for (TCell cell : cantorianSortedCells) 942 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 943 cantorianSortedCells.clear(); 944 break; 945 case DOWN: 946 for (int row = actualRows - 1; row >= 0; row--) 947 for (TCell cell : getCellsByGridRow(row)) 948 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 949 break; 950 case DOWN_RIGHT: 951 cantorianSortedCells = Lists.newArrayList(rows.values()); 952 Collections.sort(cantorianSortedCells, new Comparator<TCell>() { 953 954 @Override 955 public int compare(TCell o1, TCell o2) { 956 return IntPair.POSITIVE_CANTORIAN_COMPARATOR 957 .compare(flipBoth(o1.gridCoordinates.get()), flipBoth(o2.gridCoordinates.get())); 958 } 959 }); 960 for (TCell cell : cantorianSortedCells) 961 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 962 cantorianSortedCells.clear(); 963 break; 964 case RIGHT: 965 for (int column = actualColumns - 1; column >= 0; column--) 966 for (TCell cell : getCellsByGridColumn(column)) 967 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 968 break; 969 case UP_RIGHT: 970 cantorianSortedCells = Lists.newArrayList(rows.values()); 971 Collections.sort(cantorianSortedCells, new Comparator<TCell>() { 972 973 @Override 974 public int compare(TCell o1, TCell o2) { 975 return IntPair.POSITIVE_CANTORIAN_COMPARATOR 976 .compare(flipColumn(o1.gridCoordinates.get()), flipColumn(o2.gridCoordinates.get())); 977 } 978 }); 979 for (TCell cell : cantorianSortedCells) 980 cell.scrollDeltaCoordinatesQueue.add(new IntPair(-rowDelta, -columnDelta)); 981 cantorianSortedCells.clear(); 982 break; 983 default: 984 scrollDeltaCoordinates.set(-rowDelta, -columnDelta); 985 } 986 } 987 988 protected final SimpleMapProperty<Integer, Integer> dragRowMap = 989 new SimpleMapProperty<Integer, Integer>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Integer>())); 990 protected final SimpleMapProperty<Integer, Integer> dragColumnMap = 991 new SimpleMapProperty<Integer, Integer>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Integer>())); 992 993 protected final void drag(int sourceRow, int sourceColumn, int targetRow, int targetColumn) { 994 isDropping.set(false); 995 isDragging.set(true); 996 if (sourceRow != -1 && interactionMode.get().isRowsEnabled()) 997 dragRowMap.putAll(getRowChanges(sourceRow, targetRow, ChangeType.DRAG)); 998 if (sourceColumn != -1 && interactionMode.get().isColumnsEnabled()) 999 dragColumnMap.putAll(getColumnChanges(sourceColumn, targetColumn, ChangeType.DRAG)); 1000 } 1001 1002 public final void drop(int sourceRow, int sourceColumn, int targetRow, int targetColumn) { 1003 isDragging.set(false); 1004 isDropping.set(true); 1005 if (sourceRow != -1 && interactionMode.get().isRowsEnabled()) 1006 rowMap.putAll(getRowChanges(sourceRow, targetRow, ChangeType.DROP)); 1007 if (sourceColumn != -1 && interactionMode.get().isColumnsEnabled()) 1008 columnMap.putAll(getColumnChanges(sourceColumn, targetColumn, ChangeType.DROP)); 1009 if (sourceRow != -1 && interactionMode.get().isRowsEnabled()) 1010 dragRowMap.clear(); 1011 if (sourceColumn != -1 && interactionMode.get().isColumnsEnabled()) 1012 dragColumnMap.clear(); 1013 Platform.runLater(new Runnable() { 1014 1015 @Override 1016 public void run() { 1017 Platform.runLater(new Runnable() { 1018 1019 @Override 1020 public void run() { 1021 isDropping.set(false); 1022 } 1023 }); 1024 } 1025 }); 1026 } 1027 1028 private enum ChangeType { 1029 DRAG, 1030 DROP; 1031 } 1032 1033 private final Map<Integer, Integer> getRowChanges(final int sourceRow, final int targetRow, final ChangeType type) { 1034 final Map<Integer, Integer> changes = new HashMap<Integer, Integer>(); 1035 final int sgnRow = (int) Math.signum(targetRow - sourceRow); 1036 for (int row = 0; row < actualRows; row++) { 1037 int newDragRow = row; 1038 if (row == sourceRow) 1039 newDragRow = targetRow; 1040 else if (row * sgnRow >= (sourceRow + sgnRow) * sgnRow && row * sgnRow <= targetRow * sgnRow) 1041 newDragRow = row - sgnRow; 1042 switch (type) { 1043 case DRAG: 1044 changes.put(row, newDragRow); 1045 break; 1046 case DROP: 1047 changes.put( 1048 minRow.get() + newDragRow, 1049 rowMap.containsKey(minRow.get() + row) ? rowMap.get(minRow.get() + row) : minRow.get() + row); 1050 } 1051 } 1052 return changes; 1053 } 1054 1055 private final Map<Integer, Integer> 1056 getColumnChanges(final int sourceColumn, final int targetColumn, final ChangeType type) { 1057 final Map<Integer, Integer> changes = new HashMap<Integer, Integer>(); 1058 final int sgnColumn = (int) Math.signum(targetColumn - sourceColumn); 1059 for (int column = 0; column < actualColumns; column++) { 1060 int newDragColumn = column; 1061 if (column == sourceColumn) 1062 newDragColumn = targetColumn; 1063 else if (column * sgnColumn >= (sourceColumn + sgnColumn) * sgnColumn 1064 && column * sgnColumn <= targetColumn * sgnColumn) 1065 newDragColumn = column - sgnColumn; 1066 switch (type) { 1067 case DRAG: 1068 changes.put(column, newDragColumn); 1069 break; 1070 case DROP: 1071 changes.put( 1072 minColumn.get() + newDragColumn, 1073 columnMap.containsKey(minColumn.get() + column) ? columnMap.get(minColumn.get() + column) 1074 : minColumn.get() + column); 1075 } 1076 } 1077 return changes; 1078 } 1079 1080 protected final SimpleMapProperty<Integer, Boolean> highlightRowMap = 1081 new SimpleMapProperty<Integer, Boolean>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Boolean>())); 1082 protected final SimpleMapProperty<Integer, Boolean> highlightColumnMap = 1083 new SimpleMapProperty<Integer, Boolean>(FXCollections.observableMap(new ConcurrentHashMap<Integer, Boolean>())); 1084 public final SimpleBooleanProperty highlightConcept = new SimpleBooleanProperty(false); 1085 1086 public final void highlight(TCell cell) { 1087 if (highlight.get()) 1088 switch (interactionMode.get()) { 1089 case NONE: 1090 break; 1091 case ROWS: 1092 highlight(Collections.singleton(cell.gridCoordinates.get().x().intValue()), null); 1093 break; 1094 case COLUMNS: 1095 highlight(null, Collections.singleton(cell.gridCoordinates.get().y().intValue())); 1096 break; 1097 case ROWS_AND_COLUMNS: 1098 highlight( 1099 Collections.singleton(cell.gridCoordinates.get().x().intValue()), 1100 Collections.singleton(cell.gridCoordinates.get().y().intValue())); 1101 break; 1102 } 1103 } 1104 1105 public final void highlight(final Collection<Integer> rows, final Collection<Integer> columns) { 1106 if (highlight.get()) { 1107 dehighlight( 1108 rows == null ? Collections.<Integer> emptySet() : rows, 1109 columns == null ? Collections.<Integer> emptySet() : columns); 1110 if (rows != null) 1111 for (Integer row : rows) 1112 highlightRowMap.put(row, true); 1113 if (columns != null) 1114 for (Integer column : columns) 1115 highlightColumnMap.put(column, true); 1116 } 1117 } 1118 1119 public final void dehighlight() { 1120 highlightRowMap.clear(); 1121 highlightColumnMap.clear(); 1122 } 1123 1124 protected final void dehighlight(Collection<Integer> ignoreRows, Collection<Integer> ignoreColumns) { 1125 highlightRowMap.keySet().retainAll(ignoreRows); 1126 highlightColumnMap.keySet().retainAll(ignoreColumns); 1127 } 1128 1129 public void resetGridPositions() { 1130 for (TCell cell : rows.values()) 1131 cell.resetGridPosition(); 1132 } 1133}