rev 8703 : RT-40186: Update copyright header for files modified in 2015
1 /*
2 * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package javafx.scene.control;
27
28 import com.sun.javafx.collections.MappingChange;
29 import com.sun.javafx.collections.NonIterableChange;
30 import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
31 import com.sun.javafx.scene.control.SelectedCellsMap;
32
33 import com.sun.javafx.scene.control.behavior.TableCellBehaviorBase;
34 import com.sun.javafx.scene.control.behavior.TreeTableCellBehavior;
35 import javafx.beans.property.DoubleProperty;
36 import javafx.css.CssMetaData;
37 import javafx.css.PseudoClass;
38
39 import com.sun.javafx.css.converters.SizeConverter;
40 import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
41 import com.sun.javafx.scene.control.TableColumnComparatorBase;
42 import com.sun.javafx.scene.control.skin.TableViewSkinBase;
43
44 import javafx.css.Styleable;
45 import javafx.css.StyleableDoubleProperty;
46 import javafx.css.StyleableProperty;
47 import javafx.event.WeakEventHandler;
48
49 import com.sun.javafx.scene.control.skin.TreeTableViewSkin;
50
51 import java.lang.ref.SoftReference;
52 import java.lang.ref.WeakReference;
53 import java.util.*;
54
55 import javafx.application.Platform;
56 import javafx.beans.DefaultProperty;
57 import javafx.beans.InvalidationListener;
58 import javafx.beans.WeakInvalidationListener;
59 import javafx.beans.property.BooleanProperty;
60 import javafx.beans.property.ObjectProperty;
61 import javafx.beans.property.ObjectPropertyBase;
62 import javafx.beans.property.ReadOnlyIntegerProperty;
63 import javafx.beans.property.ReadOnlyIntegerWrapper;
64 import javafx.beans.property.ReadOnlyObjectProperty;
65 import javafx.beans.property.ReadOnlyObjectWrapper;
66 import javafx.beans.property.SimpleBooleanProperty;
67 import javafx.beans.property.SimpleObjectProperty;
68 import javafx.beans.value.ChangeListener;
69 import javafx.beans.value.WeakChangeListener;
70 import javafx.beans.value.WritableValue;
71 import javafx.collections.FXCollections;
72 import javafx.collections.ListChangeListener;
73 import javafx.collections.MapChangeListener;
74 import javafx.collections.ObservableList;
75 import javafx.collections.WeakListChangeListener;
76 import javafx.event.Event;
77 import javafx.event.EventHandler;
78 import javafx.event.EventType;
79 import javafx.scene.AccessibleAttribute;
80 import javafx.scene.AccessibleRole;
81 import javafx.scene.Node;
82 import javafx.scene.layout.Region;
83 import javafx.util.Callback;
84
85 /**
86 * The TreeTableView control is designed to visualize an unlimited number of rows
87 * of data, broken out into columns. The TreeTableView control is conceptually
88 * very similar to the {@link TreeView} and {@link TableView} controls,
89 * and as you read on you'll come to see the APIs are largely the same.
90 * However, to give a high-level overview, you'll note that the TreeTableView
91 * uses the same {@link TreeItem} API as {@link TreeView},
92 * and that you therefore are required to simply set the
93 * {@link #rootProperty() root node} in the TreeTableView. Similarly, the
94 * TreeTableView control makes use of the same TableColumn-based approach that
95 * the {@link TableView} control uses, except instead of using the
96 * TableView-specific {@link TableColumn} class, you should instead use the
97 * TreeTableView-specific {@link TreeTableColumn} class instead. For an
98 * example on how to create a TreeTableView instance, refer to the 'Creating a
99 * TreeTableView' control section below.
100 *
101 * <p>As with the {@link TableView} control, the TreeTableView control has a
102 * number of features, including:
103 * <ul>
104 * <li>Powerful {@link TreeTableColumn} API:
105 * <ul>
106 * <li>Support for {@link TreeTableColumn#cellFactoryProperty() cell factories} to
107 * easily customize {@link Cell cell} contents in both rendering and editing
108 * states.
109 * <li>Specification of {@link TreeTableColumn#minWidthProperty() minWidth}/
110 * {@link TreeTableColumn#prefWidthProperty() prefWidth}/
111 * {@link TreeTableColumn#maxWidthProperty() maxWidth},
112 * and also {@link TreeTableColumn#resizableProperty() fixed width columns}.
113 * <li>Width resizing by the user at runtime.
114 * <li>Column reordering by the user at runtime.
115 * <li>Built-in support for {@link TreeTableColumn#getColumns() column nesting}
116 * </ul>
117 * <li>Different {@link #columnResizePolicyProperty() resizing policies} to
118 * dictate what happens when the user resizes columns.
119 * <li>Support for {@link #getSortOrder() multiple column sorting} by clicking
120 * the column header (hold down Shift keyboard key whilst clicking on a
121 * header to sort by multiple columns).
122 * </ul>
123 * </p>
124 *
125 * <h2>Creating a TreeTableView</h2>
126 *
127 * <p>Creating a TreeTableView is a multi-step process, and also depends on the
128 * underlying data model needing to be represented. For this example we'll use
129 * the TreeTableView to visualise a file system, and will therefore make use
130 * of an imaginary (and vastly simplified) File class as defined below:
131 *
132 * <pre>{@code
133 * public class File {
134 * private StringProperty name;
135 * public void setName(String value) { nameProperty().set(value); }
136 * public String getName() { return nameProperty().get(); }
137 * public StringProperty nameProperty() {
138 * if (name == null) name = new SimpleStringProperty(this, "name");
139 * return name;
140 * }
141 *
142 * private LongProperty lastModified;
143 * public void setLastModified(long value) { lastModifiedProperty().set(value); }
144 * public long getLastModified() { return lastModifiedProperty().get(); }
145 * public LongProperty lastModifiedProperty() {
146 * if (lastModified == null) lastModified = new SimpleLongProperty(this, "lastModified");
147 * return lastModified;
148 * }
149 * }}</pre>
150 *
151 * <p>Firstly, a TreeTableView instance needs to be defined, as such:
152 *
153 * <pre>{@code
154 * TreeTableView<File> treeTable = new TreeTableView<>();}</pre>
155 *
156 * <p>With the basic TreeTableView instantiated, we next focus on the data model.
157 * As mentioned, for this example, we'll be representing a file system using File
158 * instances. To do this, we need to define the root node of the tree table, as such:
159 *
160 * <pre>{@code
161 * TreeItem<File> root = new TreeItem<>(new File("/"));
162 * treeTable.setRoot(root);}</pre>
163 *
164 * <p>With the root set as such, the TreeTableView will automatically update whenever
165 * the {@link TreeItem#getChildren() children} of the root changes.
166 *
167 * <p>At this point we now have a TreeTableView hooked up to observe the root
168 * TreeItem instance. The missing ingredient
169 * now is the means of splitting out the data contained within the model and
170 * representing it in one or more {@link TreeTableColumn} instances. To
171 * create a two-column TreeTableView to show the file name and last modified
172 * properties, we extend the code shown above as follows:
173 *
174 * <pre>{@code
175 * TreeTableColumns<File,String> fileNameCol = new TreeTableColumn<>("Filename");
176 * TreeTableColumns<File,Long> lastModifiedCol = new TreeTableColumn<>("Size");
177 *
178 * table.getColumns().setAll(fileNameCol, lastModifiedCol);}</pre>
179 *
180 * <p>With the code shown above we have nearly fully defined the minimum properties
181 * required to create a TreeTableView instance. The only thing missing is the
182 * {@link javafx.scene.control.TreeTableColumn#cellValueFactoryProperty() cell value factories}
183 * for the two columns - it is these that are responsible for determining the value
184 * of a cell in a given row. Commonly these can be specified using the
185 * {@link javafx.scene.control.cell.TreeItemPropertyValueFactory} class, but
186 * failing that you can also create an anonymous inner class and do whatever is
187 * necessary. For example, using {@link javafx.scene.control.cell.TreeItemPropertyValueFactory}
188 * you would do the following:
189 *
190 * <pre>{@code
191 * fileNameCol.setCellValueFactory(new TreeItemPropertyValueFactory("name"));
192 * lastModifiedCol.setCellValueFactory(new TreeItemPropertyValueFactory("lastModified"));}</pre>
193 *
194 * Running this code (assuming the file system structure is probably built up in
195 * memory) will result in a TreeTableView being shown with two columns for name
196 * and lastModified. Any other properties of the File class will not be shown, as
197 * no TreeTableColumns are defined for them.
198 *
199 * <h3>TreeTableView support for classes that don't contain properties</h3>
200 *
201 * <p>The code shown above is the shortest possible code for creating a TreeTableView
202 * when the domain objects are designed with JavaFX properties in mind
203 * (additionally, {@link javafx.scene.control.cell.TreeItemPropertyValueFactory} supports
204 * normal JavaBean properties too, although there is a caveat to this, so refer
205 * to the class documentation for more information). When this is not the case,
206 * it is necessary to provide a custom cell value factory. More information
207 * about cell value factories can be found in the {@link TreeTableColumn} API
208 * documentation, but briefly, here is how a TreeTableColumns could be specified:
209 *
210 * <pre>{@code
211 * firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
212 * public ObservableValue<String> call(CellDataFeatures<Person, String> p) {
213 * // p.getValue() returns the TreeItem<Person> instance for a particular TreeTableView row,
214 * // p.getValue().getValue() returns the Person instance inside the TreeItem<Person>
215 * return p.getValue().getValue().firstNameProperty();
216 * }
217 * });
218 * }}</pre>
219 *
220 * <h3>TreeTableView Selection / Focus APIs</h3>
221 * <p>To track selection and focus, it is necessary to become familiar with the
222 * {@link SelectionModel} and {@link FocusModel} classes. A TreeTableView has at most
223 * one instance of each of these classes, available from
224 * {@link #selectionModelProperty() selectionModel} and
225 * {@link #focusModelProperty() focusModel} properties, respectively.
226 * Whilst it is possible to use this API to set a new selection model, in
227 * most circumstances this is not necessary - the default selection and focus
228 * models should work in most circumstances.
229 *
230 * <p>The default {@link SelectionModel} used when instantiating a TreeTableView is
231 * an implementation of the {@link MultipleSelectionModel} abstract class.
232 * However, as noted in the API documentation for
233 * the {@link MultipleSelectionModel#selectionModeProperty() selectionMode}
234 * property, the default value is {@link SelectionMode#SINGLE}. To enable
235 * multiple selection in a default TreeTableView instance, it is therefore necessary
236 * to do the following:
237 *
238 * <pre>
239 * {@code
240 * treeTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}</pre>
241 *
242 * <h3>Customizing TreeTableView Visuals</h3>
243 * <p>The visuals of the TreeTableView can be entirely customized by replacing the
244 * default {@link #rowFactoryProperty() row factory}. A row factory is used to
245 * generate {@link TreeTableRow} instances, which are used to represent an entire
246 * row in the TreeTableView.
247 *
248 * <p>In many cases, this is not what is desired however, as it is more commonly
249 * the case that cells be customized on a per-column basis, not a per-row basis.
250 * It is therefore important to note that a {@link TreeTableRow} is not a
251 * {@link TreeTableCell}. A {@link TreeTableRow} is simply a container for zero or more
252 * {@link TreeTableCell}, and in most circumstances it is more likely that you'll
253 * want to create custom TreeTableCells, rather than TreeTableRows. The primary use case
254 * for creating custom TreeTableRow instances would most probably be to introduce
255 * some form of column spanning support.
256 *
257 * <p>You can create custom {@link TreeTableCell} instances per column by assigning
258 * the appropriate function to the TreeTableColumns
259 * {@link TreeTableColumn#cellFactoryProperty() cell factory} property.
260 *
261 * <p>See the {@link Cell} class documentation for a more complete
262 * description of how to write custom Cells.
263 *
264 * <h3>Editing</h3>
265 * <p>This control supports inline editing of values, and this section attempts to
266 * give an overview of the available APIs and how you should use them.</p>
267 *
268 * <p>Firstly, cell editing most commonly requires a different user interface
269 * than when a cell is not being edited. This is the responsibility of the
270 * {@link Cell} implementation being used. For TreeTableView, it is highly
271 * recommended that editing be
272 * {@link javafx.scene.control.TreeTableColumn#cellFactoryProperty() per-TreeTableColumn},
273 * rather than {@link #rowFactoryProperty() per row}, as more often than not
274 * you want users to edit each column value differently, and this approach allows
275 * for editors specific to each column. It is your choice whether the cell is
276 * permanently in an editing state (e.g. this is common for {@link CheckBox} cells),
277 * or to switch to a different UI when editing begins (e.g. when a double-click
278 * is received on a cell).</p>
279 *
280 * <p>To know when editing has been requested on a cell,
281 * simply override the {@link javafx.scene.control.Cell#startEdit()} method, and
282 * update the cell {@link javafx.scene.control.Cell#textProperty() text} and
283 * {@link javafx.scene.control.Cell#graphicProperty() graphic} properties as
284 * appropriate (e.g. set the text to null and set the graphic to be a
285 * {@link TextField}). Additionally, you should also override
286 * {@link Cell#cancelEdit()} to reset the UI back to its original visual state
287 * when the editing concludes. In both cases it is important that you also
288 * ensure that you call the super method to have the cell perform all duties it
289 * must do to enter or exit its editing mode.</p>
290 *
291 * <p>Once your cell is in an editing state, the next thing you are most probably
292 * interested in is how to commit or cancel the editing that is taking place. This is your
293 * responsibility as the cell factory provider. Your cell implementation will know
294 * when the editing is over, based on the user input (e.g. when the user presses
295 * the Enter or ESC keys on their keyboard). When this happens, it is your
296 * responsibility to call {@link Cell#commitEdit(Object)} or
297 * {@link Cell#cancelEdit()}, as appropriate.</p>
298 *
299 * <p>When you call {@link Cell#commitEdit(Object)} an event is fired to the
300 * TreeTableView, which you can observe by adding an {@link EventHandler} via
301 * {@link TreeTableColumn#setOnEditCommit(javafx.event.EventHandler)}. Similarly,
302 * you can also observe edit events for
303 * {@link TreeTableColumn#setOnEditStart(javafx.event.EventHandler) edit start}
304 * and {@link TreeTableColumn#setOnEditCancel(javafx.event.EventHandler) edit cancel}.</p>
305 *
306 * <p>By default the TreeTableColumn edit commit handler is non-null, with a default
307 * handler that attempts to overwrite the property value for the
308 * item in the currently-being-edited row. It is able to do this as the
309 * {@link Cell#commitEdit(Object)} method is passed in the new value, and this
310 * is passed along to the edit commit handler via the
311 * {@link javafx.scene.control.TreeTableColumn.CellEditEvent CellEditEvent} that is
312 * fired. It is simply a matter of calling
313 * {@link javafx.scene.control.TreeTableColumn.CellEditEvent#getNewValue()} to
314 * retrieve this value.
315 *
316 * <p>It is very important to note that if you call
317 * {@link TreeTableColumn#setOnEditCommit(javafx.event.EventHandler)} with your own
318 * {@link EventHandler}, then you will be removing the default handler. Unless
319 * you then handle the writeback to the property (or the relevant data source),
320 * nothing will happen. You can work around this by using the
321 * {@link TreeTableColumn#addEventHandler(javafx.event.EventType, javafx.event.EventHandler)}
322 * method to add a {@link TreeTableColumn#EDIT_COMMIT_EVENT} {@link EventType} with
323 * your desired {@link EventHandler} as the second argument. Using this method,
324 * you will not replace the default implementation, but you will be notified when
325 * an edit commit has occurred.</p>
326 *
327 * <p>Hopefully this summary answers some of the commonly asked questions.
328 * Fortunately, JavaFX ships with a number of pre-built cell factories that
329 * handle all the editing requirements on your behalf. You can find these
330 * pre-built cell factories in the javafx.scene.control.cell package.</p>
331 *
332 * @see TreeTableColumn
333 * @see TreeTablePosition
334 * @param <S> The type of the TreeItem instances used in this TreeTableView.
335 * @since JavaFX 8.0
336 */
337 @DefaultProperty("root")
338 public class TreeTableView<S> extends Control {
339
340 /***************************************************************************
341 * *
342 * Constructors *
343 * *
344 **************************************************************************/
345
346 /**
347 * Creates an empty TreeTableView.
348 *
349 * <p>Refer to the {@link TreeTableView} class documentation for details on the
350 * default state of other properties.
351 */
352 public TreeTableView() {
353 this(null);
354 }
355
356 /**
357 * Creates a TreeTableView with the provided root node.
358 *
359 * <p>Refer to the {@link TreeTableView} class documentation for details on the
360 * default state of other properties.
361 *
362 * @param root The node to be the root in this TreeTableView.
363 */
364 public TreeTableView(TreeItem<S> root) {
365 getStyleClass().setAll(DEFAULT_STYLE_CLASS);
366 setAccessibleRole(AccessibleRole.TREE_TABLE_VIEW);
367
368 setRoot(root);
369 updateExpandedItemCount(root);
370
371 // install default selection and focus models - it's unlikely this will be changed
372 // by many users.
373 setSelectionModel(new TreeTableViewArrayListSelectionModel<S>(this));
374 setFocusModel(new TreeTableViewFocusModel<S>(this));
375
376 // we watch the columns list, such that when it changes we can update
377 // the leaf columns and visible leaf columns lists (which are read-only).
378 getColumns().addListener(weakColumnsObserver);
379
380 // watch for changes to the sort order list - and when it changes run
381 // the sort method.
382 getSortOrder().addListener((ListChangeListener.Change<? extends TreeTableColumn<S, ?>> c) -> {
383 doSort(TableUtil.SortEventType.SORT_ORDER_CHANGE, c);
384 });
385
386 // We're watching for changes to the content width such
387 // that the resize policy can be run if necessary. This comes from
388 // TreeTableViewSkin.
389 getProperties().addListener((MapChangeListener<Object, Object>) c -> {
390 if (c.wasAdded() && TableView.SET_CONTENT_WIDTH.equals(c.getKey())) {
391 if (c.getValueAdded() instanceof Number) {
392 setContentWidth((Double) c.getValueAdded());
393 }
394 getProperties().remove(TableView.SET_CONTENT_WIDTH);
395 }
396 });
397
398 isInited = true;
399 }
400
401
402
403 /***************************************************************************
404 * *
405 * Static properties and methods *
406 * *
407 **************************************************************************/
408
409 /**
410 * An EventType that indicates some edit event has occurred. It is the parent
411 * type of all other edit events: {@link #editStartEvent},
412 * {@link #editCommitEvent} and {@link #editCancelEvent}.
413 *
414 * @return An EventType that indicates some edit event has occurred.
415 */
416 @SuppressWarnings("unchecked")
417 public static <S> EventType<TreeTableView.EditEvent<S>> editAnyEvent() {
418 return (EventType<TreeTableView.EditEvent<S>>) EDIT_ANY_EVENT;
419 }
420 private static final EventType<?> EDIT_ANY_EVENT =
421 new EventType<>(Event.ANY, "TREE_TABLE_VIEW_EDIT");
422
423 /**
424 * An EventType used to indicate that an edit event has started within the
425 * TreeTableView upon which the event was fired.
426 *
427 * @return An EventType used to indicate that an edit event has started.
428 */
429 @SuppressWarnings("unchecked")
430 public static <S> EventType<TreeTableView.EditEvent<S>> editStartEvent() {
431 return (EventType<TreeTableView.EditEvent<S>>) EDIT_START_EVENT;
432 }
433 private static final EventType<?> EDIT_START_EVENT =
434 new EventType<>(editAnyEvent(), "EDIT_START");
435
436 /**
437 * An EventType used to indicate that an edit event has just been canceled
438 * within the TreeTableView upon which the event was fired.
439 *
440 * @return An EventType used to indicate that an edit event has just been
441 * canceled.
442 */
443 @SuppressWarnings("unchecked")
444 public static <S> EventType<TreeTableView.EditEvent<S>> editCancelEvent() {
445 return (EventType<TreeTableView.EditEvent<S>>) EDIT_CANCEL_EVENT;
446 }
447 private static final EventType<?> EDIT_CANCEL_EVENT =
448 new EventType<>(editAnyEvent(), "EDIT_CANCEL");
449
450 /**
451 * An EventType that is used to indicate that an edit in a TreeTableView has been
452 * committed. This means that user has made changes to the data of a
453 * TreeItem, and that the UI should be updated.
454 *
455 * @return An EventType that is used to indicate that an edit in a TreeTableView
456 * has been committed.
457 */
458 @SuppressWarnings("unchecked")
459 public static <S> EventType<TreeTableView.EditEvent<S>> editCommitEvent() {
460 return (EventType<TreeTableView.EditEvent<S>>) EDIT_COMMIT_EVENT;
461 }
462 private static final EventType<?> EDIT_COMMIT_EVENT =
463 new EventType<>(editAnyEvent(), "EDIT_COMMIT");
464
465 /**
466 * Returns the number of levels of 'indentation' of the given TreeItem,
467 * based on how many times {@link javafx.scene.control.TreeItem#getParent()}
468 * can be recursively called. If the TreeItem does not have any parent set,
469 * the returned value will be zero. For each time getParent() is recursively
470 * called, the returned value is incremented by one.
471 *
472 * <p><strong>Important note: </strong>This method is deprecated as it does
473 * not consider the root node. This means that this method will iterate
474 * past the root node of the TreeTableView control, if the root node has a parent.
475 * If this is important, call {@link TreeTableView#getTreeItemLevel(TreeItem)}
476 * instead.
477 *
478 * @param node The TreeItem for which the level is needed.
479 * @return An integer representing the number of parents above the given node,
480 * or -1 if the given TreeItem is null.
481 * @deprecated This method does not correctly calculate the distance from the
482 * given TreeItem to the root of the TreeTableView. As of JavaFX 8.0_20,
483 * the proper way to do this is via
484 * {@link TreeTableView#getTreeItemLevel(TreeItem)}
485 */
486 @Deprecated
487 public static int getNodeLevel(TreeItem<?> node) {
488 return TreeView.getNodeLevel(node);
489 }
490
491 /**
492 * <p>Very simple resize policy that just resizes the specified column by the
493 * provided delta and shifts all other columns (to the right of the given column)
494 * further to the right (when the delta is positive) or to the left (when the
495 * delta is negative).
496 *
497 * <p>It also handles the case where we have nested columns by sharing the new space,
498 * or subtracting the removed space, evenly between all immediate children columns.
499 * Of course, the immediate children may themselves be nested, and they would
500 * then use this policy on their children.
501 */
502 public static final Callback<TreeTableView.ResizeFeatures, Boolean> UNCONSTRAINED_RESIZE_POLICY =
503 new Callback<TreeTableView.ResizeFeatures, Boolean>() {
504
505 @Override public String toString() {
506 return "unconstrained-resize";
507 }
508
509 @Override public Boolean call(TreeTableView.ResizeFeatures prop) {
510 double result = TableUtil.resize(prop.getColumn(), prop.getDelta());
511 return Double.compare(result, 0.0) == 0;
512 }
513 };
514
515 /**
516 * <p>Simple policy that ensures the width of all visible leaf columns in
517 * this table sum up to equal the width of the table itself.
518 *
519 * <p>When the user resizes a column width with this policy, the table automatically
520 * adjusts the width of the right hand side columns. When the user increases a
521 * column width, the table decreases the width of the rightmost column until it
522 * reaches its minimum width. Then it decreases the width of the second
523 * rightmost column until it reaches minimum width and so on. When all right
524 * hand side columns reach minimum size, the user cannot increase the size of
525 * resized column any more.
526 */
527 public static final Callback<TreeTableView.ResizeFeatures, Boolean> CONSTRAINED_RESIZE_POLICY =
528 new Callback<TreeTableView.ResizeFeatures, Boolean>() {
529
530 private boolean isFirstRun = true;
531
532 @Override public String toString() {
533 return "constrained-resize";
534 }
535
536 @Override public Boolean call(TreeTableView.ResizeFeatures prop) {
537 TreeTableView<?> table = prop.getTable();
538 List<? extends TableColumnBase<?,?>> visibleLeafColumns = table.getVisibleLeafColumns();
539 Boolean result = TableUtil.constrainedResize(prop,
540 isFirstRun,
541 table.contentWidth,
542 visibleLeafColumns);
543 isFirstRun = ! isFirstRun ? false : ! result;
544 return result;
545 }
546 };
547
548 /**
549 * The default {@link #sortPolicyProperty() sort policy} that this TreeTableView
550 * will use if no other policy is specified. The sort policy is a simple
551 * {@link Callback} that accepts a TreeTableView as the sole argument and expects
552 * a Boolean response representing whether the sort succeeded or not. A Boolean
553 * response of true represents success, and a response of false (or null) will
554 * be considered to represent failure.
555 */
556 public static final Callback<TreeTableView, Boolean> DEFAULT_SORT_POLICY = new Callback<TreeTableView, Boolean>() {
557 @Override public Boolean call(TreeTableView table) {
558 try {
559 TreeItem rootItem = table.getRoot();
560 if (rootItem == null) return false;
561
562 TreeSortMode sortMode = table.getSortMode();
563 if (sortMode == null) return false;
564
565 rootItem.lastSortMode = sortMode;
566 rootItem.lastComparator = table.getComparator();
567 rootItem.sort();
568 return true;
569 } catch (UnsupportedOperationException e) {
570 // TODO might need to support other exception types including:
571 // ClassCastException - if the class of the specified element prevents it from being added to this list
572 // NullPointerException - if the specified element is null and this list does not permit null elements
573 // IllegalArgumentException - if some property of this element prevents it from being added to this list
574
575 // If we are here the list does not support sorting, so we gracefully
576 // fail the sort request and ensure the UI is put back to its previous
577 // state. This is handled in the code that calls the sort policy.
578
579 return false;
580 }
581 }
582 };
583
584
585
586 /***************************************************************************
587 * *
588 * Instance Variables *
589 * *
590 **************************************************************************/
591
592 // used in the tree item modification event listener. Used by the
593 // layoutChildren method to determine whether the tree item count should
594 // be recalculated.
595 private boolean expandedItemCountDirty = true;
596
597 // Used in the getTreeItem(int row) method to act as a cache.
598 // See RT-26716 for the justification and performance gains.
599 private Map<Integer, SoftReference<TreeItem<S>>> treeItemCacheMap = new HashMap<>();
600
601 // this is the only publicly writable list for columns. This represents the
602 // columns as they are given initially by the developer.
603 private final ObservableList<TreeTableColumn<S,?>> columns = FXCollections.observableArrayList();
604
605 // Finally, as convenience, we also have an observable list that contains
606 // only the leaf columns that are currently visible.
607 private final ObservableList<TreeTableColumn<S,?>> visibleLeafColumns = FXCollections.observableArrayList();
608 private final ObservableList<TreeTableColumn<S,?>> unmodifiableVisibleLeafColumns = FXCollections.unmodifiableObservableList(visibleLeafColumns);
609
610 // Allows for multiple column sorting based on the order of the TreeTableColumns
611 // in this observableArrayList. Each TreeTableColumn is responsible for whether it is
612 // sorted using ascending or descending order.
613 private ObservableList<TreeTableColumn<S,?>> sortOrder = FXCollections.observableArrayList();
614
615 // width of VirtualFlow minus the vbar width
616 // package protected for testing only
617 double contentWidth;
618
619 // Used to minimise the amount of work performed prior to the table being
620 // completely initialised. In particular it reduces the amount of column
621 // resize operations that occur, which slightly improves startup time.
622 private boolean isInited = false;
623
624
625
626 /***************************************************************************
627 * *
628 * Callbacks and Events *
629 * *
630 **************************************************************************/
631
632 // we use this to forward events that have bubbled up TreeItem instances
633 // to the TreeTableViewSkin, to force it to recalculate teh item count and redraw
634 // if necessary
635 private final EventHandler<TreeItem.TreeModificationEvent<S>> rootEvent = e -> {
636 // this forces layoutChildren at the next pulse, and therefore
637 // updates the item count if necessary
638 EventType<?> eventType = e.getEventType();
639 boolean match = false;
640 while (eventType != null) {
641 if (eventType.equals(TreeItem.<S>expandedItemCountChangeEvent())) {
642 match = true;
643 break;
644 }
645 eventType = eventType.getSuperType();
646 }
647
648 if (match) {
649 expandedItemCountDirty = true;
650 requestLayout();
651 }
652 };
653
654 private final ListChangeListener<TreeTableColumn<S,?>> columnsObserver = new ListChangeListener<TreeTableColumn<S,?>>() {
655 @Override public void onChanged(ListChangeListener.Change<? extends TreeTableColumn<S,?>> c) {
656 final List<TreeTableColumn<S,?>> columns = getColumns();
657
658 // Fix for RT-39822 - don't allow the same column to be installed twice
659 while (c.next()) {
660 if (c.wasAdded()) {
661 List<TreeTableColumn<S,?>> duplicates = new ArrayList<>();
662 for (TreeTableColumn<S,?> addedColumn : c.getAddedSubList()) {
663 if (addedColumn == null) continue;
664
665 int count = 0;
666 for (TreeTableColumn<S,?> column : columns) {
667 if (addedColumn == column) {
668 count++;
669 }
670 }
671
672 if (count > 1) {
673 duplicates.add(addedColumn);
674 }
675 }
676
677 if (!duplicates.isEmpty()) {
678 String titleList = "";
679 for (TreeTableColumn<S,?> dupe : duplicates) {
680 titleList += "'" + dupe.getText() + "', ";
681 }
682 throw new IllegalStateException("Duplicate TreeTableColumns detected in TreeTableView columns list with titles " + titleList);
683 }
684 }
685 }
686 c.reset();
687
688 // We don't maintain a bind for leafColumns, we simply call this update
689 // function behind the scenes in the appropriate places.
690 updateVisibleLeafColumns();
691
692 // Fix for RT-15194: Need to remove removed columns from the
693 // sortOrder list.
694 List<TreeTableColumn<S,?>> toRemove = new ArrayList<TreeTableColumn<S,?>>();
695 while (c.next()) {
696 final List<? extends TreeTableColumn<S, ?>> removed = c.getRemoved();
697 final List<? extends TreeTableColumn<S, ?>> added = c.getAddedSubList();
698
699 if (c.wasRemoved()) {
700 toRemove.addAll(removed);
701 for (TreeTableColumn<S,?> tc : removed) {
702 tc.setTreeTableView(null);
703 }
704 }
705
706 if (c.wasAdded()) {
707 toRemove.removeAll(added);
708 for (TreeTableColumn<S,?> tc : added) {
709 tc.setTreeTableView(TreeTableView.this);
710 }
711 }
712
713 // set up listeners
714 TableUtil.removeColumnsListener(removed, weakColumnsObserver);
715 TableUtil.addColumnsListener(added, weakColumnsObserver);
716
717 TableUtil.removeTableColumnListener(c.getRemoved(),
718 weakColumnVisibleObserver,
719 weakColumnSortableObserver,
720 weakColumnSortTypeObserver,
721 weakColumnComparatorObserver);
722 TableUtil.addTableColumnListener(c.getAddedSubList(),
723 weakColumnVisibleObserver,
724 weakColumnSortableObserver,
725 weakColumnSortTypeObserver,
726 weakColumnComparatorObserver);
727 }
728
729 sortOrder.removeAll(toRemove);
730
731 // Fix for RT-38892.
732 final TreeTableViewFocusModel<S> fm = getFocusModel();
733 final TreeTableViewSelectionModel<S> sm = getSelectionModel();
734 c.reset();
735 while (c.next()) {
736 if (! c.wasRemoved()) continue;
737
738 List<? extends TreeTableColumn<S,?>> removed = c.getRemoved();
739
740 // Fix for focus - we simply move focus to a cell to the left
741 // of the focused cell if the focused cell was located within
742 // a column that has been removed.
743 if (fm != null) {
744 TreeTablePosition<S, ?> focusedCell = fm.getFocusedCell();
745 boolean match = false;
746 for (TreeTableColumn<S, ?> tc : removed) {
747 match = focusedCell != null && focusedCell.getTableColumn() == tc;
748 if (match) {
749 break;
750 }
751 }
752
753 if (match) {
754 int matchingColumnIndex = lastKnownColumnIndex.getOrDefault(focusedCell.getTableColumn(), 0);
755 int newFocusColumnIndex =
756 matchingColumnIndex == 0 ? 0 :
757 Math.min(getVisibleLeafColumns().size() - 1, matchingColumnIndex - 1);
758 fm.focus(focusedCell.getRow(), getVisibleLeafColumn(newFocusColumnIndex));
759 }
760 }
761
762 // Fix for selection - we remove selection from all cells that
763 // were within the removed column.
764 if (sm != null) {
765 List<TreeTablePosition> selectedCells = new ArrayList<>(sm.getSelectedCells());
766 for (TreeTablePosition selectedCell : selectedCells) {
767 boolean match = false;
768 for (TreeTableColumn<S, ?> tc : removed) {
769 match = selectedCell != null && selectedCell.getTableColumn() == tc;
770 if (match) break;
771 }
772
773 if (match) {
774 // we can't just use the selectedCell.getTableColumn(), as that
775 // column no longer exists and therefore its index is not correct.
776 int matchingColumnIndex = lastKnownColumnIndex.getOrDefault(selectedCell.getTableColumn(), -1);
777 if (matchingColumnIndex == -1) continue;
778
779 if (sm instanceof TreeTableViewArrayListSelectionModel) {
780 // Also, because the table column no longer exists in the columns
781 // list at this point, we can't just call:
782 // sm.clearSelection(selectedCell.getRow(), selectedCell.getTableColumn());
783 // as the tableColumn would map to an index of -1, which means that
784 // selection will not be cleared. Instead, we have to create
785 // a new TablePosition with a fixed column index and use that.
786 TreeTablePosition<S,?> fixedTablePosition =
787 new TreeTablePosition<S,Object>(TreeTableView.this,
788 selectedCell.getRow(),
789 selectedCell.getTableColumn());
790 fixedTablePosition.fixedColumnIndex = matchingColumnIndex;
791
792 ((TreeTableViewArrayListSelectionModel)sm).clearSelection(fixedTablePosition);
793 } else {
794 sm.clearSelection(selectedCell.getRow(), selectedCell.getTableColumn());
795 }
796 }
797 }
798 }
799 }
800
801
802 // update the lastKnownColumnIndex map
803 lastKnownColumnIndex.clear();
804 for (TreeTableColumn<S,?> tc : getColumns()) {
805 int index = getVisibleLeafIndex(tc);
806 if (index > -1) {
807 lastKnownColumnIndex.put(tc, index);
808 }
809 }
810 }
811 };
812
813 private final WeakHashMap<TreeTableColumn<S,?>, Integer> lastKnownColumnIndex = new WeakHashMap<>();
814
815 private final InvalidationListener columnVisibleObserver = valueModel -> {
816 updateVisibleLeafColumns();
817 };
818
819 private final InvalidationListener columnSortableObserver = valueModel -> {
820 TreeTableColumn col = (TreeTableColumn) ((BooleanProperty)valueModel).getBean();
821 if (! getSortOrder().contains(col)) return;
822 doSort(TableUtil.SortEventType.COLUMN_SORTABLE_CHANGE, col);
823 };
824
825 private final InvalidationListener columnSortTypeObserver = valueModel -> {
826 TreeTableColumn col = (TreeTableColumn) ((ObjectProperty)valueModel).getBean();
827 if (! getSortOrder().contains(col)) return;
828 doSort(TableUtil.SortEventType.COLUMN_SORT_TYPE_CHANGE, col);
829 };
830
831 private final InvalidationListener columnComparatorObserver = valueModel -> {
832 TreeTableColumn col = (TreeTableColumn) ((SimpleObjectProperty)valueModel).getBean();
833 if (! getSortOrder().contains(col)) return;
834 doSort(TableUtil.SortEventType.COLUMN_COMPARATOR_CHANGE, col);
835 };
836
837 /* proxy pseudo-class state change from selectionModel's cellSelectionEnabledProperty */
838 private final InvalidationListener cellSelectionModelInvalidationListener = o -> {
839 boolean isCellSelection = ((BooleanProperty)o).get();
840 pseudoClassStateChanged(PSEUDO_CLASS_CELL_SELECTION, isCellSelection);
841 pseudoClassStateChanged(PSEUDO_CLASS_ROW_SELECTION, !isCellSelection);
842 };
843
844 private WeakEventHandler<TreeItem.TreeModificationEvent<S>> weakRootEventListener;
845
846 private final WeakInvalidationListener weakColumnVisibleObserver =
847 new WeakInvalidationListener(columnVisibleObserver);
848
849 private final WeakInvalidationListener weakColumnSortableObserver =
850 new WeakInvalidationListener(columnSortableObserver);
851
852 private final WeakInvalidationListener weakColumnSortTypeObserver =
853 new WeakInvalidationListener(columnSortTypeObserver);
854
855 private final WeakInvalidationListener weakColumnComparatorObserver =
856 new WeakInvalidationListener(columnComparatorObserver);
857
858 private final WeakListChangeListener<TreeTableColumn<S,?>> weakColumnsObserver =
859 new WeakListChangeListener<TreeTableColumn<S,?>>(columnsObserver);
860
861 private final WeakInvalidationListener weakCellSelectionModelInvalidationListener =
862 new WeakInvalidationListener(cellSelectionModelInvalidationListener);
863
864
865
866 /***************************************************************************
867 * *
868 * Properties *
869 * *
870 **************************************************************************/
871
872 // --- Root
873 private ObjectProperty<TreeItem<S>> root = new SimpleObjectProperty<TreeItem<S>>(this, "root") {
874 private WeakReference<TreeItem<S>> weakOldItem;
875
876 @Override protected void invalidated() {
877 TreeItem<S> oldTreeItem = weakOldItem == null ? null : weakOldItem.get();
878 if (oldTreeItem != null && weakRootEventListener != null) {
879 oldTreeItem.removeEventHandler(TreeItem.<S>treeNotificationEvent(), weakRootEventListener);
880 }
881
882 TreeItem<S> root = getRoot();
883 if (root != null) {
884 weakRootEventListener = new WeakEventHandler<>(rootEvent);
885 getRoot().addEventHandler(TreeItem.<S>treeNotificationEvent(), weakRootEventListener);
886 weakOldItem = new WeakReference<>(root);
887 }
888
889 // Fix for RT-35763
890 getSortOrder().clear();
891
892 expandedItemCountDirty = true;
893 updateRootExpanded();
894 }
895 };
896
897 /**
898 * Sets the root node in this TreeTableView. See the {@link TreeItem} class level
899 * documentation for more details.
900 *
901 * @param value The {@link TreeItem} that will be placed at the root of the
902 * TreeTableView.
903 */
904 public final void setRoot(TreeItem<S> value) {
905 rootProperty().set(value);
906 }
907
908 /**
909 * Returns the current root node of this TreeTableView, or null if no root node
910 * is specified.
911 * @return The current root node, or null if no root node exists.
912 */
913 public final TreeItem<S> getRoot() {
914 return root == null ? null : root.get();
915 }
916
917 /**
918 * Property representing the root node of the TreeTableView.
919 */
920 public final ObjectProperty<TreeItem<S>> rootProperty() {
921 return root;
922 }
923
924
925
926 // --- Show Root
927 private BooleanProperty showRoot;
928
929 /**
930 * Specifies whether the root {@code TreeItem} should be shown within this
931 * TreeTableView.
932 *
933 * @param value If true, the root TreeItem will be shown, and if false it
934 * will be hidden.
935 */
936 public final void setShowRoot(boolean value) {
937 showRootProperty().set(value);
938 }
939
940 /**
941 * Returns true if the root of the TreeTableView should be shown, and false if
942 * it should not. By default, the root TreeItem is visible in the TreeTableView.
943 */
944 public final boolean isShowRoot() {
945 return showRoot == null ? true : showRoot.get();
946 }
947
948 /**
949 * Property that represents whether or not the TreeTableView root node is visible.
950 */
951 public final BooleanProperty showRootProperty() {
952 if (showRoot == null) {
953 showRoot = new SimpleBooleanProperty(this, "showRoot", true) {
954 @Override protected void invalidated() {
955 updateRootExpanded();
956 updateExpandedItemCount(getRoot());
957 }
958 };
959 }
960 return showRoot;
961 }
962
963
964
965 // --- Tree Column
966 private ObjectProperty<TreeTableColumn<S,?>> treeColumn;
967 /**
968 * Property that represents which column should have the disclosure node
969 * shown in it (that is, the column with the arrow). By default this will be
970 * the left-most column if this property is null, otherwise it will be the
971 * specified column assuming it is non-null and contained within the
972 * {@link #getVisibleLeafColumns() visible leaf columns} list.
973 */
974 public final ObjectProperty<TreeTableColumn<S,?>> treeColumnProperty() {
975 if (treeColumn == null) {
976 treeColumn = new SimpleObjectProperty<>(this, "treeColumn", null);
977 }
978 return treeColumn;
979 }
980 public final void setTreeColumn(TreeTableColumn<S,?> value) {
981 treeColumnProperty().set(value);
982 }
983 public final TreeTableColumn<S,?> getTreeColumn() {
984 return treeColumn == null ? null : treeColumn.get();
985 }
986
987
988
989 // --- Selection Model
990 private ObjectProperty<TreeTableViewSelectionModel<S>> selectionModel;
991
992 /**
993 * Sets the {@link MultipleSelectionModel} to be used in the TreeTableView.
994 * Despite a TreeTableView requiring a <code><b>Multiple</b>SelectionModel</code>,
995 * it is possible to configure it to only allow single selection (see
996 * {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)}
997 * for more information).
998 */
999 public final void setSelectionModel(TreeTableViewSelectionModel<S> value) {
1000 selectionModelProperty().set(value);
1001 }
1002
1003 /**
1004 * Returns the currently installed selection model.
1005 */
1006 public final TreeTableViewSelectionModel<S> getSelectionModel() {
1007 return selectionModel == null ? null : selectionModel.get();
1008 }
1009
1010 /**
1011 * The SelectionModel provides the API through which it is possible
1012 * to select single or multiple items within a TreeTableView, as well as inspect
1013 * which rows have been selected by the user. Note that it has a generic
1014 * type that must match the type of the TreeTableView itself.
1015 */
1016 public final ObjectProperty<TreeTableViewSelectionModel<S>> selectionModelProperty() {
1017 if (selectionModel == null) {
1018 selectionModel = new SimpleObjectProperty<TreeTableViewSelectionModel<S>>(this, "selectionModel") {
1019
1020 TreeTableViewSelectionModel<S> oldValue = null;
1021
1022 @Override protected void invalidated() {
1023 // need to listen to the cellSelectionEnabledProperty
1024 // in order to set pseudo-class state
1025 if (oldValue != null) {
1026 oldValue.cellSelectionEnabledProperty().removeListener(weakCellSelectionModelInvalidationListener);
1027 }
1028
1029 oldValue = get();
1030
1031 if (oldValue != null) {
1032 oldValue.cellSelectionEnabledProperty().addListener(weakCellSelectionModelInvalidationListener);
1033 // fake invalidation to ensure updated pseudo-class states
1034 weakCellSelectionModelInvalidationListener.invalidated(oldValue.cellSelectionEnabledProperty());
1035 }
1036 }
1037 };
1038 }
1039 return selectionModel;
1040 }
1041
1042
1043 // --- Focus Model
1044 private ObjectProperty<TreeTableViewFocusModel<S>> focusModel;
1045
1046 /**
1047 * Sets the {@link FocusModel} to be used in the TreeTableView.
1048 */
1049 public final void setFocusModel(TreeTableViewFocusModel<S> value) {
1050 focusModelProperty().set(value);
1051 }
1052
1053 /**
1054 * Returns the currently installed {@link FocusModel}.
1055 */
1056 public final TreeTableViewFocusModel<S> getFocusModel() {
1057 return focusModel == null ? null : focusModel.get();
1058 }
1059
1060 /**
1061 * The FocusModel provides the API through which it is possible
1062 * to control focus on zero or one rows of the TreeTableView. Generally the
1063 * default implementation should be more than sufficient.
1064 */
1065 public final ObjectProperty<TreeTableViewFocusModel<S>> focusModelProperty() {
1066 if (focusModel == null) {
1067 focusModel = new SimpleObjectProperty<TreeTableViewFocusModel<S>>(this, "focusModel");
1068 }
1069 return focusModel;
1070 }
1071
1072
1073 // --- Tree node count
1074 /**
1075 * <p>Represents the number of tree nodes presently able to be visible in the
1076 * TreeTableView. This is essentially the count of all expanded tree items, and
1077 * their children.
1078 *
1079 * <p>For example, if just the root node is visible, the expandedItemCount will
1080 * be one. If the root had three children and the root was expanded, the value
1081 * will be four.
1082 */
1083 private ReadOnlyIntegerWrapper expandedItemCount = new ReadOnlyIntegerWrapper(this, "expandedItemCount", 0);
1084 public final ReadOnlyIntegerProperty expandedItemCountProperty() {
1085 return expandedItemCount.getReadOnlyProperty();
1086 }
1087 private void setExpandedItemCount(int value) {
1088 expandedItemCount.set(value);
1089 }
1090 public final int getExpandedItemCount() {
1091 if (expandedItemCountDirty) {
1092 updateExpandedItemCount(getRoot());
1093 }
1094 return expandedItemCount.get();
1095 }
1096
1097
1098 // --- Editable
1099 private BooleanProperty editable;
1100 public final void setEditable(boolean value) {
1101 editableProperty().set(value);
1102 }
1103 public final boolean isEditable() {
1104 return editable == null ? false : editable.get();
1105 }
1106 /**
1107 * Specifies whether this TreeTableView is editable - only if the TreeTableView and
1108 * the TreeCells within it are both editable will a TreeCell be able to go
1109 * into their editing state.
1110 */
1111 public final BooleanProperty editableProperty() {
1112 if (editable == null) {
1113 editable = new SimpleBooleanProperty(this, "editable", false);
1114 }
1115 return editable;
1116 }
1117
1118
1119 // --- Editing Cell
1120 private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> editingCell;
1121 private void setEditingCell(TreeTablePosition<S,?> value) {
1122 editingCellPropertyImpl().set(value);
1123 }
1124 public final TreeTablePosition<S,?> getEditingCell() {
1125 return editingCell == null ? null : editingCell.get();
1126 }
1127
1128 /**
1129 * Represents the current cell being edited, or null if
1130 * there is no cell being edited.
1131 */
1132 public final ReadOnlyObjectProperty<TreeTablePosition<S,?>> editingCellProperty() {
1133 return editingCellPropertyImpl().getReadOnlyProperty();
1134 }
1135
1136 private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> editingCellPropertyImpl() {
1137 if (editingCell == null) {
1138 editingCell = new ReadOnlyObjectWrapper<TreeTablePosition<S,?>>(this, "editingCell");
1139 }
1140 return editingCell;
1141 }
1142
1143
1144 // --- Table menu button visible
1145 private BooleanProperty tableMenuButtonVisible;
1146 /**
1147 * This controls whether a menu button is available when the user clicks
1148 * in a designated space within the TableView, within which is a radio menu
1149 * item for each TreeTableColumn in this table. This menu allows for the user to
1150 * show and hide all TreeTableColumns easily.
1151 */
1152 public final BooleanProperty tableMenuButtonVisibleProperty() {
1153 if (tableMenuButtonVisible == null) {
1154 tableMenuButtonVisible = new SimpleBooleanProperty(this, "tableMenuButtonVisible");
1155 }
1156 return tableMenuButtonVisible;
1157 }
1158 public final void setTableMenuButtonVisible (boolean value) {
1159 tableMenuButtonVisibleProperty().set(value);
1160 }
1161 public final boolean isTableMenuButtonVisible() {
1162 return tableMenuButtonVisible == null ? false : tableMenuButtonVisible.get();
1163 }
1164
1165
1166 // --- Column Resize Policy
1167 private ObjectProperty<Callback<TreeTableView.ResizeFeatures, Boolean>> columnResizePolicy;
1168 public final void setColumnResizePolicy(Callback<TreeTableView.ResizeFeatures, Boolean> callback) {
1169 columnResizePolicyProperty().set(callback);
1170 }
1171 public final Callback<TreeTableView.ResizeFeatures, Boolean> getColumnResizePolicy() {
1172 return columnResizePolicy == null ? UNCONSTRAINED_RESIZE_POLICY : columnResizePolicy.get();
1173 }
1174
1175 /**
1176 * This is the function called when the user completes a column-resize
1177 * operation. The two most common policies are available as static functions
1178 * in the TableView class: {@link #UNCONSTRAINED_RESIZE_POLICY} and
1179 * {@link #CONSTRAINED_RESIZE_POLICY}.
1180 */
1181 public final ObjectProperty<Callback<TreeTableView.ResizeFeatures, Boolean>> columnResizePolicyProperty() {
1182 if (columnResizePolicy == null) {
1183 columnResizePolicy = new SimpleObjectProperty<Callback<TreeTableView.ResizeFeatures, Boolean>>(this, "columnResizePolicy", UNCONSTRAINED_RESIZE_POLICY) {
1184 private Callback<TreeTableView.ResizeFeatures, Boolean> oldPolicy;
1185
1186 @Override protected void invalidated() {
1187 if (isInited) {
1188 get().call(new TreeTableView.ResizeFeatures(TreeTableView.this, null, 0.0));
1189
1190 if (oldPolicy != null) {
1191 PseudoClass state = PseudoClass.getPseudoClass(oldPolicy.toString());
1192 pseudoClassStateChanged(state, false);
1193 }
1194 if (get() != null) {
1195 PseudoClass state = PseudoClass.getPseudoClass(get().toString());
1196 pseudoClassStateChanged(state, true);
1197 }
1198 oldPolicy = get();
1199 }
1200 }
1201 };
1202 }
1203 return columnResizePolicy;
1204 }
1205
1206
1207 // --- Row Factory
1208 private ObjectProperty<Callback<TreeTableView<S>, TreeTableRow<S>>> rowFactory;
1209
1210 /**
1211 * A function which produces a TreeTableRow. The system is responsible for
1212 * reusing TreeTableRows. Return from this function a TreeTableRow which
1213 * might be usable for representing a single row in a TableView.
1214 * <p>
1215 * Note that a TreeTableRow is <b>not</b> a TableCell. A TreeTableRow is
1216 * simply a container for a TableCell, and in most circumstances it is more
1217 * likely that you'll want to create custom TableCells, rather than
1218 * TreeTableRows. The primary use case for creating custom TreeTableRow
1219 * instances would most probably be to introduce some form of column
1220 * spanning support.
1221 * <p>
1222 * You can create custom TableCell instances per column by assigning the
1223 * appropriate function to the cellFactory property in the TreeTableColumn class.
1224 */
1225 public final ObjectProperty<Callback<TreeTableView<S>, TreeTableRow<S>>> rowFactoryProperty() {
1226 if (rowFactory == null) {
1227 rowFactory = new SimpleObjectProperty<Callback<TreeTableView<S>, TreeTableRow<S>>>(this, "rowFactory");
1228 }
1229 return rowFactory;
1230 }
1231 public final void setRowFactory(Callback<TreeTableView<S>, TreeTableRow<S>> value) {
1232 rowFactoryProperty().set(value);
1233 }
1234 public final Callback<TreeTableView<S>, TreeTableRow<S>> getRowFactory() {
1235 return rowFactory == null ? null : rowFactory.get();
1236 }
1237
1238
1239 // --- Placeholder Node
1240 private ObjectProperty<Node> placeholder;
1241 /**
1242 * This Node is shown to the user when the table has no content to show.
1243 * This may be the case because the table model has no data in the first
1244 * place, that a filter has been applied to the table model, resulting
1245 * in there being nothing to show the user, or that there are no currently
1246 * visible columns.
1247 */
1248 public final ObjectProperty<Node> placeholderProperty() {
1249 if (placeholder == null) {
1250 placeholder = new SimpleObjectProperty<Node>(this, "placeholder");
1251 }
1252 return placeholder;
1253 }
1254 public final void setPlaceholder(Node value) {
1255 placeholderProperty().set(value);
1256 }
1257 public final Node getPlaceholder() {
1258 return placeholder == null ? null : placeholder.get();
1259 }
1260
1261
1262 // --- Fixed cell size
1263 private DoubleProperty fixedCellSize;
1264
1265 /**
1266 * Sets the new fixed cell size for this control. Any value greater than
1267 * zero will enable fixed cell size mode, whereas a zero or negative value
1268 * (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size
1269 * mode.
1270 *
1271 * @param value The new fixed cell size value, or a value less than or equal
1272 * to zero (or Region.USE_COMPUTED_SIZE) to disable.
1273 * @since JavaFX 8.0
1274 */
1275 public final void setFixedCellSize(double value) {
1276 fixedCellSizeProperty().set(value);
1277 }
1278
1279 /**
1280 * Returns the fixed cell size value. A value less than or equal to zero is
1281 * used to represent that fixed cell size mode is disabled, and a value
1282 * greater than zero represents the size of all cells in this control.
1283 *
1284 * @return A double representing the fixed cell size of this control, or a
1285 * value less than or equal to zero if fixed cell size mode is disabled.
1286 * @since JavaFX 8.0
1287 */
1288 public final double getFixedCellSize() {
1289 return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get();
1290 }
1291 /**
1292 * Specifies whether this control has cells that are a fixed height (of the
1293 * specified value). If this value is less than or equal to zero,
1294 * then all cells are individually sized and positioned. This is a slow
1295 * operation. Therefore, when performance matters and developers are not
1296 * dependent on variable cell sizes it is a good idea to set the fixed cell
1297 * size value. Generally cells are around 24px, so setting a fixed cell size
1298 * of 24 is likely to result in very little difference in visuals, but a
1299 * improvement to performance.
1300 *
1301 * <p>To set this property via CSS, use the -fx-fixed-cell-size property.
1302 * This should not be confused with the -fx-cell-size property. The difference
1303 * between these two CSS properties is that -fx-cell-size will size all
1304 * cells to the specified size, but it will not enforce that this is the
1305 * only size (thus allowing for variable cell sizes, and preventing the
1306 * performance gains from being possible). Therefore, when performance matters
1307 * use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are
1308 * specified in CSS, -fx-fixed-cell-size takes precedence.</p>
1309 *
1310 * @since JavaFX 8.0
1311 */
1312 public final DoubleProperty fixedCellSizeProperty() {
1313 if (fixedCellSize == null) {
1314 fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) {
1315 @Override public CssMetaData<TreeTableView<?>,Number> getCssMetaData() {
1316 return StyleableProperties.FIXED_CELL_SIZE;
1317 }
1318
1319 @Override public Object getBean() {
1320 return TreeTableView.this;
1321 }
1322
1323 @Override public String getName() {
1324 return "fixedCellSize";
1325 }
1326 };
1327 }
1328 return fixedCellSize;
1329 }
1330
1331
1332 // --- SortMode
1333 /**
1334 * Specifies the sort mode to use when sorting the contents of this TreeTableView,
1335 * should any columns be specified in the {@link #getSortOrder() sort order}
1336 * list.
1337 */
1338 private ObjectProperty<TreeSortMode> sortMode;
1339 public final ObjectProperty<TreeSortMode> sortModeProperty() {
1340 if (sortMode == null) {
1341 sortMode = new SimpleObjectProperty<>(this, "sortMode", TreeSortMode.ALL_DESCENDANTS);
1342 }
1343 return sortMode;
1344 }
1345 public final void setSortMode(TreeSortMode value) {
1346 sortModeProperty().set(value);
1347 }
1348 public final TreeSortMode getSortMode() {
1349 return sortMode == null ? TreeSortMode.ALL_DESCENDANTS : sortMode.get();
1350 }
1351
1352
1353 // --- Comparator (built via sortOrder list, so read-only)
1354 /**
1355 * The comparator property is a read-only property that is representative of the
1356 * current state of the {@link #getSortOrder() sort order} list. The sort
1357 * order list contains the columns that have been added to it either programmatically
1358 * or via a user clicking on the headers themselves.
1359 */
1360 private ReadOnlyObjectWrapper<Comparator<TreeItem<S>>> comparator;
1361 private void setComparator(Comparator<TreeItem<S>> value) {
1362 comparatorPropertyImpl().set(value);
1363 }
1364 public final Comparator<TreeItem<S>> getComparator() {
1365 return comparator == null ? null : comparator.get();
1366 }
1367 public final ReadOnlyObjectProperty<Comparator<TreeItem<S>>> comparatorProperty() {
1368 return comparatorPropertyImpl().getReadOnlyProperty();
1369 }
1370 private ReadOnlyObjectWrapper<Comparator<TreeItem<S>>> comparatorPropertyImpl() {
1371 if (comparator == null) {
1372 comparator = new ReadOnlyObjectWrapper<>(this, "comparator");
1373 }
1374 return comparator;
1375 }
1376
1377
1378 // --- sortPolicy
1379 /**
1380 * The sort policy specifies how sorting in this TreeTableView should be performed.
1381 * For example, a basic sort policy may just recursively sort the children of
1382 * the root tree item, whereas a more advanced sort policy may call to a
1383 * database to perform the necessary sorting on the server-side.
1384 *
1385 * <p>TreeTableView ships with a {@link TableView#DEFAULT_SORT_POLICY default
1386 * sort policy} that does precisely as mentioned above: it simply attempts
1387 * to sort the tree hierarchy in-place.
1388 *
1389 * <p>It is recommended that rather than override the {@link TreeTableView#sort() sort}
1390 * method that a different sort policy be provided instead.
1391 */
1392 private ObjectProperty<Callback<TreeTableView<S>, Boolean>> sortPolicy;
1393 public final void setSortPolicy(Callback<TreeTableView<S>, Boolean> callback) {
1394 sortPolicyProperty().set(callback);
1395 }
1396 @SuppressWarnings("unchecked")
1397 public final Callback<TreeTableView<S>, Boolean> getSortPolicy() {
1398 return sortPolicy == null ?
1399 (Callback<TreeTableView<S>, Boolean>)(Object) DEFAULT_SORT_POLICY :
1400 sortPolicy.get();
1401 }
1402 @SuppressWarnings("unchecked")
1403 public final ObjectProperty<Callback<TreeTableView<S>, Boolean>> sortPolicyProperty() {
1404 if (sortPolicy == null) {
1405 sortPolicy = new SimpleObjectProperty<Callback<TreeTableView<S>, Boolean>>(
1406 this, "sortPolicy", (Callback<TreeTableView<S>, Boolean>)(Object) DEFAULT_SORT_POLICY) {
1407 @Override protected void invalidated() {
1408 sort();
1409 }
1410 };
1411 }
1412 return sortPolicy;
1413 }
1414
1415
1416 // onSort
1417 /**
1418 * Called when there's a request to sort the control.
1419 */
1420 private ObjectProperty<EventHandler<SortEvent<TreeTableView<S>>>> onSort;
1421
1422 public void setOnSort(EventHandler<SortEvent<TreeTableView<S>>> value) {
1423 onSortProperty().set(value);
1424 }
1425
1426 public EventHandler<SortEvent<TreeTableView<S>>> getOnSort() {
1427 if( onSort != null ) {
1428 return onSort.get();
1429 }
1430 return null;
1431 }
1432
1433 public ObjectProperty<EventHandler<SortEvent<TreeTableView<S>>>> onSortProperty() {
1434 if( onSort == null ) {
1435 onSort = new ObjectPropertyBase<EventHandler<SortEvent<TreeTableView<S>>>>() {
1436 @Override protected void invalidated() {
1437 EventType<SortEvent<TreeTableView<S>>> eventType = SortEvent.sortEvent();
1438 EventHandler<SortEvent<TreeTableView<S>>> eventHandler = get();
1439 setEventHandler(eventType, eventHandler);
1440 }
1441
1442 @Override public Object getBean() {
1443 return TreeTableView.this;
1444 }
1445
1446 @Override public String getName() {
1447 return "onSort";
1448 }
1449 };
1450 }
1451 return onSort;
1452 }
1453
1454
1455
1456 /***************************************************************************
1457 * *
1458 * Public API *
1459 * *
1460 **************************************************************************/
1461
1462 /** {@inheritDoc} */
1463 @Override protected void layoutChildren() {
1464 if (expandedItemCountDirty) {
1465 updateExpandedItemCount(getRoot());
1466 }
1467
1468 super.layoutChildren();
1469 }
1470
1471 /**
1472 * Scrolls the TreeTableView such that the item in the given index is visible to
1473 * the end user.
1474 *
1475 * @param index The index that should be made visible to the user, assuming
1476 * of course that it is greater than, or equal to 0, and less than the
1477 * number of the visible items in the TreeTableView.
1478 */
1479 public void scrollTo(int index) {
1480 ControlUtils.scrollToIndex(this, index);
1481 }
1482
1483 /**
1484 * Called when there's a request to scroll an index into view using {@link #scrollTo(int)}
1485 */
1486 private ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollTo;
1487
1488 public void setOnScrollTo(EventHandler<ScrollToEvent<Integer>> value) {
1489 onScrollToProperty().set(value);
1490 }
1491
1492 public EventHandler<ScrollToEvent<Integer>> getOnScrollTo() {
1493 if( onScrollTo != null ) {
1494 return onScrollTo.get();
1495 }
1496 return null;
1497 }
1498
1499 public ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollToProperty() {
1500 if( onScrollTo == null ) {
1501 onScrollTo = new ObjectPropertyBase<EventHandler<ScrollToEvent<Integer>>>() {
1502 @Override protected void invalidated() {
1503 setEventHandler(ScrollToEvent.scrollToTopIndex(), get());
1504 }
1505
1506 @Override public Object getBean() {
1507 return TreeTableView.this;
1508 }
1509
1510 @Override public String getName() {
1511 return "onScrollTo";
1512 }
1513 };
1514 }
1515 return onScrollTo;
1516 }
1517
1518 /**
1519 * Scrolls the TreeTableView so that the given column is visible within the viewport.
1520 * @param column The column that should be visible to the user.
1521 */
1522 public void scrollToColumn(TreeTableColumn<S, ?> column) {
1523 ControlUtils.scrollToColumn(this, column);
1524 }
1525
1526 /**
1527 * Scrolls the TreeTableView so that the given index is visible within the viewport.
1528 * @param columnIndex The index of a column that should be visible to the user.
1529 */
1530 public void scrollToColumnIndex(int columnIndex) {
1531 if( getColumns() != null ) {
1532 ControlUtils.scrollToColumn(this, getColumns().get(columnIndex));
1533 }
1534 }
1535
1536 /**
1537 * Called when there's a request to scroll a column into view using {@link #scrollToColumn(TreeTableColumn)}
1538 * or {@link #scrollToColumnIndex(int)}
1539 */
1540 private ObjectProperty<EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>>> onScrollToColumn;
1541
1542 public void setOnScrollToColumn(EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>> value) {
1543 onScrollToColumnProperty().set(value);
1544 }
1545
1546 public EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>> getOnScrollToColumn() {
1547 if( onScrollToColumn != null ) {
1548 return onScrollToColumn.get();
1549 }
1550 return null;
1551 }
1552
1553 public ObjectProperty<EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>>> onScrollToColumnProperty() {
1554 if( onScrollToColumn == null ) {
1555 onScrollToColumn = new ObjectPropertyBase<EventHandler<ScrollToEvent<TreeTableColumn<S, ?>>>>() {
1556 @Override
1557 protected void invalidated() {
1558 EventType<ScrollToEvent<TreeTableColumn<S, ?>>> type = ScrollToEvent.scrollToColumn();
1559 setEventHandler(type, get());
1560 }
1561 @Override
1562 public Object getBean() {
1563 return TreeTableView.this;
1564 }
1565
1566 @Override
1567 public String getName() {
1568 return "onScrollToColumn";
1569 }
1570 };
1571 }
1572 return onScrollToColumn;
1573 }
1574
1575 /**
1576 * Returns the index position of the given TreeItem, assuming that it is
1577 * currently accessible through the tree hierarchy (most notably, that all
1578 * parent tree items are expanded). If a parent tree item is collapsed,
1579 * the result is that this method will return -1 to indicate that the
1580 * given tree item is not accessible in the tree.
1581 *
1582 * @param item The TreeItem for which the index is sought.
1583 * @return An integer representing the location in the current TreeTableView of the
1584 * first instance of the given TreeItem, or -1 if it is null or can not
1585 * be found (for example, if a parent (all the way up to the root) is
1586 * collapsed).
1587 */
1588 public int getRow(TreeItem<S> item) {
1589 return TreeUtil.getRow(item, getRoot(), expandedItemCountDirty, isShowRoot());
1590 }
1591
1592 /**
1593 * Returns the TreeItem in the given index, or null if it is out of bounds.
1594 *
1595 * @param row The index of the TreeItem being sought.
1596 * @return The TreeItem in the given index, or null if it is out of bounds.
1597 */
1598 public TreeItem<S> getTreeItem(int row) {
1599 if (row < 0) return null;
1600
1601 // normalize the requested row based on whether showRoot is set
1602 final int _row = isShowRoot() ? row : (row + 1);
1603
1604 if (expandedItemCountDirty) {
1605 updateExpandedItemCount(getRoot());
1606 } else {
1607 if (treeItemCacheMap.containsKey(_row)) {
1608 SoftReference<TreeItem<S>> treeItemRef = treeItemCacheMap.get(_row);
1609 TreeItem<S> treeItem = treeItemRef.get();
1610 if (treeItem != null) {
1611 return treeItem;
1612 }
1613 }
1614 }
1615
1616 TreeItem<S> treeItem = TreeUtil.getItem(getRoot(), _row, expandedItemCountDirty);
1617 treeItemCacheMap.put(_row, new SoftReference<>(treeItem));
1618 return treeItem;
1619 }
1620
1621 /**
1622 * Returns the number of levels of 'indentation' of the given TreeItem,
1623 * based on how many times getParent() can be recursively called. If the
1624 * given TreeItem is the root node of this TreeTableView, or if the TreeItem
1625 * does not have any parent set, the returned value will be zero. For each
1626 * time getParent() is recursively called, the returned value is incremented
1627 * by one.
1628 *
1629 * @param node The TreeItem for which the level is needed.
1630 * @return An integer representing the number of parents above the given node,
1631 * or -1 if the given TreeItem is null.
1632 */
1633 public int getTreeItemLevel(TreeItem<?> node) {
1634 final TreeItem<?> root = getRoot();
1635
1636 if (node == null) return -1;
1637 if (node == root) return 0;
1638
1639 int level = 0;
1640 TreeItem<?> parent = node.getParent();
1641 while (parent != null) {
1642 level++;
1643
1644 if (parent == root) {
1645 break;
1646 }
1647
1648 parent = parent.getParent();
1649 }
1650
1651 return level;
1652 }
1653
1654 /**
1655 * The TreeTableColumns that are part of this TableView. As the user reorders
1656 * the TableView columns, this list will be updated to reflect the current
1657 * visual ordering.
1658 *
1659 * <p>Note: to display any data in a TableView, there must be at least one
1660 * TreeTableColumn in this ObservableList.</p>
1661 */
1662 public final ObservableList<TreeTableColumn<S,?>> getColumns() {
1663 return columns;
1664 }
1665
1666 /**
1667 * The sortOrder list defines the order in which {@link TreeTableColumn} instances
1668 * are sorted. An empty sortOrder list means that no sorting is being applied
1669 * on the TableView. If the sortOrder list has one TreeTableColumn within it,
1670 * the TableView will be sorted using the
1671 * {@link TreeTableColumn#sortTypeProperty() sortType} and
1672 * {@link TreeTableColumn#comparatorProperty() comparator} properties of this
1673 * TreeTableColumn (assuming
1674 * {@link TreeTableColumn#sortableProperty() TreeTableColumn.sortable} is true).
1675 * If the sortOrder list contains multiple TreeTableColumn instances, then
1676 * the TableView is firstly sorted based on the properties of the first
1677 * TreeTableColumn. If two elements are considered equal, then the second
1678 * TreeTableColumn in the list is used to determine ordering. This repeats until
1679 * the results from all TreeTableColumn comparators are considered, if necessary.
1680 *
1681 * @return An ObservableList containing zero or more TreeTableColumn instances.
1682 */
1683 public final ObservableList<TreeTableColumn<S,?>> getSortOrder() {
1684 return sortOrder;
1685 }
1686
1687 /**
1688 * Applies the currently installed resize policy against the given column,
1689 * resizing it based on the delta value provided.
1690 */
1691 public boolean resizeColumn(TreeTableColumn<S,?> column, double delta) {
1692 if (column == null || Double.compare(delta, 0.0) == 0) return false;
1693
1694 boolean allowed = getColumnResizePolicy().call(new TreeTableView.ResizeFeatures<S>(TreeTableView.this, column, delta));
1695 if (!allowed) return false;
1696 return true;
1697 }
1698
1699 /**
1700 * Causes the cell at the given row/column view indexes to switch into
1701 * its editing state, if it is not already in it, and assuming that the
1702 * TableView and column are also editable.
1703 */
1704 public void edit(int row, TreeTableColumn<S,?> column) {
1705 if (!isEditable() || (column != null && ! column.isEditable())) {
1706 return;
1707 }
1708
1709 if (row < 0 && column == null) {
1710 setEditingCell(null);
1711 } else {
1712 setEditingCell(new TreeTablePosition<>(this, row, column));
1713 }
1714 }
1715
1716 /**
1717 * Returns an unmodifiable list containing the currently visible leaf columns.
1718 */
1719 @ReturnsUnmodifiableCollection
1720 public ObservableList<TreeTableColumn<S,?>> getVisibleLeafColumns() {
1721 return unmodifiableVisibleLeafColumns;
1722 }
1723
1724 /**
1725 * Returns the position of the given column, relative to all other
1726 * visible leaf columns.
1727 */
1728 public int getVisibleLeafIndex(TreeTableColumn<S,?> column) {
1729 return getVisibleLeafColumns().indexOf(column);
1730 }
1731
1732 /**
1733 * Returns the TreeTableColumn in the given column index, relative to all other
1734 * visible leaf columns.
1735 */
1736 public TreeTableColumn<S,?> getVisibleLeafColumn(int column) {
1737 if (column < 0 || column >= visibleLeafColumns.size()) return null;
1738 return visibleLeafColumns.get(column);
1739 }
1740
1741 /**
1742 * The sort method forces the TreeTableView to re-run its sorting algorithm. More
1743 * often than not it is not necessary to call this method directly, as it is
1744 * automatically called when the {@link #getSortOrder() sort order},
1745 * {@link #sortPolicyProperty() sort policy}, or the state of the
1746 * TreeTableColumn {@link TreeTableColumn#sortTypeProperty() sort type} properties
1747 * change. In other words, this method should only be called directly when
1748 * something external changes and a sort is required.
1749 */
1750 public void sort() {
1751 final ObservableList<TreeTableColumn<S,?>> sortOrder = getSortOrder();
1752
1753 // update the Comparator property
1754 final Comparator<TreeItem<S>> oldComparator = getComparator();
1755 setComparator(sortOrder.isEmpty() ? null : new TableColumnComparatorBase.TreeTableColumnComparator(sortOrder));
1756
1757 // fire the onSort event and check if it is consumed, if
1758 // so, don't run the sort
1759 SortEvent<TreeTableView<S>> sortEvent = new SortEvent<>(TreeTableView.this, TreeTableView.this);
1760 fireEvent(sortEvent);
1761 if (sortEvent.isConsumed()) {
1762 // if the sort is consumed we could back out the last action (the code
1763 // is commented out right below), but we don't as we take it as a
1764 // sign that the developer has decided to handle the event themselves.
1765
1766 // sortLock = true;
1767 // TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo);
1768 // sortLock = false;
1769 return;
1770 }
1771
1772 final List<TreeTablePosition<S,?>> prevState = new ArrayList<>(getSelectionModel().getSelectedCells());
1773 final int itemCount = prevState.size();
1774
1775 // we set makeAtomic to true here, so that we don't fire intermediate
1776 // sort events - instead we send a single permutation event at the end
1777 // of this method.
1778 getSelectionModel().startAtomic();
1779
1780 // get the sort policy and run it
1781 Callback<TreeTableView<S>, Boolean> sortPolicy = getSortPolicy();
1782 if (sortPolicy == null) return;
1783 Boolean success = sortPolicy.call(this);
1784
1785 getSelectionModel().stopAtomic();
1786
1787 if (success == null || ! success) {
1788 // the sort was a failure. Need to backout if possible
1789 sortLock = true;
1790 TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo);
1791 setComparator(oldComparator);
1792 sortLock = false;
1793 } else {
1794 // sorting was a success, now we possibly fire an event on the
1795 // selection model that the items list has 'permutated' to a new ordering
1796
1797 // FIXME we should support alternative selection model implementations!
1798 if (getSelectionModel() instanceof TreeTableViewArrayListSelectionModel) {
1799 final TreeTableViewArrayListSelectionModel<S> sm = (TreeTableViewArrayListSelectionModel<S>) getSelectionModel();
1800 final ObservableList<TreeTablePosition<S, ?>> newState = sm.getSelectedCells();
1801
1802 List<TreeTablePosition<S, ?>> removed = new ArrayList<>();
1803 for (int i = 0; i < itemCount; i++) {
1804 TreeTablePosition<S, ?> prevItem = prevState.get(i);
1805 if (!newState.contains(prevItem)) {
1806 removed.add(prevItem);
1807 }
1808 }
1809
1810 if (!removed.isEmpty()) {
1811 // the sort operation effectively permutates the selectedCells list,
1812 // but we cannot fire a permutation event as we are talking about
1813 // TreeTablePosition's changing (which may reside in the same list
1814 // position before and after the sort). Therefore, we need to fire
1815 // a single add/remove event to cover the added and removed positions.
1816 ListChangeListener.Change<TreeTablePosition<S, ?>> c = new NonIterableChange.GenericAddRemoveChange<>(0, itemCount, removed, newState);
1817 sm.handleSelectedCellsListChangeEvent(c);
1818 }
1819 }
1820 }
1821 }
1822
1823 /**
1824 * Calling {@code refresh()} forces the TreeTableView control to recreate and
1825 * repopulate the cells necessary to populate the visual bounds of the control.
1826 * In other words, this forces the TreeTableView to update what it is showing to
1827 * the user. This is useful in cases where the underlying data source has
1828 * changed in a way that is not observed by the TreeTableView itself.
1829 *
1830 * @since JavaFX 8u60
1831 */
1832 public void refresh() {
1833 getProperties().put(TableViewSkinBase.RECREATE, Boolean.TRUE);
1834 }
1835
1836
1837
1838 /***************************************************************************
1839 * *
1840 * Private Implementation *
1841 * *
1842 **************************************************************************/
1843
1844 private boolean sortLock = false;
1845 private TableUtil.SortEventType lastSortEventType = null;
1846 private Object[] lastSortEventSupportInfo = null;
1847
1848 private void doSort(final TableUtil.SortEventType sortEventType, final Object... supportInfo) {
1849 if (sortLock) {
1850 return;
1851 }
1852
1853 this.lastSortEventType = sortEventType;
1854 this.lastSortEventSupportInfo = supportInfo;
1855 sort();
1856 this.lastSortEventType = null;
1857 this.lastSortEventSupportInfo = null;
1858 }
1859
1860 private void updateExpandedItemCount(TreeItem<S> treeItem) {
1861 setExpandedItemCount(TreeUtil.updateExpandedItemCount(treeItem, expandedItemCountDirty, isShowRoot()));
1862
1863 if (expandedItemCountDirty) {
1864 // this is a very inefficient thing to do, but for now having a cache
1865 // is better than nothing at all...
1866 treeItemCacheMap.clear();
1867 }
1868
1869 expandedItemCountDirty = false;
1870 }
1871
1872 private void updateRootExpanded() {
1873 // if we aren't showing the root, and the root isn't expanded, we expand
1874 // it now so that something is shown.
1875 if (!isShowRoot() && getRoot() != null && ! getRoot().isExpanded()) {
1876 getRoot().setExpanded(true);
1877 }
1878 }
1879
1880
1881 // --- Content width
1882 private void setContentWidth(double contentWidth) {
1883 this.contentWidth = contentWidth;
1884 if (isInited) {
1885 // sometimes the current column resize policy will have to modify the
1886 // column width of all columns in the table if the table width changes,
1887 // so we short-circuit the resize function and just go straight there
1888 // with a null TreeTableColumn, which indicates to the resize policy function
1889 // that it shouldn't actually do anything specific to one column.
1890 getColumnResizePolicy().call(new TreeTableView.ResizeFeatures<S>(TreeTableView.this, null, 0.0));
1891 }
1892 }
1893
1894 /**
1895 * Recomputes the currently visible leaf columns in this TableView.
1896 */
1897 private void updateVisibleLeafColumns() {
1898 // update visible leaf columns list
1899 List<TreeTableColumn<S,?>> cols = new ArrayList<TreeTableColumn<S,?>>();
1900 buildVisibleLeafColumns(getColumns(), cols);
1901 visibleLeafColumns.setAll(cols);
1902
1903 // sometimes the current column resize policy will have to modify the
1904 // column width of all columns in the table if the table width changes,
1905 // so we short-circuit the resize function and just go straight there
1906 // with a null TreeTableColumn, which indicates to the resize policy function
1907 // that it shouldn't actually do anything specific to one column.
1908 getColumnResizePolicy().call(new TreeTableView.ResizeFeatures<S>(TreeTableView.this, null, 0.0));
1909 }
1910
1911 private void buildVisibleLeafColumns(List<TreeTableColumn<S,?>> cols, List<TreeTableColumn<S,?>> vlc) {
1912 for (TreeTableColumn<S,?> c : cols) {
1913 if (c == null) continue;
1914
1915 boolean hasChildren = ! c.getColumns().isEmpty();
1916
1917 if (hasChildren) {
1918 buildVisibleLeafColumns(c.getColumns(), vlc);
1919 } else if (c.isVisible()) {
1920 vlc.add(c);
1921 }
1922 }
1923 }
1924
1925
1926
1927 /***************************************************************************
1928 * *
1929 * Stylesheet Handling *
1930 * *
1931 **************************************************************************/
1932
1933 private static final String DEFAULT_STYLE_CLASS = "tree-table-view";
1934
1935 private static final PseudoClass PSEUDO_CLASS_CELL_SELECTION =
1936 PseudoClass.getPseudoClass("cell-selection");
1937 private static final PseudoClass PSEUDO_CLASS_ROW_SELECTION =
1938 PseudoClass.getPseudoClass("row-selection");
1939
1940 /** @treatAsPrivate */
1941 private static class StyleableProperties {
1942 private static final CssMetaData<TreeTableView<?>,Number> FIXED_CELL_SIZE =
1943 new CssMetaData<TreeTableView<?>,Number>("-fx-fixed-cell-size",
1944 SizeConverter.getInstance(),
1945 Region.USE_COMPUTED_SIZE) {
1946
1947 @Override public Double getInitialValue(TreeTableView<?> node) {
1948 return node.getFixedCellSize();
1949 }
1950
1951 @Override public boolean isSettable(TreeTableView<?> n) {
1952 return n.fixedCellSize == null || !n.fixedCellSize.isBound();
1953 }
1954
1955 @Override public StyleableProperty<Number> getStyleableProperty(TreeTableView<?> n) {
1956 return (StyleableProperty<Number>)(WritableValue<Number>) n.fixedCellSizeProperty();
1957 }
1958 };
1959
1960 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1961 static {
1962 final List<CssMetaData<? extends Styleable, ?>> styleables =
1963 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
1964 styleables.add(FIXED_CELL_SIZE);
1965 STYLEABLES = Collections.unmodifiableList(styleables);
1966 }
1967 }
1968
1969 /**
1970 * @return The CssMetaData associated with this class, which may include the
1971 * CssMetaData of its super classes.
1972 */
1973 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1974 return StyleableProperties.STYLEABLES;
1975 }
1976
1977 /**
1978 * {@inheritDoc}
1979 */
1980 @Override
1981 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
1982 return getClassCssMetaData();
1983 }
1984
1985 /** {@inheritDoc} */
1986 @Override protected Skin<?> createDefaultSkin() {
1987 return new TreeTableViewSkin<S>(this);
1988 }
1989
1990
1991
1992 /***************************************************************************
1993 * *
1994 * Accessibility handling *
1995 * *
1996 **************************************************************************/
1997
1998 @Override
1999 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
2000 switch (attribute) {
2001 case ROW_COUNT: return getExpandedItemCount();
2002 case COLUMN_COUNT: return getVisibleLeafColumns().size();
2003
2004 /*
2005 * TreeTableViewSkin returns TreeTableRows back to TreeTableView.
2006 * TreeTableRowSkin returns TreeTableCells back to TreeTableRow.
2007 */
2008 case SELECTED_ITEMS: {
2009 @SuppressWarnings("unchecked")
2010 ObservableList<TreeTableRow<S>> rows = (ObservableList<TreeTableRow<S>>)super.queryAccessibleAttribute(attribute, parameters);
2011 List<Node> selection = new ArrayList<>();
2012 for (TreeTableRow<S> row : rows) {
2013 @SuppressWarnings("unchecked")
2014 ObservableList<Node> cells = (ObservableList<Node>)row.queryAccessibleAttribute(attribute, parameters);
2015 if (cells != null) selection.addAll(cells);
2016 }
2017 return FXCollections.observableArrayList(selection);
2018 }
2019 case FOCUS_ITEM: {
2020 Node row = (Node)super.queryAccessibleAttribute(attribute, parameters);
2021 if (row == null) return null;
2022 Node cell = (Node)row.queryAccessibleAttribute(attribute, parameters);
2023 /* cell equals to null means the row is a placeholder node */
2024 return cell != null ? cell : row;
2025 }
2026 case CELL_AT_ROW_COLUMN: {
2027 @SuppressWarnings("unchecked")
2028 TreeTableRow<S> row = (TreeTableRow<S>)super.queryAccessibleAttribute(attribute, parameters);
2029 return row != null ? row.queryAccessibleAttribute(attribute, parameters) : null;
2030 }
2031 case MULTIPLE_SELECTION: {
2032 TreeTableViewSelectionModel<S> sm = getSelectionModel();
2033 return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE;
2034 }
2035 default: return super.queryAccessibleAttribute(attribute, parameters);
2036 }
2037 }
2038
2039 /***************************************************************************
2040 * *
2041 * Support Classes *
2042 * *
2043 **************************************************************************/
2044
2045 /**
2046 * An immutable wrapper class for use in the TableView
2047 * {@link TreeTableView#columnResizePolicyProperty() column resize} functionality.
2048 * @since JavaFX 8.0
2049 */
2050 public static class ResizeFeatures<S> extends ResizeFeaturesBase<TreeItem<S>> {
2051 private TreeTableView<S> treeTable;
2052
2053 /**
2054 * Creates an instance of this class, with the provided TreeTableView,
2055 * TreeTableColumn and delta values being set and stored in this immutable
2056 * instance.
2057 *
2058 * @param treeTable The TreeTableView upon which the resize operation is occurring.
2059 * @param column The column upon which the resize is occurring, or null
2060 * if this ResizeFeatures instance is being created as a result of a
2061 * TreeTableView resize operation.
2062 * @param delta The amount of horizontal space added or removed in the
2063 * resize operation.
2064 */
2065 public ResizeFeatures(TreeTableView<S> treeTable, TreeTableColumn<S,?> column, Double delta) {
2066 super(column, delta);
2067 this.treeTable = treeTable;
2068 }
2069
2070 /**
2071 * Returns the column upon which the resize is occurring, or null
2072 * if this ResizeFeatures instance was created as a result of a
2073 * TreeTableView resize operation.
2074 */
2075 @Override public TreeTableColumn<S,?> getColumn() {
2076 return (TreeTableColumn<S,?>) super.getColumn();
2077 }
2078
2079 /**
2080 * Returns the TreeTableView upon which the resize operation is occurring.
2081 */
2082 public TreeTableView<S> getTable() { return treeTable; }
2083 }
2084
2085
2086
2087 /**
2088 * An {@link Event} subclass used specifically in TreeTableView for representing
2089 * edit-related events. It provides additional API to easily access the
2090 * TreeItem that the edit event took place on, as well as the input provided
2091 * by the end user.
2092 *
2093 * @param <S> The type of the input, which is the same type as the TreeTableView
2094 * itself.
2095 * @since JavaFX 8.0
2096 */
2097 public static class EditEvent<S> extends Event {
2098 private static final long serialVersionUID = -4437033058917528976L;
2099
2100 /**
2101 * Common supertype for all edit event types.
2102 */
2103 public static final EventType<?> ANY = EDIT_ANY_EVENT;
2104
2105 private final S oldValue;
2106 private final S newValue;
2107 private transient final TreeItem<S> treeItem;
2108
2109 /**
2110 * Creates a new EditEvent instance to represent an edit event. This
2111 * event is used for {@link #EDIT_START_EVENT},
2112 * {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT} types.
2113 */
2114 public EditEvent(TreeTableView<S> source,
2115 EventType<? extends TreeTableView.EditEvent> eventType,
2116 TreeItem<S> treeItem, S oldValue, S newValue) {
2117 super(source, Event.NULL_SOURCE_TARGET, eventType);
2118 this.oldValue = oldValue;
2119 this.newValue = newValue;
2120 this.treeItem = treeItem;
2121 }
2122
2123 /**
2124 * Returns the TreeTableView upon which the edit took place.
2125 */
2126 @Override public TreeTableView<S> getSource() {
2127 return (TreeTableView<S>) super.getSource();
2128 }
2129
2130 /**
2131 * Returns the {@link TreeItem} upon which the edit took place.
2132 */
2133 public TreeItem<S> getTreeItem() {
2134 return treeItem;
2135 }
2136
2137 /**
2138 * Returns the new value input into the TreeItem by the end user.
2139 */
2140 public S getNewValue() {
2141 return newValue;
2142 }
2143
2144 /**
2145 * Returns the old value that existed in the TreeItem prior to the current
2146 * edit event.
2147 */
2148 public S getOldValue() {
2149 return oldValue;
2150 }
2151 }
2152
2153
2154
2155 /**
2156 * A simple extension of the {@link SelectionModel} abstract class to
2157 * allow for special support for TreeTableView controls.
2158 *
2159 * @since JavaFX 8.0
2160 */
2161 public static abstract class TreeTableViewSelectionModel<S> extends
2162 TableSelectionModel<TreeItem<S>> {
2163
2164 /***********************************************************************
2165 * *
2166 * Private fields *
2167 * *
2168 **********************************************************************/
2169
2170 private final TreeTableView<S> treeTableView;
2171
2172
2173 /***********************************************************************
2174 * *
2175 * Constructors *
2176 * *
2177 **********************************************************************/
2178
2179 /**
2180 * Builds a default TreeTableViewSelectionModel instance with the provided
2181 * TreeTableView.
2182 * @param treeTableView The TreeTableView upon which this selection model should
2183 * operate.
2184 * @throws NullPointerException TreeTableView can not be null.
2185 */
2186 public TreeTableViewSelectionModel(final TreeTableView<S> treeTableView) {
2187 if (treeTableView == null) {
2188 throw new NullPointerException("TreeTableView can not be null");
2189 }
2190
2191 this.treeTableView = treeTableView;
2192 }
2193
2194
2195
2196 /***********************************************************************
2197 * *
2198 * Abstract API *
2199 * *
2200 **********************************************************************/
2201
2202 /**
2203 * A read-only ObservableList representing the currently selected cells
2204 * in this TreeTableView. Rather than directly modify this list, please
2205 * use the other methods provided in the TreeTableViewSelectionModel.
2206 */
2207 public abstract ObservableList<TreeTablePosition<S,?>> getSelectedCells();
2208
2209
2210
2211 /***********************************************************************
2212 * *
2213 * Public API *
2214 * *
2215 **********************************************************************/
2216
2217 /**
2218 * Returns the TreeTableView instance that this selection model is installed in.
2219 */
2220 public TreeTableView<S> getTreeTableView() {
2221 return treeTableView;
2222 }
2223
2224 /** {@inheritDoc} */
2225 @Override public TreeItem<S> getModelItem(int index) {
2226 return treeTableView.getTreeItem(index);
2227 }
2228
2229 /** {@inheritDoc} */
2230 @Override protected int getItemCount() {
2231 return treeTableView.getExpandedItemCount();
2232 }
2233
2234 /** {@inheritDoc} */
2235 @Override public void focus(int row) {
2236 focus(row, null);
2237 }
2238
2239 /** {@inheritDoc} */
2240 @Override public int getFocusedIndex() {
2241 return getFocusedCell().getRow();
2242 }
2243
2244 /** {@inheritDoc} */
2245 @Override public void selectRange(int minRow, TableColumnBase<TreeItem<S>,?> minColumn,
2246 int maxRow, TableColumnBase<TreeItem<S>,?> maxColumn) {
2247 final int minColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)minColumn);
2248 final int maxColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)maxColumn);
2249 for (int _row = minRow; _row <= maxRow; _row++) {
2250 for (int _col = minColumnIndex; _col <= maxColumnIndex; _col++) {
2251 select(_row, treeTableView.getVisibleLeafColumn(_col));
2252 }
2253 }
2254 }
2255
2256
2257
2258 /***********************************************************************
2259 * *
2260 * Private implementation *
2261 * *
2262 **********************************************************************/
2263
2264 private void focus(int row, TreeTableColumn<S,?> column) {
2265 focus(new TreeTablePosition<>(getTreeTableView(), row, column));
2266 }
2267
2268 private void focus(TreeTablePosition<S,?> pos) {
2269 if (getTreeTableView().getFocusModel() == null) return;
2270
2271 getTreeTableView().getFocusModel().focus(pos.getRow(), pos.getTableColumn());
2272 }
2273
2274 private TreeTablePosition<S,?> getFocusedCell() {
2275 if (treeTableView.getFocusModel() == null) {
2276 return new TreeTablePosition<>(treeTableView, -1, null);
2277 }
2278 return treeTableView.getFocusModel().getFocusedCell();
2279 }
2280 }
2281
2282
2283
2284 /**
2285 * A primitive selection model implementation, using a List<Integer> to store all
2286 * selected indices.
2287 */
2288 // package for testing
2289 static class TreeTableViewArrayListSelectionModel<S> extends TreeTableViewSelectionModel<S> {
2290
2291 private final MappingChange.Map<TreeTablePosition<S,?>,TreeItem<S>> cellToItemsMap = f -> getModelItem(f.getRow());
2292
2293 private final MappingChange.Map<TreeTablePosition<S,?>,Integer> cellToIndicesMap = f -> f.getRow();
2294
2295 private TreeTableView<S> treeTableView = null;
2296
2297 /***********************************************************************
2298 * *
2299 * Constructors *
2300 * *
2301 **********************************************************************/
2302
2303 public TreeTableViewArrayListSelectionModel(final TreeTableView<S> treeTableView) {
2304 super(treeTableView);
2305 this.treeTableView = treeTableView;
2306
2307 this.treeTableView.rootProperty().addListener(weakRootPropertyListener);
2308 updateTreeEventListener(null, treeTableView.getRoot());
2309
2310 selectedCellsMap = new SelectedCellsMap<TreeTablePosition<S,?>>(c -> handleSelectedCellsListChangeEvent(c)) {
2311 @Override public boolean isCellSelectionEnabled() {
2312 return TreeTableViewArrayListSelectionModel.this.isCellSelectionEnabled();
2313 }
2314 };
2315
2316 selectedItems = new ReadOnlyUnbackedObservableList<TreeItem<S>>() {
2317 @Override public TreeItem<S> get(int i) {
2318 return getModelItem(getSelectedIndices().get(i));
2319 }
2320
2321 @Override public int size() {
2322 return getSelectedIndices().size();
2323 }
2324 };
2325
2326 selectedCellsSeq = new ReadOnlyUnbackedObservableList<TreeTablePosition<S,?>>() {
2327 @Override public TreeTablePosition<S,?> get(int i) {
2328 return selectedCellsMap.get(i);
2329 }
2330
2331 @Override public int size() {
2332 return selectedCellsMap.size();
2333 }
2334 };
2335
2336
2337 updateDefaultSelection();
2338
2339 cellSelectionEnabledProperty().addListener(o -> {
2340 updateDefaultSelection();
2341 TableCellBehaviorBase.setAnchor(treeTableView, getFocusedCell(), true);
2342 });
2343 }
2344
2345 private void updateTreeEventListener(TreeItem<S> oldRoot, TreeItem<S> newRoot) {
2346 if (oldRoot != null && weakTreeItemListener != null) {
2347 oldRoot.removeEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
2348 }
2349
2350 if (newRoot != null) {
2351 weakTreeItemListener = new WeakEventHandler<>(treeItemListener);
2352 newRoot.addEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
2353 }
2354 }
2355
2356 private ChangeListener<TreeItem<S>> rootPropertyListener = (observable, oldValue, newValue) -> {
2357 updateDefaultSelection();
2358
2359 updateTreeEventListener(oldValue, newValue);
2360 };
2361
2362 private EventHandler<TreeItem.TreeModificationEvent<S>> treeItemListener = new EventHandler<TreeItem.TreeModificationEvent<S>>() {
2363 @Override public void handle(TreeItem.TreeModificationEvent<S> e) {
2364
2365 if (getSelectedIndex() == -1 && getSelectedItem() == null) return;
2366
2367 final TreeItem<S> treeItem = e.getTreeItem();
2368 if (treeItem == null) return;
2369
2370 final int oldSelectedIndex = getSelectedIndex();
2371 final TreeItem<S> oldSelectedItem = getSelectedItem();
2372
2373 treeTableView.expandedItemCountDirty = true;
2374
2375 // we only shift selection from this row - everything before it
2376 // is safe. We might change this below based on certain criteria
2377 int startRow = treeTableView.getRow(treeItem);
2378
2379 int shift = 0;
2380 ListChangeListener.Change<? extends TreeItem<?>> change = e.getChange();
2381 if (change != null) {
2382 change.next();
2383 }
2384
2385 do {
2386 final int addedSize = change == null ? 0 : change.getAddedSize();
2387 final int removedSize = change == null ? 0 : change.getRemovedSize();
2388
2389 if (e.wasExpanded()) {
2390 // need to shuffle selection by the number of visible children
2391 shift += treeItem.getExpandedDescendentCount(false) - 1;
2392 startRow++;
2393 } else if (e.wasCollapsed()) {
2394 // remove selection from any child treeItem, and also determine
2395 // if any child item was selected (in which case the parent
2396 // takes the selection on collapse)
2397 treeItem.getExpandedDescendentCount(false);
2398 final int count = treeItem.previousExpandedDescendentCount;
2399
2400 final int selectedIndex = getSelectedIndex();
2401 final boolean wasPrimarySelectionInChild =
2402 selectedIndex >= (startRow + 1) &&
2403 selectedIndex < (startRow + count);
2404
2405 boolean wasAnyChildSelected = false;
2406 final boolean isCellSelectionMode = isCellSelectionEnabled();
2407 ObservableList<TreeTableColumn<S, ?>> columns = getTreeTableView().getVisibleLeafColumns();
2408
2409 if (!isCellSelectionMode) {
2410 startAtomic();
2411 }
2412
2413 final int from = startRow + 1;
2414 final int to = startRow + count;
2415 final List<Integer> removed = new ArrayList<>();
2416 TreeTableColumn<S, ?> selectedColumn = null;
2417 for (int i = from; i < to; i++) {
2418 // we have to handle cell selection mode differently than
2419 // row selection mode. Refer to RT-34103 for the bug report
2420 // that drove this change, but in short the issue was that
2421 // when collapsing a branch that had selection, we were
2422 // always calling isSelected(row), but that always returns
2423 // false in cell selection mode.
2424 if (isCellSelectionMode) {
2425 for (int column = 0; column < columns.size(); column++) {
2426 final TreeTableColumn<S, ?> col = columns.get(column);
2427 if (isSelected(i, col)) {
2428 wasAnyChildSelected = true;
2429 clearSelection(i, col);
2430 selectedColumn = col;
2431 }
2432 }
2433 } else {
2434 if (isSelected(i)) {
2435 wasAnyChildSelected = true;
2436 clearSelection(i);
2437 removed.add(i);
2438 }
2439 }
2440 }
2441
2442 if (!isCellSelectionMode) {
2443 stopAtomic();
2444 }
2445
2446 // put selection onto the newly-collapsed tree item
2447 if (wasPrimarySelectionInChild && wasAnyChildSelected) {
2448 select(startRow, selectedColumn);
2449 } else if (!isCellSelectionMode) {
2450 // we pass in (index, index) here to represent that nothing was added
2451 // in this change.
2452 ListChangeListener.Change newChange = new NonIterableChange.GenericAddRemoveChange<>(from, from,
2453 removed, selectedIndicesSeq);
2454 selectedIndicesSeq.callObservers(newChange);
2455 }
2456
2457 shift += -count + 1;
2458 startRow++;
2459 } else if (e.wasPermutated()) {
2460 // This handles the sorting case where nothing was added or
2461 // removed, but the location of the selected index / item
2462 // has likely changed. This was added to fix RT-30156 and
2463 // unit tests exist to prevent it from regressing.
2464 quietClearSelection();
2465 select(oldSelectedItem);
2466 } else if (e.wasAdded()) {
2467 // shuffle selection by the number of added items
2468 shift += treeItem.isExpanded() ? addedSize : 0;
2469
2470 // RT-32963: We were taking the startRow from the TreeItem
2471 // in which the children were added, rather than from the
2472 // actual position of the new child. This led to selection
2473 // being moved off the parent TreeItem by mistake.
2474 // The 'if (e.getAddedSize() == 1)' condition here was
2475 // subsequently commented out due to RT-33894.
2476 startRow = treeTableView.getRow(e.getChange().getAddedSubList().get(0));
2477
2478 TreeTablePosition<S, ?> anchor = TreeTableCellBehavior.getAnchor(treeTableView, null);
2479 if (anchor != null) {
2480 boolean isAnchorSelected = isSelected(anchor.getRow(), anchor.getTableColumn());
2481 if (isAnchorSelected) {
2482 TreeTablePosition<S, ?> newAnchor = new TreeTablePosition<>(treeTableView, anchor.getRow() + shift, anchor.getTableColumn());
2483 TreeTableCellBehavior.setAnchor(treeTableView, newAnchor, false);
2484 }
2485 }
2486 } else if (e.wasRemoved()) {
2487 // shuffle selection by the number of removed items
2488 shift += treeItem.isExpanded() ? -removedSize : 0;
2489
2490 // the start row is incorrect - it is _not_ the index of the
2491 // TreeItem in which the children were removed from (which is
2492 // what it currently represents). We need to take the 'from'
2493 // value out of the event and make use of that to understand
2494 // what actually changed inside the children list.
2495 startRow += e.getFrom() + 1;
2496
2497 // whilst we are here, we should check if the removed items
2498 // are part of the selectedItems list - and remove them
2499 // from selection if they are (as per RT-15446)
2500 final List<Integer> selectedIndices = getSelectedIndices();
2501 final List<TreeItem<S>> selectedItems = getSelectedItems();
2502 final TreeItem<S> selectedItem = getSelectedItem();
2503 final List<? extends TreeItem<S>> removedChildren = e.getChange().getRemoved();
2504
2505 for (int i = 0; i < selectedIndices.size() && !selectedItems.isEmpty(); i++) {
2506 int index = selectedIndices.get(i);
2507 if (index > selectedItems.size()) break;
2508
2509 if (removedChildren.size() == 1 &&
2510 selectedItems.size() == 1 &&
2511 selectedItem != null &&
2512 selectedItem.equals(removedChildren.get(0))) {
2513 // Bug fix for RT-28637
2514 if (oldSelectedIndex < getItemCount()) {
2515 final int previousRow = oldSelectedIndex == 0 ? 0 : oldSelectedIndex - 1;
2516 TreeItem<S> newSelectedItem = getModelItem(previousRow);
2517 if (!selectedItem.equals(newSelectedItem)) {
2518 clearAndSelect(previousRow);
2519 }
2520 }
2521 }
2522 }
2523 }
2524 } while (e.getChange() != null && e.getChange().next());
2525
2526 shiftSelection(startRow, shift, new Callback<ShiftParams, Void>() {
2527 @Override public Void call(ShiftParams param) {
2528
2529 // we make the shifts atomic, as otherwise listeners to
2530 // the items / indices lists get a lot of intermediate
2531 // noise. They eventually get the summary event fired
2532 // from within shiftSelection, so this is ok.
2533 startAtomic();
2534
2535 final int clearIndex = param.getClearIndex();
2536 TreeTablePosition<S,?> oldTP = null;
2537 if (clearIndex > -1) {
2538 for (int i = 0; i < selectedCellsMap.size(); i++) {
2539 TreeTablePosition<S,?> tp = selectedCellsMap.get(i);
2540 if (tp.getRow() == clearIndex) {
2541 oldTP = tp;
2542 selectedCellsMap.remove(tp);
2543 break;
2544 }
2545 }
2546 }
2547
2548 if (oldTP != null && param.isSelected()) {
2549 TreeTablePosition<S,?> newTP = new TreeTablePosition<>(
2550 treeTableView, param.getSetIndex(), oldTP.getTableColumn());
2551
2552 selectedCellsMap.add(newTP);
2553 }
2554
2555 stopAtomic();
2556
2557 return null;
2558 }
2559 });
2560 }
2561 };
2562
2563 private WeakChangeListener<TreeItem<S>> weakRootPropertyListener =
2564 new WeakChangeListener<>(rootPropertyListener);
2565
2566 private WeakEventHandler<TreeItem.TreeModificationEvent<S>> weakTreeItemListener;
2567
2568
2569
2570 /***********************************************************************
2571 * *
2572 * Observable properties (and getters/setters) *
2573 * *
2574 **********************************************************************/
2575
2576 // the only 'proper' internal data structure, selectedItems and selectedIndices
2577 // are both 'read-only and unbacked'.
2578 private final SelectedCellsMap<TreeTablePosition<S,?>> selectedCellsMap;
2579
2580 // used to represent the _row_ backing data for the selectedCells
2581 private final ReadOnlyUnbackedObservableList<TreeItem<S>> selectedItems;
2582 @Override public ObservableList<TreeItem<S>> getSelectedItems() {
2583 return selectedItems;
2584 }
2585
2586 private final ReadOnlyUnbackedObservableList<TreeTablePosition<S,?>> selectedCellsSeq;
2587 @Override public ObservableList<TreeTablePosition<S,?>> getSelectedCells() {
2588 return selectedCellsSeq;
2589 }
2590
2591
2592 /***********************************************************************
2593 * *
2594 * Internal properties *
2595 * *
2596 **********************************************************************/
2597
2598
2599
2600 /***********************************************************************
2601 * *
2602 * Public selection API *
2603 * *
2604 **********************************************************************/
2605
2606 @Override public void clearAndSelect(int row) {
2607 clearAndSelect(row, null);
2608 }
2609
2610 @Override public void clearAndSelect(int row, TableColumnBase<TreeItem<S>,?> column) {
2611 if (row < 0 || row >= getItemCount()) return;
2612
2613 final TreeTablePosition<S,?> newTablePosition = new TreeTablePosition<>(getTreeTableView(), row, (TreeTableColumn<S,?>)column);
2614 final boolean isCellSelectionEnabled = isCellSelectionEnabled();
2615
2616 // replace the anchor
2617 TreeTableCellBehavior.setAnchor(treeTableView, newTablePosition, false);
2618
2619 // if I'm in cell selection mode but the column is null, I don't want
2620 // to select the whole row instead...
2621 if (isCellSelectionEnabled && column == null) {
2622 return;
2623 }
2624
2625 final boolean wasSelected = isSelected(row, column);
2626
2627 // firstly we make a copy of the selection, so that we can send out
2628 // the correct details in the selection change event.
2629 List<TreeTablePosition<S,?>> previousSelection = new ArrayList<>(selectedCellsMap.getSelectedCells());
2630
2631 if (wasSelected && previousSelection.size() == 1) {
2632 // before we return, we double-check that the selected item
2633 // is equal to the item in the given index
2634 TreeTablePosition<S,?> selectedCell = getSelectedCells().get(0);
2635 if (getSelectedItem() == getModelItem(row)) {
2636 if (selectedCell.getRow() == row && selectedCell.getTableColumn() == column) {
2637 return;
2638 }
2639 }
2640 }
2641
2642 // RT-32411: We used to call quietClearSelection() here, but this
2643 // resulted in the selectedItems and selectedIndices lists never
2644 // reporting that they were empty.
2645 // makeAtomic toggle added to resolve RT-32618
2646 startAtomic();
2647
2648 // then clear the current selection
2649 clearSelection();
2650
2651 // and select the new cell
2652 select(row, column);
2653
2654 stopAtomic();
2655
2656 // We remove the new selection from the list seeing as it is not removed.
2657 if (isCellSelectionEnabled) {
2658 previousSelection.remove(newTablePosition);
2659 } else {
2660 for (TreeTablePosition<S,?> tp : previousSelection) {
2661 if (tp.getRow() == row) {
2662 previousSelection.remove(tp);
2663 break;
2664 }
2665 }
2666 }
2667
2668 // fire off a single add/remove/replace notification (rather than
2669 // individual remove and add notifications) - see RT-33324
2670 ListChangeListener.Change<TreeTablePosition<S, ?>> change;
2671
2672 /*
2673 * getFrom() documentation:
2674 * If wasAdded is true, the interval contains all the values that were added.
2675 * If wasPermutated is true, the interval marks the values that were permutated.
2676 * If wasRemoved is true and wasAdded is false, getFrom() and getTo() should
2677 * return the same number - the place where the removed elements were positioned in the list.
2678 */
2679 if (wasSelected) {
2680 change = ControlUtils.buildClearAndSelectChange(selectedCellsSeq, previousSelection, row);
2681 } else {
2682 final int changeIndex = selectedCellsSeq.indexOf(newTablePosition);
2683 change = new NonIterableChange.GenericAddRemoveChange<>(
2684 changeIndex, changeIndex + 1, previousSelection, selectedCellsSeq);
2685 }
2686
2687 handleSelectedCellsListChangeEvent(change);
2688 }
2689
2690 @Override public void select(int row) {
2691 select(row, null);
2692 }
2693
2694 @Override public void select(int row, TableColumnBase<TreeItem<S>,?> column) {
2695 // TODO we need to bring in the TreeView selection stuff here...
2696 if (row < 0 || row >= getRowCount()) return;
2697
2698 // if I'm in cell selection mode but the column is null, select each
2699 // of the contained cells individually
2700 if (isCellSelectionEnabled() && column == null) {
2701 List<TreeTableColumn<S,?>> columns = getTreeTableView().getVisibleLeafColumns();
2702 for (int i = 0; i < columns.size(); i++) {
2703 select(row, columns.get(i));
2704 }
2705 return;
2706 }
2707
2708 TreeTablePosition<S,?> pos = new TreeTablePosition<>(getTreeTableView(), row, (TreeTableColumn<S,?>)column);
2709
2710 if (getSelectionMode() == SelectionMode.SINGLE) {
2711 startAtomic();
2712 quietClearSelection();
2713 stopAtomic();
2714 }
2715
2716 if (TreeTableCellBehavior.hasDefaultAnchor(treeTableView)) {
2717 TreeTableCellBehavior.removeAnchor(treeTableView);
2718 }
2719
2720 selectedCellsMap.add(pos);
2721
2722 updateSelectedIndex(row);
2723 focus(row, (TreeTableColumn<S, ?>) column);
2724 }
2725
2726 @Override public void select(TreeItem<S> obj) {
2727 if (obj == null && getSelectionMode() == SelectionMode.SINGLE) {
2728 clearSelection();
2729 return;
2730 }
2731
2732 int firstIndex = treeTableView.getRow(obj);
2733 if (firstIndex > -1) {
2734 if (isSelected(firstIndex)) {
2735 return;
2736 }
2737
2738 if (getSelectionMode() == SelectionMode.SINGLE) {
2739 quietClearSelection();
2740 }
2741
2742 select(firstIndex);
2743 } else {
2744 // if we are here, we did not find the item in the entire data model.
2745 // Even still, we allow for this item to be set to the give object.
2746 // We expect that in concrete subclasses of this class we observe the
2747 // data model such that we check to see if the given item exists in it,
2748 // whilst SelectedIndex == -1 && SelectedItem != null.
2749 setSelectedIndex(-1);
2750 setSelectedItem(obj);
2751 }
2752 }
2753
2754 @Override public void selectIndices(int row, int... rows) {
2755 if (rows == null) {
2756 select(row);
2757 return;
2758 }
2759
2760 /*
2761 * Performance optimisation - if multiple selection is disabled, only
2762 * process the end-most row index.
2763 */
2764 int rowCount = getRowCount();
2765
2766 if (getSelectionMode() == SelectionMode.SINGLE) {
2767 quietClearSelection();
2768
2769 for (int i = rows.length - 1; i >= 0; i--) {
2770 int index = rows[i];
2771 if (index >= 0 && index < rowCount) {
2772 select(index);
2773 break;
2774 }
2775 }
2776
2777 if (selectedCellsMap.isEmpty()) {
2778 if (row > 0 && row < rowCount) {
2779 select(row);
2780 }
2781 }
2782 } else {
2783 int lastIndex = -1;
2784 Set<TreeTablePosition<S,?>> positions = new LinkedHashSet<>();
2785
2786 // --- firstly, we special-case the non-varargs 'row' argument
2787 if (row >= 0 && row < rowCount) {
2788 // if I'm in cell selection mode, we want to select each
2789 // of the contained cells individually
2790 if (isCellSelectionEnabled()) {
2791 List<TreeTableColumn<S,?>> columns = getTreeTableView().getVisibleLeafColumns();
2792 for (int column = 0; column < columns.size(); column++) {
2793 if (! selectedCellsMap.isSelected(row, column)) {
2794 positions.add(new TreeTablePosition<>(getTreeTableView(), row, columns.get(column)));
2795 }
2796 }
2797 } else {
2798 boolean match = selectedCellsMap.isSelected(row, -1);
2799 if (!match) {
2800 positions.add(new TreeTablePosition<>(getTreeTableView(), row, null));
2801 }
2802 }
2803
2804 lastIndex = row;
2805 }
2806
2807 // --- now we iterate through all varargs values
2808 for (int i = 0; i < rows.length; i++) {
2809 int index = rows[i];
2810 if (index < 0 || index >= rowCount) continue;
2811 lastIndex = index;
2812
2813 if (isCellSelectionEnabled()) {
2814 List<TreeTableColumn<S,?>> columns = getTreeTableView().getVisibleLeafColumns();
2815 for (int column = 0; column < columns.size(); column++) {
2816 if (! selectedCellsMap.isSelected(index, column)) {
2817 positions.add(new TreeTablePosition<>(getTreeTableView(), index, columns.get(column)));
2818 lastIndex = index;
2819 }
2820 }
2821 } else {
2822 if (! selectedCellsMap.isSelected(index, -1)) {
2823 // if we are here then we have successfully gotten through the for-loop above
2824 positions.add(new TreeTablePosition<>(getTreeTableView(), index, null));
2825 }
2826 }
2827 }
2828
2829 selectedCellsMap.addAll(positions);
2830
2831 if (lastIndex != -1) {
2832 select(lastIndex);
2833 }
2834 }
2835 }
2836
2837 @Override public void selectAll() {
2838 if (getSelectionMode() == SelectionMode.SINGLE) return;
2839
2840 if (isCellSelectionEnabled()) {
2841 List<TreeTablePosition<S,?>> indices = new ArrayList<>();
2842 TreeTableColumn<S,?> column;
2843 TreeTablePosition<S,?> tp = null;
2844 for (int col = 0; col < getTreeTableView().getVisibleLeafColumns().size(); col++) {
2845 column = getTreeTableView().getVisibleLeafColumns().get(col);
2846 for (int row = 0; row < getRowCount(); row++) {
2847 tp = new TreeTablePosition<>(getTreeTableView(), row, column);
2848 indices.add(tp);
2849 }
2850 }
2851 selectedCellsMap.setAll(indices);
2852
2853 if (tp != null) {
2854 select(tp.getRow(), tp.getTableColumn());
2855 focus(tp.getRow(), tp.getTableColumn());
2856 }
2857 } else {
2858 List<TreeTablePosition<S,?>> indices = new ArrayList<>();
2859 for (int i = 0; i < getRowCount(); i++) {
2860 indices.add(new TreeTablePosition<>(getTreeTableView(), i, null));
2861 }
2862 selectedCellsMap.setAll(indices);
2863
2864 int focusedIndex = getFocusedIndex();
2865 if (focusedIndex == -1) {
2866 final int itemCount = getItemCount();
2867 if (itemCount > 0) {
2868 select(itemCount - 1);
2869 focus(indices.get(indices.size() - 1));
2870 }
2871 } else {
2872 select(focusedIndex);
2873 focus(focusedIndex);
2874 }
2875 }
2876 }
2877
2878 @Override public void selectRange(int minRow, TableColumnBase<TreeItem<S>,?> minColumn,
2879 int maxRow, TableColumnBase<TreeItem<S>,?> maxColumn) {
2880 if (getSelectionMode() == SelectionMode.SINGLE) {
2881 quietClearSelection();
2882 select(maxRow, maxColumn);
2883 return;
2884 }
2885
2886 startAtomic();
2887
2888 final int itemCount = getItemCount();
2889 final boolean isCellSelectionEnabled = isCellSelectionEnabled();
2890
2891 final int minColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)minColumn);
2892 final int maxColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>)maxColumn);
2893 final int _minColumnIndex = Math.min(minColumnIndex, maxColumnIndex);
2894 final int _maxColumnIndex = Math.max(minColumnIndex, maxColumnIndex);
2895
2896 final int _minRow = Math.min(minRow, maxRow);
2897 final int _maxRow = Math.max(minRow, maxRow);
2898
2899 List<TreeTablePosition<S,?>> cellsToSelect = new ArrayList<>();
2900
2901 for (int _row = _minRow; _row <= _maxRow; _row++) {
2902 // begin copy/paste of select(int, column) method (with some
2903 // slight modifications)
2904 if (_row < 0 || _row >= itemCount) continue;
2905
2906 if (! isCellSelectionEnabled) {
2907 cellsToSelect.add(new TreeTablePosition<>(treeTableView, _row, (TreeTableColumn<S,?>)minColumn));
2908 } else {
2909 for (int _col = _minColumnIndex; _col <= _maxColumnIndex; _col++) {
2910 final TreeTableColumn<S, ?> column = treeTableView.getVisibleLeafColumn(_col);
2911
2912 // if I'm in cell selection mode but the column is null, I don't want
2913 // to select the whole row instead...
2914 if (column == null && isCellSelectionEnabled) continue;
2915
2916 cellsToSelect.add(new TreeTablePosition<>(treeTableView, _row, column));
2917 // end copy/paste
2918 }
2919 }
2920 }
2921
2922 selectedCellsMap.addAll(cellsToSelect);
2923 stopAtomic();
2924
2925 // fire off events
2926 // Note that focus and selection always goes to maxRow, not _maxRow.
2927 updateSelectedIndex(maxRow);
2928 focus(maxRow, (TreeTableColumn<S,?>)maxColumn);
2929
2930 final int startChangeIndex = selectedCellsMap.indexOf(new TreeTablePosition<>(treeTableView, minRow, (TreeTableColumn<S,?>)minColumn));
2931 final int endChangeIndex = selectedCellsMap.indexOf(new TreeTablePosition<>(treeTableView, maxRow, (TreeTableColumn<S,?>)maxColumn));
2932
2933 if (startChangeIndex > -1 && endChangeIndex > -1) {
2934 final int startIndex = Math.min(startChangeIndex, endChangeIndex);
2935 final int endIndex = Math.max(startChangeIndex, endChangeIndex);
2936 ListChangeListener.Change c = new NonIterableChange.SimpleAddChange<>(startIndex, endIndex + 1, selectedCellsSeq);
2937 handleSelectedCellsListChangeEvent(c);
2938 }
2939 }
2940
2941 @Override public void clearSelection(int index) {
2942 clearSelection(index, null);
2943 }
2944
2945 @Override
2946 public void clearSelection(int row, TableColumnBase<TreeItem<S>,?> column) {
2947 clearSelection(new TreeTablePosition<S,Object>(getTreeTableView(), row, (TreeTableColumn)column));
2948 }
2949
2950 private void clearSelection(TreeTablePosition<S,?> tp) {
2951 final boolean csMode = isCellSelectionEnabled();
2952 final int row = tp.getRow();
2953
2954 for (TreeTablePosition<S,?> pos : getSelectedCells()) {
2955 if (! csMode) {
2956 if (pos.getRow() == row) {
2957 selectedCellsMap.remove(pos);
2958 break;
2959 }
2960 } else {
2961 if (pos.equals(tp)) {
2962 selectedCellsMap.remove(tp);
2963 break;
2964 }
2965 }
2966 }
2967
2968 if (isEmpty() && ! isAtomic()) {
2969 updateSelectedIndex(-1);
2970 selectedCellsMap.clear();
2971 }
2972 }
2973
2974 @Override public void clearSelection() {
2975 final List<TreeTablePosition<S,?>> removed = new ArrayList<>((Collection)getSelectedCells());
2976
2977 quietClearSelection();
2978
2979 if (! isAtomic()) {
2980 updateSelectedIndex(-1);
2981 focus(-1);
2982
2983 ListChangeListener.Change<TreeTablePosition<S, ?>> c = new NonIterableChange<TreeTablePosition<S, ?>>(0, 0, selectedCellsSeq) {
2984 @Override
2985 public List<TreeTablePosition<S, ?>> getRemoved() {
2986 return removed;
2987 }
2988 };
2989 handleSelectedCellsListChangeEvent(c);
2990 }
2991 }
2992
2993 private void quietClearSelection() {
2994 startAtomic();
2995 selectedCellsMap.clear();
2996 stopAtomic();
2997 }
2998
2999 @Override public boolean isSelected(int index) {
3000 return isSelected(index, null);
3001 }
3002
3003 @Override public boolean isSelected(int row, TableColumnBase<TreeItem<S>,?> column) {
3004 // When in cell selection mode, we currently do NOT support selecting
3005 // entire rows, so isSelected(row, null) should always return false.
3006 final boolean isCellSelectionEnabled = isCellSelectionEnabled();
3007 if (isCellSelectionEnabled && column == null) return false;
3008
3009 int columnIndex = ! isCellSelectionEnabled || column == null ? -1 : treeTableView.getVisibleLeafIndex((TreeTableColumn<S,?>) column);
3010 return selectedCellsMap.isSelected(row, columnIndex);
3011 }
3012
3013 @Override public boolean isEmpty() {
3014 return selectedCellsMap.isEmpty();
3015 }
3016
3017 @Override public void selectPrevious() {
3018 if (isCellSelectionEnabled()) {
3019 // in cell selection mode, we have to wrap around, going from
3020 // right-to-left, and then wrapping to the end of the previous line
3021 TreeTablePosition<S,?> pos = getFocusedCell();
3022 if (pos.getColumn() - 1 >= 0) {
3023 // go to previous row
3024 select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
3025 } else if (pos.getRow() < getRowCount() - 1) {
3026 // wrap to end of previous row
3027 select(pos.getRow() - 1, getTableColumn(getTreeTableView().getVisibleLeafColumns().size() - 1));
3028 }
3029 } else {
3030 int focusIndex = getFocusedIndex();
3031 if (focusIndex == -1) {
3032 select(getRowCount() - 1);
3033 } else if (focusIndex > 0) {
3034 select(focusIndex - 1);
3035 }
3036 }
3037 }
3038
3039 @Override public void selectNext() {
3040 if (isCellSelectionEnabled()) {
3041 // in cell selection mode, we have to wrap around, going from
3042 // left-to-right, and then wrapping to the start of the next line
3043 TreeTablePosition<S,?> pos = getFocusedCell();
3044 if (pos.getColumn() + 1 < getTreeTableView().getVisibleLeafColumns().size()) {
3045 // go to next column
3046 select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
3047 } else if (pos.getRow() < getRowCount() - 1) {
3048 // wrap to start of next row
3049 select(pos.getRow() + 1, getTableColumn(0));
3050 }
3051 } else {
3052 int focusIndex = getFocusedIndex();
3053 if (focusIndex == -1) {
3054 select(0);
3055 } else if (focusIndex < getRowCount() -1) {
3056 select(focusIndex + 1);
3057 }
3058 }
3059 }
3060
3061 @Override public void selectAboveCell() {
3062 TreeTablePosition<S,?> pos = getFocusedCell();
3063 if (pos.getRow() == -1) {
3064 select(getRowCount() - 1);
3065 } else if (pos.getRow() > 0) {
3066 select(pos.getRow() - 1, pos.getTableColumn());
3067 }
3068 }
3069
3070 @Override public void selectBelowCell() {
3071 TreeTablePosition<S,?> pos = getFocusedCell();
3072
3073 if (pos.getRow() == -1) {
3074 select(0);
3075 } else if (pos.getRow() < getRowCount() -1) {
3076 select(pos.getRow() + 1, pos.getTableColumn());
3077 }
3078 }
3079
3080 @Override public void selectFirst() {
3081 TreeTablePosition<S,?> focusedCell = getFocusedCell();
3082
3083 if (getSelectionMode() == SelectionMode.SINGLE) {
3084 quietClearSelection();
3085 }
3086
3087 if (getRowCount() > 0) {
3088 if (isCellSelectionEnabled()) {
3089 select(0, focusedCell.getTableColumn());
3090 } else {
3091 select(0);
3092 }
3093 }
3094 }
3095
3096 @Override public void selectLast() {
3097 TreeTablePosition<S,?> focusedCell = getFocusedCell();
3098
3099 if (getSelectionMode() == SelectionMode.SINGLE) {
3100 quietClearSelection();
3101 }
3102
3103 int numItems = getRowCount();
3104 if (numItems > 0 && getSelectedIndex() < numItems - 1) {
3105 if (isCellSelectionEnabled()) {
3106 select(numItems - 1, focusedCell.getTableColumn());
3107 } else {
3108 select(numItems - 1);
3109 }
3110 }
3111 }
3112
3113 @Override public void selectLeftCell() {
3114 if (! isCellSelectionEnabled()) return;
3115
3116 TreeTablePosition<S,?> pos = getFocusedCell();
3117 if (pos.getColumn() - 1 >= 0) {
3118 select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
3119 }
3120 }
3121
3122 @Override public void selectRightCell() {
3123 if (! isCellSelectionEnabled()) return;
3124
3125 TreeTablePosition<S,?> pos = getFocusedCell();
3126 if (pos.getColumn() + 1 < getTreeTableView().getVisibleLeafColumns().size()) {
3127 select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
3128 }
3129 }
3130
3131
3132
3133 /***********************************************************************
3134 * *
3135 * Support code *
3136 * *
3137 **********************************************************************/
3138
3139 private void updateDefaultSelection() {
3140 clearSelection();
3141
3142 // we put focus onto the first item, if there is at least
3143 // one item in the list
3144 int newFocusIndex = getItemCount() > 0 ? 0 : -1;
3145 focus(newFocusIndex, isCellSelectionEnabled() ? getTableColumn(0) : null);
3146 }
3147
3148 private TreeTableColumn<S,?> getTableColumn(int pos) {
3149 return getTreeTableView().getVisibleLeafColumn(pos);
3150 }
3151
3152 // Gets a table column to the left or right of the current one, given an offset
3153 private TreeTableColumn<S,?> getTableColumn(TreeTableColumn<S,?> column, int offset) {
3154 int columnIndex = getTreeTableView().getVisibleLeafIndex(column);
3155 int newColumnIndex = columnIndex + offset;
3156 return getTreeTableView().getVisibleLeafColumn(newColumnIndex);
3157 }
3158
3159 private void updateSelectedIndex(int row) {
3160 setSelectedIndex(row);
3161 setSelectedItem(getModelItem(row));
3162 }
3163
3164 @Override public void focus(int row) {
3165 focus(row, null);
3166 }
3167
3168 private void focus(int row, TreeTableColumn<S,?> column) {
3169 focus(new TreeTablePosition<>(getTreeTableView(), row, column));
3170 }
3171
3172 private void focus(TreeTablePosition<S,?> pos) {
3173 if (getTreeTableView().getFocusModel() == null) return;
3174
3175 getTreeTableView().getFocusModel().focus(pos.getRow(), pos.getTableColumn());
3176 getTreeTableView().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
3177 }
3178
3179 @Override public int getFocusedIndex() {
3180 return getFocusedCell().getRow();
3181 }
3182
3183 private TreeTablePosition<S,?> getFocusedCell() {
3184 if (treeTableView.getFocusModel() == null) {
3185 return new TreeTablePosition<>(treeTableView, -1, null);
3186 }
3187 return treeTableView.getFocusModel().getFocusedCell();
3188 }
3189
3190 private int getRowCount() {
3191 return treeTableView.getExpandedItemCount();
3192 }
3193
3194 private void handleSelectedCellsListChangeEvent(ListChangeListener.Change<? extends TreeTablePosition<S,?>> c) {
3195 // RT-29313: because selectedIndices and selectedItems represent
3196 // row-based selection, we need to update the
3197 // selectedIndicesBitSet when the selectedCells changes to
3198 // ensure that selectedIndices and selectedItems return only
3199 // the correct values (and only once). The issue identified
3200 // by RT-29313 is that the size and contents of selectedIndices
3201 // and selectedItems can not simply defer to the
3202 // selectedCells as selectedCells may be representing
3203 // multiple cells from one row (e.g. selectedCells of
3204 // [(0,1), (1,1), (1,2), (1,3)] should result in
3205 // selectedIndices of [0,1], not [0,1,1,1]).
3206 // An inefficient solution would rebuild the selectedIndicesBitSet
3207 // every time the change happens, but we can do better than
3208 // that. Inefficient solution:
3209 //
3210 // selectedIndicesBitSet.clear();
3211 // for (int i = 0; i < selectedCells.size(); i++) {
3212 // final TreeTablePosition<S,?> tp = selectedCells.get(i);
3213 // final int row = tp.getRow();
3214 // selectedIndicesBitSet.set(row);
3215 // }
3216 //
3217 // A more efficient solution:
3218 final List<Integer> newlySelectedRows = new ArrayList<>();
3219 final List<Integer> newlyUnselectedRows = new ArrayList<>();
3220
3221 while (c.next()) {
3222 if (c.wasRemoved()) {
3223 List<? extends TreeTablePosition<S,?>> removed = c.getRemoved();
3224 for (int i = 0; i < removed.size(); i++) {
3225 final TreeTablePosition<S,?> tp = removed.get(i);
3226 final int row = tp.getRow();
3227
3228 if (selectedIndices.get(row)) {
3229 selectedIndices.clear(row);
3230 newlyUnselectedRows.add(row);
3231 }
3232 }
3233 }
3234 if (c.wasAdded()) {
3235 List<? extends TreeTablePosition<S,?>> added = c.getAddedSubList();
3236 for (int i = 0; i < added.size(); i++) {
3237 final TreeTablePosition<S,?> tp = added.get(i);
3238 final int row = tp.getRow();
3239
3240 if (! selectedIndices.get(row)) {
3241 selectedIndices.set(row);
3242 newlySelectedRows.add(row);
3243 }
3244 }
3245 }
3246 }
3247 c.reset();
3248
3249 if (isAtomic()) {
3250 return;
3251 }
3252
3253 // when the selectedCells observableArrayList changes, we manually call
3254 // the observers of the selectedItems, selectedIndices and
3255 // selectedCells lists.
3256
3257 // here we are considering whether to notify the observers of the
3258 // selectedItems list. However, we can't just blindly do that, as
3259 // noted below. This is a part of the fix for RT-37429.
3260 c.next();
3261 boolean fireChangeEvent;
3262 outer: if (c.wasReplaced()) {
3263 // if a replace happened, we need to check to see if the
3264 // change actually impacts on the selected items - it may
3265 // be that the index changed to the new location of the same
3266 // item (i.e. if a sort occurred). Only if the item has changed
3267 // should we fire an event to the observers of the selectedItems
3268 // list
3269 final int removedSize = c.getRemovedSize();
3270 final int addedSize = c.getAddedSize();
3271 if (removedSize != addedSize) {
3272 fireChangeEvent = true;
3273 } else {
3274 for (int i = 0; i < c.getRemovedSize(); i++) {
3275 TreeTablePosition<S, ?> removed = c.getRemoved().get(i);
3276 TreeItem<S> removedTreeItem = removed.getTreeItem();
3277
3278 boolean matchFound = false;
3279 for (int j = 0; j < c.getAddedSize(); j++) {
3280 TreeTablePosition<S, ?> added = c.getAddedSubList().get(j);
3281 TreeItem<S> addedTreeItem = added.getTreeItem();
3282
3283 if (removedTreeItem != null && removedTreeItem.equals(addedTreeItem)) {
3284 matchFound = true;
3285 break;
3286 }
3287 }
3288
3289 if (!matchFound) {
3290 fireChangeEvent = true;
3291 break outer;
3292 }
3293 }
3294 fireChangeEvent = false;
3295 }
3296 } else {
3297 fireChangeEvent = true;
3298 }
3299
3300 if (fireChangeEvent) {
3301 // create an on-demand list of the removed objects contained in the
3302 // given rows.
3303 selectedItems.callObservers(new MappingChange<>(c, cellToItemsMap, selectedItems));
3304 }
3305 c.reset();
3306
3307 // Fix for RT-31577 - the selectedItems list was going to
3308 // empty, but the selectedItem property was staying non-null.
3309 // There is a unit test for this, so if a more elegant solution
3310 // can be found in the future and this code removed, the unit
3311 // test will fail if it isn't fixed elsewhere.
3312 // makeAtomic toggle added to resolve RT-32618
3313 if (selectedItems.isEmpty() && getSelectedItem() != null) {
3314 setSelectedItem(null);
3315 }
3316
3317 final ReadOnlyUnbackedObservableList<Integer> selectedIndicesSeq =
3318 (ReadOnlyUnbackedObservableList<Integer>)getSelectedIndices();
3319
3320 if (! newlySelectedRows.isEmpty() && newlyUnselectedRows.isEmpty()) {
3321 // need to come up with ranges based on the actualSelectedRows, and
3322 // then fire the appropriate number of changes. We also need to
3323 // translate from a desired row to select to where that row is
3324 // represented in the selectedIndices list. For example,
3325 // we may have requested to select row 5, and the selectedIndices
3326 // list may therefore have the following: [1,4,5], meaning row 5
3327 // is in position 2 of the selectedIndices list
3328 ListChangeListener.Change<Integer> change = createRangeChange(selectedIndicesSeq, newlySelectedRows);
3329 selectedIndicesSeq.callObservers(change);
3330 } else {
3331 selectedIndicesSeq.callObservers(new MappingChange<>(c, cellToIndicesMap, selectedIndicesSeq));
3332 c.reset();
3333 }
3334
3335 selectedCellsSeq.callObservers(new MappingChange<>(c, MappingChange.NOOP_MAP, selectedCellsSeq));
3336 c.reset();
3337 }
3338 }
3339
3340
3341
3342
3343 /**
3344 * A {@link FocusModel} with additional functionality to support the requirements
3345 * of a TableView control.
3346 *
3347 * @see TableView
3348 * @since JavaFX 8.0
3349 */
3350 public static class TreeTableViewFocusModel<S> extends TableFocusModel<TreeItem<S>, TreeTableColumn<S,?>> {
3351
3352 private final TreeTableView<S> treeTableView;
3353
3354 private final TreeTablePosition EMPTY_CELL;
3355
3356 /**
3357 * Creates a default TableViewFocusModel instance that will be used to
3358 * manage focus of the provided TableView control.
3359 *
3360 * @param treeTableView The tableView upon which this focus model operates.
3361 * @throws NullPointerException The TableView argument can not be null.
3362 */
3363 public TreeTableViewFocusModel(final TreeTableView<S> treeTableView) {
3364 if (treeTableView == null) {
3365 throw new NullPointerException("TableView can not be null");
3366 }
3367
3368 this.treeTableView = treeTableView;
3369
3370 this.treeTableView.rootProperty().addListener(weakRootPropertyListener);
3371 updateTreeEventListener(null, treeTableView.getRoot());
3372
3373 int focusRow = getItemCount() > 0 ? 0 : -1;
3374 TreeTablePosition<S,?> pos = new TreeTablePosition<>(treeTableView, focusRow, null);
3375 setFocusedCell(pos);
3376
3377 EMPTY_CELL = new TreeTablePosition<>(treeTableView, -1, null);
3378
3379 treeTableView.showRootProperty().addListener(o -> {
3380 if (isFocused(0)) {
3381 focus(-1);
3382 focus(0);
3383 }
3384 });
3385 }
3386
3387 private final ChangeListener<TreeItem<S>> rootPropertyListener = (observable, oldValue, newValue) -> {
3388 updateTreeEventListener(oldValue, newValue);
3389 };
3390
3391 private final WeakChangeListener<TreeItem<S>> weakRootPropertyListener =
3392 new WeakChangeListener<>(rootPropertyListener);
3393
3394 private void updateTreeEventListener(TreeItem<S> oldRoot, TreeItem<S> newRoot) {
3395 if (oldRoot != null && weakTreeItemListener != null) {
3396 oldRoot.removeEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
3397 }
3398
3399 if (newRoot != null) {
3400 weakTreeItemListener = new WeakEventHandler<>(treeItemListener);
3401 newRoot.addEventHandler(TreeItem.<S>expandedItemCountChangeEvent(), weakTreeItemListener);
3402 }
3403 }
3404
3405 private EventHandler<TreeItem.TreeModificationEvent<S>> treeItemListener = new EventHandler<TreeItem.TreeModificationEvent<S>>() {
3406 @Override public void handle(TreeItem.TreeModificationEvent<S> e) {
3407 // don't shift focus if the event occurred on a tree item after
3408 // the focused row, or if there is no focus index at present
3409 if (getFocusedIndex() == -1) return;
3410
3411 int shift = 0;
3412 if (e.getChange() != null) {
3413 e.getChange().next();
3414 }
3415
3416 do {
3417 int row = treeTableView.getRow(e.getTreeItem());
3418
3419 if (e.wasExpanded()) {
3420 if (row < getFocusedIndex()) {
3421 // need to shuffle selection by the number of visible children
3422 shift += e.getTreeItem().getExpandedDescendentCount(false) - 1;
3423 }
3424 } else if (e.wasCollapsed()) {
3425 if (row < getFocusedIndex()) {
3426 // need to shuffle selection by the number of visible children
3427 // that were just hidden
3428 shift += -e.getTreeItem().previousExpandedDescendentCount + 1;
3429 }
3430 } else if (e.wasAdded()) {
3431 // get the TreeItem the event occurred on - we only need to
3432 // shift if the tree item is expanded
3433 TreeItem<S> eventTreeItem = e.getTreeItem();
3434 if (eventTreeItem.isExpanded()) {
3435 for (int i = 0; i < e.getAddedChildren().size(); i++) {
3436 // get the added item and determine the row it is in
3437 TreeItem<S> item = e.getAddedChildren().get(i);
3438 row = treeTableView.getRow(item);
3439
3440 if (item != null && row <= getFocusedIndex()) {
3441 shift += item.getExpandedDescendentCount(false);
3442 }
3443 }
3444 }
3445 } else if (e.wasRemoved()) {
3446 row += e.getFrom() + 1;
3447
3448 for (int i = 0; i < e.getRemovedChildren().size(); i++) {
3449 TreeItem<S> item = e.getRemovedChildren().get(i);
3450 if (item != null && item.equals(getFocusedItem())) {
3451 focus(Math.max(0, getFocusedIndex() - 1));
3452 return;
3453 }
3454 }
3455
3456 if (row <= getFocusedIndex()) {
3457 // shuffle selection by the number of removed items
3458 shift += e.getTreeItem().isExpanded() ? -e.getRemovedSize() : 0;
3459 }
3460 }
3461 } while (e.getChange() != null && e.getChange().next());
3462
3463 if (shift != 0) {
3464 TreeTablePosition<S, ?> focusedCell = getFocusedCell();
3465 final int newFocus = focusedCell.getRow() + shift;
3466 if (newFocus >= 0) {
3467 Platform.runLater(() -> focus(newFocus, focusedCell.getTableColumn()));
3468 }
3469 }
3470 }
3471 };
3472
3473 private WeakEventHandler<TreeItem.TreeModificationEvent<S>> weakTreeItemListener;
3474
3475 /** {@inheritDoc} */
3476 @Override protected int getItemCount() {
3477 // if (tableView.getItems() == null) return -1;
3478 // return tableView.getItems().size();
3479 return treeTableView.getExpandedItemCount();
3480 }
3481
3482 /** {@inheritDoc} */
3483 @Override protected TreeItem<S> getModelItem(int index) {
3484 if (index < 0 || index >= getItemCount()) return null;
3485 return treeTableView.getTreeItem(index);
3486 }
3487
3488 /**
3489 * The position of the current item in the TableView which has the focus.
3490 */
3491 private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> focusedCell;
3492 public final ReadOnlyObjectProperty<TreeTablePosition<S,?>> focusedCellProperty() {
3493 return focusedCellPropertyImpl().getReadOnlyProperty();
3494 }
3495 private void setFocusedCell(TreeTablePosition<S,?> value) { focusedCellPropertyImpl().set(value); }
3496 public final TreeTablePosition<S,?> getFocusedCell() { return focusedCell == null ? EMPTY_CELL : focusedCell.get(); }
3497
3498 private ReadOnlyObjectWrapper<TreeTablePosition<S,?>> focusedCellPropertyImpl() {
3499 if (focusedCell == null) {
3500 focusedCell = new ReadOnlyObjectWrapper<TreeTablePosition<S,?>>(EMPTY_CELL) {
3501 private TreeTablePosition<S,?> old;
3502 @Override protected void invalidated() {
3503 if (get() == null) return;
3504
3505 if (old == null || !old.equals(get())) {
3506 setFocusedIndex(get().getRow());
3507 setFocusedItem(getModelItem(getValue().getRow()));
3508
3509 old = get();
3510 }
3511 }
3512
3513 @Override
3514 public Object getBean() {
3515 return TreeTableView.TreeTableViewFocusModel.this;
3516 }
3517
3518 @Override
3519 public String getName() {
3520 return "focusedCell";
3521 }
3522 };
3523 }
3524 return focusedCell;
3525 }
3526
3527
3528 /**
3529 * Causes the item at the given index to receive the focus.
3530 *
3531 * @param row The row index of the item to give focus to.
3532 * @param column The column of the item to give focus to. Can be null.
3533 */
3534 @Override public void focus(int row, TreeTableColumn<S,?> column) {
3535 if (row < 0 || row >= getItemCount()) {
3536 setFocusedCell(EMPTY_CELL);
3537 } else {
3538 TreeTablePosition<S,?> oldFocusCell = getFocusedCell();
3539 TreeTablePosition<S,?> newFocusCell = new TreeTablePosition<>(treeTableView, row, column);
3540 setFocusedCell(newFocusCell);
3541
3542 if (newFocusCell.equals(oldFocusCell)) {
3543 // manually update the focus properties to ensure consistency
3544 setFocusedIndex(row);
3545 setFocusedItem(getModelItem(row));
3546 }
3547 }
3548 }
3549
3550 /**
3551 * Convenience method for setting focus on a particular row or cell
3552 * using a {@link TablePosition}.
3553 *
3554 * @param pos The table position where focus should be set.
3555 */
3556 public void focus(TreeTablePosition<S,?> pos) {
3557 if (pos == null) return;
3558 focus(pos.getRow(), pos.getTableColumn());
3559 }
3560
3561
3562 /***********************************************************************
3563 * *
3564 * Public API *
3565 * *
3566 **********************************************************************/
3567
3568 /**
3569 * Tests whether the row / cell at the given location currently has the
3570 * focus within the TableView.
3571 */
3572 @Override public boolean isFocused(int row, TreeTableColumn<S,?> column) {
3573 if (row < 0 || row >= getItemCount()) return false;
3574
3575 TreeTablePosition<S,?> cell = getFocusedCell();
3576 boolean columnMatch = column == null || column.equals(cell.getTableColumn());
3577
3578 return cell.getRow() == row && columnMatch;
3579 }
3580
3581 /**
3582 * Causes the item at the given index to receive the focus. This does not
3583 * cause the current selection to change. Updates the focusedItem and
3584 * focusedIndex properties such that <code>focusedIndex = -1</code> unless
3585 * <pre><code>0 <= index < model size</code></pre>.
3586 *
3587 * @param index The index of the item to get focus.
3588 */
3589 @Override public void focus(int index) {
3590 if (treeTableView.expandedItemCountDirty) {
3591 treeTableView.updateExpandedItemCount(treeTableView.getRoot());
3592 }
3593
3594 if (index < 0 || index >= getItemCount()) {
3595 setFocusedCell(EMPTY_CELL);
3596 } else {
3597 setFocusedCell(new TreeTablePosition<>(treeTableView, index, null));
3598 }
3599 }
3600
3601 /**
3602 * Attempts to move focus to the cell above the currently focused cell.
3603 */
3604 @Override public void focusAboveCell() {
3605 TreeTablePosition<S,?> cell = getFocusedCell();
3606
3607 if (getFocusedIndex() == -1) {
3608 focus(getItemCount() - 1, cell.getTableColumn());
3609 } else if (getFocusedIndex() > 0) {
3610 focus(getFocusedIndex() - 1, cell.getTableColumn());
3611 }
3612 }
3613
3614 /**
3615 * Attempts to move focus to the cell below the currently focused cell.
3616 */
3617 @Override public void focusBelowCell() {
3618 TreeTablePosition<S,?> cell = getFocusedCell();
3619 if (getFocusedIndex() == -1) {
3620 focus(0, cell.getTableColumn());
3621 } else if (getFocusedIndex() != getItemCount() -1) {
3622 focus(getFocusedIndex() + 1, cell.getTableColumn());
3623 }
3624 }
3625
3626 /**
3627 * Attempts to move focus to the cell to the left of the currently focused cell.
3628 */
3629 @Override public void focusLeftCell() {
3630 TreeTablePosition<S,?> cell = getFocusedCell();
3631 if (cell.getColumn() <= 0) return;
3632 focus(cell.getRow(), getTableColumn(cell.getTableColumn(), -1));
3633 }
3634
3635 /**
3636 * Attempts to move focus to the cell to the right of the the currently focused cell.
3637 */
3638 @Override public void focusRightCell() {
3639 TreeTablePosition<S,?> cell = getFocusedCell();
3640 if (cell.getColumn() == getColumnCount() - 1) return;
3641 focus(cell.getRow(), getTableColumn(cell.getTableColumn(), 1));
3642 }
3643
3644 /** {@inheritDoc} */
3645 @Override public void focusPrevious() {
3646 if (getFocusedIndex() == -1) {
3647 focus(0);
3648 } else if (getFocusedIndex() > 0) {
3649 focusAboveCell();
3650 }
3651 }
3652
3653 /** {@inheritDoc} */
3654 @Override public void focusNext() {
3655 if (getFocusedIndex() == -1) {
3656 focus(0);
3657 } else if (getFocusedIndex() != getItemCount() -1) {
3658 focusBelowCell();
3659 }
3660 }
3661
3662
3663
3664 /***********************************************************************
3665 * *
3666 * Private Implementation *
3667 * *
3668 **********************************************************************/
3669
3670 private int getColumnCount() {
3671 return treeTableView.getVisibleLeafColumns().size();
3672 }
3673
3674 // Gets a table column to the left or right of the current one, given an offset
3675 private TreeTableColumn<S,?> getTableColumn(TreeTableColumn<S,?> column, int offset) {
3676 int columnIndex = treeTableView.getVisibleLeafIndex(column);
3677 int newColumnIndex = columnIndex + offset;
3678 return treeTableView.getVisibleLeafColumn(newColumnIndex);
3679 }
3680 }
3681 }
--- EOF ---