Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.control;
027
028import javafx.css.PseudoClass;
029import javafx.beans.InvalidationListener;
030import javafx.beans.Observable;
031import javafx.beans.WeakInvalidationListener;
032import javafx.beans.property.ObjectProperty;
033import javafx.beans.property.SimpleObjectProperty;
034import javafx.collections.ListChangeListener;
035import javafx.scene.Node;
036import com.sun.javafx.scene.control.skin.TreeCellSkin;
037import javafx.collections.WeakListChangeListener;
038import java.lang.ref.WeakReference;
039import javafx.beans.property.BooleanProperty;
040import javafx.beans.property.ReadOnlyObjectProperty;
041import javafx.beans.property.ReadOnlyObjectWrapper;
042import javafx.beans.value.ChangeListener;
043import javafx.beans.value.ObservableValue;
044import javafx.beans.value.WeakChangeListener;
045
046/**
047 * The {@link Cell} type used with the {@link TreeView} control. In addition to 
048 * the API defined on {@link IndexedCell}, the TreeCell
049 * exposes additional states and pseudo classes for use by CSS.
050 * <p>
051 * A TreeCell watches the selection model of the TreeView for which it is
052 * associated, ensuring that it visually indicates to the user whether it is
053 * selected. When a TreeCell is selected, this is exposed both via the
054 * {@link #selectedProperty() selected} property, as well as via the 'selected'
055 * CSS pseudo class state.
056 * <p>
057 * Due to the fact that TreeCell extends from {@link IndexedCell}, each TreeCell 
058 * also provides an {@link #indexProperty() index} property. The index will be 
059 * updated as cells are expanded and collapsed, and therefore should be
060 * considered a view index rather than a model index.
061 * <p>
062 * Finally, each TreeCell also has a reference back to the TreeView that it is
063 * being used with. Each TreeCell belongs to one and only one TreeView.
064 *
065 * @see TreeView
066 * @see TreeItem
067 * @param <T> The type of the value contained within the 
068 *      {@link #treeItemProperty() TreeItem} property.
069 */
070public class TreeCell<T> extends IndexedCell<T> {
071
072    /***************************************************************************
073     *                                                                         *
074     * Constructors                                                            *
075     *                                                                         *
076     **************************************************************************/
077
078    /**
079     * Creates a default TreeCell instance.
080     */
081    public TreeCell() {
082        getStyleClass().addAll(DEFAULT_STYLE_CLASS);
083    }
084
085
086
087    /***************************************************************************
088     *                                                                         *
089     * Callbacks and events                                                    *
090     *                                                                         *
091     **************************************************************************/
092    
093    private final ListChangeListener<Integer> selectedListener = new ListChangeListener<Integer>() {
094        @Override public void onChanged(ListChangeListener.Change<? extends Integer> c) {
095            updateSelection();
096        }
097    };
098    
099    /**
100     * Listens to the selectionModel property on the TreeView. Whenever the entire model is changed,
101     * we have to unhook the weakSelectedListener and update the selection.
102     */
103    private final ChangeListener<MultipleSelectionModel<TreeItem<T>>> selectionModelPropertyListener = new ChangeListener<MultipleSelectionModel<TreeItem<T>>>() {
104        @Override public void changed(ObservableValue<? extends MultipleSelectionModel<TreeItem<T>>> observable,
105                                      MultipleSelectionModel<TreeItem<T>> oldValue,
106                                      MultipleSelectionModel<TreeItem<T>> newValue) {
107            if (oldValue != null) {
108                oldValue.getSelectedIndices().removeListener(weakSelectedListener);
109            }
110            if (newValue != null) {
111                newValue.getSelectedIndices().addListener(weakSelectedListener);
112            }
113            updateSelection();
114        }
115    };    
116
117    private final InvalidationListener focusedListener = new InvalidationListener() {
118        @Override public void invalidated(Observable valueModel) {
119            updateFocus();
120        }
121    };
122    
123    /**
124     * Listens to the focusModel property on the TreeView. Whenever the entire model is changed,
125     * we have to unhook the weakFocusedListener and update the focus.
126     */
127    private final ChangeListener<FocusModel<TreeItem<T>>> focusModelPropertyListener = new ChangeListener<FocusModel<TreeItem<T>>>() {
128        @Override public void changed(ObservableValue<? extends FocusModel<TreeItem<T>>> observable,
129                                      FocusModel<TreeItem<T>> oldValue,
130                                      FocusModel<TreeItem<T>> newValue) {
131            if (oldValue != null) {
132                oldValue.focusedIndexProperty().removeListener(weakFocusedListener);
133            }
134            if (newValue != null) {
135                newValue.focusedIndexProperty().addListener(weakFocusedListener);
136            }
137            updateFocus();
138        }
139    };
140
141    private final InvalidationListener editingListener = new InvalidationListener() {
142        @Override public void invalidated(Observable valueModel) {
143            updateEditing();
144        }
145    };
146    
147    private final InvalidationListener leafListener = new InvalidationListener() {
148        @Override public void invalidated(Observable valueModel) {
149            // necessary to update the disclosure node in the skin when the
150            // leaf property changes
151            TreeItem<T> treeItem = getTreeItem();
152            if (treeItem != null) {
153                requestLayout();
154            }
155        }
156    };
157    
158    /* proxy pseudo-class state change from treeItem's expandedProperty */
159    private final InvalidationListener treeItemExpandedInvalidationListener = new InvalidationListener() {
160        @Override public void invalidated(Observable o) {
161            boolean isExpanded = ((BooleanProperty)o).get();
162            pseudoClassStateChanged(EXPANDED_PSEUDOCLASS_STATE,   isExpanded);
163            pseudoClassStateChanged(COLLAPSED_PSEUDOCLASS_STATE, !isExpanded);
164        }
165    };
166    
167    private final WeakListChangeListener<Integer> weakSelectedListener = new WeakListChangeListener<Integer>(selectedListener);
168    private final WeakChangeListener<MultipleSelectionModel<TreeItem<T>>> weakSelectionModelPropertyListener = new WeakChangeListener<MultipleSelectionModel<TreeItem<T>>>(selectionModelPropertyListener);
169    private final WeakInvalidationListener weakFocusedListener = new WeakInvalidationListener(focusedListener);
170    private final WeakChangeListener<FocusModel<TreeItem<T>>> weakFocusModelPropertyListener = new WeakChangeListener<FocusModel<TreeItem<T>>>(focusModelPropertyListener);
171    private final WeakInvalidationListener weakEditingListener = new WeakInvalidationListener(editingListener);
172    private final WeakInvalidationListener weakLeafListener = new WeakInvalidationListener(leafListener);
173    private final WeakInvalidationListener weakTreeItemExpandedInvalidationListener =
174            new WeakInvalidationListener(treeItemExpandedInvalidationListener);
175    
176    
177    
178    /***************************************************************************
179     *                                                                         *
180     * Properties                                                              *
181     *                                                                         *
182     **************************************************************************/
183    
184    // --- TreeItem
185    private ReadOnlyObjectWrapper<TreeItem<T>> treeItem = 
186        new ReadOnlyObjectWrapper<TreeItem<T>>(this, "treeItem") {
187
188            TreeItem<T> oldValue = null;
189            
190            @Override protected void invalidated() {
191                if (oldValue != null) {
192                    oldValue.expandedProperty().removeListener(weakTreeItemExpandedInvalidationListener);
193                }
194                
195                oldValue = get(); 
196                
197                if (oldValue != null) {
198                    oldValue.expandedProperty().addListener(weakTreeItemExpandedInvalidationListener);
199                    // fake an invalidation to ensure updated pseudo-class state
200                    weakTreeItemExpandedInvalidationListener.invalidated(oldValue.expandedProperty());            
201                }
202                
203            }
204    };
205    private void setTreeItem(TreeItem<T> value) {
206        treeItem.set(value); 
207    }
208    
209    /**
210     * Returns the TreeItem currently set in this TreeCell.
211     */
212    public final TreeItem<T> getTreeItem() { return treeItem.get(); }
213    
214    /**
215     * Each TreeCell represents at most a single {@link TreeItem}, which is
216     * represented by this property.
217     */
218    public final ReadOnlyObjectProperty<TreeItem<T>> treeItemProperty() { return treeItem.getReadOnlyProperty(); }
219
220
221    
222    // --- Disclosure Node
223    private ObjectProperty<Node> disclosureNode = new SimpleObjectProperty<Node>(this, "disclosureNode");
224
225    /**
226     * The node to use as the "disclosure" triangle, or toggle, used for
227     * expanding and collapsing items. This is only used in the case of
228     * an item in the tree which contains child items. If not specified, the
229     * TreeCell's Skin implementation is responsible for providing a default
230     * disclosure node.
231     */
232    public final void setDisclosureNode(Node value) { disclosureNodeProperty().set(value); }
233    
234    /**
235     * Returns the current disclosure node set in this TreeCell.
236     */
237    public final Node getDisclosureNode() { return disclosureNode.get(); }
238    
239    /**
240     * The disclosure node is commonly seen represented as a triangle that rotates
241     * on screen to indicate whether or not the TreeItem that it is placed
242     * beside is expanded or collapsed.
243     */
244    public final ObjectProperty<Node> disclosureNodeProperty() { return disclosureNode; }
245    
246    
247    // --- TreeView
248    private ReadOnlyObjectWrapper<TreeView<T>> treeView = new ReadOnlyObjectWrapper<TreeView<T>>() {
249        private WeakReference<TreeView<T>> weakTreeViewRef;
250        @Override protected void invalidated() {
251            MultipleSelectionModel<TreeItem<T>> sm;
252            FocusModel<TreeItem<T>> fm;
253            
254            if (weakTreeViewRef != null) {
255                TreeView<T> oldTreeView = weakTreeViewRef.get();
256                if (oldTreeView != null) {
257                    // remove old listeners
258                    sm = oldTreeView.getSelectionModel();
259                    if (sm != null) {
260                        sm.getSelectedIndices().removeListener(weakSelectedListener);
261                    }
262
263                    fm = oldTreeView.getFocusModel();
264                    if (fm != null) {
265                        fm.focusedIndexProperty().removeListener(weakFocusedListener);
266                    }
267
268                    oldTreeView.editingItemProperty().removeListener(weakEditingListener);
269                    oldTreeView.focusModelProperty().removeListener(weakFocusModelPropertyListener);
270                    oldTreeView.selectionModelProperty().removeListener(weakSelectionModelPropertyListener);
271                }
272                
273                weakTreeViewRef = null;
274            }
275
276            TreeView<T> treeView = get();
277            if (treeView != null) {
278                sm = treeView.getSelectionModel();
279                if (sm != null) {
280                    // listening for changes to treeView.selectedIndex and IndexedCell.index,
281                    // to determine if this cell is selected
282                    sm.getSelectedIndices().addListener(weakSelectedListener);
283                }
284
285                fm = treeView.getFocusModel();
286                if (fm != null) {
287                    // similar to above, but this time for focus
288                    fm.focusedIndexProperty().addListener(weakFocusedListener);
289                }
290
291                treeView.editingItemProperty().addListener(weakEditingListener);
292                treeView.focusModelProperty().addListener(weakFocusModelPropertyListener);
293                treeView.selectionModelProperty().addListener(weakSelectionModelPropertyListener);
294                
295                weakTreeViewRef = new WeakReference<TreeView<T>>(treeView);
296            }
297
298            updateItem();
299            requestLayout();
300        }
301
302        @Override
303        public Object getBean() {
304            return TreeCell.this;
305        }
306
307        @Override
308        public String getName() {
309            return "treeView";
310        }
311    };
312    
313    private void setTreeView(TreeView<T> value) { treeView.set(value); }
314
315    /**
316     * Returns the TreeView associated with this TreeCell.
317     */
318    public final TreeView<T> getTreeView() { return treeView.get(); }
319    
320    /**
321     * A TreeCell is explicitly linked to a single {@link TreeView} instance,
322     * which is represented by this property.
323     */
324    public final ReadOnlyObjectProperty<TreeView<T>> treeViewProperty() { return treeView.getReadOnlyProperty(); }
325
326
327
328    /***************************************************************************
329     *                                                                         *
330     * Public API                                                              *
331     *                                                                         *
332     **************************************************************************/
333
334    /** {@inheritDoc} */
335    @Override public void startEdit() {
336        final TreeView<T> tree = getTreeView();
337        if (! isEditable() || (tree != null && ! tree.isEditable())) {
338//            if (Logging.getControlsLogger().isLoggable(PlatformLogger.SEVERE)) {
339//                Logging.getControlsLogger().severe(
340//                    "Can not call TreeCell.startEdit() on this TreeCell, as it "
341//                        + "is not allowed to enter its editing state (TreeCell: "
342//                        + this + ", TreeView: " + tree + ").");
343//            }
344            return;
345        }
346        
347        // it makes sense to get the cell into its editing state before firing
348        // the event to the TreeView below, so that's what we're doing here
349        // by calling super.startEdit().
350        super.startEdit();
351        
352         // Inform the TreeView of the edit starting.
353        if (tree != null) {
354            tree.fireEvent(new TreeView.EditEvent<T>(tree,
355                    TreeView.<T>editStartEvent(),
356                    getTreeItem(),
357                    getItem(),
358                    null));
359            
360            tree.requestFocus();
361        }
362    }
363
364     /** {@inheritDoc} */
365    @Override public void commitEdit(T newValue) {
366        if (! isEditing()) return;
367        final TreeItem<T> treeItem = getTreeItem();
368        final TreeView<T> tree = getTreeView();
369        if (tree != null) {
370            // Inform the TreeView of the edit being ready to be committed.
371            tree.fireEvent(new TreeView.EditEvent<T>(tree,
372                    TreeView.<T>editCommitEvent(),
373                    treeItem,
374                    getItem(),
375                    newValue));
376        }
377        
378        // update the item within this cell, so that it represents the new value
379        if (treeItem != null) {
380            treeItem.setValue(newValue);
381            updateTreeItem(treeItem);
382            updateItem(newValue, false);
383        }
384        
385        // inform parent classes of the commit, so that they can switch us
386        // out of the editing state
387        super.commitEdit(newValue);
388
389        if (tree != null) {
390            // reset the editing item in the TreetView
391            tree.edit(null);
392            tree.requestFocus();
393        }
394    }
395
396    /** {@inheritDoc} */
397    @Override public void cancelEdit() {
398        if (! isEditing()) return;
399        
400        TreeView<T> tree = getTreeView();
401
402        super.cancelEdit();
403
404        if (tree != null) {
405            // reset the editing index on the TreeView
406            if (updateEditingIndex) tree.edit(null);
407            tree.requestFocus();
408        
409            tree.fireEvent(new TreeView.EditEvent<T>(tree,
410                    TreeView.<T>editCancelEvent(),
411                    getTreeItem(),
412                    getItem(),
413                    null));
414        }
415    }
416
417    /** {@inheritDoc} */
418    @Override protected Skin<?> createDefaultSkin() {
419        return new TreeCellSkin(this);
420    }
421
422    /***************************************************************************
423     *                                                                         *
424     * Private Implementation                                                  *
425     *                                                                         *
426     **************************************************************************/
427    
428    private int index = -1;
429
430    @Override void indexChanged() {
431        int oldIndex = index;
432        index = getIndex();
433        
434        // when the cell index changes, this may result in the cell
435        // changing state to be selected and/or focused.
436        updateItem();
437        updateSelection();
438        updateFocus();
439    }
440    
441    private void updateItem() {
442        TreeView<T> tv = getTreeView();
443        if (tv == null) return;
444        
445        // Compute whether the index for this cell is for a real item
446        boolean valid = index >=0 && index < tv.getExpandedItemCount();
447
448        // Cause the cell to update itself
449        if (valid) {
450            TreeItem<T> oldTreeItem = getTreeItem();
451            T oldValue = getItem();
452            
453            // update the TreeCell state.
454            // get the new treeItem that is about to go in to the TreeCell
455            TreeItem<T> newTreeItem = tv.getTreeItem(index);
456            T newValue = newTreeItem == null ? null : newTreeItem.getValue();
457        
458            // For the sake of RT-14279, it is important that the order of these
459            // method calls is as shown below. If the order is switched, it is
460            // likely that events will be fired where the item is null, even
461            // though calling cell.getTreeItem().getValue() returns the value
462            // as expected
463            if ((newTreeItem != null && ! newTreeItem.equals(oldTreeItem)) || 
464                    oldTreeItem != null && ! oldTreeItem.equals(newTreeItem)) {
465                updateTreeItem(newTreeItem);
466            }
467            
468            if ((newValue != null && ! newValue.equals(oldValue)) || 
469                    oldValue != null && ! oldValue.equals(newValue)) {
470                updateItem(newValue, false);
471            }
472        } else {
473            if (! isEmpty()) {
474                updateTreeItem(null);
475                updateItem(null, true);
476            }
477        }
478    }
479
480    private void updateSelection() {
481        if (isEmpty()) return;
482        if (index == -1 || getTreeView() == null) return;
483        if (getTreeView().getSelectionModel() == null) return;
484        
485        boolean isSelected = getTreeView().getSelectionModel().isSelected(index);
486        if (isSelected() == isSelected) return;
487        
488        updateSelected(isSelected);
489    }
490
491    private void updateFocus() {
492        if (index == -1 || getTreeView() == null) return;
493        if (getTreeView().getFocusModel() == null) return;
494        
495        setFocused(getTreeView().getFocusModel().isFocused(index));
496    }
497
498    private boolean updateEditingIndex = true;
499    private void updateEditing() {
500        final int index = getIndex();
501        final TreeView<T> tree = getTreeView();
502        final TreeItem<T> treeItem = getTreeItem();
503        final TreeItem<T> editItem = tree == null ? null : tree.getEditingItem();
504        final boolean editing = isEditing();
505        
506        if (index == -1 || tree == null || treeItem == null) return;
507        
508        final boolean match = treeItem.equals(editItem);
509        
510        // If my tree item is the item being edited and I'm not currently in
511        // the edit mode, then I need to enter the edit mode
512        if (match && !editing) {
513            startEdit();
514        } else if (match && editing) {
515            // If my tree item is not the one being edited then I need to cancel
516            // the edit. The tricky thing here is that as part of this call
517            // I cannot end up calling tree.edit(null) the way that the standard
518            // cancelEdit method would do. Yet, I need to call cancelEdit
519            // so that subclasses which override cancelEdit can execute. So,
520            // I have to use a kind of hacky flag workaround.
521            updateEditingIndex = false;
522            cancelEdit();
523            updateEditingIndex = true;
524        }
525    }
526
527
528
529    /***************************************************************************
530     *                                                                         *
531     * Expert API                                                              *
532     *                                                                         *
533     **************************************************************************/
534    
535    
536
537    /**
538     * Updates the TreeView associated with this TreeCell.
539     * 
540     * @param tree The new TreeView that should be associated with this TreeCell.
541     * @expert This function is intended to be used by experts, primarily
542     *         by those implementing new Skins. It is not common
543     *         for developers or designers to access this function directly.
544     */
545    public final void updateTreeView(TreeView<T> tree) {
546        setTreeView(tree); 
547    }
548
549    /**
550     * Updates the TreeItem associated with this TreeCell.
551     *
552     * @param treeItem The new TreeItem that should be associated with this 
553     *      TreeCell.
554     * @expert This function is intended to be used by experts, primarily
555     *      by those implementing new Skins. It is not common
556     *      for developers or designers to access this function directly.
557     */
558    public final void updateTreeItem(TreeItem<T> treeItem) {
559        TreeItem<T> _treeItem = getTreeItem();
560        if (_treeItem != null) {
561            _treeItem.leafProperty().removeListener(weakLeafListener);
562        }
563        setTreeItem(treeItem);
564        if (treeItem != null) {
565            treeItem.leafProperty().addListener(weakLeafListener);
566        }
567    }
568
569
570    
571    /***************************************************************************
572     *                                                                         *
573     * Stylesheet Handling                                                     *
574     *                                                                         *
575     **************************************************************************/
576
577    private static final String DEFAULT_STYLE_CLASS = "tree-cell";
578
579    private static final PseudoClass EXPANDED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("expanded");
580    private static final PseudoClass COLLAPSED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("collapsed");
581
582}