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}