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