javafx.scene.control.TableCell Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control;
import javafx.css.PseudoClass;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.Event;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.control.TableView.TableViewFocusModel;
import javafx.scene.control.skin.TableCellSkin;
import javafx.collections.WeakListChangeListener;
import java.lang.ref.WeakReference;
import java.util.List;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.scene.control.TableColumn.CellEditEvent;
/**
* Represents a single row/column intersection in a {@link TableView}. To
* represent this intersection, a TableCell contains an
* {@link #indexProperty() index} property, as well as a
* {@link #tableColumnProperty() tableColumn} property. In addition, a TableCell
* instance knows what {@link TableRow} it exists in.
*
* A note about selection: A TableCell visually shows it is
* selected when two conditions are met:
*
* - The {@link TableSelectionModel#isSelected(int, TableColumnBase)} method
* returns true for the row / column that this cell represents, and
* - The {@link javafx.scene.control.TableSelectionModel#cellSelectionEnabledProperty() cell selection mode}
* property is set to true (to represent that it is allowable to select
* individual cells (and not just rows of cells)).
*
*
* @see TableView
* @see TableColumn
* @see Cell
* @see IndexedCell
* @see TableRow
* @param The type of the TableView generic type (i.e. S == TableView<S>).
* This should also match with the first generic type in TableColumn.
* @param The type of the item contained within the Cell.
* @since JavaFX 2.0
*/
public class TableCell extends IndexedCell {
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Constructs a default TableCell instance with a style class of 'table-cell'
*/
public TableCell() {
getStyleClass().addAll(DEFAULT_STYLE_CLASS);
setAccessibleRole(AccessibleRole.TABLE_CELL);
updateColumnIndex();
}
/* *************************************************************************
* *
* Private fields *
* *
**************************************************************************/
/* *************************************************************************
* *
* Callbacks and Events *
* *
**************************************************************************/
private boolean itemDirty = false;
/*
* This is the list observer we use to keep an eye on the SelectedCells
* ObservableList in the table view. Because it is possible that the table can
* be mutated, we create this observer here, and add/remove it from the
* storeTableView method.
*/
private ListChangeListener selectedListener = c -> {
while (c.next()) {
if (c.wasAdded() || c.wasRemoved()) {
updateSelection();
}
}
};
// same as above, but for focus
private final InvalidationListener focusedListener = value -> {
updateFocus();
};
// same as above, but for for changes to the properties on TableRow
private final InvalidationListener tableRowUpdateObserver = value -> {
itemDirty = true;
requestLayout();
};
private final InvalidationListener editingListener = value -> {
updateEditing();
};
private ListChangeListener> visibleLeafColumnsListener = c -> {
updateColumnIndex();
};
private ListChangeListener columnStyleClassListener = c -> {
while (c.next()) {
if (c.wasRemoved()) {
getStyleClass().removeAll(c.getRemoved());
}
if (c.wasAdded()) {
getStyleClass().addAll(c.getAddedSubList());
}
}
};
private final InvalidationListener columnStyleListener = value -> {
if (getTableColumn() != null) {
possiblySetStyle(getTableColumn().getStyle());
}
};
private final InvalidationListener columnIdListener = value -> {
if (getTableColumn() != null) {
possiblySetId(getTableColumn().getId());
}
};
private final WeakListChangeListener weakSelectedListener =
new WeakListChangeListener<>(selectedListener);
private final WeakInvalidationListener weakFocusedListener =
new WeakInvalidationListener(focusedListener);
private final WeakInvalidationListener weaktableRowUpdateObserver =
new WeakInvalidationListener(tableRowUpdateObserver);
private final WeakInvalidationListener weakEditingListener =
new WeakInvalidationListener(editingListener);
private final WeakInvalidationListener weakColumnStyleListener =
new WeakInvalidationListener(columnStyleListener);
private final WeakInvalidationListener weakColumnIdListener =
new WeakInvalidationListener(columnIdListener);
private final WeakListChangeListener> weakVisibleLeafColumnsListener =
new WeakListChangeListener<>(visibleLeafColumnsListener);
private final WeakListChangeListener weakColumnStyleClassListener =
new WeakListChangeListener<>(columnStyleClassListener);
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- TableColumn
private ReadOnlyObjectWrapper> tableColumn = new ReadOnlyObjectWrapper<>() {
@Override protected void invalidated() {
updateColumnIndex();
}
@Override public Object getBean() {
return TableCell.this;
}
@Override public String getName() {
return "tableColumn";
}
};
/**
* The TableColumn instance that backs this TableCell.
* @return the TableColumn instance that backs this TableCell
*/
public final ReadOnlyObjectProperty> tableColumnProperty() { return tableColumn.getReadOnlyProperty(); }
private void setTableColumn(TableColumn value) { tableColumn.set(value); }
public final TableColumn getTableColumn() { return tableColumn.get(); }
// --- TableView
private ReadOnlyObjectWrapper> tableView;
private void setTableView(TableView value) {
tableViewPropertyImpl().set(value);
}
public final TableView getTableView() {
return tableView == null ? null : tableView.get();
}
/**
* The TableView associated with this TableCell.
* @return the TableView associated with this TableCell
*/
public final ReadOnlyObjectProperty> tableViewProperty() {
return tableViewPropertyImpl().getReadOnlyProperty();
}
private ReadOnlyObjectWrapper> tableViewPropertyImpl() {
if (tableView == null) {
tableView = new ReadOnlyObjectWrapper<>() {
private WeakReference> weakTableViewRef;
@Override protected void invalidated() {
TableView.TableViewSelectionModel sm;
TableViewFocusModel fm;
if (weakTableViewRef != null) {
cleanUpTableViewListeners(weakTableViewRef.get());
}
if (get() != null) {
sm = get().getSelectionModel();
if (sm != null) {
sm.getSelectedCells().addListener(weakSelectedListener);
}
fm = get().getFocusModel();
if (fm != null) {
fm.focusedCellProperty().addListener(weakFocusedListener);
}
get().editingCellProperty().addListener(weakEditingListener);
get().getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
weakTableViewRef = new WeakReference<>(get());
}
updateColumnIndex();
}
@Override public Object getBean() {
return TableCell.this;
}
@Override public String getName() {
return "tableView";
}
};
}
return tableView;
}
// --- TableRow
/**
* The TableRow that this TableCell currently finds itself placed within.
* The TableRow may be null early in the TableCell lifecycle, in the period
* between the TableCell being instantiated and being set into an owner
* TableRow.
*/
private ReadOnlyObjectWrapper> tableRow = new ReadOnlyObjectWrapper<>(this, "tableRow");
private void setTableRow(TableRow value) { tableRow.set(value); }
public final TableRow getTableRow() { return tableRow.get(); }
public final ReadOnlyObjectProperty> tableRowProperty() { return tableRow; }
/* *************************************************************************
* *
* Editing API *
* *
**************************************************************************/
// editing location at start of edit - fix for JDK-8187229
private TablePosition editingCellAtStartEdit;
// test-only
TablePosition getEditingCellAtStartEdit() {
return editingCellAtStartEdit;
}
/** {@inheritDoc} */
@Override public void startEdit() {
if (isEditing()) return;
final TableView table = getTableView();
final TableColumn column = getTableColumn();
final TableRow row = getTableRow();
if (!isEditable() ||
(table != null && !table.isEditable()) ||
(column != null && !column.isEditable()) ||
(row != null && !row.isEditable())) {
return;
}
updateItem(-1);
// it makes sense to get the cell into its editing state before firing
// the event to listeners below, so that's what we're doing here
// by calling super.startEdit().
super.startEdit();
if (!isEditing()) return;
editingCellAtStartEdit = new TablePosition<>(table, getIndex(), column);
if (column != null) {
CellEditEvent editEvent = new CellEditEvent<>(
table,
editingCellAtStartEdit,
TableColumn.editStartEvent(),
null
);
Event.fireEvent(column, editEvent);
}
if (table != null) {
table.edit(editingCellAtStartEdit.getRow(), editingCellAtStartEdit.getTableColumn());
}
}
/** {@inheritDoc} */
@Override public void commitEdit(T newValue) {
if (!isEditing()) return;
// inform parent classes of the commit, so that they can switch us
// out of the editing state.
// This MUST come before the updateItem call below, otherwise it will
// call cancelEdit(), resulting in both commit and cancel events being
// fired (as identified in RT-29650)
super.commitEdit(newValue);
final TableView table = getTableView();
boolean tableShouldRequestFocus = false;
if (table != null) {
// The cell is going to be updated, and the current focus owner might be removed from it.
// Before that happens, check if it has the table as a parent (otherwise the user might have
// clicked out of the table entirely and given focus to something else), so the table can
// request the focus back, once the edit commit ends.
tableShouldRequestFocus = ControlUtils.controlShouldRequestFocusIfCurrentFocusOwnerIsChild(table);
}
// JDK-8187307: fire the commit after updating cell's editing state
if (getTableColumn() != null) {
// Inform the TableColumn of the edit being ready to be committed.
CellEditEvent editEvent = new CellEditEvent<>(
table,
editingCellAtStartEdit,
TableColumn.editCommitEvent(),
newValue
);
Event.fireEvent(getTableColumn(), editEvent);
}
// Update the item within this cell, so that it represents the new value
updateItem(-1);
if (table != null) {
// reset the editing cell on the TableView
table.edit(-1, null);
// request focus back onto the table, only if the current focus
// owner had the table as a parent.
// It would be rude of us to request it back again.
if (tableShouldRequestFocus) {
table.requestFocus();
}
}
}
/** {@inheritDoc} */
@Override public void cancelEdit() {
if (!isEditing()) return;
super.cancelEdit();
final TableView table = getTableView();
if (table != null) {
// reset the editing index on the TableView
if (updateEditingIndex) table.edit(-1, null);
// request focus back onto the table, only if the current focus
// owner has the table as a parent (otherwise the user might have
// clicked out of the table entirely and given focus to something else).
// It would be rude of us to request it back again.
ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
}
if (getTableColumn() != null) {
CellEditEvent editEvent = new CellEditEvent<>(
table,
editingCellAtStartEdit,
TableColumn.editCancelEvent(),
null
);
Event.fireEvent(getTableColumn(), editEvent);
}
}
/* *************************************************************************
* *
* Overriding methods *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override public void updateSelected(boolean selected) {
// copied from Cell, with the first conditional clause below commented
// out, as it is valid for an empty TableCell to be selected, as long
// as the parent TableRow is not empty (see RT-15529).
/*if (selected && isEmpty()) return;*/
if (getTableRow() == null || getTableRow().isEmpty()) return;
setSelected(selected);
}
/** {@inheritDoc} */
@Override protected Skin> createDefaultSkin() {
return new TableCellSkin<>(this);
}
// @Override public void dispose() {
// cleanUpTableViewListeners(getTableView());
//
// if (currentObservableValue != null) {
// currentObservableValue.removeListener(weaktableRowUpdateObserver);
// }
//
// super.dispose();
// }
/* *************************************************************************
* *
* Private Implementation *
* *
**************************************************************************/
private void cleanUpTableViewListeners(TableView tableView) {
if (tableView != null) {
TableView.TableViewSelectionModel sm = tableView.getSelectionModel();
if (sm != null) {
sm.getSelectedCells().removeListener(weakSelectedListener);
}
TableViewFocusModel fm = tableView.getFocusModel();
if (fm != null) {
fm.focusedCellProperty().removeListener(weakFocusedListener);
}
tableView.editingCellProperty().removeListener(weakEditingListener);
tableView.getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener);
}
}
@Override void indexChanged(int oldIndex, int newIndex) {
super.indexChanged(oldIndex, newIndex);
// Ideally we would just use the following two lines of code, rather
// than the updateItem() call beneath, but if we do this we end up with
// RT-22428 where all the columns are collapsed.
// itemDirty = true;
// requestLayout();
updateItem(oldIndex);
updateSelection();
updateFocus();
// Fix for JDK-8150525
updateEditing();
}
private boolean isLastVisibleColumn = false;
private int columnIndex = -1;
private void updateColumnIndex() {
TableView tv = getTableView();
TableColumn tc = getTableColumn();
columnIndex = tv == null || tc == null ? -1 : tv.getVisibleLeafIndex(tc);
// update the pseudo class state regarding whether this is the last
// visible cell (i.e. the right-most).
isLastVisibleColumn = getTableColumn() != null &&
columnIndex != -1 &&
columnIndex == getTableView().getVisibleLeafColumns().size() - 1;
pseudoClassStateChanged(PSEUDO_CLASS_LAST_VISIBLE, isLastVisibleColumn);
}
private void updateSelection() {
/*
* This cell should be selected if the selection mode of the table
* is cell-based, and if the row and column that this cell represents
* is selected.
*
* If the selection mode is not cell-based, then the listener in the
* TableRow class might pick up the need to set an entire row to be
* selected.
*/
if (isEmpty()) return;
final boolean isSelected = isSelected();
if (! isInCellSelectionMode()) {
if (isSelected) {
updateSelected(false);
}
return;
}
final TableView tableView = getTableView();
if (getIndex() == -1 || tableView == null) return;
TableSelectionModel sm = tableView.getSelectionModel();
if (sm == null) {
updateSelected(false);
return;
}
boolean isSelectedNow = sm.isSelected(getIndex(), getTableColumn());
if (isSelected == isSelectedNow) return;
updateSelected(isSelectedNow);
}
private void updateFocus() {
final boolean isFocused = isFocused();
if (! isInCellSelectionMode()) {
if (isFocused) {
setFocused(false);
}
return;
}
final TableView tableView = getTableView();
final TableRow tableRow = getTableRow();
final int index = getIndex();
if (index == -1 || tableView == null || tableRow == null) return;
final TableViewFocusModel fm = tableView.getFocusModel();
if (fm == null) {
setFocused(false);
return;
}
setFocused(fm.isFocused(index, getTableColumn()));
}
private void updateEditing() {
if (getIndex() == -1 || getTableView() == null) {
// JDK-8265206: must cancel edit if index changed to -1 by re-use
if (isEditing()) {
doCancelEdit();
}
return;
}
TablePosition editCell = getTableView().getEditingCell();
boolean match = match(editCell);
if (match && ! isEditing()) {
startEdit();
} else if (! match && isEditing()) {
doCancelEdit();
}
}
/**
* Switches an editing cell into not editing without changing control's
* editing state.
*/
private void doCancelEdit() {
// If my index is not the one being edited then I need to cancel
// the edit. The tricky thing here is that as part of this call
// I cannot end up calling list.edit(-1) the way that the standard
// cancelEdit method would do. Yet, I need to call cancelEdit
// so that subclasses which override cancelEdit can execute. So,
// I have to use a kind of hacky flag workaround.
try {
// try-finally to make certain that the flag is reliably reset to true
updateEditingIndex = false;
cancelEdit();
} finally {
updateEditingIndex = true;
}
}
private boolean updateEditingIndex = true;
private boolean match(TablePosition pos) {
return pos != null && pos.getRow() == getIndex() && pos.getTableColumn() == getTableColumn();
}
private boolean isInCellSelectionMode() {
TableView tableView = getTableView();
if (tableView == null) return false;
TableSelectionModel sm = tableView.getSelectionModel();
return sm != null && sm.isCellSelectionEnabled();
}
/*
* This was brought in to fix the issue in RT-22077, namely that the
* ObservableValue was being GC'd, meaning that changes to the value were
* no longer being delivered. By extracting this value out of the method,
* it is now referred to from TableCell and will therefore no longer be
* GC'd.
*/
private ObservableValue currentObservableValue = null;
private boolean isFirstRun = true;
private WeakReference oldRowItemRef;
/*
* This is called when we think that the data within this TableCell may have
* changed. You'll note that this is a private function - it is only called
* when one of the triggers above call it.
*/
private void updateItem(int oldIndex) {
if (currentObservableValue != null) {
currentObservableValue.removeListener(weaktableRowUpdateObserver);
}
// get the total number of items in the data model
final TableView tableView = getTableView();
final List items = tableView == null ? FXCollections.emptyObservableList() : tableView.getItems();
final TableColumn tableColumn = getTableColumn();
final int itemCount = items == null ? -1 : items.size();
final int index = getIndex();
final boolean isEmpty = isEmpty();
final T oldValue = getItem();
final TableRow tableRow = getTableRow();
final S rowItem = tableRow == null ? null : tableRow.getItem();
final boolean indexExceedsItemCount = index >= itemCount;
// there is a whole heap of reasons why we should just punt...
outer: if (indexExceedsItemCount ||
index < 0 ||
columnIndex < 0 ||
!isVisible() ||
tableColumn == null ||
!tableColumn.isVisible()) {
// RT-30484 We need to allow a first run to be special-cased to allow
// for the updateItem method to be called at least once to allow for
// the correct visual state to be set up. In particular, in RT-30484
// refer to Ensemble8PopUpTree.png - in this case the arrows are being
// shown as the new cells are instantiated with the arrows in the
// children list, and are only hidden in updateItem.
// RT-32621: There are circumstances where we need to updateItem,
// even when the index is greater than the itemCount. For example,
// RT-32621 identifies issues where a TreeTableView collapses a
// TreeItem but the custom cells remain visible. This is now
// resolved with the check for indexExceedsItemCount.
if ((!isEmpty && oldValue != null) || isFirstRun || indexExceedsItemCount) {
updateItem(null, true);
isFirstRun = false;
}
return;
} else {
currentObservableValue = tableColumn.getCellObservableValue(index);
final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
// RT-35864 - if the index didn't change, then avoid calling updateItem
// unless the item has changed.
if (oldIndex == index) {
if (!isItemChanged(oldValue, newValue)) {
// RT-36670: we need to check the row item here to prevent
// the issue where the cell value and index doesn't change,
// but the backing row object does.
S oldRowItem = oldRowItemRef != null ? oldRowItemRef.get() : null;
if (oldRowItem != null && oldRowItem.equals(rowItem)) {
// RT-37054: we break out of the if/else code here and
// proceed with the code following this, so that we may
// still update references, listeners, etc as required//.
break outer;
}
}
}
updateItem(newValue, false);
}
oldRowItemRef = new WeakReference<>(rowItem);
if (currentObservableValue == null) {
return;
}
// add property change listeners to this item
currentObservableValue.addListener(weaktableRowUpdateObserver);
}
@Override protected void layoutChildren() {
if (itemDirty) {
updateItem(-1);
itemDirty = false;
}
super.layoutChildren();
}
/* *************************************************************************
* *
* Expert API *
* *
**************************************************************************/
/**
* Updates the TableView associated with this TableCell. This is typically
* only done once when the TableCell is first added to the TableView.
*
* Note: This function is intended to be used by experts, primarily
* by those implementing new Skins. It is not common
* for developers or designers to access this function directly.
* @param tv the TableView associated with this TableCell
*/
public final void updateTableView(TableView tv) {
setTableView(tv);
}
/**
* Updates the TableRow associated with this TableCell.
*
* Note: This function is intended to be used by experts, primarily
* by those implementing new Skins. It is not common
* for developers or designers to access this function directly.
* @param tableRow the TableRow associated with this TableCell
*/
public final void updateTableRow(TableRow tableRow) {
this.setTableRow(tableRow);
}
/**
* Updates the TableColumn associated with this TableCell.
*
* Note: This function is intended to be used by experts, primarily
* by those implementing new Skins. It is not common
* for developers or designers to access this function directly.
* @param col the TableColumn associated with this TableCell
*/
public final void updateTableColumn(TableColumn col) {
// remove style class of existing table column, if it is non-null
TableColumn oldCol = getTableColumn();
if (oldCol != null) {
oldCol.getStyleClass().removeListener(weakColumnStyleClassListener);
getStyleClass().removeAll(oldCol.getStyleClass());
oldCol.idProperty().removeListener(weakColumnIdListener);
oldCol.styleProperty().removeListener(weakColumnStyleListener);
String id = getId();
String style = getStyle();
if (id != null && id.equals(oldCol.getId())) {
setId(null);
}
if (style != null && style.equals(oldCol.getStyle())) {
setStyle("");
}
}
setTableColumn(col);
if (col != null) {
getStyleClass().addAll(col.getStyleClass());
col.getStyleClass().addListener(weakColumnStyleClassListener);
col.idProperty().addListener(weakColumnIdListener);
col.styleProperty().addListener(weakColumnStyleListener);
possiblySetId(col.getId());
possiblySetStyle(col.getStyle());
}
}
/* *************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "table-cell";
private static final PseudoClass PSEUDO_CLASS_LAST_VISIBLE =
PseudoClass.getPseudoClass("last-visible");
private void possiblySetId(String idCandidate) {
if (getId() == null || getId().isEmpty()) {
setId(idCandidate);
}
}
private void possiblySetStyle(String styleCandidate) {
if (getStyle() == null || getStyle().isEmpty()) {
setStyle(styleCandidate);
}
}
/* *************************************************************************
* *
* Accessibility handling *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case ROW_INDEX:
return getIndex();
case COLUMN_INDEX:
return columnIndex;
case SELECTED:
if (isInCellSelectionMode()) {
return isSelected();
} else {
if(getTableRow() == null) {
return null;
} else {
return getTableRow().isSelected();
}
}
default:
return super.queryAccessibleAttribute(attribute, parameters);
}
}
/** {@inheritDoc} */
@Override
public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
switch (action) {
case REQUEST_FOCUS: {
TableView tableView = getTableView();
if (tableView != null) {
TableViewFocusModel fm = tableView.getFocusModel();
if (fm != null) {
fm.focus(getIndex(), getTableColumn());
}
}
break;
}
default: super.executeAccessibleAction(action, parameters);
}
}
}