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