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 com.sun.javafx.scene.control.skin;
27
28 import javafx.beans.InvalidationListener;
29 import javafx.beans.Observable;
30 import javafx.beans.WeakInvalidationListener;
31 import javafx.collections.FXCollections;
32 import javafx.collections.MapChangeListener;
33 import javafx.collections.ObservableList;
34 import javafx.collections.ObservableMap;
35 import javafx.event.EventHandler;
36 import javafx.event.EventType;
37 import javafx.event.WeakEventHandler;
38 import javafx.scene.AccessibleAction;
39 import javafx.scene.AccessibleAttribute;
40 import javafx.scene.Node;
41 import javafx.scene.control.*;
42 import javafx.scene.control.TreeItem.TreeModificationEvent;
43 import javafx.scene.input.MouseEvent;
44 import javafx.scene.layout.HBox;
45 import javafx.scene.layout.StackPane;
46 import java.lang.ref.WeakReference;
47 import java.security.AccessController;
48 import java.security.PrivilegedAction;
49 import java.util.ArrayList;
50 import java.util.List;
51
52 import com.sun.javafx.scene.control.behavior.TreeViewBehavior;
53
54 public class TreeViewSkin<T> extends VirtualContainerBase<TreeView<T>, TreeViewBehavior<T>, TreeCell<T>> {
55
56 public static final String RECREATE = "treeRecreateKey";
57
58 // RT-34744 : IS_PANNABLE will be false unless
59 // com.sun.javafx.scene.control.skin.TreeViewSkin.pannable
60 // is set to true. This is done in order to make TreeView functional
61 // on embedded systems with touch screens which do not generate scroll
62 // events for touch drag gestures.
63 private static final boolean IS_PANNABLE =
64 AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("com.sun.javafx.scene.control.skin.TreeViewSkin.pannable"));
65
66
67 public TreeViewSkin(final TreeView treeView) {
68 super(treeView, new TreeViewBehavior(treeView));
69
70 // init the VirtualFlow
71 flow.setPannable(IS_PANNABLE);
72 flow.setCreateCell(flow1 -> TreeViewSkin.this.createCell());
73 flow.setFixedCellSize(treeView.getFixedCellSize());
74 getChildren().add(flow);
75
76 setRoot(getSkinnable().getRoot());
77
78 EventHandler<MouseEvent> ml = event -> {
79 // RT-15127: cancel editing on scroll. This is a bit extreme
80 // (we are cancelling editing on touching the scrollbars).
81 // This can be improved at a later date.
82 if (treeView.getEditingItem() != null) {
83 treeView.edit(null);
84 }
85
86 // This ensures that the tree maintains the focus, even when the vbar
87 // and hbar controls inside the flow are clicked. Without this, the
88 // focus border will not be shown when the user interacts with the
89 // scrollbars, and more importantly, keyboard navigation won't be
90 // available to the user.
91 if (treeView.isFocusTraversable()) {
92 treeView.requestFocus();
93 }
94 };
95 flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
96 flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
97
98 final ObservableMap<Object, Object> properties = treeView.getProperties();
99 properties.remove(RECREATE);
100 properties.addListener(propertiesMapListener);
101
102 // init the behavior 'closures'
103 getBehavior().setOnFocusPreviousRow(() -> { onFocusPreviousCell(); });
104 getBehavior().setOnFocusNextRow(() -> { onFocusNextCell(); });
105 getBehavior().setOnMoveToFirstCell(() -> { onMoveToFirstCell(); });
106 getBehavior().setOnMoveToLastCell(() -> { onMoveToLastCell(); });
107 getBehavior().setOnScrollPageDown(isFocusDriven -> onScrollPageDown(isFocusDriven));
108 getBehavior().setOnScrollPageUp(isFocusDriven -> onScrollPageUp(isFocusDriven));
109 getBehavior().setOnSelectPreviousRow(() -> { onSelectPreviousCell(); });
110 getBehavior().setOnSelectNextRow(() -> { onSelectNextCell(); });
111
112 registerChangeListener(treeView.rootProperty(), "ROOT");
113 registerChangeListener(treeView.showRootProperty(), "SHOW_ROOT");
114 registerChangeListener(treeView.cellFactoryProperty(), "CELL_FACTORY");
115 registerChangeListener(treeView.fixedCellSizeProperty(), "FIXED_CELL_SIZE");
116
117 updateRowCount();
118 }
119
120 @Override protected void handleControlPropertyChanged(String p) {
121 super.handleControlPropertyChanged(p);
122
123 if ("ROOT".equals(p)) {
124 setRoot(getSkinnable().getRoot());
125 } else if ("SHOW_ROOT".equals(p)) {
126 // if we turn off showing the root, then we must ensure the root
127 // is expanded - otherwise we end up with no visible items in
128 // the tree.
129 if (! getSkinnable().isShowRoot() && getRoot() != null) {
130 getRoot().setExpanded(true);
131 }
132 // update the item count in the flow and behavior instances
133 updateRowCount();
134 } else if ("CELL_FACTORY".equals(p)) {
135 flow.recreateCells();
136 } else if ("FIXED_CELL_SIZE".equals(p)) {
137 flow.setFixedCellSize(getSkinnable().getFixedCellSize());
138 }
139 }
140
141 // private boolean needItemCountUpdate = false;
142 private boolean needCellsRebuilt = true;
143 private boolean needCellsReconfigured = false;
144
145 private MapChangeListener<Object, Object> propertiesMapListener = c -> {
146 if (! c.wasAdded()) return;
147 if (RECREATE.equals(c.getKey())) {
148 needCellsRebuilt = true;
149 getSkinnable().requestLayout();
150 getSkinnable().getProperties().remove(RECREATE);
151 }
152 };
153
154 private EventHandler<TreeModificationEvent<T>> rootListener = e -> {
155 if (e.wasAdded() && e.wasRemoved() && e.getAddedSize() == e.getRemovedSize()) {
156 // Fix for RT-14842, where the children of a TreeItem were changing,
157 // but because the overall item count was staying the same, there was
158 // no event being fired to the skin to be informed that the items
159 // had changed. So, here we just watch for the case where the number
160 // of items being added is equal to the number of items being removed.
161 rowCountDirty = true;
162 getSkinnable().requestLayout();
163 } else if (e.getEventType().equals(TreeItem.valueChangedEvent())) {
164 // Fix for RT-14971 and RT-15338.
165 needCellsRebuilt = true;
166 getSkinnable().requestLayout();
167 } else {
168 // Fix for RT-20090. We are checking to see if the event coming
169 // from the TreeItem root is an event where the count has changed.
170 EventType<?> eventType = e.getEventType();
171 while (eventType != null) {
172 if (eventType.equals(TreeItem.<T>expandedItemCountChangeEvent())) {
173 rowCountDirty = true;
174 getSkinnable().requestLayout();
175 break;
176 }
177 eventType = eventType.getSuperType();
178 }
179 }
180
181 // fix for RT-37853
182 getSkinnable().edit(null);
183 };
184
185 private WeakEventHandler<TreeModificationEvent<T>> weakRootListener;
186
187
188 private WeakReference<TreeItem<T>> weakRoot;
189 private TreeItem<T> getRoot() {
190 return weakRoot == null ? null : weakRoot.get();
191 }
192 private void setRoot(TreeItem<T> newRoot) {
193 if (getRoot() != null && weakRootListener != null) {
194 getRoot().removeEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
195 }
196 weakRoot = new WeakReference<>(newRoot);
197 if (getRoot() != null) {
198 weakRootListener = new WeakEventHandler<>(rootListener);
199 getRoot().addEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
200 }
201
202 updateRowCount();
203 }
204
205 @Override public int getItemCount() {
206 return getSkinnable().getExpandedItemCount();
207 }
208
209 @Override protected void updateRowCount() {
210 // int oldCount = flow.getCellCount();
211 int newCount = getItemCount();
212
213 // if this is not called even when the count is the same, we get a
214 // memory leak in VirtualFlow.sheet.children. This can probably be
215 // optimised in the future when time permits.
216 flow.setCellCount(newCount);
217
218 // Ideally we would be more nuanced here, toggling a cheaper needs*
219 // field, but if we do we hit issues such as those identified in
220 // RT-27852, where the expended item count of the new root equals the
221 // EIC of the old root, which would lead to the visuals not updating
222 // properly.
223 needCellsRebuilt = true;
224 getSkinnable().requestLayout();
225 }
226
227 @Override public TreeCell<T> createCell() {
228 final TreeCell<T> cell;
229 if (getSkinnable().getCellFactory() != null) {
230 cell = getSkinnable().getCellFactory().call(getSkinnable());
231 } else {
232 cell = createDefaultCellImpl();
233 }
234
235 // If there is no disclosure node, then add one of my own
236 if (cell.getDisclosureNode() == null) {
237 final StackPane disclosureNode = new StackPane();
238
239 /* This code is intentionally commented.
240 * Currently as it stands it does provided any functionality and interferes
241 * with TreeView. The VO cursor move over the DISCLOSURE_NODE instead of the
242 * tree item itself. This is possibly caused by the order of item's children
243 * (the Labeled and the disclosure node).
244 */
245 // final StackPane disclosureNode = new StackPane() {
246 // @Override protected Object accGetAttribute(Attribute attribute, Object... parameters) {
247 // switch (attribute) {
248 // case ROLE: return Role.DISCLOSURE_NODE;
249 // default: return super.accGetAttribute(attribute, parameters);
250 // }
251 // }
252 // };
253 disclosureNode.getStyleClass().setAll("tree-disclosure-node");
254
255 final StackPane disclosureNodeArrow = new StackPane();
256 disclosureNodeArrow.getStyleClass().setAll("arrow");
257 disclosureNode.getChildren().add(disclosureNodeArrow);
258
259 cell.setDisclosureNode(disclosureNode);
260 }
261
262 cell.updateTreeView(getSkinnable());
263
264 return cell;
265 }
266
267 // Note: This is a copy/paste of javafx.scene.control.cell.DefaultTreeCell,
268 // which is package-protected
269 private TreeCell<T> createDefaultCellImpl() {
270 return new TreeCell<T>() {
271 private HBox hbox;
272
273 private WeakReference<TreeItem<T>> treeItemRef;
274
275 private InvalidationListener treeItemGraphicListener = observable -> {
276 updateDisplay(getItem(), isEmpty());
277 };
278
279 private InvalidationListener treeItemListener = new InvalidationListener() {
280 @Override public void invalidated(Observable observable) {
281 TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
282 if (oldTreeItem != null) {
283 oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
284 }
285
286 TreeItem<T> newTreeItem = getTreeItem();
334 } else {
335 hbox = null;
336 if (item instanceof Node) {
337 setText(null);
338 setGraphic((Node)item);
339 } else {
340 setText(item.toString());
341 setGraphic(null);
342 }
343 }
344 }
345 }
346
347 @Override public void updateItem(T item, boolean empty) {
348 super.updateItem(item, empty);
349 updateDisplay(item, empty);
350 }
351 };
352 }
353
354 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
355 return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987;
356 }
357
358 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
359 return 400;
360 }
361
362 @Override
363 protected void layoutChildren(final double x, final double y,
364 final double w, final double h) {
365 super.layoutChildren(x, y, w, h);
366
367 if (needCellsRebuilt) {
368 flow.rebuildCells();
369 } else if (needCellsReconfigured) {
370 flow.reconfigureCells();
371 }
372
373 needCellsRebuilt = false;
374 needCellsReconfigured = false;
375
376 flow.resizeRelocate(x, y, w, h);
377 }
378
379 private void onFocusPreviousCell() {
380 FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
381 if (fm == null) return;
382 flow.show(fm.getFocusedIndex());
383 }
384
385 private void onFocusNextCell() {
386 FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
387 if (fm == null) return;
388 flow.show(fm.getFocusedIndex());
389 }
390
391 private void onSelectPreviousCell() {
392 int row = getSkinnable().getSelectionModel().getSelectedIndex();
393 flow.show(row);
394 }
395
396 private void onSelectNextCell() {
397 int row = getSkinnable().getSelectionModel().getSelectedIndex();
398 flow.show(row);
399 }
400
401 private void onMoveToFirstCell() {
402 flow.show(0);
403 flow.setPosition(0);
404 }
405
406 private void onMoveToLastCell() {
407 flow.show(getItemCount());
408 flow.setPosition(1);
409 }
410
411 /**
412 * Function used to scroll the container down by one 'page'.
413 */
414 public int onScrollPageDown(boolean isFocusDriven) {
415 TreeCell<T> lastVisibleCell = flow.getLastVisibleCellWithinViewPort();
416 if (lastVisibleCell == null) return -1;
417
418 final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
419 final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
420 if (sm == null || fm == null) return -1;
421
422 int lastVisibleCellIndex = lastVisibleCell.getIndex();
423
424 // isSelected represents focus OR selection
425 boolean isSelected = false;
426 if (isFocusDriven) {
427 isSelected = lastVisibleCell.isFocused() || fm.isFocused(lastVisibleCellIndex);
428 } else {
429 isSelected = lastVisibleCell.isSelected() || sm.isSelected(lastVisibleCellIndex);
430 }
431
432 if (isSelected) {
433 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == lastVisibleCellIndex)
434 || (! isFocusDriven && sm.getSelectedIndex() == lastVisibleCellIndex);
435
436 if (isLeadIndex) {
437 // if the last visible cell is selected, we want to shift that cell up
438 // to be the top-most cell, or at least as far to the top as we can go.
439 flow.showAsFirst(lastVisibleCell);
440
441 TreeCell<T> newLastVisibleCell = flow.getLastVisibleCellWithinViewPort();
442 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell;
443 }
444 } else {
445 // if the selection is not on the 'bottom' most cell, we firstly move
446 // the selection down to that, without scrolling the contents, so
447 // this is a no-op
448 }
449
450 int newSelectionIndex = lastVisibleCell.getIndex();
451 flow.show(lastVisibleCell);
452 return newSelectionIndex;
453 }
454
455 /**
456 * Function used to scroll the container up by one 'page'.
457 */
458 public int onScrollPageUp(boolean isFocusDriven) {
459 TreeCell<T> firstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
460 if (firstVisibleCell == null) return -1;
461
462 final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
463 final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
464 if (sm == null || fm == null) return -1;
465
466 int firstVisibleCellIndex = firstVisibleCell.getIndex();
467
468 // isSelected represents focus OR selection
469 boolean isSelected = false;
470 if (isFocusDriven) {
471 isSelected = firstVisibleCell.isFocused() || fm.isFocused(firstVisibleCellIndex);
472 } else {
473 isSelected = firstVisibleCell.isSelected() || sm.isSelected(firstVisibleCellIndex);
474 }
475
476 if (isSelected) {
477 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == firstVisibleCellIndex)
478 || (! isFocusDriven && sm.getSelectedIndex() == firstVisibleCellIndex);
479
480 if (isLeadIndex) {
481 // if the first visible cell is selected, we want to shift that cell down
482 // to be the bottom-most cell, or at least as far to the bottom as we can go.
483 flow.showAsLast(firstVisibleCell);
484
485 TreeCell<T> newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
486 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell;
487 }
488 } else {
489 // if the selection is not on the 'top' most cell, we firstly move
490 // the selection up to that, without scrolling the contents, so
491 // this is a no-op
492 }
493
494 int newSelectionIndex = firstVisibleCell.getIndex();
495 flow.show(firstVisibleCell);
496 return newSelectionIndex;
497 }
498
499 @Override
500 protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
501 switch (attribute) {
502 case FOCUS_ITEM: {
503 FocusModel<?> fm = getSkinnable().getFocusModel();
504 int focusedIndex = fm.getFocusedIndex();
505 if (focusedIndex == -1) {
506 if (getItemCount() > 0) {
507 focusedIndex = 0;
508 } else {
509 return null;
510 }
511 }
512 return flow.getPrivateCell(focusedIndex);
513 }
514 case ROW_AT_INDEX: {
515 final int rowIndex = (Integer)parameters[0];
516 return rowIndex < 0 ? null : flow.getPrivateCell(rowIndex);
517 }
518 case SELECTED_ITEMS: {
519 MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
520 ObservableList<Integer> indices = sm.getSelectedIndices();
521 List<Node> selection = new ArrayList<>(indices.size());
522 for (int i : indices) {
523 TreeCell<T> row = flow.getPrivateCell(i);
524 if (row != null) selection.add(row);
525 }
526 return FXCollections.observableArrayList(selection);
527 }
528 case VERTICAL_SCROLLBAR: return flow.getVbar();
529 case HORIZONTAL_SCROLLBAR: return flow.getHbar();
530 default: return super.queryAccessibleAttribute(attribute, parameters);
531 }
532 }
533
534 @Override
535 protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
536 switch (action) {
537 case SHOW_ITEM: {
538 Node item = (Node)parameters[0];
539 if (item instanceof TreeCell) {
540 @SuppressWarnings("unchecked")
541 TreeCell<T> cell = (TreeCell<T>)item;
542 flow.show(cell.getIndex());
543 }
544 break;
545 }
546 case SET_SELECTED_ITEMS: {
547 @SuppressWarnings("unchecked")
548 ObservableList<Node> items = (ObservableList<Node>)parameters[0];
549 if (items != null) {
550 MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
551 if (sm != null) {
552 sm.clearSelection();
553 for (Node item : items) {
554 if (item instanceof TreeCell) {
555 @SuppressWarnings("unchecked")
556 TreeCell<T> cell = (TreeCell<T>)item;
557 sm.select(cell.getIndex());
558 }
559 }
560 }
561 }
562 break;
563 }
564 default: super.executeAccessibleAction(action, parameters);
565 }
566 }
567 }
|
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.skin;
27
28 import com.sun.javafx.scene.control.Properties;
29 import com.sun.javafx.scene.control.behavior.BehaviorBase;
30 import javafx.beans.InvalidationListener;
31 import javafx.beans.Observable;
32 import javafx.beans.WeakInvalidationListener;
33 import javafx.collections.FXCollections;
34 import javafx.collections.MapChangeListener;
35 import javafx.collections.ObservableList;
36 import javafx.collections.ObservableMap;
37 import javafx.event.EventHandler;
38 import javafx.event.EventType;
39 import javafx.event.WeakEventHandler;
40 import javafx.scene.AccessibleAction;
41 import javafx.scene.AccessibleAttribute;
42 import javafx.scene.Node;
43 import javafx.scene.control.*;
44 import javafx.scene.control.TreeItem.TreeModificationEvent;
45 import javafx.scene.input.MouseEvent;
46 import javafx.scene.layout.HBox;
47 import javafx.scene.layout.StackPane;
48 import java.lang.ref.WeakReference;
49 import java.security.AccessController;
50 import java.security.PrivilegedAction;
51 import java.util.ArrayList;
52 import java.util.List;
53
54 import com.sun.javafx.scene.control.behavior.TreeViewBehavior;
55
56 /**
57 * Default skin implementation for the {@link TreeView} control.
58 *
59 * @see TreeView
60 * @since 9
61 */
62 public class TreeViewSkin<T> extends VirtualContainerBase<TreeView<T>, TreeCell<T>> {
63
64 /***************************************************************************
65 * *
66 * Static fields *
67 * *
68 **************************************************************************/
69
70 // RT-34744 : IS_PANNABLE will be false unless
71 // javafx.scene.control.skin.TreeViewSkin.pannable
72 // is set to true. This is done in order to make TreeView functional
73 // on embedded systems with touch screens which do not generate scroll
74 // events for touch drag gestures.
75 private static final boolean IS_PANNABLE =
76 AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.TreeViewSkin.pannable"));
77
78
79
80 /***************************************************************************
81 * *
82 * Private fields *
83 * *
84 **************************************************************************/
85
86 private final VirtualFlow<TreeCell<T>> flow;
87 private WeakReference<TreeItem<T>> weakRoot;
88 private final TreeViewBehavior<T> behavior;
89
90 // private boolean needItemCountUpdate = false;
91 private boolean needCellsRebuilt = true;
92 private boolean needCellsReconfigured = false;
93
94
95
96 /***************************************************************************
97 * *
98 * Listeners *
99 * *
100 **************************************************************************/
101
102 private MapChangeListener<Object, Object> propertiesMapListener = c -> {
103 if (! c.wasAdded()) return;
104 if (Properties.RECREATE.equals(c.getKey())) {
105 needCellsRebuilt = true;
106 getSkinnable().requestLayout();
107 getSkinnable().getProperties().remove(Properties.RECREATE);
108 }
109 };
110
111 private EventHandler<TreeModificationEvent<T>> rootListener = e -> {
112 if (e.wasAdded() && e.wasRemoved() && e.getAddedSize() == e.getRemovedSize()) {
113 // Fix for RT-14842, where the children of a TreeItem were changing,
114 // but because the overall item count was staying the same, there was
115 // no event being fired to the skin to be informed that the items
116 // had changed. So, here we just watch for the case where the number
117 // of items being added is equal to the number of items being removed.
118 rowCountDirty = true;
119 getSkinnable().requestLayout();
120 } else if (e.getEventType().equals(TreeItem.valueChangedEvent())) {
121 // Fix for RT-14971 and RT-15338.
122 needCellsRebuilt = true;
123 getSkinnable().requestLayout();
124 } else {
125 // Fix for RT-20090. We are checking to see if the event coming
126 // from the TreeItem root is an event where the count has changed.
127 EventType<?> eventType = e.getEventType();
128 while (eventType != null) {
129 if (eventType.equals(TreeItem.<T>expandedItemCountChangeEvent())) {
130 rowCountDirty = true;
131 getSkinnable().requestLayout();
132 break;
133 }
134 eventType = eventType.getSuperType();
135 }
136 }
137
138 // fix for RT-37853
139 getSkinnable().edit(null);
140 };
141
142 private WeakEventHandler<TreeModificationEvent<T>> weakRootListener;
143
144
145
146 /***************************************************************************
147 * *
148 * Constructors *
149 * *
150 **************************************************************************/
151
152 /**
153 * Creates a new TreeViewSkin instance, installing the necessary child
154 * nodes into the Control {@link Control#getChildren() children} list, as
155 * well as the necessary input mappings for handling key, mouse, etc events.
156 *
157 * @param control The control that this skin should be installed onto.
158 */
159 public TreeViewSkin(final TreeView control) {
160 super(control);
161
162 // install default input map for the TreeView control
163 behavior = new TreeViewBehavior<>(control);
164 // control.setInputMap(behavior.getInputMap());
165
166 // init the VirtualFlow
167 flow = getVirtualFlow();
168 flow.setPannable(IS_PANNABLE);
169 flow.setCellFactory(flow1 -> createCell());
170 flow.setFixedCellSize(control.getFixedCellSize());
171 getChildren().add(flow);
172
173 setRoot(getSkinnable().getRoot());
174
175 EventHandler<MouseEvent> ml = event -> {
176 // RT-15127: cancel editing on scroll. This is a bit extreme
177 // (we are cancelling editing on touching the scrollbars).
178 // This can be improved at a later date.
179 if (control.getEditingItem() != null) {
180 control.edit(null);
181 }
182
183 // This ensures that the tree maintains the focus, even when the vbar
184 // and hbar controls inside the flow are clicked. Without this, the
185 // focus border will not be shown when the user interacts with the
186 // scrollbars, and more importantly, keyboard navigation won't be
187 // available to the user.
188 if (control.isFocusTraversable()) {
189 control.requestFocus();
190 }
191 };
192 flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
193 flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
194
195 final ObservableMap<Object, Object> properties = control.getProperties();
196 properties.remove(Properties.RECREATE);
197 properties.addListener(propertiesMapListener);
198
199 // init the behavior 'closures'
200 behavior.setOnFocusPreviousRow(() -> { onFocusPreviousCell(); });
201 behavior.setOnFocusNextRow(() -> { onFocusNextCell(); });
202 behavior.setOnMoveToFirstCell(() -> { onMoveToFirstCell(); });
203 behavior.setOnMoveToLastCell(() -> { onMoveToLastCell(); });
204 behavior.setOnScrollPageDown(this::onScrollPageDown);
205 behavior.setOnScrollPageUp(this::onScrollPageUp);
206 behavior.setOnSelectPreviousRow(() -> { onSelectPreviousCell(); });
207 behavior.setOnSelectNextRow(() -> { onSelectNextCell(); });
208
209 registerChangeListener(control.rootProperty(), e -> setRoot(getSkinnable().getRoot()));
210 registerChangeListener(control.showRootProperty(), e -> {
211 // if we turn off showing the root, then we must ensure the root
212 // is expanded - otherwise we end up with no visible items in
213 // the tree.
214 if (! getSkinnable().isShowRoot() && getRoot() != null) {
215 getRoot().setExpanded(true);
216 }
217 // update the item count in the flow and behavior instances
218 updateRowCount();
219 });
220 registerChangeListener(control.cellFactoryProperty(), e -> flow.recreateCells());
221 registerChangeListener(control.fixedCellSizeProperty(), e -> flow.setFixedCellSize(getSkinnable().getFixedCellSize()));
222
223 updateRowCount();
224 }
225
226
227
228 /***************************************************************************
229 * *
230 * Public API *
231 * *
232 **************************************************************************/
233
234 /** {@inheritDoc} */
235 @Override public void dispose() {
236 super.dispose();
237
238 if (behavior != null) {
239 behavior.dispose();
240 }
241 }
242
243 /** {@inheritDoc} */
244 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
245 return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987;
246 }
247
248 /** {@inheritDoc} */
249 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
250 return 400;
251 }
252
253 /** {@inheritDoc} */
254 @Override protected void layoutChildren(final double x, final double y,
255 final double w, final double h) {
256 super.layoutChildren(x, y, w, h);
257
258 if (needCellsRebuilt) {
259 flow.rebuildCells();
260 } else if (needCellsReconfigured) {
261 flow.reconfigureCells();
262 }
263
264 needCellsRebuilt = false;
265 needCellsReconfigured = false;
266
267 flow.resizeRelocate(x, y, w, h);
268 }
269
270 /** {@inheritDoc} */
271 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
272 switch (attribute) {
273 case FOCUS_ITEM: {
274 FocusModel<?> fm = getSkinnable().getFocusModel();
275 int focusedIndex = fm.getFocusedIndex();
276 if (focusedIndex == -1) {
277 if (getItemCount() > 0) {
278 focusedIndex = 0;
279 } else {
280 return null;
281 }
282 }
283 return flow.getPrivateCell(focusedIndex);
284 }
285 case ROW_AT_INDEX: {
286 final int rowIndex = (Integer)parameters[0];
287 return rowIndex < 0 ? null : flow.getPrivateCell(rowIndex);
288 }
289 case SELECTED_ITEMS: {
290 MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
291 ObservableList<Integer> indices = sm.getSelectedIndices();
292 List<Node> selection = new ArrayList<>(indices.size());
293 for (int i : indices) {
294 TreeCell<T> row = flow.getPrivateCell(i);
295 if (row != null) selection.add(row);
296 }
297 return FXCollections.observableArrayList(selection);
298 }
299 case VERTICAL_SCROLLBAR: return flow.getVbar();
300 case HORIZONTAL_SCROLLBAR: return flow.getHbar();
301 default: return super.queryAccessibleAttribute(attribute, parameters);
302 }
303 }
304
305 /** {@inheritDoc} */
306 @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
307 switch (action) {
308 case SHOW_ITEM: {
309 Node item = (Node)parameters[0];
310 if (item instanceof TreeCell) {
311 @SuppressWarnings("unchecked")
312 TreeCell<T> cell = (TreeCell<T>)item;
313 flow.scrollTo(cell.getIndex());
314 }
315 break;
316 }
317 case SET_SELECTED_ITEMS: {
318 @SuppressWarnings("unchecked")
319 ObservableList<Node> items = (ObservableList<Node>)parameters[0];
320 if (items != null) {
321 MultipleSelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
322 if (sm != null) {
323 sm.clearSelection();
324 for (Node item : items) {
325 if (item instanceof TreeCell) {
326 @SuppressWarnings("unchecked")
327 TreeCell<T> cell = (TreeCell<T>)item;
328 sm.select(cell.getIndex());
329 }
330 }
331 }
332 }
333 break;
334 }
335 default: super.executeAccessibleAction(action, parameters);
336 }
337 }
338
339
340 /***************************************************************************
341 * *
342 * Private implementation *
343 * *
344 **************************************************************************/
345
346 /** {@inheritDoc} */
347 private TreeCell<T> createCell() {
348 final TreeCell<T> cell;
349 if (getSkinnable().getCellFactory() != null) {
350 cell = getSkinnable().getCellFactory().call(getSkinnable());
351 } else {
352 cell = createDefaultCellImpl();
353 }
354
355 // If there is no disclosure node, then add one of my own
356 if (cell.getDisclosureNode() == null) {
357 final StackPane disclosureNode = new StackPane();
358
359 /* This code is intentionally commented.
360 * Currently as it stands it does provided any functionality and interferes
361 * with TreeView. The VO cursor move over the DISCLOSURE_NODE instead of the
362 * tree item itself. This is possibly caused by the order of item's children
363 * (the Labeled and the disclosure node).
364 */
365 // final StackPane disclosureNode = new StackPane() {
366 // @Override protected Object accGetAttribute(Attribute attribute, Object... parameters) {
367 // switch (attribute) {
368 // case ROLE: return Role.DISCLOSURE_NODE;
369 // default: return super.accGetAttribute(attribute, parameters);
370 // }
371 // }
372 // };
373 disclosureNode.getStyleClass().setAll("tree-disclosure-node");
374
375 final StackPane disclosureNodeArrow = new StackPane();
376 disclosureNodeArrow.getStyleClass().setAll("arrow");
377 disclosureNode.getChildren().add(disclosureNodeArrow);
378
379 cell.setDisclosureNode(disclosureNode);
380 }
381
382 cell.updateTreeView(getSkinnable());
383
384 return cell;
385 }
386
387 private TreeItem<T> getRoot() {
388 return weakRoot == null ? null : weakRoot.get();
389 }
390 private void setRoot(TreeItem<T> newRoot) {
391 if (getRoot() != null && weakRootListener != null) {
392 getRoot().removeEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
393 }
394 weakRoot = new WeakReference<>(newRoot);
395 if (getRoot() != null) {
396 weakRootListener = new WeakEventHandler<>(rootListener);
397 getRoot().addEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootListener);
398 }
399
400 updateRowCount();
401 }
402
403 /** {@inheritDoc} */
404 @Override int getItemCount() {
405 return getSkinnable().getExpandedItemCount();
406 }
407
408 /** {@inheritDoc} */
409 @Override void updateRowCount() {
410 // int oldCount = flow.getCellCount();
411 int newCount = getItemCount();
412
413 // if this is not called even when the count is the same, we get a
414 // memory leak in VirtualFlow.sheet.children. This can probably be
415 // optimised in the future when time permits.
416 flow.setCellCount(newCount);
417
418 // Ideally we would be more nuanced here, toggling a cheaper needs*
419 // field, but if we do we hit issues such as those identified in
420 // RT-27852, where the expended item count of the new root equals the
421 // EIC of the old root, which would lead to the visuals not updating
422 // properly.
423 needCellsRebuilt = true;
424 getSkinnable().requestLayout();
425 }
426
427 // Note: This is a copy/paste of javafx.scene.control.cell.DefaultTreeCell,
428 // which is package-protected
429 private TreeCell<T> createDefaultCellImpl() {
430 return new TreeCell<T>() {
431 private HBox hbox;
432
433 private WeakReference<TreeItem<T>> treeItemRef;
434
435 private InvalidationListener treeItemGraphicListener = observable -> {
436 updateDisplay(getItem(), isEmpty());
437 };
438
439 private InvalidationListener treeItemListener = new InvalidationListener() {
440 @Override public void invalidated(Observable observable) {
441 TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
442 if (oldTreeItem != null) {
443 oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
444 }
445
446 TreeItem<T> newTreeItem = getTreeItem();
494 } else {
495 hbox = null;
496 if (item instanceof Node) {
497 setText(null);
498 setGraphic((Node)item);
499 } else {
500 setText(item.toString());
501 setGraphic(null);
502 }
503 }
504 }
505 }
506
507 @Override public void updateItem(T item, boolean empty) {
508 super.updateItem(item, empty);
509 updateDisplay(item, empty);
510 }
511 };
512 }
513
514 private void onFocusPreviousCell() {
515 FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
516 if (fm == null) return;
517 flow.scrollTo(fm.getFocusedIndex());
518 }
519
520 private void onFocusNextCell() {
521 FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
522 if (fm == null) return;
523 flow.scrollTo(fm.getFocusedIndex());
524 }
525
526 private void onSelectPreviousCell() {
527 int row = getSkinnable().getSelectionModel().getSelectedIndex();
528 flow.scrollTo(row);
529 }
530
531 private void onSelectNextCell() {
532 int row = getSkinnable().getSelectionModel().getSelectedIndex();
533 flow.scrollTo(row);
534 }
535
536 private void onMoveToFirstCell() {
537 flow.scrollTo(0);
538 flow.setPosition(0);
539 }
540
541 private void onMoveToLastCell() {
542 flow.scrollTo(getItemCount());
543 flow.setPosition(1);
544 }
545
546 /**
547 * Function used to scroll the container down by one 'page'.
548 */
549 private int onScrollPageDown(boolean isFocusDriven) {
550 TreeCell<T> lastVisibleCell = flow.getLastVisibleCellWithinViewPort();
551 if (lastVisibleCell == null) return -1;
552
553 final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
554 final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
555 if (sm == null || fm == null) return -1;
556
557 int lastVisibleCellIndex = lastVisibleCell.getIndex();
558
559 // isSelected represents focus OR selection
560 boolean isSelected = false;
561 if (isFocusDriven) {
562 isSelected = lastVisibleCell.isFocused() || fm.isFocused(lastVisibleCellIndex);
563 } else {
564 isSelected = lastVisibleCell.isSelected() || sm.isSelected(lastVisibleCellIndex);
565 }
566
567 if (isSelected) {
568 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == lastVisibleCellIndex)
569 || (! isFocusDriven && sm.getSelectedIndex() == lastVisibleCellIndex);
570
571 if (isLeadIndex) {
572 // if the last visible cell is selected, we want to shift that cell up
573 // to be the top-most cell, or at least as far to the top as we can go.
574 flow.scrollToTop(lastVisibleCell);
575
576 TreeCell<T> newLastVisibleCell = flow.getLastVisibleCellWithinViewPort();
577 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell;
578 }
579 } else {
580 // if the selection is not on the 'bottom' most cell, we firstly move
581 // the selection down to that, without scrolling the contents, so
582 // this is a no-op
583 }
584
585 int newSelectionIndex = lastVisibleCell.getIndex();
586 flow.scrollTo(lastVisibleCell);
587 return newSelectionIndex;
588 }
589
590 /**
591 * Function used to scroll the container up by one 'page'.
592 */
593 private int onScrollPageUp(boolean isFocusDriven) {
594 TreeCell<T> firstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
595 if (firstVisibleCell == null) return -1;
596
597 final SelectionModel<TreeItem<T>> sm = getSkinnable().getSelectionModel();
598 final FocusModel<TreeItem<T>> fm = getSkinnable().getFocusModel();
599 if (sm == null || fm == null) return -1;
600
601 int firstVisibleCellIndex = firstVisibleCell.getIndex();
602
603 // isSelected represents focus OR selection
604 boolean isSelected = false;
605 if (isFocusDriven) {
606 isSelected = firstVisibleCell.isFocused() || fm.isFocused(firstVisibleCellIndex);
607 } else {
608 isSelected = firstVisibleCell.isSelected() || sm.isSelected(firstVisibleCellIndex);
609 }
610
611 if (isSelected) {
612 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == firstVisibleCellIndex)
613 || (! isFocusDriven && sm.getSelectedIndex() == firstVisibleCellIndex);
614
615 if (isLeadIndex) {
616 // if the first visible cell is selected, we want to shift that cell down
617 // to be the bottom-most cell, or at least as far to the bottom as we can go.
618 flow.scrollToBottom(firstVisibleCell);
619
620 TreeCell<T> newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
621 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell;
622 }
623 } else {
624 // if the selection is not on the 'top' most cell, we firstly move
625 // the selection up to that, without scrolling the contents, so
626 // this is a no-op
627 }
628
629 int newSelectionIndex = firstVisibleCell.getIndex();
630 flow.scrollTo(firstVisibleCell);
631 return newSelectionIndex;
632 }
633 }
|