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}