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