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