All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.control.TreeTableCell Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.control;

import javafx.css.PseudoClass;
import com.sun.javafx.scene.control.skin.TreeTableCellSkin;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.Event;

import javafx.collections.WeakListChangeListener;
import java.lang.ref.WeakReference;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;

import javafx.scene.control.TreeTableColumn.CellEditEvent;


/**
 * Represents a single row/column intersection in a {@link TreeTableView}. To 
 * represent this intersection, a TreeTableCell contains an 
 * {@link #indexProperty() index} property, as well as a 
 * {@link #tableColumnProperty() tableColumn} property. In addition, a TreeTableCell
 * instance knows what {@link TreeTableRow} it exists in.
 * 
 * @see TreeTableView
 * @see TreeTableColumn
 * @see Cell
 * @see IndexedCell
 * @see TreeTableRow
 * @param  The type of the item contained within the Cell.
 * @since JavaFX 8.0
 */
public class TreeTableCell extends IndexedCell {
    
    /***************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/

    /**
     * Constructs a default TreeTableCell instance with a style class of
     * 'tree-table-cell'.
     */
    public TreeTableCell() {
        getStyleClass().addAll(DEFAULT_STYLE_CLASS);
        
        updateColumnIndex();
    }



    /***************************************************************************
     *                                                                         *
     * Callbacks and Events                                                    *
     *                                                                         *
     **************************************************************************/
    
    private boolean itemDirty = false;
    
    /*
     * This is the list observer we use to keep an eye on the SelectedCells
     * ObservableList in the tree table view. Because it is possible that the table can
     * be mutated, we create this observer here, and add/remove it from the
     * storeTableView method.
     */
    private ListChangeListener> selectedListener = new ListChangeListener>() {
        @Override public void onChanged(Change> c) {
            updateSelection();
        }
    };

    // same as above, but for focus
    private final InvalidationListener focusedListener = new InvalidationListener() {
        @Override public void invalidated(@SuppressWarnings("unused") Observable value) {
            updateFocus();
        }
    };

    // same as above, but for for changes to the properties on TableRow
    private final InvalidationListener tableRowUpdateObserver = new InvalidationListener() {
        @Override public void invalidated(@SuppressWarnings("unused") Observable value) {
            itemDirty = true;
            requestLayout();
        }
    };
    
    private final InvalidationListener editingListener = new InvalidationListener() {
        @Override public void invalidated(@SuppressWarnings("unused") Observable value) {
            updateEditing();
        }
    };
    
    private ListChangeListener> visibleLeafColumnsListener = new ListChangeListener>() {
        @Override public void onChanged(Change> c) {
            updateColumnIndex();
        }
    };
    
    private ListChangeListener columnStyleClassListener = new ListChangeListener() {
        @Override public void onChanged(Change c) {
            while (c.next()) {
                if (c.wasRemoved()) {
                    getStyleClass().removeAll(c.getRemoved());
                }
                
                if (c.wasAdded()) {
                    getStyleClass().addAll(c.getAddedSubList());
                }
            }
        }
    };

    private final InvalidationListener rootPropertyListener = new InvalidationListener() {
        @Override public void invalidated(Observable observable) {
            updateItem();
        }
    };
    
    private final WeakListChangeListener> weakSelectedListener = 
            new WeakListChangeListener>(selectedListener);
    private final WeakInvalidationListener weakFocusedListener = 
            new WeakInvalidationListener(focusedListener);
    private final WeakInvalidationListener weaktableRowUpdateObserver = 
            new WeakInvalidationListener(tableRowUpdateObserver);
    private final WeakInvalidationListener weakEditingListener = 
            new WeakInvalidationListener(editingListener);
    private final WeakListChangeListener> weakVisibleLeafColumnsListener =
            new WeakListChangeListener>(visibleLeafColumnsListener);
    private final WeakListChangeListener weakColumnStyleClassListener =
            new WeakListChangeListener(columnStyleClassListener);
    private final WeakInvalidationListener weakRootPropertyListener =
            new WeakInvalidationListener(rootPropertyListener);

    
    /***************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/
    
    // --- TableColumn
    /**
     * The TreeTableColumn instance that backs this TreeTableCell.
     */
    private ReadOnlyObjectWrapper> treeTableColumn = 
            new ReadOnlyObjectWrapper>(this, "treeTableColumn") {
        @Override protected void invalidated() {
            updateColumnIndex();
        }
    };
    public final ReadOnlyObjectProperty> tableColumnProperty() { return treeTableColumn.getReadOnlyProperty(); }
    private void setTableColumn(TreeTableColumn value) { treeTableColumn.set(value); }
    public final TreeTableColumn getTableColumn() { return treeTableColumn.get(); }
    
    
    // --- TableView
    /**
     * The TreeTableView associated with this TreeTableCell.
     */
    private ReadOnlyObjectWrapper> treeTableView;
    private void setTreeTableView(TreeTableView value) {
        treeTableViewPropertyImpl().set(value);
    }
    public final TreeTableView getTreeTableView() {
        return treeTableView == null ? null : treeTableView.get();
    }
    public final ReadOnlyObjectProperty> treeTableViewProperty() {
        return treeTableViewPropertyImpl().getReadOnlyProperty();
    }

    private ReadOnlyObjectWrapper> treeTableViewPropertyImpl() {
        if (treeTableView == null) {
            treeTableView = new ReadOnlyObjectWrapper>(this, "treeTableView") {
                private WeakReference> weakTableViewRef;
                @Override protected void invalidated() {
                    TreeTableView.TreeTableViewSelectionModel sm;
                    TreeTableView.TreeTableViewFocusModel fm;
                    
                    if (weakTableViewRef != null) {
                        TreeTableView oldTableView = weakTableViewRef.get();
                        if (oldTableView != null) {
                            sm = oldTableView.getSelectionModel();
                            if (sm != null) {
                                sm.getSelectedCells().removeListener(weakSelectedListener);
                            }

                            fm = oldTableView.getFocusModel();
                            if (fm != null) {
                                fm.focusedCellProperty().removeListener(weakFocusedListener);
                            }

                            oldTableView.editingCellProperty().removeListener(weakEditingListener);
                            oldTableView.getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener);
                            oldTableView.rootProperty().removeListener(weakRootPropertyListener);
                        }
                    }

                    TreeTableView newTreeTableView = get();
                    if (newTreeTableView != null) {
                        sm = newTreeTableView.getSelectionModel();
                        if (sm != null) {
                            sm.getSelectedCells().addListener(weakSelectedListener);
                        }

                        fm = newTreeTableView.getFocusModel();
                        if (fm != null) {
                            fm.focusedCellProperty().addListener(weakFocusedListener);
                        }

                        newTreeTableView.editingCellProperty().addListener(weakEditingListener);
                        newTreeTableView.getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
                        newTreeTableView.rootProperty().addListener(weakRootPropertyListener);
                        
                        weakTableViewRef = new WeakReference>(newTreeTableView);
                    }
                    
                    updateColumnIndex();
                }
            };
        }
        return treeTableView;
    }
    
    
    // --- TableRow
    /**
     * The TreeTableRow that this TreeTableCell currently finds itself placed within.
     */
    private ReadOnlyObjectWrapper> treeTableRow = 
            new ReadOnlyObjectWrapper>(this, "treeTableRow");
    private void setTreeTableRow(TreeTableRow value) { treeTableRow.set(value); }
    public final TreeTableRow getTreeTableRow() { return treeTableRow.get(); }
    public final ReadOnlyObjectProperty> tableRowProperty() { return treeTableRow;  }



    /***************************************************************************
     *                                                                         *
     * Editing API                                                             *
     *                                                                         *
     **************************************************************************/

    /** {@inheritDoc} */
    @Override public void startEdit() {
        if (isEditing()) return;

        final TreeTableView table = getTreeTableView();
        final TreeTableColumn column = getTableColumn();
        if (! isEditable() ||
                (table != null && ! table.isEditable()) ||
                (column != null && ! getTableColumn().isEditable())) {
            return;
        }

        updateItem();

        // it makes sense to get the cell into its editing state before firing
        // the event to listeners below, so that's what we're doing here
        // by calling super.startEdit().
        super.startEdit();
        
        @SuppressWarnings("unchecked")
        TreeTablePosition editingCell = (TreeTablePosition) table.getEditingCell();
        
        if (column != null) {
            CellEditEvent editEvent = new CellEditEvent(
                table,
                editingCell,
                TreeTableColumn.editStartEvent(),
                null
            );

            Event.fireEvent(column, editEvent);
        }
    }

    /** {@inheritDoc} */
    @Override public void commitEdit(T newValue) {
        if (! isEditing()) return;
        
        final TreeTableView table = getTreeTableView();
        if (table != null) {
            @SuppressWarnings("unchecked")
            TreeTablePosition editingCell = (TreeTablePosition) table.getEditingCell();
            
            // Inform the TableView of the edit being ready to be committed.
            CellEditEvent editEvent = new CellEditEvent(
                table,
                editingCell,
                TreeTableColumn.editCommitEvent(),
                newValue
            );

            Event.fireEvent(getTableColumn(), editEvent);
        }

        // update the item within this cell, so that it represents the new value
        updateItem(newValue, false);

        // inform parent classes of the commit, so that they can switch us
        // out of the editing state
        super.commitEdit(newValue);
        
        if (table != null) {
            // reset the editing cell on the TableView
            table.edit(-1, null);
            table.requestFocus();
        }
    }

    /** {@inheritDoc} */
    @Override public void cancelEdit() {
        if (! isEditing()) return;

        final TreeTableView table = getTreeTableView();

        super.cancelEdit();

        // reset the editing index on the TableView
        if (table != null) {
            @SuppressWarnings("unchecked")
            TreeTablePosition editingCell = (TreeTablePosition) table.getEditingCell();
            
            if (updateEditingIndex) table.edit(-1, null);

            CellEditEvent editEvent = new CellEditEvent(
                table,
                editingCell,
                TreeTableColumn.editCancelEvent(),
                null
            );

            Event.fireEvent(getTableColumn(), editEvent);
        }
    }
    
    
    
    /* *************************************************************************
     *                                                                         *
     * Overriding methods                                                      *
     *                                                                         *
     **************************************************************************/

    /** {@inheritDoc} */
    @Override public void updateSelected(boolean selected) {
        // copied from Cell, with the first conditional clause below commented 
        // out, as it is valid for an empty TableCell to be selected, as long 
        // as the parent TableRow is not empty (see RT-15529).
        /*if (selected && isEmpty()) return;*/
        if (getTreeTableRow() == null || getTreeTableRow().isEmpty()) return;
        setSelected(selected);
    }

    

    /* *************************************************************************
     *                                                                         *
     * Private Implementation                                                  *
     *                                                                         *
     **************************************************************************/

    private int index = -1;

    @Override void indexChanged() {
        super.indexChanged();
        // Ideally we would just use the following two lines of code, rather
        // than the updateItem() call beneath, but if we do this we end up with
        // RT-22428 where all the columns are collapsed.
        // itemDirty = true;
        // requestLayout();

        final int oldIndex = index;
        super.indexChanged();
        index = getIndex();

        if (isEditing() && index == oldIndex) {
            // no-op
            // Fix for RT-31165 - if we (needlessly) update the index whilst the
            // cell is being edited it will no longer be in an editing state.
            // This means that in certain (common) circumstances that it will
            // appear that a cell is uneditable as, despite being clicked, it
            // will not change to the editing state as a layout of VirtualFlow
            // is immediately invoked, which forces all cells to be updated.
        } else {
            updateItem();
            updateSelection();
            updateFocus();
        }
    }
    
    private boolean isLastVisibleColumn = false;
    private int columnIndex = -1;
    
    private void updateColumnIndex() {
        final TreeTableView tv = getTreeTableView();
        TreeTableColumn tc = getTableColumn();
        columnIndex = tv == null || tc == null ? -1 : tv.getVisibleLeafIndex(tc);
        
        // update the pseudo class state regarding whether this is the last
        // visible cell (i.e. the right-most). 
        isLastVisibleColumn = getTableColumn() != null &&
                columnIndex != -1 && 
                columnIndex == tv.getVisibleLeafColumns().size() - 1;
        pseudoClassStateChanged(PSEUDO_CLASS_LAST_VISIBLE, isLastVisibleColumn);
    }

    private void updateSelection() {
        /*
         * This cell should be selected if the selection mode of the table
         * is cell-based, and if the row and column that this cell represents
         * is selected.
         *
         * If the selection mode is not cell-based, then the listener in the
         * TableRow class might pick up the need to set an entire row to be
         * selected.
         */
        if (isEmpty()) return;
        
        final TreeTableView tv = getTreeTableView();
        if (getIndex() == -1 || getTreeTableView() == null) return;
        if (tv.getSelectionModel() == null) return;
        
        boolean isSelected = isInCellSelectionMode() &&
                tv.getSelectionModel().isSelected(getIndex(), getTableColumn());
        if (isSelected() == isSelected) return;

        updateSelected(isSelected);
    }

    private void updateFocus() {
        final TreeTableView tv = getTreeTableView();
        if (getIndex() == -1 || tv == null) return;
        if (tv.getFocusModel() == null) return;
        
        boolean isFocused = isInCellSelectionMode() &&
                tv.getFocusModel() != null &&
                tv.getFocusModel().isFocused(getIndex(), getTableColumn());

        setFocused(isFocused);
    }

    private void updateEditing() {
        final TreeTableView tv = getTreeTableView();
        if (getIndex() == -1 || tv == null) return;

        TreeTablePosition editCell = tv.getEditingCell();
        boolean match = match(editCell);
        
        if (match && ! isEditing()) {
            startEdit();
        } else if (! match && isEditing()) {
            // If my index is not the one being edited then I need to cancel
            // the edit. The tricky thing here is that as part of this call
            // I cannot end up calling list.edit(-1) the way that the standard
            // cancelEdit method would do. Yet, I need to call cancelEdit
            // so that subclasses which override cancelEdit can execute. So,
            // I have to use a kind of hacky flag workaround.
            updateEditingIndex = false;
            cancelEdit();
            updateEditingIndex = true;
        }
    }
    private boolean updateEditingIndex = true;

    private boolean match(TreeTablePosition pos) {
        return pos != null && pos.getRow() == getIndex() && pos.getTableColumn() == getTableColumn();
    }

    private boolean isInCellSelectionMode() {
        TreeTableView treeTable = getTreeTableView();
        return treeTable != null &&
                treeTable.getSelectionModel() != null &&
                treeTable.getSelectionModel().isCellSelectionEnabled();
    }
    
    /*
     * This was brought in to fix the issue in RT-22077, namely that the 
     * ObservableValue was being GC'd, meaning that changes to the value were
     * no longer being delivered. By extracting this value out of the method, 
     * it is now referred to from TableCell and will therefore no longer be
     * GC'd.
     */
    private ObservableValue currentObservableValue = null;

    /*
     * This is called when we think that the data within this TreeTableCell may have
     * changed. You'll note that this is a private function - it is only called
     * when one of the triggers above call it.
     */
    private void updateItem() {
        if (currentObservableValue != null) {
            currentObservableValue.removeListener(weaktableRowUpdateObserver);
        }
        
        // get the total number of items in the data model
        final TreeTableView tableView = getTreeTableView();
        final TreeTableColumn tableColumn = getTableColumn();
        final int itemCount = tableView == null ? -1 : getTreeTableView().getExpandedItemCount();
        final int index = getIndex();
        
        // there is a whole heap of reasons why we should just punt...
        if (index >= itemCount ||
                index < 0 || 
                columnIndex < 0 ||
                !isVisible() ||
                tableColumn == null || 
                !tableColumn.isVisible() ||
                tableView.getRoot() == null) {
            
            updateItem(null, true);
            return;
        } else {
            currentObservableValue = tableColumn.getCellObservableValue(index);
            
            T oldValue = getItem();
            T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
            
            // update the 'item' property of this cell.
            updateItem(newValue, false);
        }
        
        if (currentObservableValue == null) {
            return;
        }
        
        // add property change listeners to this item
        currentObservableValue.addListener(weaktableRowUpdateObserver);
    }

    @Override protected void layoutChildren() {
        if (itemDirty) {
            updateItem();
            itemDirty = false;
        }
        super.layoutChildren();
    }

    


    /***************************************************************************
     *                                                                         *
     *                              Expert API                                 *
     *                                                                         *
     **************************************************************************/

    /**
     * Updates the TreeTableView associated with this TreeTableCell. This is typically
     * only done once when the TreeTableCell is first added to the TreeTableView.
     *
     * @expert This function is intended to be used by experts, primarily
     *         by those implementing new Skins. It is not common
     *         for developers or designers to access this function directly.
     */
    public final void updateTreeTableView(TreeTableView tv) {
        setTreeTableView(tv);
    }

    /**
     * Updates the TreeTableRow associated with this TreeTableCell.
     *
     * @expert This function is intended to be used by experts, primarily
     *         by those implementing new Skins. It is not common
     *         for developers or designers to access this function directly.
     */
    public final void updateTreeTableRow(TreeTableRow treeTableRow) {
        this.setTreeTableRow(treeTableRow);
    }

    /**
     * Updates the TreeTableColumn associated with this TreeTableCell.
     *
     * @expert This function is intended to be used by experts, primarily
     *         by those implementing new Skins. It is not common
     *         for developers or designers to access this function directly.
     */
    public final void updateTreeTableColumn(TreeTableColumn col) {
        // remove style class of existing tree table column, if it is non-null
        TreeTableColumn oldCol = getTableColumn();
        if (oldCol != null) {
            oldCol.getStyleClass().removeListener(weakColumnStyleClassListener);
            getStyleClass().removeAll(oldCol.getStyleClass());
        }
        
        setTableColumn(col);
        
        if (col != null) {
            getStyleClass().addAll(col.getStyleClass());
            col.getStyleClass().addListener(weakColumnStyleClassListener);
        }
    }



    /***************************************************************************
     *                                                                         *
     * Stylesheet Handling                                                     *
     *                                                                         *
     **************************************************************************/

    private static final String DEFAULT_STYLE_CLASS = "tree-table-cell";
    private static final PseudoClass PSEUDO_CLASS_LAST_VISIBLE = 
            PseudoClass.getPseudoClass("last-visible");

    /** {@inheritDoc} */
    @Override protected Skin createDefaultSkin() {
        return new TreeTableCellSkin(this);
    }
}