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