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