Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 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 com.sun.javafx.scene.control.skin.TreeTableCellSkin;
030import javafx.beans.InvalidationListener;
031import javafx.beans.Observable;
032import javafx.beans.WeakInvalidationListener;
033import javafx.beans.value.ObservableValue;
034import javafx.collections.ListChangeListener;
035import javafx.event.Event;
036
037import javafx.collections.WeakListChangeListener;
038import java.lang.ref.WeakReference;
039import javafx.beans.property.ReadOnlyObjectProperty;
040import javafx.beans.property.ReadOnlyObjectWrapper;
041
042import javafx.scene.control.TreeTableColumn.CellEditEvent;
043
044
045/**
046 * Represents a single row/column intersection in a {@link TreeTableView}. To 
047 * represent this intersection, a TreeTableCell contains an 
048 * {@link #indexProperty() index} property, as well as a 
049 * {@link #tableColumnProperty() tableColumn} property. In addition, a TreeTableCell
050 * instance knows what {@link TreeTableRow} it exists in.
051 * 
052 * @see TreeTableView
053 * @see TreeTableColumn
054 * @see Cell
055 * @see IndexedCell
056 * @see TreeTableRow
057 * @param <T> The type of the item contained within the Cell.
058 */
059public class TreeTableCell<S,T> extends IndexedCell<T> {
060    
061    /***************************************************************************
062     *                                                                         *
063     * Constructors                                                            *
064     *                                                                         *
065     **************************************************************************/
066
067    /**
068     * Constructs a default TreeTableCell instance with a style class of
069     * 'tree-table-cell'.
070     */
071    public TreeTableCell() {
072        getStyleClass().addAll(DEFAULT_STYLE_CLASS);
073        
074        updateColumnIndex();
075    }
076
077
078
079    /***************************************************************************
080     *                                                                         *
081     * Callbacks and Events                                                    *
082     *                                                                         *
083     **************************************************************************/
084    
085    private boolean itemDirty = false;
086    
087    /*
088     * This is the list observer we use to keep an eye on the SelectedCells
089     * ObservableList in the tree table view. Because it is possible that the table can
090     * be mutated, we create this observer here, and add/remove it from the
091     * storeTableView method.
092     */
093    private ListChangeListener<TreeTablePosition<S,?>> selectedListener = new ListChangeListener<TreeTablePosition<S,?>>() {
094        @Override public void onChanged(Change<? extends TreeTablePosition<S,?>> c) {
095            updateSelection();
096        }
097    };
098
099    // same as above, but for focus
100    private final InvalidationListener focusedListener = new InvalidationListener() {
101        @Override public void invalidated(@SuppressWarnings("unused") Observable value) {
102            updateFocus();
103        }
104    };
105
106    // same as above, but for for changes to the properties on TableRow
107    private final InvalidationListener tableRowUpdateObserver = new InvalidationListener() {
108        @Override public void invalidated(@SuppressWarnings("unused") Observable value) {
109            itemDirty = true;
110            requestLayout();
111        }
112    };
113    
114    private final InvalidationListener editingListener = new InvalidationListener() {
115        @Override public void invalidated(@SuppressWarnings("unused") Observable value) {
116            updateEditing();
117        }
118    };
119    
120    private ListChangeListener<TreeTableColumn<S,?>> visibleLeafColumnsListener = new ListChangeListener<TreeTableColumn<S,?>>() {
121        @Override public void onChanged(Change<? extends TreeTableColumn<S,?>> c) {
122            updateColumnIndex();
123        }
124    };
125    
126    private ListChangeListener<String> columnStyleClassListener = new ListChangeListener<String>() {
127        @Override public void onChanged(Change<? extends String> c) {
128            while (c.next()) {
129                if (c.wasRemoved()) {
130                    getStyleClass().removeAll(c.getRemoved());
131                }
132                
133                if (c.wasAdded()) {
134                    getStyleClass().addAll(c.getAddedSubList());
135                }
136            }
137        }
138    };
139    
140    private final WeakListChangeListener<TreeTablePosition<S,?>> weakSelectedListener = 
141            new WeakListChangeListener<TreeTablePosition<S,?>>(selectedListener);
142    private final WeakInvalidationListener weakFocusedListener = 
143            new WeakInvalidationListener(focusedListener);
144    private final WeakInvalidationListener weaktableRowUpdateObserver = 
145            new WeakInvalidationListener(tableRowUpdateObserver);
146    private final WeakInvalidationListener weakEditingListener = 
147            new WeakInvalidationListener(editingListener);
148    private final WeakListChangeListener<TreeTableColumn<S,?>> weakVisibleLeafColumnsListener =
149            new WeakListChangeListener<TreeTableColumn<S,?>>(visibleLeafColumnsListener);
150    private final WeakListChangeListener<String> weakColumnStyleClassListener =
151            new WeakListChangeListener<String>(columnStyleClassListener);
152
153    
154    /***************************************************************************
155     *                                                                         *
156     * Properties                                                              *
157     *                                                                         *
158     **************************************************************************/
159    
160    // --- TableColumn
161    /**
162     * The TreeTableColumn instance that backs this TreeTableCell.
163     */
164    private ReadOnlyObjectWrapper<TreeTableColumn<S,T>> treeTableColumn = 
165            new ReadOnlyObjectWrapper<TreeTableColumn<S,T>>(this, "treeTableColumn") {
166        @Override protected void invalidated() {
167            updateColumnIndex();
168        }
169    };
170    public final ReadOnlyObjectProperty<TreeTableColumn<S,T>> tableColumnProperty() { return treeTableColumn.getReadOnlyProperty(); }
171    private void setTableColumn(TreeTableColumn<S,T> value) { treeTableColumn.set(value); }
172    public final TreeTableColumn<S,T> getTableColumn() { return treeTableColumn.get(); }
173    
174    
175    // --- TableView
176    /**
177     * The TreeTableView associated with this TreeTableCell.
178     */
179    private ReadOnlyObjectWrapper<TreeTableView<S>> treeTableView;
180    private void setTreeTableView(TreeTableView<S> value) {
181        treeTableViewPropertyImpl().set(value);
182    }
183    public final TreeTableView<S> getTreeTableView() {
184        return treeTableView == null ? null : treeTableView.get();
185    }
186    public final ReadOnlyObjectProperty<TreeTableView<S>> treeTableViewProperty() {
187        return treeTableViewPropertyImpl().getReadOnlyProperty();
188    }
189
190    private ReadOnlyObjectWrapper<TreeTableView<S>> treeTableViewPropertyImpl() {
191        if (treeTableView == null) {
192            treeTableView = new ReadOnlyObjectWrapper<TreeTableView<S>>(this, "treeTableView") {
193                private WeakReference<TreeTableView<S>> weakTableViewRef;
194                @Override protected void invalidated() {
195                    TreeTableView.TreeTableViewSelectionModel<S> sm;
196                    TreeTableView.TreeTableViewFocusModel<S> fm;
197                    
198                    if (weakTableViewRef != null) {
199                        TreeTableView<S> oldTableView = weakTableViewRef.get();
200                        if (oldTableView != null) {
201                            sm = oldTableView.getSelectionModel();
202                            if (sm != null) {
203                                sm.getSelectedCells().removeListener(weakSelectedListener);
204                            }
205
206                            fm = oldTableView.getFocusModel();
207                            if (fm != null) {
208                                fm.focusedCellProperty().removeListener(weakFocusedListener);
209                            }
210
211                            oldTableView.editingCellProperty().removeListener(weakEditingListener);
212                            oldTableView.getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener);
213                        }
214                    }
215                    
216                    if (get() != null) {
217                        sm = get().getSelectionModel();
218                        if (sm != null) {
219                            sm.getSelectedCells().addListener(weakSelectedListener);
220                        }
221
222                        fm = get().getFocusModel();
223                        if (fm != null) {
224                            fm.focusedCellProperty().addListener(weakFocusedListener);
225                        }
226
227                        get().editingCellProperty().addListener(weakEditingListener);
228                        get().getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
229                        
230                        weakTableViewRef = new WeakReference<TreeTableView<S>>(get());
231                    }
232                    
233                    updateColumnIndex();
234                }
235            };
236        }
237        return treeTableView;
238    }
239    
240    
241    // --- TableRow
242    /**
243     * The TreeTableRow that this TreeTableCell currently finds itself placed within.
244     */
245    private ReadOnlyObjectWrapper<TreeTableRow<S>> treeTableRow = 
246            new ReadOnlyObjectWrapper<TreeTableRow<S>>(this, "treeTableRow");
247    private void setTreeTableRow(TreeTableRow<S> value) { treeTableRow.set(value); }
248    public final TreeTableRow<S> getTreeTableRow() { return treeTableRow.get(); }
249    public final ReadOnlyObjectProperty<TreeTableRow<S>> tableRowProperty() { return treeTableRow;  }
250
251
252
253    /***************************************************************************
254     *                                                                         *
255     * Editing API                                                             *
256     *                                                                         *
257     **************************************************************************/
258
259    /** {@inheritDoc} */
260    @Override public void startEdit() {
261        final TreeTableView<S> table = getTreeTableView();
262        final TreeTableColumn<S,T> column = getTableColumn();
263        if (! isEditable() ||
264                (table != null && ! table.isEditable()) ||
265                (column != null && ! getTableColumn().isEditable())) {
266            return;
267        }
268        
269        // it makes sense to get the cell into its editing state before firing
270        // the event to listeners below, so that's what we're doing here
271        // by calling super.startEdit().
272        super.startEdit();
273        
274        @SuppressWarnings("unchecked")
275        TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
276        
277        if (column != null) {
278            CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
279                table,
280                editingCell,
281                TreeTableColumn.<S,T>editStartEvent(),
282                null
283            );
284
285            Event.fireEvent(column, editEvent);
286        }
287    }
288
289    /** {@inheritDoc} */
290    @Override public void commitEdit(T newValue) {
291        if (! isEditing()) return;
292        
293        final TreeTableView<S> table = getTreeTableView();
294        if (table != null) {
295            @SuppressWarnings("unchecked")
296            TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
297            
298            // Inform the TableView of the edit being ready to be committed.
299            CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
300                table,
301                editingCell,
302                TreeTableColumn.<S,T>editCommitEvent(),
303                newValue
304            );
305
306            Event.fireEvent(getTableColumn(), editEvent);
307        }
308
309        // update the item within this cell, so that it represents the new value
310        updateItem(newValue, false);
311
312        // inform parent classes of the commit, so that they can switch us
313        // out of the editing state
314        super.commitEdit(newValue);
315        
316        if (table != null) {
317            // reset the editing cell on the TableView
318            table.edit(-1, null);
319            table.requestFocus();
320        }
321    }
322
323    /** {@inheritDoc} */
324    @Override public void cancelEdit() {
325        if (! isEditing()) return;
326
327        final TreeTableView<S> table = getTreeTableView();
328
329        super.cancelEdit();
330
331        // reset the editing index on the TableView
332        if (table != null) {
333            @SuppressWarnings("unchecked")
334            TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
335            
336            if (updateEditingIndex) table.edit(-1, null);
337
338            CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
339                table,
340                editingCell,
341                TreeTableColumn.<S,T>editCancelEvent(),
342                null
343            );
344
345            Event.fireEvent(getTableColumn(), editEvent);
346        }
347    }
348    
349    
350    
351    /* *************************************************************************
352     *                                                                         *
353     * Overriding methods                                                      *
354     *                                                                         *
355     **************************************************************************/
356
357    /** {@inheritDoc} */
358    @Override public void updateSelected(boolean selected) {
359        // copied from Cell, with the first conditional clause below commented 
360        // out, as it is valid for an empty TableCell to be selected, as long 
361        // as the parent TableRow is not empty (see RT-15529).
362        /*if (selected && isEmpty()) return;*/
363        if (getTreeTableRow() == null || getTreeTableRow().isEmpty()) return;
364        setSelected(selected);
365    }
366
367    
368
369    /* *************************************************************************
370     *                                                                         *
371     * Private Implementation                                                  *
372     *                                                                         *
373     **************************************************************************/
374    
375    @Override void indexChanged() {
376        super.indexChanged();
377        // Ideally we would just use the following two lines of code, rather
378        // than the updateItem() call beneath, but if we do this we end up with
379        // RT-22428 where all the columns are collapsed.
380        // itemDirty = true;
381        // requestLayout();
382        updateItem();
383        updateSelection();
384        updateFocus();
385    }
386    
387    private boolean isLastVisibleColumn = false;
388    private int columnIndex = -1;
389    
390    private void updateColumnIndex() {
391        final TreeTableView<S> tv = getTreeTableView();
392        TreeTableColumn<S,T> tc = getTableColumn();
393        columnIndex = tv == null || tc == null ? -1 : tv.getVisibleLeafIndex(tc);
394        
395        // update the pseudo class state regarding whether this is the last
396        // visible cell (i.e. the right-most). 
397        isLastVisibleColumn = getTableColumn() != null &&
398                columnIndex != -1 && 
399                columnIndex == tv.getVisibleLeafColumns().size() - 1;
400        pseudoClassStateChanged(PSEUDO_CLASS_LAST_VISIBLE, isLastVisibleColumn);
401    }
402
403    private void updateSelection() {
404        /*
405         * This cell should be selected if the selection mode of the table
406         * is cell-based, and if the row and column that this cell represents
407         * is selected.
408         *
409         * If the selection mode is not cell-based, then the listener in the
410         * TableRow class might pick up the need to set an entire row to be
411         * selected.
412         */
413        if (isEmpty()) return;
414        
415        final TreeTableView<S> tv = getTreeTableView();
416        if (getIndex() == -1 || getTreeTableView() == null) return;
417        if (tv.getSelectionModel() == null) return;
418        
419        boolean isSelected = isInCellSelectionMode() &&
420                tv.getSelectionModel().isSelected(getIndex(), getTableColumn());
421        if (isSelected() == isSelected) return;
422
423        updateSelected(isSelected);
424    }
425
426    private void updateFocus() {
427        final TreeTableView<S> tv = getTreeTableView();
428        if (getIndex() == -1 || tv == null) return;
429        if (tv.getFocusModel() == null) return;
430        
431        boolean isFocused = isInCellSelectionMode() &&
432                tv.getFocusModel() != null &&
433                tv.getFocusModel().isFocused(getIndex(), getTableColumn());
434
435        setFocused(isFocused);
436    }
437
438    private void updateEditing() {
439        final TreeTableView<S> tv = getTreeTableView();
440        if (getIndex() == -1 || tv == null) return;
441
442        TreeTablePosition<S,?> editCell = tv.getEditingCell();
443        boolean match = match(editCell);
444        
445        if (match && ! isEditing()) {
446            startEdit();
447        } else if (! match && isEditing()) {
448            // If my index is not the one being edited then I need to cancel
449            // the edit. The tricky thing here is that as part of this call
450            // I cannot end up calling list.edit(-1) the way that the standard
451            // cancelEdit method would do. Yet, I need to call cancelEdit
452            // so that subclasses which override cancelEdit can execute. So,
453            // I have to use a kind of hacky flag workaround.
454            updateEditingIndex = false;
455            cancelEdit();
456            updateEditingIndex = true;
457        }
458    }
459    private boolean updateEditingIndex = true;
460
461    private boolean match(TreeTablePosition pos) {
462        return pos != null && pos.getRow() == getIndex() && pos.getTableColumn() == getTableColumn();
463    }
464
465    private boolean isInCellSelectionMode() {
466        TreeTableView<S> treeTable = getTreeTableView();
467        return treeTable != null &&
468                treeTable.getSelectionModel() != null &&
469                treeTable.getSelectionModel().isCellSelectionEnabled();
470    }
471    
472    /*
473     * This was brought in to fix the issue in RT-22077, namely that the 
474     * ObservableValue was being GC'd, meaning that changes to the value were
475     * no longer being delivered. By extracting this value out of the method, 
476     * it is now referred to from TableCell and will therefore no longer be
477     * GC'd.
478     */
479    private ObservableValue<T> currentObservableValue = null;
480
481    /*
482     * This is called when we think that the data within this TreeTableCell may have
483     * changed. You'll note that this is a private function - it is only called
484     * when one of the triggers above call it.
485     */
486    private void updateItem() {
487        if (currentObservableValue != null) {
488            currentObservableValue.removeListener(weaktableRowUpdateObserver);
489        }
490        
491        // get the total number of items in the data model
492        final TreeTableView<S> tableView = getTreeTableView();
493        final TreeTableColumn<S,T> tableColumn = getTableColumn();
494        final int itemCount = tableView == null ? -1 : getTreeTableView().getExpandedItemCount();
495        final int index = getIndex();
496        
497        // there is a whole heap of reasons why we should just punt...
498        if (index >= itemCount ||
499                index < 0 || 
500                columnIndex < 0 ||
501                !isVisible() ||
502                tableColumn == null || 
503                !tableColumn.isVisible() ||
504                tableView.getRoot() == null) {
505            
506            if (! isEmpty()) {
507                updateItem(null, true);
508            }
509            return;
510        } else {
511            currentObservableValue = tableColumn.getCellObservableValue(index);
512            
513            T oldValue = getItem();
514            T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
515            
516            // update the 'item' property of this cell.
517            if ((newValue != null && ! newValue.equals(oldValue)) || 
518                    oldValue != null && ! oldValue.equals(newValue)) {
519                updateItem(newValue, false);
520            }
521        }
522        
523        if (currentObservableValue == null) {
524            return;
525        }
526        
527        // add property change listeners to this item
528        currentObservableValue.addListener(weaktableRowUpdateObserver);
529    }
530
531    @Override protected void layoutChildren() {
532        if (itemDirty) {
533            updateItem();
534            itemDirty = false;
535        }
536        super.layoutChildren();
537    }
538
539    
540
541
542    /***************************************************************************
543     *                                                                         *
544     *                              Expert API                                 *
545     *                                                                         *
546     **************************************************************************/
547
548    /**
549     * Updates the TreeTableView associated with this TreeTableCell. This is typically
550     * only done once when the TreeTableCell is first added to the TreeTableView.
551     *
552     * @expert This function is intended to be used by experts, primarily
553     *         by those implementing new Skins. It is not common
554     *         for developers or designers to access this function directly.
555     */
556    public final void updateTreeTableView(TreeTableView<S> tv) {
557        setTreeTableView(tv);
558    }
559
560    /**
561     * Updates the TreeTableRow associated with this TreeTableCell.
562     *
563     * @expert This function is intended to be used by experts, primarily
564     *         by those implementing new Skins. It is not common
565     *         for developers or designers to access this function directly.
566     */
567    public final void updateTreeTableRow(TreeTableRow<S> treeTableRow) {
568        this.setTreeTableRow(treeTableRow);
569    }
570
571    /**
572     * Updates the TreeTableColumn associated with this TreeTableCell.
573     *
574     * @expert This function is intended to be used by experts, primarily
575     *         by those implementing new Skins. It is not common
576     *         for developers or designers to access this function directly.
577     */
578    public final void updateTreeTableColumn(TreeTableColumn<S,T> col) {
579        // remove style class of existing tree table column, if it is non-null
580        TreeTableColumn<S,T> oldCol = getTableColumn();
581        if (oldCol != null) {
582            oldCol.getStyleClass().removeListener(weakColumnStyleClassListener);
583            getStyleClass().removeAll(oldCol.getStyleClass());
584        }
585        
586        setTableColumn(col);
587        
588        if (col != null) {
589            getStyleClass().addAll(col.getStyleClass());
590            col.getStyleClass().addListener(weakColumnStyleClassListener);
591        }
592    }
593
594
595
596    /***************************************************************************
597     *                                                                         *
598     * Stylesheet Handling                                                     *
599     *                                                                         *
600     **************************************************************************/
601
602    private static final String DEFAULT_STYLE_CLASS = "tree-table-cell";
603    private static final PseudoClass PSEUDO_CLASS_LAST_VISIBLE = 
604            PseudoClass.getPseudoClass("last-visible");
605
606    /** {@inheritDoc} */
607    @Override protected Skin<?> createDefaultSkin() {
608        return new TreeTableCellSkin<S,T>(this);
609    }
610}