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