1 /* 2 * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control; 27 28 import 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.scene.control.Logging; 40 import com.sun.javafx.scene.control.Properties; 41 import com.sun.javafx.scene.control.SelectedCellsMap; 42 import com.sun.javafx.scene.control.SelectedItemsReadOnlyObservableList; 43 import com.sun.javafx.scene.control.behavior.TableCellBehavior; 44 import com.sun.javafx.scene.control.behavior.TableCellBehaviorBase; 45 import javafx.beans.*; 46 import javafx.beans.Observable; 47 import javafx.beans.property.BooleanProperty; 48 import javafx.beans.property.DoubleProperty; 49 import javafx.beans.property.ObjectProperty; 50 import javafx.beans.property.ObjectPropertyBase; 51 import javafx.beans.property.Property; 52 import javafx.beans.property.ReadOnlyObjectProperty; 53 import javafx.beans.property.ReadOnlyObjectWrapper; 54 import javafx.beans.property.SimpleBooleanProperty; 55 import javafx.beans.property.SimpleObjectProperty; 56 import javafx.collections.FXCollections; 57 import javafx.collections.ListChangeListener; 58 import javafx.collections.MapChangeListener; 59 import javafx.collections.ObservableList; 60 import javafx.collections.WeakListChangeListener; 61 import javafx.collections.transformation.SortedList; 62 import javafx.css.CssMetaData; 63 import javafx.css.PseudoClass; 64 import javafx.css.Styleable; 65 import javafx.css.StyleableDoubleProperty; 66 import javafx.css.StyleableProperty; 67 import javafx.event.EventHandler; 68 import javafx.event.EventType; 69 import javafx.scene.AccessibleAttribute; 70 import javafx.scene.AccessibleRole; 71 import javafx.scene.Node; 72 import javafx.scene.layout.Region; 73 import javafx.util.Callback; 74 75 import com.sun.javafx.collections.MappingChange; 76 import com.sun.javafx.collections.NonIterableChange; 77 import javafx.css.converter.SizeConverter; 78 import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList; 79 import com.sun.javafx.scene.control.TableColumnComparatorBase.TableColumnComparator; 80 import javafx.scene.control.skin.TableViewSkin; 81 82 /** 83 * The TableView control is designed to visualize an unlimited number of rows 84 * of data, broken out into columns. A TableView is therefore very similar to the 85 * {@link ListView} control, with the addition of support for columns. For an 86 * example on how to create a TableView, refer to the 'Creating a TableView' 87 * control section below. 88 * 89 * <p>The TableView control has a number of features, including: 90 * <ul> 91 * <li>Powerful {@link TableColumn} API: 92 * <ul> 93 * <li>Support for {@link TableColumn#cellFactoryProperty() cell factories} to 94 * easily customize {@link Cell cell} contents in both rendering and editing 95 * states. 96 * <li>Specification of {@link TableColumn#minWidthProperty() minWidth}/ 97 * {@link TableColumn#prefWidthProperty() prefWidth}/ 98 * {@link TableColumn#maxWidthProperty() maxWidth}, 99 * and also {@link TableColumn#resizableProperty() fixed width columns}. 100 * <li>Width resizing by the user at runtime. 101 * <li>Column reordering by the user at runtime. 102 * <li>Built-in support for {@link TableColumn#getColumns() column nesting} 103 * </ul> 104 * <li>Different {@link #columnResizePolicyProperty() resizing policies} to 105 * dictate what happens when the user resizes columns. 106 * <li>Support for {@link #getSortOrder() multiple column sorting} by clicking 107 * the column header (hold down Shift keyboard key whilst clicking on a 108 * header to sort by multiple columns). 109 * </ul> 110 * 111 * <p>Note that TableView is intended to be used to visualize data - it is not 112 * intended to be used for laying out your user interface. If you want to lay 113 * your user interface out in a grid-like fashion, consider the 114 * {@link javafx.scene.layout.GridPane} layout instead.</p> 115 * 116 * <h2>Creating a TableView</h2> 117 * 118 * <p> 119 * {@literal Creating a TableView is a multi-step process, and also depends on the 120 * underlying data model needing to be represented. For this example we'll use 121 * an ObservableList<Person>, as it is the simplest way of showing data in a 122 * TableView. The <code>Person</code> class will consist of a first 123 * name and last name properties. That is:} 124 * 125 * <pre> 126 * {@code 127 * 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 * }}</pre> 144 * 145 * <p>Firstly, a TableView instance needs to be defined, as such: 146 * 147 * <pre> 148 * {@code 149 * TableView<Person> table = new TableView<Person>();}</pre> 150 * 151 * {@literal 152 * <p>With the basic table defined, we next focus on the data model. As mentioned, 153 * for this example, we'll be using a ObservableList<Person>. We can immediately 154 * set such a list directly in to the TableView, as such:} 155 * 156 * <pre> 157 * {@code 158 * ObservableList<Person> teamMembers = getTeamMembers(); 159 * table.setItems(teamMembers);}</pre> 160 * 161 * <p>With the items set as such, TableView will automatically update whenever 162 * the <code>teamMembers</code> list changes. If the items list is available 163 * before the TableView is instantiated, it is possible to pass it directly into 164 * the constructor. 165 * 166 * <p>At this point we now have a TableView hooked up to observe the 167 * <code>teamMembers</code> observableList. The missing ingredient 168 * now is the means of splitting out the data contained within the model and 169 * representing it in one or more {@link TableColumn TableColumn} instances. To 170 * create a two-column TableView to show the firstName and lastName properties, 171 * we extend the last code sample as follows: 172 * 173 * <pre> 174 * {@code 175 * ObservableList<Person> teamMembers = ...; 176 * table.setItems(teamMembers); 177 * 178 * TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>("First Name"); 179 * firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName")); 180 * TableColumn<Person,String> lastNameCol = new TableColumn<Person,String>("Last Name"); 181 * lastNameCol.setCellValueFactory(new PropertyValueFactory("lastName")); 182 * 183 * table.getColumns().setAll(firstNameCol, lastNameCol);}</pre> 184 * 185 * <p>With the code shown above we have fully defined the minimum properties 186 * required to create a TableView instance. Running this code (assuming the 187 * people ObservableList is appropriately created) will result in a TableView being 188 * shown with two columns for firstName and lastName. Any other properties of the 189 * Person class will not be shown, as no TableColumns are defined. 190 * 191 * <h3>TableView support for classes that don't contain properties</h3> 192 * 193 * <p>The code shown above is the shortest possible code for creating a TableView 194 * when the domain objects are designed with JavaFX properties in mind 195 * (additionally, {@link javafx.scene.control.cell.PropertyValueFactory} supports 196 * normal JavaBean properties too, although there is a caveat to this, so refer 197 * to the class documentation for more information). When this is not the case, 198 * it is necessary to provide a custom cell value factory. More information 199 * about cell value factories can be found in the {@link TableColumn} API 200 * documentation, but briefly, here is how a TableColumn could be specified: 201 * 202 * <pre> 203 * {@code 204 * firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() { 205 * public ObservableValue<String> call(CellDataFeatures<Person, String> p) { 206 * // p.getValue() returns the Person instance for a particular TableView row 207 * return p.getValue().firstNameProperty(); 208 * } 209 * }); 210 * }}</pre> 211 * 212 * <h3>TableView Selection / Focus APIs</h3> 213 * <p>To track selection and focus, it is necessary to become familiar with the 214 * {@link SelectionModel} and {@link FocusModel} classes. A TableView has at most 215 * one instance of each of these classes, available from 216 * {@link #selectionModelProperty() selectionModel} and 217 * {@link #focusModelProperty() focusModel} properties respectively. 218 * Whilst it is possible to use this API to set a new selection model, in 219 * most circumstances this is not necessary - the default selection and focus 220 * models should work in most circumstances. 221 * 222 * <p>The default {@link SelectionModel} used when instantiating a TableView is 223 * an implementation of the {@link MultipleSelectionModel} abstract class. 224 * However, as noted in the API documentation for 225 * the {@link MultipleSelectionModel#selectionModeProperty() selectionMode} 226 * property, the default value is {@link SelectionMode#SINGLE}. To enable 227 * multiple selection in a default TableView instance, it is therefore necessary 228 * to do the following: 229 * 230 * <pre> 231 * {@code 232 * tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}</pre> 233 * 234 * <h3>Customizing TableView Visuals</h3> 235 * <p>The visuals of the TableView can be entirely customized by replacing the 236 * default {@link #rowFactoryProperty() row factory}. A row factory is used to 237 * generate {@link TableRow} instances, which are used to represent an entire 238 * row in the TableView. 239 * 240 * <p>In many cases, this is not what is desired however, as it is more commonly 241 * the case that cells be customized on a per-column basis, not a per-row basis. 242 * It is therefore important to note that a {@link TableRow} is not a 243 * {@link TableCell}. A {@link TableRow} is simply a container for zero or more 244 * {@link TableCell}, and in most circumstances it is more likely that you'll 245 * want to create custom TableCells, rather than TableRows. The primary use case 246 * for creating custom TableRow instances would most probably be to introduce 247 * some form of column spanning support. 248 * 249 * <p>You can create custom {@link TableCell} instances per column by assigning 250 * the appropriate function to the TableColumn 251 * {@link TableColumn#cellFactoryProperty() cell factory} property. 252 * 253 * <p>See the {@link Cell} class documentation for a more complete 254 * description of how to write custom Cells. 255 * 256 * <h3>Sorting</h3> 257 * <p>Prior to JavaFX 8.0, the TableView control would treat the 258 * {@link #getItems() items} list as the view model, meaning that any changes to 259 * the list would be immediately reflected visually. TableView would also modify 260 * the order of this list directly when a user initiated a sort. This meant that 261 * (again, prior to JavaFX 8.0) it was not possible to have the TableView return 262 * to an unsorted state (after iterating through ascending and descending 263 * orders).</p> 264 * 265 * <p>Starting with JavaFX 8.0 (and the introduction of {@link SortedList}), it 266 * is now possible to have the collection return to the unsorted state when 267 * there are no columns as part of the TableView 268 * {@link #getSortOrder() sort order}. To do this, you must create a SortedList 269 * instance, and bind its 270 * {@link javafx.collections.transformation.SortedList#comparatorProperty() comparator} 271 * property to the TableView {@link #comparatorProperty() comparator} property, 272 * list so:</p> 273 * 274 * <pre> 275 * {@code 276 * // create a SortedList based on the provided ObservableList 277 * SortedList sortedList = new SortedList(FXCollections.observableArrayList(2, 1, 3)); 278 * 279 * // create a TableView with the sorted list set as the items it will show 280 * final TableView<Integer> tableView = new TableView<>(sortedList); 281 * 282 * // bind the sortedList comparator to the TableView comparator 283 * sortedList.comparatorProperty().bind(tableView.comparatorProperty()); 284 * 285 * // Don't forget to define columns! 286 * }</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().isEnabled()) { 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 * This is the function called when the user completes a column-resize 900 * operation. The two most common policies are available as static functions 901 * in the TableView class: {@link #UNCONSTRAINED_RESIZE_POLICY} and 902 * {@link #CONSTRAINED_RESIZE_POLICY}. 903 * @return columnResizePolicy property 904 */ 905 public final ObjectProperty<Callback<ResizeFeatures, Boolean>> columnResizePolicyProperty() { 906 if (columnResizePolicy == null) { 907 columnResizePolicy = new SimpleObjectProperty<Callback<ResizeFeatures, Boolean>>(this, "columnResizePolicy", UNCONSTRAINED_RESIZE_POLICY) { 908 private Callback<ResizeFeatures, Boolean> oldPolicy; 909 910 @Override protected void invalidated() { 911 if (isInited) { 912 get().call(new ResizeFeatures(TableView.this, null, 0.0)); 913 914 if (oldPolicy != null) { 915 PseudoClass state = PseudoClass.getPseudoClass(oldPolicy.toString()); 916 pseudoClassStateChanged(state, false); 917 } 918 if (get() != null) { 919 PseudoClass state = PseudoClass.getPseudoClass(get().toString()); 920 pseudoClassStateChanged(state, true); 921 } 922 oldPolicy = get(); 923 } 924 } 925 }; 926 } 927 return columnResizePolicy; 928 } 929 930 931 // --- Row Factory 932 private ObjectProperty<Callback<TableView<S>, TableRow<S>>> rowFactory; 933 934 /** 935 * A function which produces a TableRow. The system is responsible for 936 * reusing TableRows. Return from this function a TableRow which 937 * might be usable for representing a single row in a TableView. 938 * <p> 939 * Note that a TableRow is <b>not</b> a TableCell. A TableRow is 940 * simply a container for a TableCell, and in most circumstances it is more 941 * likely that you'll want to create custom TableCells, rather than 942 * TableRows. The primary use case for creating custom TableRow 943 * instances would most probably be to introduce some form of column 944 * spanning support. 945 * <p> 946 * You can create custom TableCell instances per column by assigning the 947 * appropriate function to the cellFactory property in the TableColumn class. 948 * @return rowFactory property 949 */ 950 public final ObjectProperty<Callback<TableView<S>, TableRow<S>>> rowFactoryProperty() { 951 if (rowFactory == null) { 952 rowFactory = new SimpleObjectProperty<Callback<TableView<S>, TableRow<S>>>(this, "rowFactory"); 953 } 954 return rowFactory; 955 } 956 public final void setRowFactory(Callback<TableView<S>, TableRow<S>> value) { 957 rowFactoryProperty().set(value); 958 } 959 public final Callback<TableView<S>, TableRow<S>> getRowFactory() { 960 return rowFactory == null ? null : rowFactory.get(); 961 } 962 963 964 // --- Placeholder Node 965 private ObjectProperty<Node> placeholder; 966 /** 967 * This Node is shown to the user when the table has no content to show. 968 * This may be the case because the table model has no data in the first 969 * place, that a filter has been applied to the table model, resulting 970 * in there being nothing to show the user, or that there are no currently 971 * visible columns. 972 * @return placeholder property 973 */ 974 public final ObjectProperty<Node> placeholderProperty() { 975 if (placeholder == null) { 976 placeholder = new SimpleObjectProperty<Node>(this, "placeholder"); 977 } 978 return placeholder; 979 } 980 public final void setPlaceholder(Node value) { 981 placeholderProperty().set(value); 982 } 983 public final Node getPlaceholder() { 984 return placeholder == null ? null : placeholder.get(); 985 } 986 987 988 // --- Selection Model 989 private ObjectProperty<TableViewSelectionModel<S>> selectionModel 990 = new SimpleObjectProperty<TableViewSelectionModel<S>>(this, "selectionModel") { 991 992 TableViewSelectionModel<S> oldValue = null; 993 994 @Override protected void invalidated() { 995 996 if (oldValue != null) { 997 oldValue.cellSelectionEnabledProperty().removeListener(weakCellSelectionModelInvalidationListener); 998 999 if (oldValue instanceof TableViewArrayListSelectionModel) { 1000 ((TableViewArrayListSelectionModel)oldValue).dispose(); 1001 } 1002 } 1003 1004 oldValue = get(); 1005 1006 if (oldValue != null) { 1007 oldValue.cellSelectionEnabledProperty().addListener(weakCellSelectionModelInvalidationListener); 1008 // fake an invalidation to ensure updated pseudo-class state 1009 weakCellSelectionModelInvalidationListener.invalidated(oldValue.cellSelectionEnabledProperty()); 1010 } 1011 } 1012 }; 1013 1014 /** 1015 * The SelectionModel provides the API through which it is possible 1016 * to select single or multiple items within a TableView, as well as inspect 1017 * which items have been selected by the user. Note that it has a generic 1018 * type that must match the type of the TableView itself. 1019 * @return selectionModel property 1020 */ 1021 public final ObjectProperty<TableViewSelectionModel<S>> selectionModelProperty() { 1022 return selectionModel; 1023 } 1024 public final void setSelectionModel(TableViewSelectionModel<S> value) { 1025 selectionModelProperty().set(value); 1026 } 1027 1028 public final TableViewSelectionModel<S> getSelectionModel() { 1029 return selectionModel.get(); 1030 } 1031 1032 1033 // --- Focus Model 1034 private ObjectProperty<TableViewFocusModel<S>> focusModel; 1035 public final void setFocusModel(TableViewFocusModel<S> value) { 1036 focusModelProperty().set(value); 1037 } 1038 public final TableViewFocusModel<S> getFocusModel() { 1039 return focusModel == null ? null : focusModel.get(); 1040 } 1041 /** 1042 * Represents the currently-installed {@link TableViewFocusModel} for this 1043 * TableView. Under almost all circumstances leaving this as the default 1044 * focus model will suffice. 1045 * @return focusModel property 1046 */ 1047 public final ObjectProperty<TableViewFocusModel<S>> focusModelProperty() { 1048 if (focusModel == null) { 1049 focusModel = new SimpleObjectProperty<TableViewFocusModel<S>>(this, "focusModel"); 1050 } 1051 return focusModel; 1052 } 1053 1054 1055 // // --- Span Model 1056 // private ObjectProperty<SpanModel<S>> spanModel 1057 // = new SimpleObjectProperty<SpanModel<S>>(this, "spanModel") { 1058 // 1059 // @Override protected void invalidated() { 1060 // ObservableList<String> styleClass = getStyleClass(); 1061 // if (getSpanModel() == null) { 1062 // styleClass.remove(CELL_SPAN_TABLE_VIEW_STYLE_CLASS); 1063 // } else if (! styleClass.contains(CELL_SPAN_TABLE_VIEW_STYLE_CLASS)) { 1064 // styleClass.add(CELL_SPAN_TABLE_VIEW_STYLE_CLASS); 1065 // } 1066 // } 1067 // }; 1068 // 1069 // public final ObjectProperty<SpanModel<S>> spanModelProperty() { 1070 // return spanModel; 1071 // } 1072 // public final void setSpanModel(SpanModel<S> value) { 1073 // spanModelProperty().set(value); 1074 // } 1075 // 1076 // public final SpanModel<S> getSpanModel() { 1077 // return spanModel.get(); 1078 // } 1079 1080 // --- Editable 1081 private BooleanProperty editable; 1082 public final void setEditable(boolean value) { 1083 editableProperty().set(value); 1084 } 1085 public final boolean isEditable() { 1086 return editable == null ? false : editable.get(); 1087 } 1088 /** 1089 * Specifies whether this TableView is editable - only if the TableView, the 1090 * TableColumn (if applicable) and the TableCells within it are both 1091 * editable will a TableCell be able to go into their editing state. 1092 * @return the editable property 1093 */ 1094 public final BooleanProperty editableProperty() { 1095 if (editable == null) { 1096 editable = new SimpleBooleanProperty(this, "editable", false); 1097 } 1098 return editable; 1099 } 1100 1101 1102 // --- Fixed cell size 1103 private DoubleProperty fixedCellSize; 1104 1105 /** 1106 * Sets the new fixed cell size for this control. Any value greater than 1107 * zero will enable fixed cell size mode, whereas a zero or negative value 1108 * (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size 1109 * mode. 1110 * 1111 * @param value The new fixed cell size value, or a value less than or equal 1112 * to zero (or Region.USE_COMPUTED_SIZE) to disable. 1113 * @since JavaFX 8.0 1114 */ 1115 public final void setFixedCellSize(double value) { 1116 fixedCellSizeProperty().set(value); 1117 } 1118 1119 /** 1120 * Returns the fixed cell size value. A value less than or equal to zero is 1121 * used to represent that fixed cell size mode is disabled, and a value 1122 * greater than zero represents the size of all cells in this control. 1123 * 1124 * @return A double representing the fixed cell size of this control, or a 1125 * value less than or equal to zero if fixed cell size mode is disabled. 1126 * @since JavaFX 8.0 1127 */ 1128 public final double getFixedCellSize() { 1129 return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get(); 1130 } 1131 /** 1132 * Specifies whether this control has cells that are a fixed height (of the 1133 * specified value). If this value is less than or equal to zero, 1134 * then all cells are individually sized and positioned. This is a slow 1135 * operation. Therefore, when performance matters and developers are not 1136 * dependent on variable cell sizes it is a good idea to set the fixed cell 1137 * size value. Generally cells are around 24px, so setting a fixed cell size 1138 * of 24 is likely to result in very little difference in visuals, but a 1139 * improvement to performance. 1140 * 1141 * <p>To set this property via CSS, use the -fx-fixed-cell-size property. 1142 * This should not be confused with the -fx-cell-size property. The difference 1143 * between these two CSS properties is that -fx-cell-size will size all 1144 * cells to the specified size, but it will not enforce that this is the 1145 * only size (thus allowing for variable cell sizes, and preventing the 1146 * performance gains from being possible). Therefore, when performance matters 1147 * use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are 1148 * specified in CSS, -fx-fixed-cell-size takes precedence.</p> 1149 * 1150 * @return fixedCellSize property 1151 * @since JavaFX 8.0 1152 */ 1153 public final DoubleProperty fixedCellSizeProperty() { 1154 if (fixedCellSize == null) { 1155 fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) { 1156 @Override public CssMetaData<TableView<?>,Number> getCssMetaData() { 1157 return StyleableProperties.FIXED_CELL_SIZE; 1158 } 1159 1160 @Override public Object getBean() { 1161 return TableView.this; 1162 } 1163 1164 @Override public String getName() { 1165 return "fixedCellSize"; 1166 } 1167 }; 1168 } 1169 return fixedCellSize; 1170 } 1171 1172 1173 // --- Editing Cell 1174 private ReadOnlyObjectWrapper<TablePosition<S,?>> editingCell; 1175 private void setEditingCell(TablePosition<S,?> value) { 1176 editingCellPropertyImpl().set(value); 1177 } 1178 public final TablePosition<S,?> getEditingCell() { 1179 return editingCell == null ? null : editingCell.get(); 1180 } 1181 1182 /** 1183 * Represents the current cell being edited, or null if 1184 * there is no cell being edited. 1185 * @return the editingCell property 1186 */ 1187 public final ReadOnlyObjectProperty<TablePosition<S,?>> editingCellProperty() { 1188 return editingCellPropertyImpl().getReadOnlyProperty(); 1189 } 1190 1191 private ReadOnlyObjectWrapper<TablePosition<S,?>> editingCellPropertyImpl() { 1192 if (editingCell == null) { 1193 editingCell = new ReadOnlyObjectWrapper<TablePosition<S,?>>(this, "editingCell"); 1194 } 1195 return editingCell; 1196 } 1197 1198 1199 // --- Comparator (built via sortOrder list, so read-only) 1200 /** 1201 * The comparator property is a read-only property that is representative of the 1202 * current state of the {@link #getSortOrder() sort order} list. The sort 1203 * order list contains the columns that have been added to it either programmatically 1204 * or via a user clicking on the headers themselves. 1205 * @since JavaFX 8.0 1206 */ 1207 private ReadOnlyObjectWrapper<Comparator<S>> comparator; 1208 private void setComparator(Comparator<S> value) { 1209 comparatorPropertyImpl().set(value); 1210 } 1211 public final Comparator<S> getComparator() { 1212 return comparator == null ? null : comparator.get(); 1213 } 1214 public final ReadOnlyObjectProperty<Comparator<S>> comparatorProperty() { 1215 return comparatorPropertyImpl().getReadOnlyProperty(); 1216 } 1217 private ReadOnlyObjectWrapper<Comparator<S>> comparatorPropertyImpl() { 1218 if (comparator == null) { 1219 comparator = new ReadOnlyObjectWrapper<Comparator<S>>(this, "comparator"); 1220 } 1221 return comparator; 1222 } 1223 1224 1225 // --- sortPolicy 1226 /** 1227 * The sort policy specifies how sorting in this TableView should be performed. 1228 * For example, a basic sort policy may just call 1229 * {@code FXCollections.sort(tableView.getItems())}, whereas a more advanced 1230 * sort policy may call to a database to perform the necessary sorting on the 1231 * server-side. 1232 * 1233 * <p>TableView ships with a {@link TableView#DEFAULT_SORT_POLICY default 1234 * sort policy} that does precisely as mentioned above: it simply attempts 1235 * to sort the items list in-place. 1236 * 1237 * <p>It is recommended that rather than override the {@link TableView#sort() sort} 1238 * method that a different sort policy be provided instead. 1239 * @since JavaFX 8.0 1240 */ 1241 private ObjectProperty<Callback<TableView<S>, Boolean>> sortPolicy; 1242 public final void setSortPolicy(Callback<TableView<S>, Boolean> callback) { 1243 sortPolicyProperty().set(callback); 1244 } 1245 @SuppressWarnings("unchecked") 1246 public final Callback<TableView<S>, Boolean> getSortPolicy() { 1247 return sortPolicy == null ? 1248 (Callback<TableView<S>, Boolean>)(Object) DEFAULT_SORT_POLICY : 1249 sortPolicy.get(); 1250 } 1251 @SuppressWarnings("unchecked") 1252 public final ObjectProperty<Callback<TableView<S>, Boolean>> sortPolicyProperty() { 1253 if (sortPolicy == null) { 1254 sortPolicy = new SimpleObjectProperty<Callback<TableView<S>, Boolean>>( 1255 this, "sortPolicy", (Callback<TableView<S>, Boolean>)(Object) DEFAULT_SORT_POLICY) { 1256 @Override protected void invalidated() { 1257 sort(); 1258 } 1259 }; 1260 } 1261 return sortPolicy; 1262 } 1263 1264 1265 // onSort 1266 /** 1267 * Called when there's a request to sort the control. 1268 * @since JavaFX 8.0 1269 */ 1270 private ObjectProperty<EventHandler<SortEvent<TableView<S>>>> onSort; 1271 1272 public void setOnSort(EventHandler<SortEvent<TableView<S>>> value) { 1273 onSortProperty().set(value); 1274 } 1275 1276 public EventHandler<SortEvent<TableView<S>>> getOnSort() { 1277 if( onSort != null ) { 1278 return onSort.get(); 1279 } 1280 return null; 1281 } 1282 1283 public ObjectProperty<EventHandler<SortEvent<TableView<S>>>> onSortProperty() { 1284 if( onSort == null ) { 1285 onSort = new ObjectPropertyBase<EventHandler<SortEvent<TableView<S>>>>() { 1286 @Override protected void invalidated() { 1287 EventType<SortEvent<TableView<S>>> eventType = SortEvent.sortEvent(); 1288 EventHandler<SortEvent<TableView<S>>> eventHandler = get(); 1289 setEventHandler(eventType, eventHandler); 1290 } 1291 1292 @Override public Object getBean() { 1293 return TableView.this; 1294 } 1295 1296 @Override public String getName() { 1297 return "onSort"; 1298 } 1299 }; 1300 } 1301 return onSort; 1302 } 1303 1304 1305 /*************************************************************************** 1306 * * 1307 * Public API * 1308 * * 1309 **************************************************************************/ 1310 /** 1311 * The TableColumns that are part of this TableView. As the user reorders 1312 * the TableView columns, this list will be updated to reflect the current 1313 * visual ordering. 1314 * 1315 * <p>Note: to display any data in a TableView, there must be at least one 1316 * TableColumn in this ObservableList.</p> 1317 * @return the columns 1318 */ 1319 public final ObservableList<TableColumn<S,?>> getColumns() { 1320 return columns; 1321 } 1322 1323 /** 1324 * The sortOrder list defines the order in which {@link TableColumn} instances 1325 * are sorted. An empty sortOrder list means that no sorting is being applied 1326 * on the TableView. If the sortOrder list has one TableColumn within it, 1327 * the TableView will be sorted using the 1328 * {@link TableColumn#sortTypeProperty() sortType} and 1329 * {@link TableColumn#comparatorProperty() comparator} properties of this 1330 * TableColumn (assuming 1331 * {@link TableColumn#sortableProperty() TableColumn.sortable} is true). 1332 * If the sortOrder list contains multiple TableColumn instances, then 1333 * the TableView is firstly sorted based on the properties of the first 1334 * TableColumn. If two elements are considered equal, then the second 1335 * TableColumn in the list is used to determine ordering. This repeats until 1336 * the results from all TableColumn comparators are considered, if necessary. 1337 * 1338 * @return An ObservableList containing zero or more TableColumn instances. 1339 */ 1340 public final ObservableList<TableColumn<S,?>> getSortOrder() { 1341 return sortOrder; 1342 } 1343 1344 /** 1345 * Scrolls the TableView so that the given index is visible within the viewport. 1346 * @param index The index of an item that should be visible to the user. 1347 */ 1348 public void scrollTo(int index) { 1349 ControlUtils.scrollToIndex(this, index); 1350 } 1351 1352 /** 1353 * Scrolls the TableView so that the given object is visible within the viewport. 1354 * @param object The object that should be visible to the user. 1355 * @since JavaFX 8.0 1356 */ 1357 public void scrollTo(S object) { 1358 if( getItems() != null ) { 1359 int idx = getItems().indexOf(object); 1360 if( idx >= 0 ) { 1361 ControlUtils.scrollToIndex(this, idx); 1362 } 1363 } 1364 } 1365 1366 /** 1367 * Called when there's a request to scroll an index into view using {@link #scrollTo(int)} 1368 * or {@link #scrollTo(Object)} 1369 * @since JavaFX 8.0 1370 */ 1371 private ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollTo; 1372 1373 public void setOnScrollTo(EventHandler<ScrollToEvent<Integer>> value) { 1374 onScrollToProperty().set(value); 1375 } 1376 1377 public EventHandler<ScrollToEvent<Integer>> getOnScrollTo() { 1378 if( onScrollTo != null ) { 1379 return onScrollTo.get(); 1380 } 1381 return null; 1382 } 1383 1384 public ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollToProperty() { 1385 if( onScrollTo == null ) { 1386 onScrollTo = new ObjectPropertyBase<EventHandler<ScrollToEvent<Integer>>>() { 1387 @Override 1388 protected void invalidated() { 1389 setEventHandler(ScrollToEvent.scrollToTopIndex(), get()); 1390 } 1391 @Override 1392 public Object getBean() { 1393 return TableView.this; 1394 } 1395 1396 @Override 1397 public String getName() { 1398 return "onScrollTo"; 1399 } 1400 }; 1401 } 1402 return onScrollTo; 1403 } 1404 1405 /** 1406 * Scrolls the TableView so that the given column is visible within the viewport. 1407 * @param column The column that should be visible to the user. 1408 * @since JavaFX 8.0 1409 */ 1410 public void scrollToColumn(TableColumn<S, ?> column) { 1411 ControlUtils.scrollToColumn(this, column); 1412 } 1413 1414 /** 1415 * Scrolls the TableView so that the given index is visible within the viewport. 1416 * @param columnIndex The index of a column that should be visible to the user. 1417 * @since JavaFX 8.0 1418 */ 1419 public void scrollToColumnIndex(int columnIndex) { 1420 if( getColumns() != null ) { 1421 ControlUtils.scrollToColumn(this, getColumns().get(columnIndex)); 1422 } 1423 } 1424 1425 /** 1426 * Called when there's a request to scroll a column into view using {@link #scrollToColumn(TableColumn)} 1427 * or {@link #scrollToColumnIndex(int)} 1428 * @since JavaFX 8.0 1429 */ 1430 private ObjectProperty<EventHandler<ScrollToEvent<TableColumn<S, ?>>>> onScrollToColumn; 1431 1432 public void setOnScrollToColumn(EventHandler<ScrollToEvent<TableColumn<S, ?>>> value) { 1433 onScrollToColumnProperty().set(value); 1434 } 1435 1436 public EventHandler<ScrollToEvent<TableColumn<S, ?>>> getOnScrollToColumn() { 1437 if( onScrollToColumn != null ) { 1438 return onScrollToColumn.get(); 1439 } 1440 return null; 1441 } 1442 1443 public ObjectProperty<EventHandler<ScrollToEvent<TableColumn<S, ?>>>> onScrollToColumnProperty() { 1444 if( onScrollToColumn == null ) { 1445 onScrollToColumn = new ObjectPropertyBase<EventHandler<ScrollToEvent<TableColumn<S, ?>>>>() { 1446 @Override protected void invalidated() { 1447 EventType<ScrollToEvent<TableColumn<S, ?>>> type = ScrollToEvent.scrollToColumn(); 1448 setEventHandler(type, get()); 1449 } 1450 1451 @Override public Object getBean() { 1452 return TableView.this; 1453 } 1454 1455 @Override public String getName() { 1456 return "onScrollToColumn"; 1457 } 1458 }; 1459 } 1460 return onScrollToColumn; 1461 } 1462 1463 /** 1464 * Applies the currently installed resize policy against the given column, 1465 * resizing it based on the delta value provided. 1466 * @param column the column 1467 * @param delta the delta 1468 * @return true if column resize is allowed 1469 */ 1470 public boolean resizeColumn(TableColumn<S,?> column, double delta) { 1471 if (column == null || Double.compare(delta, 0.0) == 0) return false; 1472 1473 boolean allowed = getColumnResizePolicy().call(new ResizeFeatures<S>(TableView.this, column, delta)); 1474 if (!allowed) return false; 1475 1476 return true; 1477 } 1478 1479 /** 1480 * Causes the cell at the given row/column view indexes to switch into 1481 * its editing state, if it is not already in it, and assuming that the 1482 * TableView and column are also editable. 1483 * 1484 * <p><strong>Note:</strong> This method will cancel editing if the given row 1485 * value is less than zero and the given column is null.</p> 1486 * @param row the row 1487 * @param column the column 1488 */ 1489 public void edit(int row, TableColumn<S,?> column) { 1490 if (!isEditable() || (column != null && ! column.isEditable())) { 1491 return; 1492 } 1493 1494 if (row < 0 && column == null) { 1495 setEditingCell(null); 1496 } else { 1497 setEditingCell(new TablePosition<>(this, row, column)); 1498 } 1499 } 1500 1501 /** 1502 * Returns an unmodifiable list containing the currently visible leaf columns. 1503 * @return an unmodifiable list containing the currently visible leaf columns 1504 */ 1505 public ObservableList<TableColumn<S,?>> getVisibleLeafColumns() { 1506 return unmodifiableVisibleLeafColumns; 1507 } 1508 1509 /** 1510 * Returns the position of the given column, relative to all other 1511 * visible leaf columns. 1512 * @param column the column 1513 * @return the position of the given column, relative to all other 1514 * visible leaf columns 1515 */ 1516 public int getVisibleLeafIndex(TableColumn<S,?> column) { 1517 return visibleLeafColumns.indexOf(column); 1518 } 1519 1520 /** 1521 * Returns the TableColumn in the given column index, relative to all other 1522 * visible leaf columns. 1523 * @param column the column 1524 * @return the TableColumn in the given column index, relative to all other 1525 * visible leaf columns 1526 */ 1527 public TableColumn<S,?> getVisibleLeafColumn(int column) { 1528 if (column < 0 || column >= visibleLeafColumns.size()) return null; 1529 return visibleLeafColumns.get(column); 1530 } 1531 1532 /** {@inheritDoc} */ 1533 @Override protected Skin<?> createDefaultSkin() { 1534 return new TableViewSkin<S>(this); 1535 } 1536 1537 /** 1538 * The sort method forces the TableView to re-run its sorting algorithm. More 1539 * often than not it is not necessary to call this method directly, as it is 1540 * automatically called when the {@link #getSortOrder() sort order}, 1541 * {@link #sortPolicyProperty() sort policy}, or the state of the 1542 * TableColumn {@link TableColumn#sortTypeProperty() sort type} properties 1543 * change. In other words, this method should only be called directly when 1544 * something external changes and a sort is required. 1545 * @since JavaFX 8.0 1546 */ 1547 public void sort() { 1548 final ObservableList<? extends TableColumnBase<S,?>> sortOrder = getSortOrder(); 1549 1550 // update the Comparator property 1551 final Comparator<S> oldComparator = getComparator(); 1552 setComparator(sortOrder.isEmpty() ? null : new TableColumnComparator(sortOrder)); 1553 1554 // fire the onSort event and check if it is consumed, if 1555 // so, don't run the sort 1556 SortEvent<TableView<S>> sortEvent = new SortEvent<>(TableView.this, TableView.this); 1557 fireEvent(sortEvent); 1558 if (sortEvent.isConsumed()) { 1559 // if the sort is consumed we could back out the last action (the code 1560 // is commented out right below), but we don't as we take it as a 1561 // sign that the developer has decided to handle the event themselves. 1562 1563 // sortLock = true; 1564 // TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo); 1565 // sortLock = false; 1566 return; 1567 } 1568 1569 final List<TablePosition> prevState = new ArrayList<>(getSelectionModel().getSelectedCells()); 1570 final int itemCount = prevState.size(); 1571 1572 // we set makeAtomic to true here, so that we don't fire intermediate 1573 // sort events - instead we send a single permutation event at the end 1574 // of this method. 1575 getSelectionModel().startAtomic(); 1576 1577 // get the sort policy and run it 1578 Callback<TableView<S>, Boolean> sortPolicy = getSortPolicy(); 1579 if (sortPolicy == null) return; 1580 Boolean success = sortPolicy.call(this); 1581 1582 getSelectionModel().stopAtomic(); 1583 1584 if (success == null || ! success) { 1585 // the sort was a failure. Need to backout if possible 1586 sortLock = true; 1587 TableUtil.handleSortFailure(sortOrder, lastSortEventType, lastSortEventSupportInfo); 1588 setComparator(oldComparator); 1589 sortLock = false; 1590 } else { 1591 // sorting was a success, now we possibly fire an event on the 1592 // selection model that the items list has 'permutated' to a new ordering 1593 1594 // FIXME we should support alternative selection model implementations! 1595 if (getSelectionModel() instanceof TableViewArrayListSelectionModel) { 1596 final TableViewArrayListSelectionModel<S> sm = (TableViewArrayListSelectionModel<S>) getSelectionModel(); 1597 final ObservableList<TablePosition<S,?>> newState = (ObservableList<TablePosition<S,?>>)(Object)sm.getSelectedCells(); 1598 1599 List<TablePosition<S, ?>> removed = new ArrayList<>(); 1600 for (int i = 0; i < itemCount; i++) { 1601 TablePosition<S, ?> prevItem = prevState.get(i); 1602 if (!newState.contains(prevItem)) { 1603 removed.add(prevItem); 1604 } 1605 } 1606 1607 if (!removed.isEmpty()) { 1608 // the sort operation effectively permutates the selectedCells list, 1609 // but we cannot fire a permutation event as we are talking about 1610 // TablePosition's changing (which may reside in the same list 1611 // position before and after the sort). Therefore, we need to fire 1612 // a single add/remove event to cover the added and removed positions. 1613 ListChangeListener.Change<TablePosition<S, ?>> c = new NonIterableChange.GenericAddRemoveChange<>(0, itemCount, removed, newState); 1614 sm.fireCustomSelectedCellsListChangeEvent(c); 1615 } 1616 } 1617 } 1618 } 1619 1620 /** 1621 * Calling {@code refresh()} forces the TableView control to recreate and 1622 * repopulate the cells necessary to populate the visual bounds of the control. 1623 * In other words, this forces the TableView to update what it is showing to 1624 * the user. This is useful in cases where the underlying data source has 1625 * changed in a way that is not observed by the TableView itself. 1626 * 1627 * @since JavaFX 8u60 1628 */ 1629 public void refresh() { 1630 getProperties().put(Properties.RECREATE, Boolean.TRUE); 1631 } 1632 1633 1634 1635 /*************************************************************************** 1636 * * 1637 * Private Implementation * 1638 * * 1639 **************************************************************************/ 1640 1641 private boolean sortLock = false; 1642 private TableUtil.SortEventType lastSortEventType = null; 1643 private Object[] lastSortEventSupportInfo = null; 1644 1645 private void doSort(final TableUtil.SortEventType sortEventType, final Object... supportInfo) { 1646 if (sortLock) { 1647 return; 1648 } 1649 1650 this.lastSortEventType = sortEventType; 1651 this.lastSortEventSupportInfo = supportInfo; 1652 sort(); 1653 this.lastSortEventType = null; 1654 this.lastSortEventSupportInfo = null; 1655 } 1656 1657 1658 // --- Content width 1659 private void setContentWidth(double contentWidth) { 1660 this.contentWidth = contentWidth; 1661 if (isInited) { 1662 // sometimes the current column resize policy will have to modify the 1663 // column width of all columns in the table if the table width changes, 1664 // so we short-circuit the resize function and just go straight there 1665 // with a null TableColumn, which indicates to the resize policy function 1666 // that it shouldn't actually do anything specific to one column. 1667 getColumnResizePolicy().call(new ResizeFeatures<S>(TableView.this, null, 0.0)); 1668 } 1669 } 1670 1671 /** 1672 * Recomputes the currently visible leaf columns in this TableView. 1673 */ 1674 private void updateVisibleLeafColumns() { 1675 // update visible leaf columns list 1676 List<TableColumn<S,?>> cols = new ArrayList<TableColumn<S,?>>(); 1677 buildVisibleLeafColumns(getColumns(), cols); 1678 visibleLeafColumns.setAll(cols); 1679 1680 // sometimes the current column resize policy will have to modify the 1681 // column width of all columns in the table if the table width changes, 1682 // so we short-circuit the resize function and just go straight there 1683 // with a null TableColumn, which indicates to the resize policy function 1684 // that it shouldn't actually do anything specific to one column. 1685 getColumnResizePolicy().call(new ResizeFeatures<S>(TableView.this, null, 0.0)); 1686 } 1687 1688 private void buildVisibleLeafColumns(List<TableColumn<S,?>> cols, List<TableColumn<S,?>> vlc) { 1689 for (TableColumn<S,?> c : cols) { 1690 if (c == null) continue; 1691 1692 boolean hasChildren = ! c.getColumns().isEmpty(); 1693 1694 if (hasChildren) { 1695 buildVisibleLeafColumns(c.getColumns(), vlc); 1696 } else if (c.isVisible()) { 1697 vlc.add(c); 1698 } 1699 } 1700 } 1701 1702 1703 1704 /*************************************************************************** 1705 * * 1706 * Stylesheet Handling * 1707 * * 1708 **************************************************************************/ 1709 1710 private static final String DEFAULT_STYLE_CLASS = "table-view"; 1711 1712 private static final PseudoClass PSEUDO_CLASS_CELL_SELECTION = 1713 PseudoClass.getPseudoClass("cell-selection"); 1714 private static final PseudoClass PSEUDO_CLASS_ROW_SELECTION = 1715 PseudoClass.getPseudoClass("row-selection"); 1716 1717 private static class StyleableProperties { 1718 private static final CssMetaData<TableView<?>,Number> FIXED_CELL_SIZE = 1719 new CssMetaData<TableView<?>,Number>("-fx-fixed-cell-size", 1720 SizeConverter.getInstance(), 1721 Region.USE_COMPUTED_SIZE) { 1722 1723 @Override public Double getInitialValue(TableView<?> node) { 1724 return node.getFixedCellSize(); 1725 } 1726 1727 @Override public boolean isSettable(TableView<?> n) { 1728 return n.fixedCellSize == null || !n.fixedCellSize.isBound(); 1729 } 1730 1731 @Override public StyleableProperty<Number> getStyleableProperty(TableView<?> n) { 1732 return (StyleableProperty<Number>) n.fixedCellSizeProperty(); 1733 } 1734 }; 1735 1736 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1737 static { 1738 final List<CssMetaData<? extends Styleable, ?>> styleables = 1739 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData()); 1740 styleables.add(FIXED_CELL_SIZE); 1741 STYLEABLES = Collections.unmodifiableList(styleables); 1742 } 1743 } 1744 1745 /** 1746 * @return The CssMetaData associated with this class, which may include the 1747 * CssMetaData of its superclasses. 1748 * @since JavaFX 8.0 1749 */ 1750 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1751 return StyleableProperties.STYLEABLES; 1752 } 1753 1754 /** 1755 * {@inheritDoc} 1756 * @since JavaFX 8.0 1757 */ 1758 @Override 1759 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 1760 return getClassCssMetaData(); 1761 } 1762 1763 1764 1765 /*************************************************************************** 1766 * * 1767 * Accessibility handling * 1768 * * 1769 **************************************************************************/ 1770 1771 /** {@inheritDoc} */ 1772 @Override 1773 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1774 switch (attribute) { 1775 case COLUMN_COUNT: return getVisibleLeafColumns().size(); 1776 case ROW_COUNT: return getItems().size(); 1777 case SELECTED_ITEMS: { 1778 // TableViewSkin returns TableRows back to TableView. 1779 // TableRowSkin returns TableCells back to TableRow. 1780 @SuppressWarnings("unchecked") 1781 ObservableList<TableRow<S>> rows = (ObservableList<TableRow<S>>)super.queryAccessibleAttribute(attribute, parameters); 1782 List<Node> selection = new ArrayList<>(); 1783 for (TableRow<S> row : rows) { 1784 @SuppressWarnings("unchecked") 1785 ObservableList<Node> cells = (ObservableList<Node>)row.queryAccessibleAttribute(attribute, parameters); 1786 if (cells != null) selection.addAll(cells); 1787 } 1788 return FXCollections.observableArrayList(selection); 1789 } 1790 case FOCUS_ITEM: { 1791 Node row = (Node)super.queryAccessibleAttribute(attribute, parameters); 1792 if (row == null) return null; 1793 Node cell = (Node)row.queryAccessibleAttribute(attribute, parameters); 1794 /* cell equals to null means the row is a placeholder node */ 1795 return cell != null ? cell : row; 1796 } 1797 case CELL_AT_ROW_COLUMN: { 1798 @SuppressWarnings("unchecked") 1799 TableRow<S> row = (TableRow<S>)super.queryAccessibleAttribute(attribute, parameters); 1800 return row != null ? row.queryAccessibleAttribute(attribute, parameters) : null; 1801 } 1802 case MULTIPLE_SELECTION: { 1803 MultipleSelectionModel<S> sm = getSelectionModel(); 1804 return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE; 1805 } 1806 default: return super.queryAccessibleAttribute(attribute, parameters); 1807 } 1808 } 1809 1810 1811 /*************************************************************************** 1812 * * 1813 * Support Interfaces * 1814 * * 1815 **************************************************************************/ 1816 1817 /** 1818 * An immutable wrapper class for use in the TableView 1819 * {@link TableView#columnResizePolicyProperty() column resize} functionality. 1820 * @since JavaFX 2.0 1821 */ 1822 public static class ResizeFeatures<S> extends ResizeFeaturesBase<S> { 1823 private TableView<S> table; 1824 1825 /** 1826 * Creates an instance of this class, with the provided TableView, 1827 * TableColumn and delta values being set and stored in this immutable 1828 * instance. 1829 * 1830 * @param table The TableView upon which the resize operation is occurring. 1831 * @param column The column upon which the resize is occurring, or null 1832 * if this ResizeFeatures instance is being created as a result of a 1833 * TableView resize operation. 1834 * @param delta The amount of horizontal space added or removed in the 1835 * resize operation. 1836 */ 1837 public ResizeFeatures(TableView<S> table, TableColumn<S,?> column, Double delta) { 1838 super(column, delta); 1839 this.table = table; 1840 } 1841 1842 /** 1843 * Returns the column upon which the resize is occurring, or null 1844 * if this ResizeFeatures instance was created as a result of a 1845 * TableView resize operation. 1846 */ 1847 @Override public TableColumn<S,?> getColumn() { 1848 return (TableColumn<S,?>) super.getColumn(); 1849 } 1850 1851 /** 1852 * Returns the TableView upon which the resize operation is occurring. 1853 * @return the TableView 1854 */ 1855 public TableView<S> getTable() { 1856 return table; 1857 } 1858 } 1859 1860 1861 1862 /*************************************************************************** 1863 * * 1864 * Support Classes * 1865 * * 1866 **************************************************************************/ 1867 1868 1869 /** 1870 * A simple extension of the {@link SelectionModel} abstract class to 1871 * allow for special support for TableView controls. 1872 * @since JavaFX 2.0 1873 */ 1874 public static abstract class TableViewSelectionModel<S> extends TableSelectionModel<S> { 1875 1876 /*********************************************************************** 1877 * * 1878 * Private fields * 1879 * * 1880 **********************************************************************/ 1881 1882 private final TableView<S> tableView; 1883 1884 boolean blockFocusCall = false; 1885 1886 1887 1888 /*********************************************************************** 1889 * * 1890 * Constructors * 1891 * * 1892 **********************************************************************/ 1893 1894 /** 1895 * Builds a default TableViewSelectionModel instance with the provided 1896 * TableView. 1897 * @param tableView The TableView upon which this selection model should 1898 * operate. 1899 * @throws NullPointerException TableView can not be null. 1900 */ 1901 public TableViewSelectionModel(final TableView<S> tableView) { 1902 if (tableView == null) { 1903 throw new NullPointerException("TableView can not be null"); 1904 } 1905 1906 this.tableView = tableView; 1907 } 1908 1909 1910 1911 /*********************************************************************** 1912 * * 1913 * Abstract API * 1914 * * 1915 **********************************************************************/ 1916 1917 /** 1918 * A read-only ObservableList representing the currently selected cells 1919 * in this TableView. Rather than directly modify this list, please 1920 * use the other methods provided in the TableViewSelectionModel. 1921 * @return a read-only ObservableList representing the currently 1922 * selected cells in this TableView 1923 */ 1924 public abstract ObservableList<TablePosition> getSelectedCells(); 1925 1926 1927 /*********************************************************************** 1928 * * 1929 * Generic (type erasure) bridging * 1930 * * 1931 **********************************************************************/ 1932 1933 // --- isSelected 1934 /** {@inheritDoc} */ 1935 @Override public boolean isSelected(int row, TableColumnBase<S, ?> column) { 1936 return isSelected(row, (TableColumn<S,?>)column); 1937 } 1938 1939 /** 1940 * Convenience function which tests whether the given row and column index 1941 * is currently selected in this table instance. 1942 * @param row the row 1943 * @param column the column 1944 * @return true if row and column index is currently selected 1945 */ 1946 public abstract boolean isSelected(int row, TableColumn<S, ?> column); 1947 1948 1949 // --- select 1950 /** {@inheritDoc} */ 1951 @Override public void select(int row, TableColumnBase<S, ?> column) { 1952 select(row, (TableColumn<S,?>)column); 1953 } 1954 1955 /** 1956 * Selects the cell at the given row/column intersection. 1957 * @param row the row 1958 * @param column the column 1959 */ 1960 public abstract void select(int row, TableColumn<S, ?> column); 1961 1962 1963 // --- clearAndSelect 1964 /** {@inheritDoc} */ 1965 @Override public void clearAndSelect(int row, TableColumnBase<S,?> column) { 1966 clearAndSelect(row, (TableColumn<S,?>) column); 1967 } 1968 1969 /** 1970 * Clears all selection, and then selects the cell at the given row/column 1971 * intersection. 1972 * @param row the row 1973 * @param column the column 1974 */ 1975 public abstract void clearAndSelect(int row, TableColumn<S,?> column); 1976 1977 1978 // --- clearSelection 1979 /** {@inheritDoc} */ 1980 @Override public void clearSelection(int row, TableColumnBase<S,?> column) { 1981 clearSelection(row, (TableColumn<S,?>) column); 1982 } 1983 1984 /** 1985 * Removes selection from the specified row/column position (in view indexes). 1986 * If this particular cell (or row if the column value is -1) is not selected, 1987 * nothing happens. 1988 * @param row the row 1989 * @param column the column 1990 */ 1991 public abstract void clearSelection(int row, TableColumn<S, ?> column); 1992 1993 /** {@inheritDoc} */ 1994 @Override public void selectRange(int minRow, TableColumnBase<S,?> minColumn, 1995 int maxRow, TableColumnBase<S,?> maxColumn) { 1996 final int minColumnIndex = tableView.getVisibleLeafIndex((TableColumn<S,?>)minColumn); 1997 final int maxColumnIndex = tableView.getVisibleLeafIndex((TableColumn<S,?>)maxColumn); 1998 for (int _row = minRow; _row <= maxRow; _row++) { 1999 for (int _col = minColumnIndex; _col <= maxColumnIndex; _col++) { 2000 select(_row, tableView.getVisibleLeafColumn(_col)); 2001 } 2002 } 2003 } 2004 2005 2006 2007 /*********************************************************************** 2008 * * 2009 * Public API * 2010 * * 2011 **********************************************************************/ 2012 2013 /** 2014 * Returns the TableView instance that this selection model is installed in. 2015 * @return the TableView 2016 */ 2017 public TableView<S> getTableView() { 2018 return tableView; 2019 } 2020 2021 /** 2022 * Convenience method that returns getTableView().getItems(). 2023 * @return The items list of the current TableView. 2024 */ 2025 protected List<S> getTableModel() { 2026 return tableView.getItems(); 2027 } 2028 2029 /** {@inheritDoc} */ 2030 @Override protected S getModelItem(int index) { 2031 if (index < 0 || index >= getItemCount()) return null; 2032 return tableView.getItems().get(index); 2033 } 2034 2035 /** {@inheritDoc} */ 2036 @Override protected int getItemCount() { 2037 return getTableModel().size(); 2038 } 2039 2040 /** {@inheritDoc} */ 2041 @Override public void focus(int row) { 2042 focus(row, null); 2043 } 2044 2045 /** {@inheritDoc} */ 2046 @Override public int getFocusedIndex() { 2047 return getFocusedCell().getRow(); 2048 } 2049 2050 2051 2052 /*********************************************************************** 2053 * * 2054 * Private implementation * 2055 * * 2056 **********************************************************************/ 2057 2058 void focus(int row, TableColumn<S,?> column) { 2059 focus(new TablePosition<>(getTableView(), row, column)); 2060 getTableView().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); 2061 } 2062 2063 void focus(TablePosition<S,?> pos) { 2064 if (blockFocusCall) return; 2065 if (getTableView().getFocusModel() == null) return; 2066 2067 getTableView().getFocusModel().focus(pos.getRow(), pos.getTableColumn()); 2068 } 2069 2070 TablePosition<S,?> getFocusedCell() { 2071 if (getTableView().getFocusModel() == null) { 2072 return new TablePosition<>(getTableView(), -1, null); 2073 } 2074 return getTableView().getFocusModel().getFocusedCell(); 2075 } 2076 } 2077 2078 2079 2080 /** 2081 * A primitive selection model implementation, using a List<Integer> to store all 2082 * selected indices. 2083 */ 2084 // package for testing 2085 static class TableViewArrayListSelectionModel<S> extends TableViewSelectionModel<S> { 2086 2087 private int itemCount = 0; 2088 2089 private final MappingChange.Map<TablePosition<S,?>,Integer> cellToIndicesMap = f -> f.getRow(); 2090 2091 /*********************************************************************** 2092 * * 2093 * Constructors * 2094 * * 2095 **********************************************************************/ 2096 2097 public TableViewArrayListSelectionModel(final TableView<S> tableView) { 2098 super(tableView); 2099 this.tableView = tableView; 2100 2101 this.itemsPropertyListener = new InvalidationListener() { 2102 private WeakReference<ObservableList<S>> weakItemsRef = new WeakReference<>(tableView.getItems()); 2103 2104 @Override public void invalidated(Observable observable) { 2105 ObservableList<S> oldItems = weakItemsRef.get(); 2106 weakItemsRef = new WeakReference<>(tableView.getItems()); 2107 updateItemsObserver(oldItems, tableView.getItems()); 2108 2109 ((SelectedItemsReadOnlyObservableList)getSelectedItems()).setItemsList(tableView.getItems()); 2110 } 2111 }; 2112 this.tableView.itemsProperty().addListener(itemsPropertyListener); 2113 2114 selectedCellsMap = new SelectedCellsMap<TablePosition<S,?>>(this::fireCustomSelectedCellsListChangeEvent) { 2115 @Override public boolean isCellSelectionEnabled() { 2116 return TableViewArrayListSelectionModel.this.isCellSelectionEnabled(); 2117 } 2118 }; 2119 2120 selectedCellsSeq = new ReadOnlyUnbackedObservableList<TablePosition<S,?>>() { 2121 @Override public TablePosition<S,?> get(int i) { 2122 return selectedCellsMap.get(i); 2123 } 2124 2125 @Override public int size() { 2126 return selectedCellsMap.size(); 2127 } 2128 }; 2129 // selectedCellsSeq.addListener((ListChangeListener<? super TablePosition<S,?>>) c -> { 2130 // ControlUtils.updateSelectedIndices(this, c); 2131 // }); 2132 2133 2134 /* 2135 * The following listener is used in conjunction with 2136 * SelectionModel.select(T obj) to allow for a developer to select 2137 * an item that is not actually in the data model. When this occurs, 2138 * we actively try to find an index that matches this object, going 2139 * so far as to actually watch for all changes to the items list, 2140 * rechecking each time. 2141 */ 2142 2143 // watching for changes to the items list content 2144 ObservableList<S> items = getTableView().getItems(); 2145 if (items != null) { 2146 ((SelectedItemsReadOnlyObservableList)getSelectedItems()).setItemsList(items); 2147 items.addListener(weakItemsContentListener); 2148 } 2149 2150 2151 updateItemCount(); 2152 2153 updateDefaultSelection(); 2154 2155 cellSelectionEnabledProperty().addListener(o -> { 2156 updateDefaultSelection(); 2157 TableCellBehaviorBase.setAnchor(tableView, getFocusedCell(), true); 2158 }); 2159 } 2160 2161 private void dispose() { 2162 this.tableView.itemsProperty().removeListener(itemsPropertyListener); 2163 2164 ObservableList<S> items = getTableView().getItems(); 2165 if (items != null) { 2166 items.removeListener(weakItemsContentListener); 2167 } 2168 } 2169 2170 private final TableView<S> tableView; 2171 2172 final InvalidationListener itemsPropertyListener; 2173 2174 final ListChangeListener<S> itemsContentListener = c -> { 2175 updateItemCount(); 2176 2177 List<S> items1 = getTableModel(); 2178 boolean doSelectionUpdate = true; 2179 2180 while (c.next()) { 2181 if (c.wasReplaced() || c.getAddedSize() == getItemCount()) { 2182 this.selectedItemChange = c; 2183 updateDefaultSelection(); 2184 this.selectedItemChange = null; 2185 return; 2186 } 2187 2188 final S selectedItem = getSelectedItem(); 2189 final int selectedIndex = getSelectedIndex(); 2190 2191 if (items1 == null || items1.isEmpty()) { 2192 clearSelection(); 2193 } else if (getSelectedIndex() == -1 && getSelectedItem() != null) { 2194 int newIndex = items1.indexOf(getSelectedItem()); 2195 if (newIndex != -1) { 2196 setSelectedIndex(newIndex); 2197 doSelectionUpdate = false; 2198 } 2199 } else if (c.wasRemoved() && 2200 c.getRemovedSize() == 1 && 2201 ! c.wasAdded() && 2202 selectedItem != null && 2203 selectedItem.equals(c.getRemoved().get(0))) { 2204 // Bug fix for RT-28637 2205 if (getSelectedIndex() < getItemCount()) { 2206 final int previousRow = selectedIndex == 0 ? 0 : selectedIndex - 1; 2207 S newSelectedItem = getModelItem(previousRow); 2208 if (! selectedItem.equals(newSelectedItem)) { 2209 clearAndSelect(previousRow); 2210 } 2211 } 2212 } 2213 } 2214 2215 if (doSelectionUpdate) { 2216 updateSelection(c); 2217 } 2218 }; 2219 2220 final WeakListChangeListener<S> weakItemsContentListener 2221 = new WeakListChangeListener<>(itemsContentListener); 2222 2223 2224 2225 /*********************************************************************** 2226 * * 2227 * Observable properties (and getters/setters) * 2228 * * 2229 **********************************************************************/ 2230 2231 // the only 'proper' internal data structure, selectedItems and selectedIndices 2232 // are both 'read-only and unbacked'. 2233 private final SelectedCellsMap<TablePosition<S,?>> selectedCellsMap; 2234 2235 // we create a ReadOnlyUnbackedObservableList of selectedCells here so 2236 // that we can fire custom list change events. 2237 private final ReadOnlyUnbackedObservableList<TablePosition<S,?>> selectedCellsSeq; 2238 @Override public ObservableList<TablePosition> getSelectedCells() { 2239 return (ObservableList<TablePosition>)(Object)selectedCellsSeq; 2240 } 2241 2242 2243 2244 /*********************************************************************** 2245 * * 2246 * Internal properties * 2247 * * 2248 **********************************************************************/ 2249 2250 private int previousModelSize = 0; 2251 2252 // Listen to changes in the tableview items list, such that when it 2253 // changes we can update the selected indices list to refer to the 2254 // new indices. 2255 private void updateSelection(ListChangeListener.Change<? extends S> c) { 2256 c.reset(); 2257 2258 int shift = 0; 2259 int startRow = -1; 2260 while (c.next()) { 2261 if (c.wasReplaced()) { 2262 if (c.getList().isEmpty()) { 2263 // the entire items list was emptied - clear selection 2264 clearSelection(); 2265 } else { 2266 int index = getSelectedIndex(); 2267 2268 if (previousModelSize == c.getRemovedSize()) { 2269 // all items were removed from the model 2270 clearSelection(); 2271 } else if (index < getItemCount() && index >= 0) { 2272 // Fix for RT-18969: the list had setAll called on it 2273 // Use of makeAtomic is a fix for RT-20945 2274 startAtomic(); 2275 clearSelection(index); 2276 stopAtomic(); 2277 select(index); 2278 } else { 2279 // Fix for RT-22079 2280 clearSelection(); 2281 } 2282 } 2283 } else if (c.wasAdded() || c.wasRemoved()) { 2284 startRow = c.getFrom(); 2285 shift += c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize(); 2286 } else if (c.wasPermutated()) { 2287 // General approach: 2288 // -- detected a sort has happened 2289 // -- Create a permutation lookup map (1) 2290 // -- dump all the selected indices into a list (2) 2291 // -- create a list containing the new indices (3) 2292 // -- for each previously-selected index (4) 2293 // -- if index is in the permutation lookup map 2294 // -- add the new index to the new indices list 2295 // -- Perform batch selection (5) 2296 2297 startAtomic(); 2298 2299 final int oldSelectedIndex = getSelectedIndex(); 2300 2301 // (1) 2302 int length = c.getTo() - c.getFrom(); 2303 HashMap<Integer, Integer> pMap = new HashMap<> (length); 2304 for (int i = c.getFrom(); i < c.getTo(); i++) { 2305 pMap.put(i, c.getPermutation(i)); 2306 } 2307 2308 // (2) 2309 List<TablePosition<S,?>> selectedIndices = new ArrayList<>((ObservableList<TablePosition<S,?>>)(Object)getSelectedCells()); 2310 2311 // (3) 2312 List<TablePosition<S,?>> newIndices = new ArrayList<>(selectedIndices.size()); 2313 2314 // (4) 2315 boolean selectionIndicesChanged = false; 2316 for (int i = 0; i < selectedIndices.size(); i++) { 2317 final TablePosition<S,?> oldIndex = selectedIndices.get(i); 2318 final int oldRow = oldIndex.getRow(); 2319 2320 if (pMap.containsKey(oldRow)) { 2321 int newIndex = pMap.get(oldRow); 2322 2323 selectionIndicesChanged = selectionIndicesChanged || newIndex != oldRow; 2324 2325 newIndices.add(new TablePosition<>(oldIndex.getTableView(), newIndex, oldIndex.getTableColumn())); 2326 } 2327 } 2328 2329 if (selectionIndicesChanged) { 2330 // (5) 2331 quietClearSelection(); 2332 stopAtomic(); 2333 2334 selectedCellsMap.setAll(newIndices); 2335 2336 if (oldSelectedIndex >= 0 && oldSelectedIndex < itemCount) { 2337 int newIndex = c.getPermutation(oldSelectedIndex); 2338 setSelectedIndex(newIndex); 2339 focus(newIndex); 2340 } 2341 } else { 2342 stopAtomic(); 2343 } 2344 } 2345 } 2346 2347 TablePosition<S,?> anchor = TableCellBehavior.getAnchor(tableView, null); 2348 if (shift != 0 && startRow >= 0 && anchor != null && (c.wasRemoved() || c.wasAdded())) { 2349 if (isSelected(anchor.getRow(), anchor.getTableColumn())) { 2350 TablePosition<S,?> newAnchor = new TablePosition<>(tableView, anchor.getRow() + shift, anchor.getTableColumn()); 2351 TableCellBehavior.setAnchor(tableView, newAnchor, false); 2352 } 2353 } 2354 2355 shiftSelection(startRow, shift, new Callback<ShiftParams, Void>() { 2356 @Override public Void call(ShiftParams param) { 2357 2358 // we make the shifts atomic, as otherwise listeners to 2359 // the items / indices lists get a lot of intermediate 2360 // noise. They eventually get the summary event fired 2361 // from within shiftSelection, so this is ok. 2362 startAtomic(); 2363 2364 final int clearIndex = param.getClearIndex(); 2365 final int setIndex = param.getSetIndex(); 2366 TablePosition<S,?> oldTP = null; 2367 if (clearIndex > -1) { 2368 for (int i = 0; i < selectedCellsMap.size(); i++) { 2369 TablePosition<S,?> tp = selectedCellsMap.get(i); 2370 if (tp.getRow() == clearIndex) { 2371 oldTP = tp; 2372 selectedCellsMap.remove(tp); 2373 } else if (tp.getRow() == setIndex && !param.isSelected()) { 2374 selectedCellsMap.remove(tp); 2375 } 2376 } 2377 } 2378 2379 if (oldTP != null && param.isSelected()) { 2380 TablePosition<S,?> newTP = new TablePosition<>( 2381 tableView, param.getSetIndex(), oldTP.getTableColumn()); 2382 2383 selectedCellsMap.add(newTP); 2384 } 2385 2386 stopAtomic(); 2387 2388 return null; 2389 } 2390 }); 2391 2392 previousModelSize = getItemCount(); 2393 } 2394 2395 /*********************************************************************** 2396 * * 2397 * Public selection API * 2398 * * 2399 **********************************************************************/ 2400 2401 @Override public void clearAndSelect(int row) { 2402 clearAndSelect(row, null); 2403 } 2404 2405 @Override public void clearAndSelect(int row, TableColumn<S,?> column) { 2406 if (row < 0 || row >= getItemCount()) return; 2407 2408 final TablePosition<S,?> newTablePosition = new TablePosition<>(getTableView(), row, column); 2409 final boolean isCellSelectionEnabled = isCellSelectionEnabled(); 2410 2411 // replace the anchor 2412 TableCellBehavior.setAnchor(tableView, newTablePosition, false); 2413 2414 // firstly we make a copy of the selection, so that we can send out 2415 // the correct details in the selection change event. 2416 List<TablePosition<S,?>> previousSelection = new ArrayList<>(selectedCellsMap.getSelectedCells()); 2417 2418 // secondly we check if we can short-circuit out of here because the new selection 2419 // equals the current selection 2420 final boolean wasSelected = isSelected(row, column); 2421 if (wasSelected && previousSelection.size() == 1) { 2422 // before we return, we double-check that the selected item 2423 // is equal to the item in the given index 2424 TablePosition<S,?> selectedCell = getSelectedCells().get(0); 2425 if (getSelectedItem() == getModelItem(row)) { 2426 if (selectedCell.getRow() == row && selectedCell.getTableColumn() == column) { 2427 return; 2428 } 2429 } 2430 } 2431 2432 // RT-32411 We used to call quietClearSelection() here, but this 2433 // resulted in the selectedItems and selectedIndices lists never 2434 // reporting that they were empty. 2435 // makeAtomic toggle added to resolve RT-32618 2436 startAtomic(); 2437 2438 // then clear the current selection 2439 clearSelection(); 2440 2441 // and select the new cell 2442 select(row, column); 2443 2444 stopAtomic(); 2445 2446 2447 // We remove the new selection from the list seeing as it is not removed. 2448 if (isCellSelectionEnabled) { 2449 previousSelection.remove(newTablePosition); 2450 } else { 2451 for (TablePosition<S,?> tp : previousSelection) { 2452 if (tp.getRow() == row) { 2453 previousSelection.remove(tp); 2454 break; 2455 } 2456 } 2457 } 2458 2459 // fire off a single add/remove/replace notification (rather than 2460 // individual remove and add notifications) - see RT-33324 2461 ListChangeListener.Change<TablePosition<S, ?>> change; 2462 2463 /* 2464 * getFrom() documentation: 2465 * If wasAdded is true, the interval contains all the values that were added. 2466 * If wasPermutated is true, the interval marks the values that were permutated. 2467 * If wasRemoved is true and wasAdded is false, getFrom() and getTo() should 2468 * return the same number - the place where the removed elements were positioned in the list. 2469 */ 2470 if (wasSelected) { 2471 change = ControlUtils.buildClearAndSelectChange(selectedCellsSeq, previousSelection, row); 2472 } else { 2473 final int changeIndex = isCellSelectionEnabled ? 0 : Math.max(0, selectedCellsSeq.indexOf(newTablePosition)); 2474 final int changeSize = isCellSelectionEnabled ? getSelectedCells().size() : 1; 2475 change = new NonIterableChange.GenericAddRemoveChange<>( 2476 changeIndex, changeIndex + changeSize, previousSelection, selectedCellsSeq); 2477 // selectedCellsSeq._beginChange(); 2478 // selectedCellsSeq._nextAdd(changeIndex, changeIndex + changeSize); 2479 // selectedCellsSeq._nextRemove(changeIndex, previousSelection); 2480 // selectedCellsSeq._endChange(); 2481 } 2482 fireCustomSelectedCellsListChangeEvent(change); 2483 } 2484 2485 @Override public void select(int row) { 2486 select(row, null); 2487 } 2488 2489 @Override 2490 public void select(int row, TableColumn<S,?> column) { 2491 if (row < 0 || row >= getItemCount()) return; 2492 2493 // if I'm in cell selection mode but the column is null, select each 2494 // of the contained cells individually 2495 if (isCellSelectionEnabled() && column == null) { 2496 List<TableColumn<S,?>> columns = getTableView().getVisibleLeafColumns(); 2497 for (int i = 0; i < columns.size(); i++) { 2498 select(row, columns.get(i)); 2499 } 2500 return; 2501 } 2502 2503 if (TableCellBehavior.hasDefaultAnchor(tableView)) { 2504 TableCellBehavior.removeAnchor(tableView); 2505 } 2506 2507 if (getSelectionMode() == SelectionMode.SINGLE) { 2508 quietClearSelection(); 2509 } 2510 selectedCellsMap.add(new TablePosition<>(getTableView(), row, column)); 2511 2512 updateSelectedIndex(row); 2513 focus(row, column); 2514 } 2515 2516 @Override public void select(S obj) { 2517 if (obj == null && getSelectionMode() == SelectionMode.SINGLE) { 2518 clearSelection(); 2519 return; 2520 } 2521 2522 // We have no option but to iterate through the model and select the 2523 // first occurrence of the given object. Once we find the first one, we 2524 // don't proceed to select any others. 2525 S rowObj = null; 2526 for (int i = 0; i < getItemCount(); i++) { 2527 rowObj = getModelItem(i); 2528 if (rowObj == null) continue; 2529 2530 if (rowObj.equals(obj)) { 2531 if (isSelected(i)) { 2532 return; 2533 } 2534 2535 if (getSelectionMode() == SelectionMode.SINGLE) { 2536 quietClearSelection(); 2537 } 2538 2539 select(i); 2540 return; 2541 } 2542 } 2543 2544 // if we are here, we did not find the item in the entire data model. 2545 // Even still, we allow for this item to be set to the give object. 2546 // We expect that in concrete subclasses of this class we observe the 2547 // data model such that we check to see if the given item exists in it, 2548 // whilst SelectedIndex == -1 && SelectedItem != null. 2549 setSelectedIndex(-1); 2550 setSelectedItem(obj); 2551 } 2552 2553 @Override public void selectIndices(int row, int... rows) { 2554 if (rows == null) { 2555 select(row); 2556 return; 2557 } 2558 2559 /* 2560 * Performance optimisation - if multiple selection is disabled, only 2561 * process the end-most row index. 2562 */ 2563 int rowCount = getItemCount(); 2564 2565 if (getSelectionMode() == SelectionMode.SINGLE) { 2566 quietClearSelection(); 2567 2568 for (int i = rows.length - 1; i >= 0; i--) { 2569 int index = rows[i]; 2570 if (index >= 0 && index < rowCount) { 2571 select(index); 2572 break; 2573 } 2574 } 2575 2576 if (selectedCellsMap.isEmpty()) { 2577 if (row > 0 && row < rowCount) { 2578 select(row); 2579 } 2580 } 2581 } else { 2582 int lastIndex = -1; 2583 Set<TablePosition<S,?>> positions = new LinkedHashSet<>(); 2584 2585 // --- firstly, we special-case the non-varargs 'row' argument 2586 if (row >= 0 && row < rowCount) { 2587 // if I'm in cell selection mode, we want to select each 2588 // of the contained cells individually 2589 if (isCellSelectionEnabled()) { 2590 List<TableColumn<S,?>> columns = getTableView().getVisibleLeafColumns(); 2591 for (int column = 0; column < columns.size(); column++) { 2592 if (! selectedCellsMap.isSelected(row, column)) { 2593 positions.add(new TablePosition<>(getTableView(), row, columns.get(column))); 2594 lastIndex = row; 2595 } 2596 } 2597 } else { 2598 boolean match = selectedCellsMap.isSelected(row, -1); 2599 if (!match) { 2600 positions.add(new TablePosition<>(getTableView(), row, null)); 2601 } 2602 } 2603 2604 lastIndex = row; 2605 } 2606 2607 // --- now we iterate through all varargs values 2608 for (int i = 0; i < rows.length; i++) { 2609 int index = rows[i]; 2610 if (index < 0 || index >= rowCount) continue; 2611 lastIndex = index; 2612 2613 if (isCellSelectionEnabled()) { 2614 List<TableColumn<S,?>> columns = getTableView().getVisibleLeafColumns(); 2615 for (int column = 0; column < columns.size(); column++) { 2616 if (! selectedCellsMap.isSelected(index, column)) { 2617 positions.add(new TablePosition<>(getTableView(), index, columns.get(column))); 2618 lastIndex = index; 2619 } 2620 } 2621 } else { 2622 if (! selectedCellsMap.isSelected(index, -1)) { 2623 // if we are here then we have successfully gotten through the for-loop above 2624 positions.add(new TablePosition<>(getTableView(), index, null)); 2625 } 2626 } 2627 } 2628 2629 selectedCellsMap.addAll(positions); 2630 2631 if (lastIndex != -1) { 2632 select(lastIndex); 2633 } 2634 } 2635 } 2636 2637 @Override public void selectAll() { 2638 if (getSelectionMode() == SelectionMode.SINGLE) return; 2639 2640 if (isCellSelectionEnabled()) { 2641 List<TablePosition<S,?>> indices = new ArrayList<>(); 2642 TableColumn<S,?> column; 2643 TablePosition<S,?> tp = null; 2644 for (int col = 0; col < getTableView().getVisibleLeafColumns().size(); col++) { 2645 column = getTableView().getVisibleLeafColumns().get(col); 2646 for (int row = 0; row < getItemCount(); row++) { 2647 tp = new TablePosition<>(getTableView(), row, column); 2648 indices.add(tp); 2649 } 2650 } 2651 selectedCellsMap.setAll(indices); 2652 2653 if (tp != null) { 2654 select(tp.getRow(), tp.getTableColumn()); 2655 focus(tp.getRow(), tp.getTableColumn()); 2656 } 2657 } else { 2658 List<TablePosition<S,?>> indices = new ArrayList<>(); 2659 for (int i = 0; i < getItemCount(); i++) { 2660 indices.add(new TablePosition<>(getTableView(), i, null)); 2661 } 2662 selectedCellsMap.setAll(indices); 2663 2664 int focusedIndex = getFocusedIndex(); 2665 if (focusedIndex == -1) { 2666 final int itemCount = getItemCount(); 2667 if (itemCount > 0) { 2668 select(itemCount - 1); 2669 focus(indices.get(indices.size() - 1)); 2670 } 2671 } else { 2672 select(focusedIndex); 2673 focus(focusedIndex); 2674 } 2675 } 2676 } 2677 2678 @Override public void selectRange(int minRow, TableColumnBase<S,?> minColumn, 2679 int maxRow, TableColumnBase<S,?> maxColumn) { 2680 if (getSelectionMode() == SelectionMode.SINGLE) { 2681 quietClearSelection(); 2682 select(maxRow, maxColumn); 2683 return; 2684 } 2685 2686 startAtomic(); 2687 2688 final int itemCount = getItemCount(); 2689 final boolean isCellSelectionEnabled = isCellSelectionEnabled(); 2690 2691 final int minColumnIndex = tableView.getVisibleLeafIndex((TableColumn<S,?>)minColumn); 2692 final int maxColumnIndex = tableView.getVisibleLeafIndex((TableColumn<S,?>)maxColumn); 2693 final int _minColumnIndex = Math.min(minColumnIndex, maxColumnIndex); 2694 final int _maxColumnIndex = Math.max(minColumnIndex, maxColumnIndex); 2695 2696 final int _minRow = Math.min(minRow, maxRow); 2697 final int _maxRow = Math.max(minRow, maxRow); 2698 2699 List<TablePosition<S,?>> cellsToSelect = new ArrayList<>(); 2700 2701 for (int _row = _minRow; _row <= _maxRow; _row++) { 2702 // begin copy/paste of select(int, column) method (with some 2703 // slight modifications) 2704 if (_row < 0 || _row >= itemCount) continue; 2705 2706 if (! isCellSelectionEnabled) { 2707 cellsToSelect.add(new TablePosition<>(tableView, _row, (TableColumn<S,?>)minColumn)); 2708 } else { 2709 for (int _col = _minColumnIndex; _col <= _maxColumnIndex; _col++) { 2710 final TableColumn<S, ?> column = tableView.getVisibleLeafColumn(_col); 2711 2712 // if I'm in cell selection mode but the column is null, I don't want 2713 // to select the whole row instead... 2714 if (column == null && isCellSelectionEnabled) continue; 2715 2716 cellsToSelect.add(new TablePosition<>(tableView, _row, column)); 2717 // end copy/paste 2718 } 2719 } 2720 } 2721 2722 // to prevent duplication we remove all currently selected cells from 2723 // our list of cells to select. 2724 cellsToSelect.removeAll(getSelectedCells()); 2725 2726 selectedCellsMap.addAll(cellsToSelect); 2727 stopAtomic(); 2728 2729 // fire off events. 2730 // Note that focus and selection always goes to maxRow, not _maxRow. 2731 updateSelectedIndex(maxRow); 2732 focus(maxRow, (TableColumn<S,?>)maxColumn); 2733 2734 final TableColumn<S,?> startColumn = (TableColumn<S,?>)minColumn; 2735 final TableColumn<S,?> endColumn = isCellSelectionEnabled ? (TableColumn<S,?>)maxColumn : startColumn; 2736 final int startChangeIndex = selectedCellsMap.indexOf(new TablePosition<>(tableView, minRow, startColumn)); 2737 final int endChangeIndex = selectedCellsMap.indexOf(new TablePosition<>(tableView, maxRow, endColumn)); 2738 2739 if (startChangeIndex > -1 && endChangeIndex > -1) { 2740 final int startIndex = Math.min(startChangeIndex, endChangeIndex); 2741 final int endIndex = Math.max(startChangeIndex, endChangeIndex); 2742 2743 ListChangeListener.Change c = new NonIterableChange.SimpleAddChange<>(startIndex, endIndex + 1, selectedCellsSeq); 2744 fireCustomSelectedCellsListChangeEvent(c); 2745 // selectedCellsSeq.fireChange(() -> selectedCellsSeq._nextAdd(startIndex, endIndex + 1)); 2746 } 2747 } 2748 2749 @Override public void clearSelection(int index) { 2750 clearSelection(index, null); 2751 } 2752 2753 @Override 2754 public void clearSelection(int row, TableColumn<S,?> column) { 2755 clearSelection(new TablePosition<>(getTableView(), row, column)); 2756 } 2757 2758 private void clearSelection(TablePosition<S,?> tp) { 2759 final boolean csMode = isCellSelectionEnabled(); 2760 final int row = tp.getRow(); 2761 final boolean columnIsNull = tp.getTableColumn() == null; 2762 2763 List<TablePosition> toRemove = new ArrayList<>(); 2764 for (TablePosition pos : getSelectedCells()) { 2765 if (!csMode) { 2766 if (pos.getRow() == row) { 2767 toRemove.add(pos); 2768 break; 2769 } 2770 } else { 2771 if (columnIsNull && pos.getRow() == row) { 2772 // if we are in cell selection mode and the column is null, 2773 // we remove all items in the row 2774 toRemove.add(pos); 2775 } else if (pos.equals(tp)) { 2776 toRemove.add(tp); 2777 break; 2778 } 2779 } 2780 } 2781 toRemove.stream().forEach(selectedCellsMap::remove); 2782 2783 if (isEmpty() && ! isAtomic()) { 2784 updateSelectedIndex(-1); 2785 selectedCellsMap.clear(); 2786 } 2787 } 2788 2789 @Override public void clearSelection() { 2790 final List<TablePosition<S,?>> removed = new ArrayList<>((Collection)getSelectedCells()); 2791 2792 quietClearSelection(); 2793 2794 if (! isAtomic()) { 2795 updateSelectedIndex(-1); 2796 focus(-1); 2797 2798 if (!removed.isEmpty()) { 2799 ListChangeListener.Change<TablePosition<S, ?>> c = new NonIterableChange<TablePosition<S, ?>>(0, 0, selectedCellsSeq) { 2800 @Override public List<TablePosition<S, ?>> getRemoved() { 2801 return removed; 2802 } 2803 }; 2804 fireCustomSelectedCellsListChangeEvent(c); 2805 // selectedCellsSeq.fireChange(() -> selectedCellsSeq._nextRemove(0, removed)); 2806 } 2807 } 2808 } 2809 2810 private void quietClearSelection() { 2811 startAtomic(); 2812 selectedCellsMap.clear(); 2813 stopAtomic(); 2814 } 2815 2816 @Override public boolean isSelected(int index) { 2817 return isSelected(index, null); 2818 } 2819 2820 @Override 2821 public boolean isSelected(int row, TableColumn<S,?> column) { 2822 // When in cell selection mode, if the column is null, then we interpret 2823 // the users query to be asking if _all_ of the cells in the row are selected, 2824 // rather than if _any_ of the cells in the row are selected. 2825 final boolean isCellSelectionEnabled = isCellSelectionEnabled(); 2826 if (isCellSelectionEnabled && column == null) { 2827 int columnCount = tableView.getVisibleLeafColumns().size(); 2828 for (int col = 0; col < columnCount; col++) { 2829 if (!selectedCellsMap.isSelected(row, col)) { 2830 return false; 2831 } 2832 } 2833 return true; 2834 } else { 2835 int columnIndex = !isCellSelectionEnabled || column == null ? -1 : tableView.getVisibleLeafIndex(column); 2836 return selectedCellsMap.isSelected(row, columnIndex); 2837 } 2838 } 2839 2840 @Override public boolean isEmpty() { 2841 return selectedCellsMap.isEmpty(); 2842 } 2843 2844 @Override public void selectPrevious() { 2845 if (isCellSelectionEnabled()) { 2846 // in cell selection mode, we have to wrap around, going from 2847 // right-to-left, and then wrapping to the end of the previous line 2848 TablePosition<S,?> pos = getFocusedCell(); 2849 if (pos.getColumn() - 1 >= 0) { 2850 // go to previous row 2851 select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1)); 2852 } else if (pos.getRow() < getItemCount() - 1) { 2853 // wrap to end of previous row 2854 select(pos.getRow() - 1, getTableColumn(getTableView().getVisibleLeafColumns().size() - 1)); 2855 } 2856 } else { 2857 int focusIndex = getFocusedIndex(); 2858 if (focusIndex == -1) { 2859 select(getItemCount() - 1); 2860 } else if (focusIndex > 0) { 2861 select(focusIndex - 1); 2862 } 2863 } 2864 } 2865 2866 @Override public void selectNext() { 2867 if (isCellSelectionEnabled()) { 2868 // in cell selection mode, we have to wrap around, going from 2869 // left-to-right, and then wrapping to the start of the next line 2870 TablePosition<S,?> pos = getFocusedCell(); 2871 if (pos.getColumn() + 1 < getTableView().getVisibleLeafColumns().size()) { 2872 // go to next column 2873 select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1)); 2874 } else if (pos.getRow() < getItemCount() - 1) { 2875 // wrap to start of next row 2876 select(pos.getRow() + 1, getTableColumn(0)); 2877 } 2878 } else { 2879 int focusIndex = getFocusedIndex(); 2880 if (focusIndex == -1) { 2881 select(0); 2882 } else if (focusIndex < getItemCount() -1) { 2883 select(focusIndex + 1); 2884 } 2885 } 2886 } 2887 2888 @Override public void selectAboveCell() { 2889 TablePosition<S,?> pos = getFocusedCell(); 2890 if (pos.getRow() == -1) { 2891 select(getItemCount() - 1); 2892 } else if (pos.getRow() > 0) { 2893 select(pos.getRow() - 1, pos.getTableColumn()); 2894 } 2895 } 2896 2897 @Override public void selectBelowCell() { 2898 TablePosition<S,?> pos = getFocusedCell(); 2899 2900 if (pos.getRow() == -1) { 2901 select(0); 2902 } else if (pos.getRow() < getItemCount() -1) { 2903 select(pos.getRow() + 1, pos.getTableColumn()); 2904 } 2905 } 2906 2907 @Override public void selectFirst() { 2908 TablePosition<S,?> focusedCell = getFocusedCell(); 2909 2910 if (getSelectionMode() == SelectionMode.SINGLE) { 2911 quietClearSelection(); 2912 } 2913 2914 if (getItemCount() > 0) { 2915 if (isCellSelectionEnabled()) { 2916 select(0, focusedCell.getTableColumn()); 2917 } else { 2918 select(0); 2919 } 2920 } 2921 } 2922 2923 @Override public void selectLast() { 2924 TablePosition<S,?> focusedCell = getFocusedCell(); 2925 2926 if (getSelectionMode() == SelectionMode.SINGLE) { 2927 quietClearSelection(); 2928 } 2929 2930 int numItems = getItemCount(); 2931 if (numItems > 0 && getSelectedIndex() < numItems - 1) { 2932 if (isCellSelectionEnabled()) { 2933 select(numItems - 1, focusedCell.getTableColumn()); 2934 } else { 2935 select(numItems - 1); 2936 } 2937 } 2938 } 2939 2940 @Override 2941 public void selectLeftCell() { 2942 if (! isCellSelectionEnabled()) return; 2943 2944 TablePosition<S,?> pos = getFocusedCell(); 2945 if (pos.getColumn() - 1 >= 0) { 2946 select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1)); 2947 } 2948 } 2949 2950 @Override 2951 public void selectRightCell() { 2952 if (! isCellSelectionEnabled()) return; 2953 2954 TablePosition<S,?> pos = getFocusedCell(); 2955 if (pos.getColumn() + 1 < getTableView().getVisibleLeafColumns().size()) { 2956 select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1)); 2957 } 2958 } 2959 2960 2961 2962 /*********************************************************************** 2963 * * 2964 * Support code * 2965 * * 2966 **********************************************************************/ 2967 2968 private void updateItemsObserver(ObservableList<S> oldList, ObservableList<S> newList) { 2969 // the items list has changed, we need to observe 2970 // the new list, and remove any observer we had from the old list 2971 if (oldList != null) { 2972 oldList.removeListener(weakItemsContentListener); 2973 } 2974 if (newList != null) { 2975 newList.addListener(weakItemsContentListener); 2976 } 2977 2978 updateItemCount(); 2979 updateDefaultSelection(); 2980 } 2981 2982 private void updateDefaultSelection() { 2983 // when the items list totally changes, we should clear out 2984 // the selection 2985 int newSelectionIndex = -1; 2986 if (tableView.getItems() != null) { 2987 S selectedItem = getSelectedItem(); 2988 if (selectedItem != null) { 2989 newSelectionIndex = tableView.getItems().indexOf(selectedItem); 2990 } 2991 } 2992 2993 clearSelection(); 2994 select(newSelectionIndex, isCellSelectionEnabled() ? getTableColumn(0) : null); 2995 } 2996 2997 private TableColumn<S,?> getTableColumn(int pos) { 2998 return getTableView().getVisibleLeafColumn(pos); 2999 } 3000 3001 // Gets a table column to the left or right of the current one, given an offset 3002 private TableColumn<S,?> getTableColumn(TableColumn<S,?> column, int offset) { 3003 int columnIndex = getTableView().getVisibleLeafIndex(column); 3004 int newColumnIndex = columnIndex + offset; 3005 return getTableView().getVisibleLeafColumn(newColumnIndex); 3006 } 3007 3008 private void updateSelectedIndex(int row) { 3009 setSelectedIndex(row); 3010 setSelectedItem(getModelItem(row)); 3011 } 3012 3013 /** {@inheritDoc} */ 3014 @Override protected int getItemCount() { 3015 return itemCount; 3016 } 3017 3018 private void updateItemCount() { 3019 if (tableView == null) { 3020 itemCount = -1; 3021 } else { 3022 List<S> items = getTableModel(); 3023 itemCount = items == null ? -1 : items.size(); 3024 } 3025 } 3026 3027 private void fireCustomSelectedCellsListChangeEvent(ListChangeListener.Change<? extends TablePosition<S,?>> c) { 3028 ControlUtils.updateSelectedIndices(this, c); 3029 3030 if (isAtomic()) { 3031 return; 3032 } 3033 3034 selectedCellsSeq.callObservers(new MappingChange<>(c, MappingChange.NOOP_MAP, selectedCellsSeq)); 3035 } 3036 } 3037 3038 3039 3040 3041 /** 3042 * A {@link FocusModel} with additional functionality to support the requirements 3043 * of a TableView control. 3044 * 3045 * @see TableView 3046 * @since JavaFX 2.0 3047 */ 3048 public static class TableViewFocusModel<S> extends TableFocusModel<S, TableColumn<S, ?>> { 3049 3050 private final TableView<S> tableView; 3051 3052 private final TablePosition<S,?> EMPTY_CELL; 3053 3054 /** 3055 * Creates a default TableViewFocusModel instance that will be used to 3056 * manage focus of the provided TableView control. 3057 * 3058 * @param tableView The tableView upon which this focus model operates. 3059 * @throws NullPointerException The TableView argument can not be null. 3060 */ 3061 public TableViewFocusModel(final TableView<S> tableView) { 3062 if (tableView == null) { 3063 throw new NullPointerException("TableView can not be null"); 3064 } 3065 3066 this.tableView = tableView; 3067 this.EMPTY_CELL = new TablePosition<>(tableView, -1, null); 3068 3069 itemsObserver = new InvalidationListener() { 3070 private WeakReference<ObservableList<S>> weakItemsRef = new WeakReference<>(tableView.getItems()); 3071 3072 @Override public void invalidated(Observable observable) { 3073 ObservableList<S> oldItems = weakItemsRef.get(); 3074 weakItemsRef = new WeakReference<>(tableView.getItems()); 3075 updateItemsObserver(oldItems, tableView.getItems()); 3076 } 3077 }; 3078 this.tableView.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); 3079 if (tableView.getItems() != null) { 3080 this.tableView.getItems().addListener(weakItemsContentListener); 3081 } 3082 3083 updateDefaultFocus(); 3084 3085 focusedCellProperty().addListener(o -> { 3086 tableView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); 3087 }); 3088 } 3089 3090 private final InvalidationListener itemsObserver; 3091 3092 // Listen to changes in the tableview items list, such that when it 3093 // changes we can update the focused index to refer to the new indices. 3094 private final ListChangeListener<S> itemsContentListener = c -> { 3095 c.next(); 3096 3097 if (c.wasReplaced() || c.getAddedSize() == getItemCount()) { 3098 updateDefaultFocus(); 3099 return; 3100 } 3101 3102 TablePosition<S,?> focusedCell = getFocusedCell(); 3103 final int focusedIndex = focusedCell.getRow(); 3104 if (focusedIndex == -1 || c.getFrom() > focusedIndex) { 3105 return; 3106 } 3107 3108 c.reset(); 3109 boolean added = false; 3110 boolean removed = false; 3111 int addedSize = 0; 3112 int removedSize = 0; 3113 while (c.next()) { 3114 added |= c.wasAdded(); 3115 removed |= c.wasRemoved(); 3116 addedSize += c.getAddedSize(); 3117 removedSize += c.getRemovedSize(); 3118 } 3119 3120 if (added && ! removed) { 3121 if (addedSize < c.getList().size()) { 3122 final int newFocusIndex = Math.min(getItemCount() - 1, getFocusedIndex() + addedSize); 3123 focus(newFocusIndex, focusedCell.getTableColumn()); 3124 } 3125 } else if (!added && removed) { 3126 final int newFocusIndex = Math.max(0, getFocusedIndex() - removedSize); 3127 if (newFocusIndex < 0) { 3128 focus(0, focusedCell.getTableColumn()); 3129 } else { 3130 focus(newFocusIndex, focusedCell.getTableColumn()); 3131 } 3132 } 3133 }; 3134 3135 private WeakListChangeListener<S> weakItemsContentListener 3136 = new WeakListChangeListener<>(itemsContentListener); 3137 3138 private void updateItemsObserver(ObservableList<S> oldList, ObservableList<S> newList) { 3139 // the tableview items list has changed, we need to observe 3140 // the new list, and remove any observer we had from the old list 3141 if (oldList != null) oldList.removeListener(weakItemsContentListener); 3142 if (newList != null) newList.addListener(weakItemsContentListener); 3143 3144 updateDefaultFocus(); 3145 } 3146 3147 /** {@inheritDoc} */ 3148 @Override protected int getItemCount() { 3149 if (tableView.getItems() == null) return -1; 3150 return tableView.getItems().size(); 3151 } 3152 3153 /** {@inheritDoc} */ 3154 @Override protected S getModelItem(int index) { 3155 if (tableView.getItems() == null) return null; 3156 3157 if (index < 0 || index >= getItemCount()) return null; 3158 3159 return tableView.getItems().get(index); 3160 } 3161 3162 /** 3163 * The position of the current item in the TableView which has the focus. 3164 */ 3165 private ReadOnlyObjectWrapper<TablePosition> focusedCell; 3166 public final ReadOnlyObjectProperty<TablePosition> focusedCellProperty() { 3167 return focusedCellPropertyImpl().getReadOnlyProperty(); 3168 } 3169 private void setFocusedCell(TablePosition value) { focusedCellPropertyImpl().set(value); } 3170 public final TablePosition getFocusedCell() { return focusedCell == null ? EMPTY_CELL : focusedCell.get(); } 3171 3172 private ReadOnlyObjectWrapper<TablePosition> focusedCellPropertyImpl() { 3173 if (focusedCell == null) { 3174 focusedCell = new ReadOnlyObjectWrapper<TablePosition>(EMPTY_CELL) { 3175 private TablePosition old; 3176 @Override protected void invalidated() { 3177 if (get() == null) return; 3178 3179 if (old == null || !old.equals(get())) { 3180 setFocusedIndex(get().getRow()); 3181 setFocusedItem(getModelItem(getValue().getRow())); 3182 3183 old = get(); 3184 } 3185 } 3186 3187 @Override 3188 public Object getBean() { 3189 return TableViewFocusModel.this; 3190 } 3191 3192 @Override 3193 public String getName() { 3194 return "focusedCell"; 3195 } 3196 }; 3197 } 3198 return focusedCell; 3199 } 3200 3201 3202 /** 3203 * Causes the item at the given index to receive the focus. 3204 * 3205 * @param row The row index of the item to give focus to. 3206 * @param column The column of the item to give focus to. Can be null. 3207 */ 3208 @Override public void focus(int row, TableColumn<S,?> column) { 3209 if (row < 0 || row >= getItemCount()) { 3210 setFocusedCell(EMPTY_CELL); 3211 } else { 3212 TablePosition<S,?> oldFocusCell = getFocusedCell(); 3213 TablePosition<S,?> newFocusCell = new TablePosition<>(tableView, row, column); 3214 setFocusedCell(newFocusCell); 3215 3216 if (newFocusCell.equals(oldFocusCell)) { 3217 // manually update the focus properties to ensure consistency 3218 setFocusedIndex(row); 3219 setFocusedItem(getModelItem(row)); 3220 } 3221 } 3222 } 3223 3224 /** 3225 * Convenience method for setting focus on a particular row or cell 3226 * using a {@link TablePosition}. 3227 * 3228 * @param pos The table position where focus should be set. 3229 */ 3230 public void focus(TablePosition pos) { 3231 if (pos == null) return; 3232 focus(pos.getRow(), pos.getTableColumn()); 3233 } 3234 3235 3236 /*********************************************************************** 3237 * * 3238 * Public API * 3239 * * 3240 **********************************************************************/ 3241 3242 /** 3243 * Tests whether the row / cell at the given location currently has the 3244 * focus within the TableView. 3245 */ 3246 @Override public boolean isFocused(int row, TableColumn<S,?> column) { 3247 if (row < 0 || row >= getItemCount()) return false; 3248 3249 TablePosition cell = getFocusedCell(); 3250 boolean columnMatch = column == null || column.equals(cell.getTableColumn()); 3251 3252 return cell.getRow() == row && columnMatch; 3253 } 3254 3255 /** 3256 * Causes the item at the given index to receive the focus. This does not 3257 * cause the current selection to change. Updates the focusedItem and 3258 * focusedIndex properties such that <code>focusedIndex = -1</code> unless 3259 * <pre><code>0 <= index < model size</code></pre>. 3260 * 3261 * @param index The index of the item to get focus. 3262 */ 3263 @Override public void focus(int index) { 3264 if (index < 0 || index >= getItemCount()) { 3265 setFocusedCell(EMPTY_CELL); 3266 } else { 3267 setFocusedCell(new TablePosition<>(tableView, index, null)); 3268 } 3269 } 3270 3271 /** 3272 * Attempts to move focus to the cell above the currently focused cell. 3273 */ 3274 @Override public void focusAboveCell() { 3275 TablePosition cell = getFocusedCell(); 3276 3277 if (getFocusedIndex() == -1) { 3278 focus(getItemCount() - 1, cell.getTableColumn()); 3279 } else if (getFocusedIndex() > 0) { 3280 focus(getFocusedIndex() - 1, cell.getTableColumn()); 3281 } 3282 } 3283 3284 /** 3285 * Attempts to move focus to the cell below the currently focused cell. 3286 */ 3287 @Override public void focusBelowCell() { 3288 TablePosition cell = getFocusedCell(); 3289 if (getFocusedIndex() == -1) { 3290 focus(0, cell.getTableColumn()); 3291 } else if (getFocusedIndex() != getItemCount() -1) { 3292 focus(getFocusedIndex() + 1, cell.getTableColumn()); 3293 } 3294 } 3295 3296 /** 3297 * Attempts to move focus to the cell to the left of the currently focused cell. 3298 */ 3299 @Override public void focusLeftCell() { 3300 TablePosition cell = getFocusedCell(); 3301 if (cell.getColumn() <= 0) return; 3302 focus(cell.getRow(), getTableColumn(cell.getTableColumn(), -1)); 3303 } 3304 3305 /** 3306 * Attempts to move focus to the cell to the right of the the currently focused cell. 3307 */ 3308 @Override public void focusRightCell() { 3309 TablePosition cell = getFocusedCell(); 3310 if (cell.getColumn() == getColumnCount() - 1) return; 3311 focus(cell.getRow(), getTableColumn(cell.getTableColumn(), 1)); 3312 } 3313 3314 /** {@inheritDoc} */ 3315 @Override public void focusPrevious() { 3316 if (getFocusedIndex() == -1) { 3317 focus(0); 3318 } else if (getFocusedIndex() > 0) { 3319 focusAboveCell(); 3320 } 3321 } 3322 3323 /** {@inheritDoc} */ 3324 @Override public void focusNext() { 3325 if (getFocusedIndex() == -1) { 3326 focus(0); 3327 } else if (getFocusedIndex() != getItemCount() -1) { 3328 focusBelowCell(); 3329 } 3330 } 3331 3332 /*********************************************************************** 3333 * * 3334 * Private Implementation * 3335 * * 3336 **********************************************************************/ 3337 3338 private void updateDefaultFocus() { 3339 // when the items list totally changes, we should clear out 3340 // the focus 3341 int newValueIndex = -1; 3342 if (tableView.getItems() != null) { 3343 S focusedItem = getFocusedItem(); 3344 if (focusedItem != null) { 3345 newValueIndex = tableView.getItems().indexOf(focusedItem); 3346 } 3347 3348 // we put focus onto the first item, if there is at least 3349 // one item in the list 3350 if (newValueIndex == -1) { 3351 newValueIndex = tableView.getItems().size() > 0 ? 0 : -1; 3352 } 3353 } 3354 3355 TablePosition<S,?> focusedCell = getFocusedCell(); 3356 TableColumn<S,?> focusColumn = focusedCell != null && !EMPTY_CELL.equals(focusedCell) ? 3357 focusedCell.getTableColumn() : tableView.getVisibleLeafColumn(0); 3358 3359 focus(newValueIndex, focusColumn); 3360 } 3361 3362 private int getColumnCount() { 3363 return tableView.getVisibleLeafColumns().size(); 3364 } 3365 3366 // Gets a table column to the left or right of the current one, given an offset 3367 private TableColumn<S,?> getTableColumn(TableColumn<S,?> column, int offset) { 3368 int columnIndex = tableView.getVisibleLeafIndex(column); 3369 int newColumnIndex = columnIndex + offset; 3370 return tableView.getVisibleLeafColumn(newColumnIndex); 3371 } 3372 } 3373 }