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