modules/controls/src/main/java/javafx/scene/control/skin/TreeViewSkin.java
Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
@@ -21,12 +21,14 @@
* 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 com.sun.javafx.scene.control.skin;
+package javafx.scene.control.skin;
+import com.sun.javafx.scene.control.Properties;
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
@@ -49,107 +51,62 @@
import java.util.ArrayList;
import java.util.List;
import com.sun.javafx.scene.control.behavior.TreeViewBehavior;
-public class TreeViewSkin<T> extends VirtualContainerBase<TreeView<T>, TreeViewBehavior<T>, TreeCell<T>> {
+/**
+ * Default skin implementation for the {@link TreeView} control.
+ *
+ * @see TreeView
+ * @since 9
+ */
+public class TreeViewSkin<T> extends VirtualContainerBase<TreeView<T>, TreeCell<T>> {
- public static final String RECREATE = "treeRecreateKey";
+ /***************************************************************************
+ * *
+ * Static fields *
+ * *
+ **************************************************************************/
// RT-34744 : IS_PANNABLE will be false unless
- // com.sun.javafx.scene.control.skin.TreeViewSkin.pannable
+ // javafx.scene.control.skin.TreeViewSkin.pannable
// is set to true. This is done in order to make TreeView functional
// on embedded systems with touch screens which do not generate scroll
// events for touch drag gestures.
private static final boolean IS_PANNABLE =
- AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("com.sun.javafx.scene.control.skin.TreeViewSkin.pannable"));
-
-
- public TreeViewSkin(final TreeView treeView) {
- super(treeView, new TreeViewBehavior(treeView));
-
- // init the VirtualFlow
- flow.setPannable(IS_PANNABLE);
- flow.setCreateCell(flow1 -> TreeViewSkin.this.createCell());
- flow.setFixedCellSize(treeView.getFixedCellSize());
- getChildren().add(flow);
+ AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.TreeViewSkin.pannable"));
- setRoot(getSkinnable().getRoot());
-
- EventHandler<MouseEvent> ml = event -> {
- // RT-15127: cancel editing on scroll. This is a bit extreme
- // (we are cancelling editing on touching the scrollbars).
- // This can be improved at a later date.
- if (treeView.getEditingItem() != null) {
- treeView.edit(null);
- }
- // This ensures that the tree maintains the focus, even when the vbar
- // and hbar controls inside the flow are clicked. Without this, the
- // focus border will not be shown when the user interacts with the
- // scrollbars, and more importantly, keyboard navigation won't be
- // available to the user.
- if (treeView.isFocusTraversable()) {
- treeView.requestFocus();
- }
- };
- flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
- flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
- final ObservableMap<Object, Object> properties = treeView.getProperties();
- properties.remove(RECREATE);
- properties.addListener(propertiesMapListener);
+ /***************************************************************************
+ * *
+ * Private fields *
+ * *
+ **************************************************************************/
- // init the behavior 'closures'
- getBehavior().setOnFocusPreviousRow(() -> { onFocusPreviousCell(); });
- getBehavior().setOnFocusNextRow(() -> { onFocusNextCell(); });
- getBehavior().setOnMoveToFirstCell(() -> { onMoveToFirstCell(); });
- getBehavior().setOnMoveToLastCell(() -> { onMoveToLastCell(); });
- getBehavior().setOnScrollPageDown(isFocusDriven -> onScrollPageDown(isFocusDriven));
- getBehavior().setOnScrollPageUp(isFocusDriven -> onScrollPageUp(isFocusDriven));
- getBehavior().setOnSelectPreviousRow(() -> { onSelectPreviousCell(); });
- getBehavior().setOnSelectNextRow(() -> { onSelectNextCell(); });
-
- registerChangeListener(treeView.rootProperty(), "ROOT");
- registerChangeListener(treeView.showRootProperty(), "SHOW_ROOT");
- registerChangeListener(treeView.cellFactoryProperty(), "CELL_FACTORY");
- registerChangeListener(treeView.fixedCellSizeProperty(), "FIXED_CELL_SIZE");
+ private final VirtualFlow<TreeCell<T>> flow;
+ private WeakReference<TreeItem<T>> weakRoot;
+ private final TreeViewBehavior<T> behavior;
- updateRowCount();
- }
+ // private boolean needItemCountUpdate = false;
+ private boolean needCellsRebuilt = true;
+ private boolean needCellsReconfigured = false;
- @Override protected void handleControlPropertyChanged(String p) {
- super.handleControlPropertyChanged(p);
- if ("ROOT".equals(p)) {
- setRoot(getSkinnable().getRoot());
- } else if ("SHOW_ROOT".equals(p)) {
- // if we turn off showing the root, then we must ensure the root
- // is expanded - otherwise we end up with no visible items in
- // the tree.
- if (! getSkinnable().isShowRoot() && getRoot() != null) {
- getRoot().setExpanded(true);
- }
- // update the item count in the flow and behavior instances
- updateRowCount();
- } else if ("CELL_FACTORY".equals(p)) {
- flow.recreateCells();
- } else if ("FIXED_CELL_SIZE".equals(p)) {
- flow.setFixedCellSize(getSkinnable().getFixedCellSize());
- }
- }
-// private boolean needItemCountUpdate = false;
- private boolean needCellsRebuilt = true;
- private boolean needCellsReconfigured = false;
+ /***************************************************************************
+ * *
+ * Listeners *
+ * *
+ **************************************************************************/
private MapChangeListener<Object, Object> propertiesMapListener = c -> {
if (! c.wasAdded()) return;
- if (RECREATE.equals(c.getKey())) {
+ if (Properties.RECREATE.equals(c.getKey())) {
needCellsRebuilt = true;
getSkinnable().requestLayout();
- getSkinnable().getProperties().remove(RECREATE);
+ getSkinnable().getProperties().remove(Properties.RECREATE);
}
};
private EventHandler<TreeModificationEvent<T>> rootListener = e -> {
if (e.wasAdded() && e.wasRemoved() && e.getAddedSize() == e.getRemovedSize()) {
@@ -183,50 +140,213 @@
};
private WeakEventHandler<TreeModificationEvent<T>> weakRootListener;
- private WeakReference<TreeItem<T>> weakRoot;
- private TreeItem<T> getRoot() {
- return weakRoot == null ? null : weakRoot.get();
+
+ /***************************************************************************
+ * *
+ * Constructors *
+ * *
+ **************************************************************************/
+
+ /**
+ * Creates a new TreeViewSkin instance, installing the necessary child
+ * nodes into the Control {@link Control#getChildren() children} list, as
+ * well as the necessary input mappings for handling key, mouse, etc events.
+ *
+ * @param control The control that this skin should be installed onto.
+ */
+ public TreeViewSkin(final TreeView control) {
+ super(control);
+
+ // install default input map for the TreeView control
+ behavior = new TreeViewBehavior<>(control);
+// control.setInputMap(behavior.getInputMap());
+
+ // init the VirtualFlow
+ flow = getVirtualFlow();
+ flow.setPannable(IS_PANNABLE);
+ flow.setCellFactory(flow1 -> createCell());
+ flow.setFixedCellSize(control.getFixedCellSize());
+ getChildren().add(flow);
+
+ setRoot(getSkinnable().getRoot());
+
+ EventHandler<MouseEvent> ml = event -> {
+ // RT-15127: cancel editing on scroll. This is a bit extreme
+ // (we are cancelling editing on touching the scrollbars).
+ // This can be improved at a later date.
+ if (control.getEditingItem() != null) {
+ control.edit(null);
}
- private void setRoot(TreeItem<T> newRoot) {
- if (getRoot() != null && weakRootListener != null) {
- getRoot().removeEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
+
+ // This ensures that the tree maintains the focus, even when the vbar
+ // and hbar controls inside the flow are clicked. Without this, the
+ // focus border will not be shown when the user interacts with the
+ // scrollbars, and more importantly, keyboard navigation won't be
+ // available to the user.
+ if (control.isFocusTraversable()) {
+ control.requestFocus();
}
- weakRoot = new WeakReference<>(newRoot);
- if (getRoot() != null) {
- weakRootListener = new WeakEventHandler<>(rootListener);
- getRoot().addEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
+ };
+ flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
+ flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
+
+ final ObservableMap<Object, Object> properties = control.getProperties();
+ properties.remove(Properties.RECREATE);
+ properties.addListener(propertiesMapListener);
+
+ // init the behavior 'closures'
+ behavior.setOnFocusPreviousRow(() -> { onFocusPreviousCell(); });
+ behavior.setOnFocusNextRow(() -> { onFocusNextCell(); });
+ behavior.setOnMoveToFirstCell(() -> { onMoveToFirstCell(); });
+ behavior.setOnMoveToLastCell(() -> { onMoveToLastCell(); });
+ behavior.setOnScrollPageDown(this::onScrollPageDown);
+ behavior.setOnScrollPageUp(this::onScrollPageUp);
+ behavior.setOnSelectPreviousRow(() -> { onSelectPreviousCell(); });
+ behavior.setOnSelectNextRow(() -> { onSelectNextCell(); });
+
+ registerChangeListener(control.rootProperty(), e -> setRoot(getSkinnable().getRoot()));
+ registerChangeListener(control.showRootProperty(), e -> {
+ // if we turn off showing the root, then we must ensure the root
+ // is expanded - otherwise we end up with no visible items in
+ // the tree.
+ if (! getSkinnable().isShowRoot() && getRoot() != null) {
+ getRoot().setExpanded(true);
}
+ // update the item count in the flow and behavior instances
+ updateRowCount();
+ });
+ registerChangeListener(control.cellFactoryProperty(), e -> flow.recreateCells());
+ registerChangeListener(control.fixedCellSizeProperty(), e -> flow.setFixedCellSize(getSkinnable().getFixedCellSize()));
updateRowCount();
}
- @Override public int getItemCount() {
- return getSkinnable().getExpandedItemCount();
+
+
+ /***************************************************************************
+ * *
+ * Public API *
+ * *
+ **************************************************************************/
+
+ /** {@inheritDoc} */
+ @Override public void dispose() {
+ super.dispose();
+
+ if (behavior != null) {
+ behavior.dispose();
+ }
}
- @Override protected void updateRowCount() {
-// int oldCount = flow.getCellCount();
- int newCount = getItemCount();
+ /** {@inheritDoc} */
+ @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+ return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987;
+ }
- // if this is not called even when the count is the same, we get a
- // memory leak in VirtualFlow.sheet.children. This can probably be
- // optimised in the future when time permits.
- flow.setCellCount(newCount);
+ /** {@inheritDoc} */
+ @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+ return 400;
+ }
- // Ideally we would be more nuanced here, toggling a cheaper needs*
- // field, but if we do we hit issues such as those identified in
- // RT-27852, where the expended item count of the new root equals the
- // EIC of the old root, which would lead to the visuals not updating
- // properly.
- needCellsRebuilt = true;
- getSkinnable().requestLayout();
+ /** {@inheritDoc} */
+ @Override protected void layoutChildren(final double x, final double y,
+ final double w, final double h) {
+ super.layoutChildren(x, y, w, h);
+
+ if (needCellsRebuilt) {
+ flow.rebuildCells();
+ } else if (needCellsReconfigured) {
+ flow.reconfigureCells();
+ }
+
+ needCellsRebuilt = false;
+ needCellsReconfigured = false;
+
+ flow.resizeRelocate(x, y, w, h);
}
- @Override public TreeCell<T> createCell() {
+ /** {@inheritDoc} */
+ @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+ switch (attribute) {
+ case FOCUS_ITEM: {
+ FocusModel<?> fm = getSkinnable().getFocusModel();
+ int focusedIndex = fm.getFocusedIndex();
+ if (focusedIndex == -1) {
+ if (getItemCount() > 0) {
+ focusedIndex = 0;
+ } else {
+ return null;
+ }
+ }
+ return flow.getPrivateCell(focusedIndex);
+ }
+ case ROW_AT_INDEX: {
+ final int rowIndex = (Integer)parameters[0];
+ return rowIndex < 0 ? null : flow.getPrivateCell(rowIndex);
+ }
+ case SELECTED_ITEMS: {
+ MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
+ ObservableList<Integer> indices = sm.getSelectedIndices();
+ List<Node> selection = new ArrayList<>(indices.size());
+ for (int i : indices) {
+ TreeCell<T> row = flow.getPrivateCell(i);
+ if (row != null) selection.add(row);
+ }
+ return FXCollections.observableArrayList(selection);
+ }
+ case VERTICAL_SCROLLBAR: return flow.getVbar();
+ case HORIZONTAL_SCROLLBAR: return flow.getHbar();
+ default: return super.queryAccessibleAttribute(attribute, parameters);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
+ switch (action) {
+ case SHOW_ITEM: {
+ Node item = (Node)parameters[0];
+ if (item instanceof TreeCell) {
+ @SuppressWarnings("unchecked")
+ TreeCell<T> cell = (TreeCell<T>)item;
+ flow.scrollTo(cell.getIndex());
+ }
+ break;
+ }
+ case SET_SELECTED_ITEMS: {
+ @SuppressWarnings("unchecked")
+ ObservableList<Node> items = (ObservableList<Node>)parameters[0];
+ if (items != null) {
+ MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
+ if (sm != null) {
+ sm.clearSelection();
+ for (Node item : items) {
+ if (item instanceof TreeCell) {
+ @SuppressWarnings("unchecked")
+ TreeCell<T> cell = (TreeCell<T>)item;
+ sm.select(cell.getIndex());
+ }
+ }
+ }
+ }
+ break;
+ }
+ default: super.executeAccessibleAction(action, parameters);
+ }
+ }
+
+
+ /***************************************************************************
+ * *
+ * Private implementation *
+ * *
+ **************************************************************************/
+
+ /** {@inheritDoc} */
+ private TreeCell<T> createCell() {
final TreeCell<T> cell;
if (getSkinnable().getCellFactory() != null) {
cell = getSkinnable().getCellFactory().call(getSkinnable());
} else {
cell = createDefaultCellImpl();
@@ -262,10 +382,50 @@
cell.updateTreeView(getSkinnable());
return cell;
}
+ private TreeItem<T> getRoot() {
+ return weakRoot == null ? null : weakRoot.get();
+ }
+ private void setRoot(TreeItem<T> newRoot) {
+ if (getRoot() != null && weakRootListener != null) {
+ getRoot().removeEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
+ }
+ weakRoot = new WeakReference<>(newRoot);
+ if (getRoot() != null) {
+ weakRootListener = new WeakEventHandler<>(rootListener);
+ getRoot().addEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
+ }
+
+ updateRowCount();
+ }
+
+ /** {@inheritDoc} */
+ @Override int getItemCount() {
+ return getSkinnable().getExpandedItemCount();
+ }
+
+ /** {@inheritDoc} */
+ @Override void updateRowCount() {
+// int oldCount = flow.getCellCount();
+ int newCount = getItemCount();
+
+ // if this is not called even when the count is the same, we get a
+ // memory leak in VirtualFlow.sheet.children. This can probably be
+ // optimised in the future when time permits.
+ flow.setCellCount(newCount);
+
+ // Ideally we would be more nuanced here, toggling a cheaper needs*
+ // field, but if we do we hit issues such as those identified in
+ // RT-27852, where the expended item count of the new root equals the
+ // EIC of the old root, which would lead to the visuals not updating
+ // properly.
+ needCellsRebuilt = true;
+ getSkinnable().requestLayout();
+ }
+
// Note: This is a copy/paste of javafx.scene.control.cell.DefaultTreeCell,
// which is package-protected
private TreeCell<T> createDefaultCellImpl() {
return new TreeCell<T>() {
private HBox hbox;
@@ -349,71 +509,46 @@
updateDisplay(item, empty);
}
};
}
- @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
- return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987;
- }
-
- @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
- return 400;
- }
-
- @Override
- protected void layoutChildren(final double x, final double y,
- final double w, final double h) {
- super.layoutChildren(x, y, w, h);
-
- if (needCellsRebuilt) {
- flow.rebuildCells();
- } else if (needCellsReconfigured) {
- flow.reconfigureCells();
- }
-
- needCellsRebuilt = false;
- needCellsReconfigured = false;
-
- flow.resizeRelocate(x, y, w, h);
- }
-
private void onFocusPreviousCell() {
FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
if (fm == null) return;
- flow.show(fm.getFocusedIndex());
+ flow.scrollTo(fm.getFocusedIndex());
}
private void onFocusNextCell() {
FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
if (fm == null) return;
- flow.show(fm.getFocusedIndex());
+ flow.scrollTo(fm.getFocusedIndex());
}
private void onSelectPreviousCell() {
int row = getSkinnable().getSelectionModel().getSelectedIndex();
- flow.show(row);
+ flow.scrollTo(row);
}
private void onSelectNextCell() {
int row = getSkinnable().getSelectionModel().getSelectedIndex();
- flow.show(row);
+ flow.scrollTo(row);
}
private void onMoveToFirstCell() {
- flow.show(0);
+ flow.scrollTo(0);
flow.setPosition(0);
}
private void onMoveToLastCell() {
- flow.show(getItemCount());
+ flow.scrollTo(getItemCount());
flow.setPosition(1);
}
/**
* Function used to scroll the container down by one 'page'.
*/
- public int onScrollPageDown(boolean isFocusDriven) {
+ private int onScrollPageDown(boolean isFocusDriven) {
TreeCell<T> lastVisibleCell = flow.getLastVisibleCellWithinViewPort();
if (lastVisibleCell == null) return -1;
final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
@@ -434,11 +569,11 @@
|| (! isFocusDriven && sm.getSelectedIndex() == lastVisibleCellIndex);
if (isLeadIndex) {
// if the last visible cell is selected, we want to shift that cell up
// to be the top-most cell, or at least as far to the top as we can go.
- flow.showAsFirst(lastVisibleCell);
+ flow.scrollToTop(lastVisibleCell);
TreeCell<T> newLastVisibleCell = flow.getLastVisibleCellWithinViewPort();
lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell;
}
} else {
@@ -446,18 +581,18 @@
// the selection down to that, without scrolling the contents, so
// this is a no-op
}
int newSelectionIndex = lastVisibleCell.getIndex();
- flow.show(lastVisibleCell);
+ flow.scrollTo(lastVisibleCell);
return newSelectionIndex;
}
/**
* Function used to scroll the container up by one 'page'.
*/
- public int onScrollPageUp(boolean isFocusDriven) {
+ private int onScrollPageUp(boolean isFocusDriven) {
TreeCell<T> firstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
if (firstVisibleCell == null) return -1;
final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
@@ -478,11 +613,11 @@
|| (! isFocusDriven && sm.getSelectedIndex() == firstVisibleCellIndex);
if (isLeadIndex) {
// if the first visible cell is selected, we want to shift that cell down
// to be the bottom-most cell, or at least as far to the bottom as we can go.
- flow.showAsLast(firstVisibleCell);
+ flow.scrollToBottom(firstVisibleCell);
TreeCell<T> newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell;
}
} else {
@@ -490,78 +625,9 @@
// the selection up to that, without scrolling the contents, so
// this is a no-op
}
int newSelectionIndex = firstVisibleCell.getIndex();
- flow.show(firstVisibleCell);
+ flow.scrollTo(firstVisibleCell);
return newSelectionIndex;
}
-
- @Override
- protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
- switch (attribute) {
- case FOCUS_ITEM: {
- FocusModel<?> fm = getSkinnable().getFocusModel();
- int focusedIndex = fm.getFocusedIndex();
- if (focusedIndex == -1) {
- if (getItemCount() > 0) {
- focusedIndex = 0;
- } else {
- return null;
- }
- }
- return flow.getPrivateCell(focusedIndex);
- }
- case ROW_AT_INDEX: {
- final int rowIndex = (Integer)parameters[0];
- return rowIndex < 0 ? null : flow.getPrivateCell(rowIndex);
- }
- case SELECTED_ITEMS: {
- MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
- ObservableList<Integer> indices = sm.getSelectedIndices();
- List<Node> selection = new ArrayList<>(indices.size());
- for (int i : indices) {
- TreeCell<T> row = flow.getPrivateCell(i);
- if (row != null) selection.add(row);
- }
- return FXCollections.observableArrayList(selection);
- }
- case VERTICAL_SCROLLBAR: return flow.getVbar();
- case HORIZONTAL_SCROLLBAR: return flow.getHbar();
- default: return super.queryAccessibleAttribute(attribute, parameters);
- }
- }
-
- @Override
- protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
- switch (action) {
- case SHOW_ITEM: {
- Node item = (Node)parameters[0];
- if (item instanceof TreeCell) {
- @SuppressWarnings("unchecked")
- TreeCell<T> cell = (TreeCell<T>)item;
- flow.show(cell.getIndex());
- }
- break;
- }
- case SET_SELECTED_ITEMS: {
- @SuppressWarnings("unchecked")
- ObservableList<Node> items = (ObservableList<Node>)parameters[0];
- if (items != null) {
- MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
- if (sm != null) {
- sm.clearSelection();
- for (Node item : items) {
- if (item instanceof TreeCell) {
- @SuppressWarnings("unchecked")
- TreeCell<T> cell = (TreeCell<T>)item;
- sm.select(cell.getIndex());
- }
- }
- }
- }
- break;
- }
- default: super.executeAccessibleAction(action, parameters);
- }
- }
}