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.io.Serializable; 026import java.util.Collections; 027import java.util.Iterator; 028import java.util.LinkedList; 029 030import conexp.fx.core.collections.IntPair; 031import conexp.fx.core.util.IdGenerator; 032import conexp.fx.gui.util.Platform2; 033import javafx.animation.FadeTransition; 034import javafx.animation.FadeTransitionBuilder; 035import javafx.animation.FillTransition; 036import javafx.animation.FillTransitionBuilder; 037import javafx.animation.TranslateTransition; 038import javafx.animation.TranslateTransitionBuilder; 039import javafx.application.Platform; 040import javafx.beans.binding.BooleanBinding; 041import javafx.beans.binding.DoubleBinding; 042import javafx.beans.binding.ObjectBinding; 043import javafx.beans.binding.StringBinding; 044import javafx.beans.property.BooleanProperty; 045import javafx.beans.property.DoubleProperty; 046import javafx.beans.property.IntegerProperty; 047import javafx.beans.property.ObjectProperty; 048import javafx.beans.property.ReadOnlyLongProperty; 049import javafx.beans.property.ReadOnlyLongWrapper; 050import javafx.beans.property.ReadOnlyObjectProperty; 051import javafx.beans.property.ReadOnlyObjectWrapper; 052import javafx.beans.property.SimpleBooleanProperty; 053import javafx.beans.property.SimpleDoubleProperty; 054import javafx.beans.property.SimpleIntegerProperty; 055import javafx.beans.property.SimpleObjectProperty; 056import javafx.beans.property.SimpleStringProperty; 057import javafx.beans.property.StringProperty; 058import javafx.beans.value.ChangeListener; 059import javafx.beans.value.ObservableValue; 060import javafx.collections.FXCollections; 061import javafx.collections.ListChangeListener; 062import javafx.collections.ObservableList; 063import javafx.event.ActionEvent; 064import javafx.event.EventHandler; 065import javafx.geometry.Bounds; 066import javafx.geometry.Pos; 067import javafx.scene.effect.BlurType; 068import javafx.scene.effect.DropShadowBuilder; 069import javafx.scene.effect.Effect; 070import javafx.scene.input.ClipboardContent; 071import javafx.scene.input.DataFormat; 072import javafx.scene.input.DragEvent; 073import javafx.scene.input.Dragboard; 074import javafx.scene.input.MouseButton; 075import javafx.scene.input.MouseEvent; 076import javafx.scene.input.TransferMode; 077import javafx.scene.layout.BorderPane; 078import javafx.scene.layout.StackPane; 079import javafx.scene.paint.Color; 080import javafx.scene.shape.Rectangle; 081import javafx.scene.shape.RectangleBuilder; 082import javafx.scene.text.Text; 083import javafx.scene.text.TextAlignment; 084import javafx.scene.text.TextBuilder; 085import javafx.scene.transform.Rotate; 086import javafx.scene.transform.Translate; 087import javafx.util.Duration; 088 089public abstract class Cell<TCell extends Cell<TCell, TCellPane>, TCellPane extends CellPane<TCellPane, TCell>> { 090 091 public static final DataFormat CELL_COORDINATES_DATA_FORMAT = new DataFormat("CellCoordinates"); 092 093 public enum MouseEventType { 094 SCROLL, 095 DRAG; 096 } 097 098 public static final class CellCoordinates implements Serializable { 099 100 private static final long serialVersionUID = 8412756602066000809L; 101 public final String cellPaneId; 102 public final MouseEventType mouseEventType; 103 public int gridRow; 104 public int gridColumn; 105 public int contentRow; 106 public int contentColumn; 107 108 protected CellCoordinates( 109 String cellPaneId, 110 final MouseEventType mouseEventType, 111 int gridRow, 112 int gridColumn, 113 int contentRow, 114 int contentColumn) { 115 super(); 116 this.cellPaneId = cellPaneId; 117 this.mouseEventType = mouseEventType; 118 this.gridRow = gridRow; 119 this.gridColumn = gridColumn; 120 this.contentRow = contentRow; 121 this.contentColumn = contentColumn; 122 } 123 } 124 125 public Color dehighlightColor = Color.TRANSPARENT; 126 protected final TCellPane cellPane; 127 public final ReadOnlyLongProperty id = 128 new ReadOnlyLongWrapper(IdGenerator.getNextId()).getReadOnlyProperty(); 129 public final ReadOnlyObjectProperty<IntPair> gridCoordinates; 130 public final ObjectBinding<IntPair> contentCoordinates; 131 protected final ObjectBinding<IntPair> snapToCoordinates; 132 protected final ObservableList<IntPair> scrollDeltaCoordinatesQueue = 133 FXCollections.observableList(Collections.synchronizedList(new LinkedList<IntPair>())); 134 public final DoubleProperty width = new SimpleDoubleProperty(); 135 public final DoubleProperty height = new SimpleDoubleProperty(); 136 public final IntegerProperty textSize = new SimpleIntegerProperty(); 137 public final StringBinding textStyle = new StringBinding() { 138 139 { 140 super.bind(textSize); 141 } 142 143 @Override 144 public String computeValue() { 145 return "-fx-font-size: " 146 + textSize.get() + ";"; 147 } 148 }; 149 public final StringProperty textContent = new SimpleStringProperty(); 150 public final DoubleProperty opacity = new SimpleDoubleProperty(); 151 public final BooleanProperty highlight = new SimpleBooleanProperty(); 152 public final BooleanProperty animate = new SimpleBooleanProperty(); 153 protected final ObjectProperty<CellInteractionPane> interactionPane = 154 new SimpleObjectProperty<CellInteractionPane>(new CellInteractionPane()); 155 public final ObjectProperty<CellContentPane> contentPane = 156 new SimpleObjectProperty<CellContentPane>(new CellContentPane()); 157 private boolean runningLoop = false; 158 private ObjectProperty<TranslateTransition> translateTransition = 159 new SimpleObjectProperty<TranslateTransition>(TranslateTransitionBuilder.create().build()); 160 private ObjectProperty<FillTransition> fillTransition = 161 new SimpleObjectProperty<FillTransition>(FillTransitionBuilder.create().build()); 162 private ObjectProperty<FadeTransition> fadeTransition = 163 new SimpleObjectProperty<FadeTransition>(FadeTransitionBuilder.create().build()); 164 private ChangeListener<IntPair> snapToCoordinatesChangeListener; 165 private ListChangeListener<IntPair> scrollDeltaCoordinatesQueueChangeListener; 166 private ChangeListener<IntPair> contentCoordinatesChangeListener; 167 private ChangeListener<String> textContentChangeListener; 168 private ChangeListener<Number> opacityChangeListener; 169 private ChangeListener<Boolean> highlightChangeListener; 170 private ChangeListener<TranslateTransition> translateTransitionChangeListener; 171 private ChangeListener<FillTransition> fillTransitionChangeListener; 172 private ChangeListener<FadeTransition> fadeTransitionChangeListener; 173 private EventHandler<MouseEvent> mouseEnteredEventHandler; 174 private EventHandler<MouseEvent> mouseExitedEventHandler; 175 private EventHandler<MouseEvent> dragDetectedEventHandler; 176 private EventHandler<DragEvent> dragOverEventHandler; 177 private EventHandler<DragEvent> dragEnteredEventHandler; 178 private EventHandler<DragEvent> dragExitedEventHandler; 179 private EventHandler<DragEvent> dragDroppedEventHandler; 180 private EventHandler<DragEvent> dragDoneEventHandler; 181 private ChangeListener<IntPair> scrollDeltaCoordinatesChangeListener; 182 183 public class CellContentPane extends StackPane { 184 185 public final Rectangle background = RectangleBuilder.create().fill(dehighlightColor).build(); 186 public final Text text = TextBuilder.create().build(); // .fontSmoothingType(FontSmoothingType.GRAY) 187 } 188 189 public class CellInteractionPane extends BorderPane { 190 191 public final Rectangle interactionRectangle = RectangleBuilder.create().fill(Color.TRANSPARENT).build(); 192 } 193 194 @SuppressWarnings("unchecked") 195 public Cell( 196 final TCellPane cellPane, 197 final int gridRow, 198 final int gridColumn, 199 final Pos alignment, 200 final TextAlignment textAlignment, 201 final boolean rotated, 202 final EventHandler<ActionEvent> onFinishedEventHandler, 203 final boolean createTextSizeListener) { 204 super(); 205 this.cellPane = cellPane; 206 this.width.bind(cellPane.columnWidth); 207 this.height.bind(cellPane.rowHeight); 208 this.textSize.bind(cellPane.textSize); 209 this.highlight.bind(new BooleanBinding() { 210 211 { 212 super.bind(cellPane.highlightRowMap, cellPane.highlightColumnMap); 213 } 214 215 protected boolean computeValue() { 216 final Boolean highlightedRow = cellPane.highlightRowMap.get(gridRow); 217 final Boolean highlightedColumn = cellPane.highlightColumnMap.get(gridColumn); 218 if (cellPane.highlightConcept.get()) 219 return (highlightedRow == null ? false : highlightedRow) 220 && (highlightedColumn == null ? false : highlightedColumn); 221 else 222 return (highlightedRow == null ? false : highlightedRow) 223 || (highlightedColumn == null ? false : highlightedColumn); 224 }; 225 }); 226 this.animate.bind(cellPane.animate); 227 this.gridCoordinates = new ReadOnlyObjectWrapper<IntPair>(new IntPair(gridRow, gridColumn)).getReadOnlyProperty(); 228 this.snapToCoordinates = new ObjectBinding<IntPair>() { 229 230 { 231 super.bind(cellPane.dragRowMap, cellPane.dragColumnMap); 232 } 233 234 @Override 235 protected IntPair computeValue() { 236 final Integer row = cellPane.dragRowMap.get(gridRow); 237 final Integer column = cellPane.dragColumnMap.get(gridColumn); 238 return IntPair.valueOf(row == null ? gridRow : row, column == null ? gridColumn : column); 239 } 240 }; 241 this.contentCoordinates = new ObjectBinding<IntPair>() { 242 243 { 244 super.bind(gridCoordinates, cellPane.minRow, cellPane.minColumn, cellPane.rowMap, cellPane.columnMap); 245 } 246 247 @Override 248 protected IntPair computeValue() { 249 final int gridRow = cellPane.minRow.get() + gridCoordinates.get().x(); 250 final int gridColumn = cellPane.minColumn.get() + gridCoordinates.get().y(); 251 final Integer contentRow = cellPane.rowMap.get(gridRow); 252 final Integer contentColumn = cellPane.columnMap.get(gridColumn); 253 return IntPair 254 .valueOf(contentRow == null ? gridRow : contentRow, contentColumn == null ? gridColumn : contentColumn); 255 } 256 }; 257 this.opacity.bind(new DoubleBinding() { 258 259 { 260 super.bind(contentCoordinates, cellPane.rowOpacityMap, cellPane.columnOpacityMap); 261 } 262 263 @Override 264 protected double computeValue() { 265 final Double rowOpacity = cellPane.rowOpacityMap.get(contentCoordinates.get().x()); 266 final Double columnOpacity = cellPane.columnOpacityMap.get(contentCoordinates.get().y()); 267 return Math.min(rowOpacity == null ? 1 : rowOpacity, columnOpacity == null ? 1 : columnOpacity); 268 } 269 }); 270 this.contentPane.get().setAlignment(alignment); 271 this.contentPane.get().setOpacity(Constants.HIDE_OPACITY); 272 this.contentPane.get().getChildren().addAll(contentPane.get().background, contentPane.get().text); 273 this.contentPane.get().text.setTextAlignment(textAlignment); 274 this.contentPane.get().text.styleProperty().bind(textStyle); 275 this.interactionPane.get().setCenter(interactionPane.get().interactionRectangle); 276 this.interactionPane.get().minWidthProperty().bind(width); 277 this.interactionPane.get().maxWidthProperty().bind(width); 278 this.interactionPane.get().interactionRectangle.widthProperty().bind(width); 279 this.interactionPane.get().minHeightProperty().bind(height); 280 this.interactionPane.get().maxHeightProperty().bind(height); 281 this.interactionPane.get().interactionRectangle.heightProperty().bind(height); 282 if (rotated) { 283 this.contentPane.get().minWidthProperty().bind(height); 284 this.contentPane.get().maxWidthProperty().bind(height); 285 this.contentPane.get().background.widthProperty().bind(height); 286 this.contentPane.get().minHeightProperty().bind(width); 287 this.contentPane.get().maxHeightProperty().bind(width); 288 this.contentPane.get().background.heightProperty().bind(width); 289 this.createRotation(); 290 } else { 291 this.contentPane.get().minWidthProperty().bind(width); 292 this.contentPane.get().maxWidthProperty().bind(width); 293 this.contentPane.get().background.widthProperty().bind(width); 294 this.contentPane.get().minHeightProperty().bind(height); 295 this.contentPane.get().maxHeightProperty().bind(height); 296 this.contentPane.get().background.heightProperty().bind(height); 297 } 298 this.contentPane.get().text.effectProperty().bind(new ObjectBinding<Effect>() { 299 300 { 301 super.bind(animate); 302 } 303 304 @Override 305 protected Effect computeValue() { 306 if (animate.get()) 307 return DropShadowBuilder 308 .create() 309 .radius(1) 310 .blurType(BlurType.GAUSSIAN) 311 .color(Color.ALICEBLUE) 312 .spread(1) 313 .build(); 314 else 315 return null; 316 } 317 }); 318 this.createPropertyListeners(); 319 this.createMouseHandlers(); 320 if (cellPane.interactive) 321 this.createDragAndDropHandlers(); 322 cellPane.rows.put(gridRow, (TCell) Cell.this); 323 cellPane.columns.put(gridColumn, (TCell) Cell.this); 324// Platform.runLater(new Runnable() { 325// 326// @Override 327// public void run() { 328 cellPane.contentPane.add(getContentPane(), gridColumn, gridRow); 329 cellPane.interactionPane.add(getInteractionPane(), gridColumn, gridRow); 330// updateContent(); 331 fade( 332 Constants.HIDE_OPACITY, 333 opacity.getValue() == null ? Constants.SHOW_OPACITY : opacity.get(), 334 TransitionType.SMOOTH, 335 onFinishedEventHandler); 336// } 337// }); 338 if (createTextSizeListener) 339 if (cellPane.autoSizeRows.get() || cellPane.autoSizeColumns.get()) 340 contentPane.get().text.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() { 341 342 @Override 343 public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) { 344 final double width = newValue.getWidth(); 345 if (width > cellPane.maximalTextWidth.get()) 346 cellPane.maximalTextWidth.set(width); 347 } 348 }); 349 } 350 351 private final void createRotation() { 352 final Rotate rotate = new Rotate(-90); 353 final Translate translate = new Translate(0, 0); 354 rotate.pivotXProperty().bind(new DoubleBinding() { 355 356 { 357 super.bind(height); 358 } 359 360 protected double computeValue() { 361 return height.get() / 2d; 362 }; 363 }); 364 rotate.pivotYProperty().bind(new DoubleBinding() { 365 366 { 367 super.bind(width); 368 } 369 370 protected double computeValue() { 371 return width.get() / 2d; 372 }; 373 }); 374 translate.yProperty().bind(new DoubleBinding() { 375 376 { 377 super.bind(width, height); 378 } 379 380 protected double computeValue() { 381 return (width.get() - height.get()) / 2d; 382 }; 383 }); 384 this.contentPane.get().getTransforms().addAll(rotate, translate); 385// this.contentPane.get().setRotate(-90); 386// final Translate translate = new Translate(); 387// translate.xProperty().bind(new DoubleBinding() { 388// 389// { 390// super.bind(width, height); 391// } 392// 393// @Override 394// protected double computeValue() { 395// return (width.get() - height.get()) / 2d; 396// } 397// }); 398// this.contentPane.get().getTransforms().add(translate); 399 } 400 401 private final void createPropertyListeners() { 402 snapToCoordinatesChangeListener = new ChangeListener<IntPair>() { 403 404 @Override 405 public void changed(ObservableValue<? extends IntPair> observable, IntPair oldValue, final IntPair newValue) { 406 if (cellPane.isDragging.get() && !cellPane.isDropping.get()) 407 snapToGrid(newValue, TransitionType.SMOOTH, null); 408 else { 409 snapToGrid(newValue, TransitionType.DISCRETE, null); 410 updateContent(); 411 } 412 } 413 }; 414 scrollDeltaCoordinatesChangeListener = new ChangeListener<IntPair>() { 415 416 @Override 417 public void changed(ObservableValue<? extends IntPair> observable, IntPair oldValue, IntPair newValue) { 418 scrollDeltaCoordinatesQueue.add(newValue); 419 } 420 }; 421 scrollDeltaCoordinatesQueueChangeListener = new ListChangeListener<IntPair>() { 422 423 @Override 424 public void onChanged(Change<? extends IntPair> c) { 425 if (c.next() && c.wasAdded()) 426 scrollLoop(); 427 } 428 }; 429 contentCoordinatesChangeListener = new ChangeListener<IntPair>() { 430 431 @Override 432 public void changed(ObservableValue<? extends IntPair> observable, IntPair oldValue, IntPair newValue) { 433 if (!cellPane.isDragging.get() && !cellPane.isDropping.get()) 434 updateContent(); 435 } 436 }; 437// this.textStyle.addListener(new ChangeListener<String>() { 438// 439// @Override 440// public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { 441// System.out.println("newTextStyle: " + newValue); 442//// contentPane.get().text.setStyle(newValue); 443// updateContent(); 444// } 445// }); 446 textContentChangeListener = new ChangeListener<String>() { 447 448 @Override 449 public void changed(ObservableValue<? extends String> observable, String oldValue, final String newValue) { 450// if (cellPane.isDropping.get()) 451 Platform2.runOnFXThread(new Runnable() { 452 453 @Override 454 public void run() { 455 contentPane.get().text.setText(newValue); 456 } 457 }); 458// else 459// fadeOut(TransitionType.DEFAULT, new EventHandler<ActionEvent>() { 460// 461// @Override 462// public void handle(ActionEvent event) { 463// contentPane.get().text.setText(newValue); 464// fadeIn(TransitionType.DEFAULT, null); 465// } 466// }); 467 } 468 }; 469 opacityChangeListener = new ChangeListener<Number>() { 470 471 @Override 472 public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { 473 fade(oldValue.doubleValue(), newValue.doubleValue(), TransitionType.DEFAULT, null); 474 } 475 }; 476 highlightChangeListener = new ChangeListener<Boolean>() { 477 478 @Override 479 public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { 480 highlight( 481 newValue.booleanValue(), 482 // cellPane.isDragging.get() ? TransitionType.DISCRETE : newValue 483 // ? 484 TransitionType.DISCRETE 485// : TransitionType.DEFAULT 486 , 487 null); 488 } 489 }; 490 translateTransitionChangeListener = new ChangeListener<TranslateTransition>() { 491 492 @Override 493 public void changed( 494 ObservableValue<? extends TranslateTransition> observable, 495 TranslateTransition oldValue, 496 TranslateTransition newValue) { 497 try { 498 oldValue.jumpTo(Constants.ANIMATION_DURATION); 499 oldValue.stop(); 500 } catch (NullPointerException e) {} 501 try { 502 newValue.play(); 503 } catch (NullPointerException e) {} 504 } 505 }; 506 fillTransitionChangeListener = new ChangeListener<FillTransition>() { 507 508 @Override 509 public void changed( 510 ObservableValue<? extends FillTransition> observable, 511 FillTransition oldValue, 512 FillTransition newValue) { 513 oldValue.jumpTo(Constants.ANIMATION_DURATION); 514 oldValue.stop(); 515 newValue.play(); 516 } 517 }; 518 fadeTransitionChangeListener = new ChangeListener<FadeTransition>() { 519 520 @Override 521 public void changed( 522 ObservableValue<? extends FadeTransition> observable, 523 FadeTransition oldValue, 524 FadeTransition newValue) { 525 oldValue.jumpTo(Constants.ANIMATION_DURATION); 526 oldValue.stop(); 527 newValue.play(); 528 } 529 }; 530 this.snapToCoordinates.addListener(snapToCoordinatesChangeListener); 531 cellPane.scrollDeltaCoordinates.addListener(scrollDeltaCoordinatesChangeListener); 532 this.scrollDeltaCoordinatesQueue.addListener(scrollDeltaCoordinatesQueueChangeListener); 533 this.contentCoordinates.addListener(contentCoordinatesChangeListener); 534 this.textContent.addListener(textContentChangeListener); 535 this.opacity.addListener(opacityChangeListener); 536 this.highlight.addListener(highlightChangeListener); 537 this.translateTransition.addListener(translateTransitionChangeListener); 538 this.fillTransition.addListener(fillTransitionChangeListener); 539 this.fadeTransition.addListener(fadeTransitionChangeListener); 540 } 541 542 private final void createMouseHandlers() { 543 mouseEnteredEventHandler = new EventHandler<MouseEvent>() { 544 545 @SuppressWarnings("unchecked") 546 public void handle(MouseEvent event) { 547// interactionPane.get().interactionRectangle.setStroke(Color.RED); 548// interactionPane.get().interactionRectangle.setStrokeType(StrokeType.INSIDE); 549// interactionPane.get().interactionRectangle.setStrokeWidth(1); 550 cellPane.highlight((TCell) Cell.this); 551 } 552 }; 553// mouseExitedEventHandler = new EventHandler<MouseEvent>() { 554// 555// public void handle(MouseEvent event) { 556// interactionPane.get().interactionRectangle.setStroke(Color.TRANSPARENT); 557// } 558// }; 559 this.getInteractionPane().setOnMouseEntered(mouseEnteredEventHandler); 560// this.getInteractionPane().setOnMouseExited(mouseExitedEventHandler); 561 } 562 563 private final void createDragAndDropHandlers() { 564 dragDetectedEventHandler = new EventHandler<MouseEvent>() { 565 566 @SuppressWarnings("unchecked") 567 public void handle(MouseEvent event) { 568 cellPane.highlight((TCell) Cell.this); 569 final ClipboardContent clipboardContent = new ClipboardContent(); 570 clipboardContent.put( 571 CELL_COORDINATES_DATA_FORMAT, 572 new CellCoordinates( 573 cellPane.id.get(), 574 event.getButton().equals(MouseButton.PRIMARY) ? MouseEventType.SCROLL : MouseEventType.DRAG, 575 gridCoordinates.get().x().intValue(), 576 gridCoordinates.get().y().intValue(), 577 contentCoordinates.get().x().intValue(), 578 contentCoordinates.get().y().intValue())); 579 contentPane.get().startDragAndDrop(TransferMode.MOVE).setContent(clipboardContent); 580 event.consume(); 581 } 582 }; 583 dragOverEventHandler = new EventHandler<DragEvent>() { 584 585 public void handle(final DragEvent event) { 586 final String sourceCellPaneId = 587 ((CellCoordinates) event.getDragboard().getContent(CELL_COORDINATES_DATA_FORMAT)).cellPaneId; 588 final String targetCellPaneId = cellPane.id.get(); 589 if (sourceCellPaneId.equals(targetCellPaneId)) 590 event.acceptTransferModes(TransferMode.MOVE); 591 else 592 event.acceptTransferModes(TransferMode.NONE); 593 event.consume(); 594 } 595 }; 596 dragEnteredEventHandler = new EventHandler<DragEvent>() { 597 598 @SuppressWarnings("unchecked") 599 @Override 600 public void handle(DragEvent event) { 601 final Dragboard dragboard = event.getDragboard(); 602 final CellCoordinates sourceCellCoordinates = 603 (CellCoordinates) dragboard.getContent(CELL_COORDINATES_DATA_FORMAT); 604 final String sourceId = sourceCellCoordinates.cellPaneId; 605 final String targetId = cellPane.id.get(); 606 if (sourceId.equals(targetId)) 607 switch (sourceCellCoordinates.mouseEventType) { 608 case SCROLL: 609 cellPane.highlight((TCell) Cell.this); 610 final int rowDelta = gridCoordinates.get().x() - sourceCellCoordinates.gridRow; 611 final int columnDelta = gridCoordinates.get().y() - sourceCellCoordinates.gridColumn; 612 sourceCellCoordinates.gridRow = gridCoordinates.get().x(); 613 sourceCellCoordinates.gridColumn = gridCoordinates.get().y(); 614 dragboard.setContent( 615 Collections.<DataFormat, Object> singletonMap(CELL_COORDINATES_DATA_FORMAT, sourceCellCoordinates)); 616 cellPane.minCoordinates.add(-rowDelta, -columnDelta); 617// final boolean down = rowDelta < 0; 618// final boolean right = columnDelta < 0; 619// for (int i = 0; i < Math.abs(rowDelta); i++) 620// if (down) 621// cellPane.rowScrollBar.increment(); 622// else 623// cellPane.rowScrollBar.decrement(); 624// for (int i = 0; i < Math.abs(columnDelta); i++) 625// if (right) 626// cellPane.columnScrollBar.increment(); 627// else 628// cellPane.columnScrollBar.decrement(); 629 break; 630 case DRAG: 631 cellPane.drag( 632 sourceCellCoordinates.gridRow, 633 sourceCellCoordinates.gridColumn, 634 gridCoordinates.get().x().intValue(), 635 gridCoordinates.get().y().intValue()); 636 } 637 event.consume(); 638 } 639 }; 640 dragExitedEventHandler = new EventHandler<DragEvent>() { 641 642 public void handle(DragEvent event) { 643 event.consume(); 644 } 645 }; 646 dragDroppedEventHandler = new EventHandler<DragEvent>() { 647 648 public void handle(DragEvent event) { 649 cellPane.dehighlight(); 650 final Dragboard dragboard = event.getDragboard(); 651 final CellCoordinates sourceCellCoordinates = 652 (CellCoordinates) dragboard.getContent(CELL_COORDINATES_DATA_FORMAT); 653 switch (sourceCellCoordinates.mouseEventType) { 654 case SCROLL: 655 break; 656 case DRAG: 657 cellPane.drop( 658 sourceCellCoordinates.gridRow, 659 sourceCellCoordinates.gridColumn, 660 gridCoordinates.get().x().intValue(), 661 gridCoordinates.get().y().intValue()); 662 } 663 event.setDropCompleted(true); 664 event.consume(); 665 } 666 }; 667 dragDoneEventHandler = new EventHandler<DragEvent>() { 668 669 public void handle(DragEvent event) { 670 // TODO Why is this handler never called upon drag done? 671 System.out.println("drag done!"); 672 event.consume(); 673 } 674 }; 675 this.getInteractionPane().setOnDragDetected(dragDetectedEventHandler); 676 this.getInteractionPane().setOnDragOver(dragOverEventHandler); 677 this.getInteractionPane().setOnDragEntered(dragEnteredEventHandler); 678 this.getInteractionPane().setOnDragExited(dragExitedEventHandler); 679 this.getInteractionPane().setOnDragDropped(dragDroppedEventHandler); 680 this.getInteractionPane().setOnDragDone(dragDoneEventHandler); 681 } 682 683 @Override 684 public boolean equals(Object obj) { 685 return obj != null && obj instanceof Cell && ((Cell<?, ?>) obj).id == this.id; 686 } 687 688 @Override 689 public int hashCode() { 690 return (int) id.get(); 691 } 692 693 public final CellContentPane getContentPane() { 694 return contentPane.get(); 695 } 696 697 public final CellInteractionPane getInteractionPane() { 698 return interactionPane.get(); 699 } 700 701 /** 702 * Abstract method to update textual content (and possibly other properties) based on e.g. content coordinates 703 * property <code>this.contentCoordinates.get()</code>. Just call <code>this.textContent.set(String)</code> within the 704 * implementation body to update the text. 705 */ 706 protected abstract void updateContent(); 707 708 public final void dispose() { 709 fadeOut(TransitionType.SMOOTH, new EventHandler<ActionEvent>() { 710 711 @Override 712 @SuppressWarnings("unchecked") 713 public void handle(ActionEvent event) { 714 cellPane.contentPane.getChildren().remove(getContentPane()); 715 cellPane.interactionPane.getChildren().remove(getInteractionPane()); 716 cellPane.rows.remove(gridCoordinates.get().x(), (TCell) Cell.this); 717 cellPane.columns.remove(gridCoordinates.get().y(), (TCell) Cell.this); 718 contentCoordinates.dispose(); 719 snapToCoordinates.dispose(); 720 scrollDeltaCoordinatesQueue.clear(); 721 textStyle.dispose(); 722 snapToCoordinates.removeListener(snapToCoordinatesChangeListener); 723 cellPane.scrollDeltaCoordinates.removeListener(scrollDeltaCoordinatesChangeListener); 724 scrollDeltaCoordinatesQueue.removeListener(scrollDeltaCoordinatesQueueChangeListener); 725 contentCoordinates.removeListener(contentCoordinatesChangeListener); 726 textContent.removeListener(textContentChangeListener); 727 opacity.removeListener(opacityChangeListener); 728 highlight.removeListener(highlightChangeListener); 729 translateTransition.removeListener(translateTransitionChangeListener); 730 fillTransition.removeListener(fillTransitionChangeListener); 731 fadeTransition.removeListener(fadeTransitionChangeListener); 732 if (cellPane.interactive) { 733 getInteractionPane().removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler); 734// getInteractionPane().removeEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler); 735 getInteractionPane().removeEventHandler(MouseEvent.DRAG_DETECTED, dragDetectedEventHandler); 736 getInteractionPane().removeEventHandler(DragEvent.DRAG_OVER, dragOverEventHandler); 737 getInteractionPane().removeEventHandler(DragEvent.DRAG_ENTERED, dragEnteredEventHandler); 738 getInteractionPane().removeEventHandler(DragEvent.DRAG_EXITED, dragExitedEventHandler); 739 getInteractionPane().removeEventHandler(DragEvent.DRAG_DROPPED, dragDroppedEventHandler); 740 getInteractionPane().removeEventHandler(DragEvent.DRAG_DONE, dragDoneEventHandler); 741 } 742 } 743 }); 744 } 745 746 private final void scrollLoop() { 747 if (!animate.get()) 748 return; 749 if (runningLoop) 750 return; 751 runningLoop = true; 752 _scrollLoop(); 753 } 754 755 private final void _scrollLoop() { 756 final IntPair deltaCoordinates = cummulateScrollDeltaCoordinates(); 757 final Integer deltaX = deltaCoordinates.x(); 758 final Integer deltaY = deltaCoordinates.y(); 759 if (deltaX != 0 || deltaY != 0) { 760 snapToGrid( 761 gridCoordinates.get().x() + deltaX, 762 gridCoordinates.get().y() + deltaY, 763 TransitionType.SMOOTH, 764 new EventHandler<ActionEvent>() { 765 766 @Override 767 public void handle(ActionEvent event) { 768 Platform.runLater(new Runnable() { 769 770 @Override 771 public void run() { 772 resetGridPosition(); 773 } 774 }); 775 } 776 }); 777 _scrollLoop(); 778 } else 779 runningLoop = false; 780 } 781 782 protected void resetGridPosition() { 783 snapToGrid(gridCoordinates.get(), TransitionType.DISCRETE, null); 784 } 785 786 private final IntPair cummulateScrollDeltaCoordinates() { 787 int deltaRowSum = 0; 788 int deltaColumnSum = 0; 789 synchronized (scrollDeltaCoordinatesQueue) { 790 Iterator<IntPair> iterator = scrollDeltaCoordinatesQueue.iterator(); 791 while (iterator.hasNext()) { 792 final IntPair nextDeltaCoordinates = iterator.next(); 793 deltaRowSum += nextDeltaCoordinates.x(); 794 deltaColumnSum += nextDeltaCoordinates.y(); 795 iterator.remove(); 796 } 797 } 798 return IntPair.valueOf(deltaRowSum, deltaColumnSum); 799 } 800 801 private final void snapToGrid( 802 final IntPair coordinates, 803 final TransitionType translationType, 804 final EventHandler<ActionEvent> onFinishedEventHandler) { 805 snapToGrid(coordinates.x(), coordinates.y(), translationType, onFinishedEventHandler); 806 } 807 808 private final void snapToGrid( 809 final int row, 810 final int column, 811 final TransitionType translationType, 812 final EventHandler<ActionEvent> onFinishedEventHandler) { 813 translateTo(column * width.get(), row * height.get(), translationType, onFinishedEventHandler); 814 } 815 816 private final void translateTo( 817 final double minXInParent, 818 final double minYInParent, 819 final TransitionType translationType, 820 final EventHandler<ActionEvent> onFinishedEventHandler) { 821 translateTransition.get().jumpTo(Constants.ANIMATION_DURATION); 822 translateTransition.get().stop(); 823 final Bounds boundsInLocal = contentPane.get().getBoundsInLocal(); 824 final Bounds boundsInParent = contentPane.get().getBoundsInParent(); 825 final double deltaX = minXInParent - (boundsInParent.getMinX() - boundsInLocal.getMinX()); 826 final double deltaY = minYInParent - (boundsInParent.getMinY() - boundsInLocal.getMinY()); 827 translateBy(deltaX, deltaY, translationType, onFinishedEventHandler); 828 } 829 830 private final void translateBy( 831 final double x, 832 final double y, 833 final TransitionType translationType, 834 final EventHandler<ActionEvent> onFinishedEventHandler) { 835 final boolean smooth = translationType == TransitionType.SMOOTH 836 || (translationType == TransitionType.DEFAULT && Cell.this.animate.get()); 837 translateTransition.set( 838 TranslateTransitionBuilder 839 .create() 840 .duration(smooth ? Constants.ANIMATION_DURATION : Duration.ONE) 841 .byX(x) 842 .byY(y) 843 .node(contentPane.get()) 844 .onFinished(onFinishedEventHandler) 845 .build()); 846 } 847 848 protected final void toFront() { 849 contentPane.get().toFront(); 850 } 851 852 private final void highlight( 853 final boolean highlight, 854 final TransitionType translationType, 855 final EventHandler<ActionEvent> onFinishedEventHandler) { 856 if (highlight) 857 toFront(); 858 final boolean smooth = translationType == TransitionType.SMOOTH 859 || (translationType == TransitionType.DEFAULT && Cell.this.animate.get()); 860 fillTransition.set( 861 FillTransitionBuilder 862 .create() 863 .fromValue(highlight ? dehighlightColor : cellPane.colorScheme.get().getColor(4)) 864 .toValue(highlight ? cellPane.colorScheme.get().getColor(4) : dehighlightColor) 865 .duration(smooth ? Constants.ANIMATION_DURATION : Duration.ONE) 866 .shape(contentPane.get().background) 867 .onFinished(onFinishedEventHandler) 868 .build()); 869 } 870 871 private final void 872 fadeIn(final TransitionType translationType, final EventHandler<ActionEvent> onFinishedEventHandler) { 873 fade(Constants.HIDE_OPACITY, Constants.SHOW_OPACITY, translationType, onFinishedEventHandler); 874 } 875 876 private final void 877 fadeOut(final TransitionType translationType, final EventHandler<ActionEvent> onFinishedEventHandler) { 878 fade(Constants.SHOW_OPACITY, Constants.HIDE_OPACITY, translationType, onFinishedEventHandler); 879 } 880 881 private final void fade( 882 final double fromValue, 883 final double toValue, 884 final TransitionType translationType, 885 final EventHandler<ActionEvent> onFinishedEventHandler) { 886 final boolean smooth = translationType == TransitionType.SMOOTH 887 || (translationType == TransitionType.DEFAULT && Cell.this.animate.get()); 888 fadeTransition.set( 889 FadeTransitionBuilder 890 .create() 891 .node(contentPane.get()) 892 .duration(smooth ? Constants.ANIMATION_DURATION : Duration.ONE) 893 .fromValue(fromValue) 894 .toValue(toValue) 895 .onFinished(onFinishedEventHandler) 896 .build()); 897 } 898}