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