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