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