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

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

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2010, 2024, 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 javafx.scene.control.skin.TreeTableCellSkin;
import javafx.beans.InvalidationListener;
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.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.control.TreeTableColumn.CellEditEvent;
import javafx.scene.control.TreeTableView.TreeTableViewFocusModel;


/**
 * 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.
 *
 * 

A note about selection: A TreeTableCell visually shows it is * selected when two conditions are met: *

    *
  1. The {@link TableSelectionModel#isSelected(int, TableColumnBase)} method * returns true for the row / column that this cell represents, and
  2. *
  3. The {@link javafx.scene.control.TableSelectionModel#cellSelectionEnabledProperty() cell selection mode} * property is set to true (to represent that it is allowable to select * individual cells (and not just rows of cells)).
  4. *
* * @param The type of the TreeTableView generic type * @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); setAccessibleRole(AccessibleRole.TREE_TABLE_CELL); updateColumnIndex(); } /* ************************************************************************* * * * Private fields * * * **************************************************************************/ /* ************************************************************************* * * * 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 = c -> { while (c.next()) { if (c.wasAdded() || c.wasRemoved()) { updateSelection(); } } }; // same as above, but for focus private final InvalidationListener focusedListener = value -> { updateFocus(); }; // same as above, but for for changes to the properties on TableRow private final InvalidationListener tableRowUpdateObserver = value -> { itemDirty = true; requestLayout(); }; private final InvalidationListener editingListener = value -> { updateEditing(); }; private ListChangeListener> visibleLeafColumnsListener = c -> { updateColumnIndex(); }; private ListChangeListener columnStyleClassListener = c -> { while (c.next()) { if (c.wasRemoved()) { getStyleClass().removeAll(c.getRemoved()); } if (c.wasAdded()) { getStyleClass().addAll(c.getAddedSubList()); } } }; private final InvalidationListener rootPropertyListener = observable -> { updateItem(-1); }; private final InvalidationListener columnStyleListener = value -> { if (getTableColumn() != null) { possiblySetStyle(getTableColumn().getStyle()); } }; private final InvalidationListener columnIdListener = value -> { if (getTableColumn() != null) { possiblySetId(getTableColumn().getId()); } }; 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 weakColumnStyleListener = new WeakInvalidationListener(columnStyleListener); private final WeakInvalidationListener weakColumnIdListener = new WeakInvalidationListener(columnIdListener); private final WeakInvalidationListener weakRootPropertyListener = new WeakInvalidationListener(rootPropertyListener); /* ************************************************************************* * * * Properties * * * **************************************************************************/ // --- TableColumn /** * The {@code TreeTableColumn} instance that backs this {@code TreeTableCell}. */ private ReadOnlyObjectWrapper> tableColumn = new ReadOnlyObjectWrapper<>(this, "tableColumn") { @Override protected void invalidated() { updateColumnIndex(); } }; public final ReadOnlyObjectProperty> tableColumnProperty() { return tableColumn.getReadOnlyProperty(); } private void setTableColumn(TreeTableColumn value) { tableColumn.set(value); } public final TreeTableColumn getTableColumn() { return tableColumn.get(); } // --- TableView /** * The {@code TreeTableView} associated with this {@code 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 {@code TreeTableRow} that this {@code TreeTableCell} currently finds itself placed within. */ private ReadOnlyObjectWrapper> tableRow = new ReadOnlyObjectWrapper<>(this, "tableRow"); private void setTableRow(TreeTableRow value) { tableRow.set(value); } /** * Gets the value of the property {@code tableRow}. * @return the value of the property {@code tableRow} * @since 17 */ public final TreeTableRow getTableRow() { return tableRow.get(); } public final ReadOnlyObjectProperty> tableRowProperty() { return tableRow.getReadOnlyProperty(); } // The following method was misnamed and is deprecated in favor of the // correctly named method. /** * @deprecated Use {@link getTableRow} instead. * @return the {@code TreeTableRow} */ @Deprecated(since = "17") public final TreeTableRow getTreeTableRow() { return getTableRow(); } /* ************************************************************************* * * * Editing API * * * **************************************************************************/ // editing location at start of edit - fix for JDK-8187229 private TreeTablePosition editingCellAtStartEdit = null; // test-only TreeTablePosition getEditingCellAtStartEdit() { return editingCellAtStartEdit; } /** {@inheritDoc} */ @Override public void startEdit() { if (isEditing()) return; final TreeTableView table = getTreeTableView(); final TreeTableColumn column = getTableColumn(); final TreeTableRow row = getTableRow(); if (!isEditable() || (table != null && !table.isEditable()) || (column != null && !column.isEditable()) || (row != null && !row.isEditable())) { return; } updateItem(-1); // 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(); if (!isEditing()) return; editingCellAtStartEdit = new TreeTablePosition<>(table, getIndex(), column); if (column != null) { CellEditEvent editEvent = new CellEditEvent<>( table, editingCellAtStartEdit, TreeTableColumn.editStartEvent(), null ); Event.fireEvent(column, editEvent); } if (table != null) { table.edit(editingCellAtStartEdit.getRow(), editingCellAtStartEdit.getTableColumn()); } } /** {@inheritDoc} */ @Override public void commitEdit(T newValue) { if (!isEditing()) return; // inform parent classes of the commit, so that they can switch us // out of the editing state. // This MUST come before the updateItem call below, otherwise it will // call cancelEdit(), resulting in both commit and cancel events being // fired (as identified in RT-29650) super.commitEdit(newValue); final TreeTableView table = getTreeTableView(); boolean tableShouldRequestFocus = false; if (table != null) { // The cell is going to be updated, and the current focus owner might be removed from it. // Before that happens, check if it has the table as a parent (otherwise the user might have // clicked out of the table entirely and given focus to something else), so the table can // request the focus back, once the edit commit ends. tableShouldRequestFocus = ControlUtils.controlShouldRequestFocusIfCurrentFocusOwnerIsChild(table); } // JDK-8187307: fire the commit after updating cell's editing state if (getTableColumn() != null) { // Inform the TreeTableColumn of the edit being ready to be committed. CellEditEvent editEvent = new CellEditEvent<>( table, editingCellAtStartEdit, TreeTableColumn.editCommitEvent(), newValue ); Event.fireEvent(getTableColumn(), editEvent); } // Update the item within this cell, so that it represents the new value updateItem(-1); if (table != null) { // reset the editing cell on the TableView table.edit(-1, null); // request focus back onto the table, only if the current focus // owner had the table as a parent. // It would be rude of us to request it back again. if (tableShouldRequestFocus) { table.requestFocus(); } } } /** {@inheritDoc} */ @Override public void cancelEdit() { if (!isEditing()) return; super.cancelEdit(); final TreeTableView table = getTreeTableView(); if (table != null) { // reset the editing index on the TableView if (updateEditingIndex) table.edit(-1, null); // request focus back onto the table, only if the current focus // owner has the table as a parent (otherwise the user might have // clicked out of the table entirely and given focus to something else). // It would be rude of us to request it back again. ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table); } if (getTableColumn() != null) { CellEditEvent editEvent = new CellEditEvent<>( table, editingCellAtStartEdit, 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 (getTableRow() == null || getTableRow().isEmpty()) return; setSelected(selected); } /* ************************************************************************* * * * Private Implementation * * * **************************************************************************/ /** {@inheritDoc} */ @Override void indexChanged(int oldIndex, int newIndex) { super.indexChanged(oldIndex, newIndex); if (isEditing() && newIndex == 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 { // 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(); updateItem(oldIndex); updateSelection(); updateFocus(); updateEditing(); } } 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 boolean isSelected = isSelected(); if (! isInCellSelectionMode()) { if (isSelected) { updateSelected(false); } return; } final TreeTableView tv = getTreeTableView(); if (getIndex() == -1 || tv == null) return; TreeTableView.TreeTableViewSelectionModel sm = tv.getSelectionModel(); if (sm == null) { updateSelected(false); return; } boolean isSelectedNow = sm.isSelected(getIndex(), getTableColumn()); if (isSelected == isSelectedNow) return; updateSelected(isSelectedNow); } private void updateFocus() { final boolean isFocused = isFocused(); if (! isInCellSelectionMode()) { if (isFocused) { setFocused(false); } return; } final TreeTableView tv = getTreeTableView(); if (getIndex() == -1 || tv == null) return; TreeTableView.TreeTableViewFocusModel fm = tv.getFocusModel(); if (fm == null) { setFocused(false); return; } setFocused(fm.isFocused(getIndex(), getTableColumn())); } private void updateEditing() { final TreeTableView tv = getTreeTableView(); if (getIndex() == -1 || tv == null) { // JDK-8265206: must cancel edit if index changed to -1 by re-use if (isEditing()) { doCancelEdit(); } return; } TreeTablePosition editCell = tv.getEditingCell(); boolean match = match(editCell); if (match && ! isEditing()) { startEdit(); } else if (! match && isEditing()) { doCancelEdit(); } } /** * Switches an editing cell into not editing without changing control's * editing state. */ private void doCancelEdit() { // 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. try { // try-finally to make certain that the flag is reliably reset to true updateEditingIndex = false; cancelEdit(); } finally { updateEditingIndex = true; } } private boolean updateEditingIndex = true; private boolean match(TreeTablePosition pos) { return pos != null && pos.getRow() == getIndex() && pos.getTableColumn() == getTableColumn(); } private boolean isInCellSelectionMode() { TreeTableView tv = getTreeTableView(); if (tv == null) return false; TreeTableView.TreeTableViewSelectionModel sm = tv.getSelectionModel(); return sm != null && sm.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; private boolean isFirstRun = true; private WeakReference oldRowItemRef; /* * 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(int oldIndex) { 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(); final boolean isEmpty = isEmpty(); final T oldValue = getItem(); final TreeTableRow tableRow = getTableRow(); final S rowItem = tableRow == null ? null : tableRow.getItem(); final boolean indexExceedsItemCount = index >= itemCount; // there is a whole heap of reasons why we should just punt... outer: if (indexExceedsItemCount || index < 0 || columnIndex < 0 || !isVisible() || tableColumn == null || !tableColumn.isVisible() || tableView.getRoot() == null) { // RT-30484 We need to allow a first run to be special-cased to allow // for the updateItem method to be called at least once to allow for // the correct visual state to be set up. In particular, in RT-30484 // refer to Ensemble8PopUpTree.png - in this case the arrows are being // shown as the new cells are instantiated with the arrows in the // children list, and are only hidden in updateItem. // RT-32621: There are circumstances where we need to updateItem, // even when the index is greater than the itemCount. For example, // RT-32621 identifies issues where a TreeTableView collapses a // TreeItem but the custom cells remain visible. This is now // resolved with the check for indexExceedsItemCount. if ((!isEmpty && oldValue != null) || isFirstRun || indexExceedsItemCount) { updateItem(null, true); isFirstRun = false; } return; } else { currentObservableValue = tableColumn.getCellObservableValue(index); final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue(); // RT-35864 - if the index didn't change, then avoid calling updateItem // unless the item has changed. if (oldIndex == index) { if (!isItemChanged(oldValue, newValue)) { // RT-36670: we need to check the row item here to prevent // the issue where the cell value and index doesn't change, // but the backing row object does. S oldRowItem = oldRowItemRef != null ? oldRowItemRef.get() : null; if (oldRowItem != null && oldRowItem.equals(rowItem)) { // RT-37054: we break out of the if/else code here and // proceed with the code following this, so that we may // still update references, listeners, etc as required. break outer; } } } updateItem(newValue, false); } oldRowItemRef = new WeakReference<>(rowItem); if (currentObservableValue == null) { return; } // add property change listeners to this item currentObservableValue.addListener(weaktableRowUpdateObserver); } @Override protected void layoutChildren() { if (itemDirty) { updateItem(-1); itemDirty = false; } super.layoutChildren(); } /* ************************************************************************* * * * Expert API * * * **************************************************************************/ /** * Updates the {@code TreeTableView} associated with this {@code TreeTableCell}. * This is typically only done once when the {@code TreeTableCell} is first * added to the {@code TreeTableView}. *

* Note: 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. * @param tv the {@code TreeTableView} associated with this {@code TreeTableCell} */ public final void updateTreeTableView(TreeTableView tv) { setTreeTableView(tv); } /** * Updates the {@code TreeTableRow} associated with this {@code TreeTableCell}. *

* Note: 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. * @param row the {@code TreeTableRow} associated with this {@code TreeTableCell} * @since 17 */ public final void updateTableRow(TreeTableRow row) { this.setTableRow(row); } /** * Updates the {@code TreeTableColumn} associated with this {@code TreeTableCell}. *

* Note: 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. * @param column the {@code TreeTableColumn} associated with this {@code TreeTableCell} * @since 17 */ public final void updateTableColumn(TreeTableColumn column) { // 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()); oldCol.idProperty().removeListener(weakColumnIdListener); oldCol.styleProperty().removeListener(weakColumnStyleListener); String id = getId(); String style = getStyle(); if (id != null && id.equals(oldCol.getId())) { setId(null); } if (style != null && style.equals(oldCol.getStyle())) { setStyle(""); } } setTableColumn(column); if (column != null) { getStyleClass().addAll(column.getStyleClass()); column.getStyleClass().addListener(weakColumnStyleClassListener); column.idProperty().addListener(weakColumnIdListener); column.styleProperty().addListener(weakColumnStyleListener); possiblySetId(column.getId()); possiblySetStyle(column.getStyle()); } } // The following methods were misnamed and are deprecated in favor of the // correctly named methods. /** * @deprecated Use {@link updateTableRow} instead. * @param row the {@code TreeTableRow} */ @Deprecated(since = "17") public final void updateTreeTableRow(TreeTableRow row) { updateTableRow(row); } /** * @deprecated Use {@link updateTableColumn} instead. * @param column the {@code TreeTableColumn} */ @Deprecated(since = "17") public final void updateTreeTableColumn(TreeTableColumn column) { updateTableColumn(column); } /* ************************************************************************* * * * 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); } private void possiblySetId(String idCandidate) { if (getId() == null || getId().isEmpty()) { setId(idCandidate); } } private void possiblySetStyle(String styleCandidate) { if (getStyle() == null || getStyle().isEmpty()) { setStyle(styleCandidate); } } /* ************************************************************************* * * * Accessibility handling * * * **************************************************************************/ /** {@inheritDoc} */ @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { switch (attribute) { case ROW_INDEX: return getIndex(); case COLUMN_INDEX: return columnIndex; case SELECTED: if (isInCellSelectionMode()) { return isSelected(); } else { if (getTableRow() == null) { return null; } else { return getTableRow().isSelected(); } } default: return super.queryAccessibleAttribute(attribute, parameters); } } /** {@inheritDoc} */ @Override public void executeAccessibleAction(AccessibleAction action, Object... parameters) { switch (action) { case REQUEST_FOCUS: { TreeTableView treeTableView = getTreeTableView(); if (treeTableView != null) { TreeTableViewFocusModel fm = treeTableView.getFocusModel(); if (fm != null) { fm.focus(getIndex(), getTableColumn()); } } break; } default: super.executeAccessibleAction(action, parameters); } } }