/*******************************************************************************
* Copyright (c) 2011, 2015 Wind River Systems, Inc. and others. All rights reserved.
* This program and the accompanying materials are made available under the terms
* of the Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Wind River Systems - initial API and implementation
*******************************************************************************/
package org.eclipse.tcf.te.ui.trees;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.viewers.ColumnViewerEditor;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationStrategy;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.TreeViewerEditor;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.tcf.te.core.interfaces.IViewerInput;
import org.eclipse.tcf.te.ui.WorkbenchPartControl;
import org.eclipse.tcf.te.ui.forms.CustomFormToolkit;
import org.eclipse.tcf.te.ui.interfaces.ITreeControlInputChangedListener;
import org.eclipse.ui.IDecoratorManager;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
/**
* Abstract tree control implementation.
*/
public abstract class AbstractTreeControl extends WorkbenchPartControl implements SelectionListener {
// Reference to the tree viewer instance
private TreeViewer viewer;
// Reference to the selection changed listener
private ISelectionChangedListener selectionChangedListener;
// The descriptors of the viewer filters configured for this viewer.
private FilterDescriptor[] filterDescriptors;
// The tree viewer columns of this viewer.
private ColumnDescriptor[] columnDescriptors;
// The content contributions configured for this viewer.
private ContentDescriptor[] contentDescriptors;
// The action of the tree viewer used to restore and save the the tree viewer's action.
private TreeViewerState viewerState;
// The action to configure the filters.
private ConfigFilterAction configFilterAction;
// The menu manager
private MenuManager manager;
// Tree control input changed listener list
private final ListenerList inputChangedListeners = new ListenerList();
/**
* Constructor.
*/
public AbstractTreeControl() {
super();
}
/**
* Constructor.
*
* @param parent The parent workbench part this control is embedded in or <code>null</code>.
*/
public AbstractTreeControl(IWorkbenchPart parent) {
super(parent);
}
/* (non-Javadoc)
* @see org.eclipse.tcf.te.ui.WorkbenchPartControl#dispose()
*/
@Override
public void dispose() {
saveViewerState();
// Unregister the selection changed listener
if (selectionChangedListener != null) {
if (getViewer() != null) {
getViewer().removeSelectionChangedListener(selectionChangedListener);
}
selectionChangedListener = null;
}
// Dispose the descriptors
disposeDescriptors();
// clear out the input changed listeners
inputChangedListeners.clear();
super.dispose();
}
/**
* Dispose all descriptors.
*/
protected void disposeDescriptors() {
// Dispose the columns' resources.
if (columnDescriptors != null) {
for (ColumnDescriptor column : columnDescriptors) {
if (column.getImage() != null) {
column.getImage().dispose();
}
if (column.getLabelProvider() != null) {
column.getLabelProvider().dispose();
}
if (column.getTreeColumn() != null) {
column.getTreeColumn().dispose();
}
}
columnDescriptors = null;
}
if (filterDescriptors != null) {
for (FilterDescriptor filterDescriptor : filterDescriptors) {
if (filterDescriptor.getImage() != null) {
filterDescriptor.getImage().dispose();
}
}
filterDescriptors = null;
}
if (contentDescriptors != null) {
for (ContentDescriptor contentDescriptor : contentDescriptors) {
contentDescriptor.dispose();
}
contentDescriptors = null;
}
}
/* (non-Javadoc)
* @see org.eclipse.tcf.te.ui.WorkbenchPartControl#setupFormPanel(org.eclipse.swt.widgets.Composite, org.eclipse.tcf.te.ui.forms.CustomFormToolkit)
*/
@Override
public void setupFormPanel(Composite parent, CustomFormToolkit toolkit) {
super.setupFormPanel(parent, toolkit);
// Create the tree viewer
viewer = doCreateTreeViewer(parent);
// And configure the tree viewer
doConfigureTreeViewer(viewer);
// Prepare popup menu and toolbar
doCreateContributionItems(viewer);
}
/**
* Creates the tree viewer instance.
*
* @param parent The parent composite. Must not be <code>null</code>.
* @return The tree viewer.
*/
protected TreeViewer doCreateTreeViewer(Composite parent) {
Assert.isNotNull(parent);
return new TreeViewer(parent, SWT.FULL_SELECTION | SWT.SINGLE);
}
/**
* Configure the tree viewer.
*
* @param viewer The tree viewer. Must not be <code>null</code>.
*/
protected void doConfigureTreeViewer(TreeViewer viewer) {
Assert.isNotNull(viewer);
viewer.setAutoExpandLevel(getAutoExpandLevel());
viewer.setLabelProvider(doCreateTreeViewerLabelProvider(this, viewer));
ITreeContentProvider mainContentProvider = doCreateTreeViewerContentProvider(viewer);
Assert.isNotNull(mainContentProvider);
final ITreeContentProvider contentProvider = new TreeViewerDelegatingContentProvider(this, mainContentProvider);
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("inputChanged")) { //$NON-NLS-1$
// Determine if the input really changed
Object oldInput = args[1];
Object newInput = args[2];
if ((oldInput == null && newInput != null) || (oldInput != null && newInput == null) || (oldInput != null && !oldInput.equals(newInput))) {
onInputChanged(oldInput, newInput);
}
}
return method.invoke(contentProvider, args);
}
};
ClassLoader classLoader = getClass().getClassLoader();
Class<?>[] interfaces = new Class[] { ITreeContentProvider.class };
ITreeContentProvider proxy = (ITreeContentProvider) Proxy.newProxyInstance(classLoader, interfaces, handler);
viewer.setContentProvider(proxy);
viewer.setComparator(doCreateTreeViewerComparator(this, viewer));
viewer.getTree().setLayoutData(doCreateTreeViewerLayoutData(viewer));
// Attach the selection changed listener
selectionChangedListener = doCreateTreeViewerSelectionChangedListener(viewer);
if (selectionChangedListener != null) {
viewer.addSelectionChangedListener(selectionChangedListener);
}
// Define an editor activation strategy for the common viewer so as to be invoked only programmatically.
ColumnViewerEditorActivationStrategy activationStrategy = new TreeViewerEditorActivationStrategy(getViewerId(), viewer);
TreeViewerEditor.create(viewer, null, activationStrategy, ColumnViewerEditor.DEFAULT);
}
/**
* Handle the event when the new input is set. Get the viewer's action
* and update the action of the viewer's columns and filters.
*
* @param oldInput the old input.
* @param newInput The new input.
*/
protected void onInputChanged(Object oldInput, Object newInput) {
if (newInput != null) {
// Dispose the old descriptors first
disposeDescriptors();
columnDescriptors = doCreateViewerColumns(newInput);
filterDescriptors = doCreateFilterDescriptors(newInput);
contentDescriptors = doCreateContentDescriptors(newInput);
if (isStatePersistent()) {
updateViewerState(newInput);
}
doCreateTreeColumns(viewer);
viewer.getTree().setHeaderVisible(true);
updateFilters();
createHeaderMenu(this).create();
if (configFilterAction != null) configFilterAction.updateEnablement();
}
// Invoke the registered tree control input changed listeners
Object[] listeners = inputChangedListeners.getListeners();
for (Object candidate : listeners) {
if (!(candidate instanceof ITreeControlInputChangedListener)) continue;
((ITreeControlInputChangedListener)candidate).inputChanged(this, oldInput, newInput);
}
}
/**
* Create the tree viewer header menu.
*
* @param control The parent tree control. Must not be <code>null</code>.
* @return The tree viewer header menu.
*/
protected TreeViewerHeaderMenu createHeaderMenu(AbstractTreeControl control) {
Assert.isNotNull(control);
return new TreeViewerHeaderMenu(control);
}
/**
* Update the viewer action using the states from the viewerState which
* is retrieved or created based on the input.
*
* @param newInput The new input of the viewer.
*/
private void updateViewerState(Object newInput) {
IViewerInput viewerInput = ViewerStateManager.getViewerInput(newInput);
if (viewerInput != null) {
String inputId = viewerInput.getInputId();
if (inputId != null) {
inputId = getViewerId() + "." + inputId; //$NON-NLS-1$
viewerState = ViewerStateManager.getInstance().getViewerState(inputId);
if (viewerState == null) {
viewerState = ViewerStateManager.createViewerState(columnDescriptors, filterDescriptors);
ViewerStateManager.getInstance().putViewerState(inputId, viewerState);
}
else {
viewerState.updateColumnDescriptor(columnDescriptors);
viewerState.updateFilterDescriptor(filterDescriptors);
}
}
}
}
/**
* Save the viewer's action.
*/
private void saveViewerState() {
if (isStatePersistent() && viewerState != null) {
viewerState.updateColumnState(columnDescriptors);
viewerState.updateFilterState(filterDescriptors);
}
}
/**
* Update the filter's action using the latest filter descriptors.
*/
void updateFilterState() {
if (isStatePersistent() && viewerState != null) {
viewerState.updateFilterState(filterDescriptors);
}
}
/**
* Show or hide the specified column. Return true if the visible
* action has changed.
*
* @param column The column to be changed.
* @param visible The new visible value.
* @return true if the action has changed.
*/
boolean setColumnVisible(ColumnDescriptor column, boolean visible) {
if (column.isVisible() && !visible) {
TreeColumn treeColumn = column.getTreeColumn();
treeColumn.dispose();
column.setTreeColumn(null);
column.setVisible(visible);
return true;
}
else if (!column.isVisible() && visible) {
TreeColumn treeColumn = doCreateTreeColumn(column, false);
column.setTreeColumn(treeColumn);
column.setVisible(visible);
return true;
}
return false;
}
/**
* Return if this tree viewer's action is persistent. If it is persistent,
* then its viewer action will be persisted during different session.
*
* @return true if the viewer's action is persistent.
*/
protected boolean isStatePersistent() {
return true;
}
/**
* Create the tree viewer columns from the viewers extension.
* Subclass may override it to provide its customized viewer columns.
*
* @param newInput the input when the columns are created.
* @return The tree viewer columns.
*/
protected ColumnDescriptor[] doCreateViewerColumns(Object newInput) {
if (columnDescriptors == null) {
TreeViewerExtension viewerExtension = new TreeViewerExtension(getViewerId());
columnDescriptors = viewerExtension.parseColumns(newInput);
}
return columnDescriptors;
}
/**
* Create the viewer filters from the viewers extension. Subclass may
* override it to provide its customized viewer filters.
*
* @param newInput the input when the filters are initialized.
* @return The filter descriptors for the viewer.
*/
protected FilterDescriptor[] doCreateFilterDescriptors(Object newInput) {
if (filterDescriptors == null) {
TreeViewerExtension viewerExtension = new TreeViewerExtension(getViewerId());
filterDescriptors = viewerExtension.parseFilters(newInput);
}
return filterDescriptors;
}
/**
* Create the viewer content contributions from the viewers extension. Subclass may
* override it to provide its customized viewer content contributions.
*
* @param newInput the input when the content contributions are initialized.
* @return The content descriptors for the viewer.
*/
protected ContentDescriptor[] doCreateContentDescriptors(Object newInput) {
if (contentDescriptors == null) {
TreeViewerExtension viewerExtension = new TreeViewerExtension(getViewerId());
contentDescriptors = viewerExtension.parseContents(newInput);
}
return contentDescriptors;
}
/**
* Returns the current list of viewer content descriptors. The list of content
* descriptors may change if the viewers input element changes.
*
* @return The list of viewer content descriptors or <code>null</code>.
*/
public ContentDescriptor[] getContentDescriptors() {
return contentDescriptors;
}
/**
* Update the tree viewer's filters using the current filter descriptors.
*/
public void updateFilters() {
if (filterDescriptors != null) {
List<ViewerFilter> newFilters = new ArrayList<ViewerFilter>();
for (FilterDescriptor descriptor : filterDescriptors) {
if (descriptor.getFilter() != null) {
if (descriptor.isEnabled()) {
newFilters.add(descriptor.getFilter());
}
}
}
viewer.setFilters(newFilters.toArray(new ViewerFilter[newFilters.size()]));
}
}
/**
* Create the tree columns for the viewer from the tree viewer columns.
* Subclass may override to create its customized the creation.
*
* @param viewer The tree viewer.
*/
protected void doCreateTreeColumns(TreeViewer viewer) {
Assert.isTrue(columnDescriptors != null && columnDescriptors.length > 0);
List<ColumnDescriptor> visibleColumns = new ArrayList<ColumnDescriptor>();
for (ColumnDescriptor column : columnDescriptors) {
if (column.isVisible()) visibleColumns.add(column);
}
Collections.sort(visibleColumns, new Comparator<ColumnDescriptor>(){
@Override
public int compare(ColumnDescriptor o1, ColumnDescriptor o2) {
return o1.getOrder() < o2.getOrder() ? -1 : (o1.getOrder() > o2.getOrder() ? 1 : 0);
}});
for(ColumnDescriptor visibleColumn : visibleColumns) {
doCreateTreeColumn(visibleColumn, true);
}
// Set the default sort column to the first column (the tree column).
Assert.isTrue(viewer.getTree().getColumnCount() > 0);
TreeColumn treeColumn = viewer.getTree().getColumn(0);
ColumnDescriptor column = (ColumnDescriptor) treeColumn.getData();
if (column != null) {
viewer.getTree().setSortColumn(treeColumn);
viewer.getTree().setSortDirection(column.isAscending() ? SWT.UP : SWT.DOWN);
}
}
/**
* Create the tree column described by the specified column descriptor.
*
* @param column The column descriptor.
* @param append If the new column should be appended.
* @return The tree column created.
*/
protected TreeColumn doCreateTreeColumn(final ColumnDescriptor column, boolean append) {
Tree tree = viewer.getTree();
final TreeColumn treeColumn = append ? new TreeColumn(tree, column.getStyle()) : new TreeColumn(tree, column.getStyle(), getColumnIndex(column));
treeColumn.setData(column);
treeColumn.setText(column.getName());
treeColumn.setToolTipText(column.getDescription());
treeColumn.setAlignment(column.getAlignment());
treeColumn.setImage(column.getImage());
treeColumn.setMoveable(column.isMoveable());
treeColumn.setResizable(column.isResizable());
treeColumn.setData("widthHint", Integer.valueOf(column.getWidth())); //$NON-NLS-1$
treeColumn.setWidth(column.getWidth());
treeColumn.addSelectionListener(this);
treeColumn.addControlListener(new ControlAdapter(){
@Override
public void controlMoved(ControlEvent e) {
columnMoved();
}
@Override
public void controlResized(ControlEvent e) {
column.setWidth(treeColumn.getWidth());
}});
column.setTreeColumn(treeColumn);
// Custom tree column configuration
configureTreeColumn(treeColumn);
return treeColumn;
}
/**
* Customize the given tree column.
* <p>
* Called from {@link #doCreateTreeColumn(ColumnDescriptor, boolean)}.
*
* @param treeColumn The tree column. Must not be <code>null</code>.
*/
protected void configureTreeColumn(TreeColumn treeColumn) {
Assert.isNotNull(treeColumn);
}
/**
* Called when a column is moved. Store the column's order.
*/
protected void columnMoved() {
Tree tree = viewer.getTree();
TreeColumn[] treeColumns = tree.getColumns();
if(treeColumns != null && treeColumns.length > 0) {
int[] orders = tree.getColumnOrder();
for(int i=0;i<orders.length;i++) {
TreeColumn treeColumn = treeColumns[orders[i]];
ColumnDescriptor column = (ColumnDescriptor) treeColumn.getData();
if (column != null) {
column.setOrder(i);
}
}
}
}
/**
* Get the column index of the specified column. The column index
* equals to the count of the visible columns before this column.
*
* @param column The column descriptor.
* @return The column index.
*/
private int getColumnIndex(ColumnDescriptor column) {
Assert.isTrue(columnDescriptors != null);
int visibleCount = 0;
for(int i=0;i<columnDescriptors.length;i++) {
if(columnDescriptors[i] == column)
break;
if(columnDescriptors[i].isVisible()) {
visibleCount++;
}
}
return visibleCount;
}
/**
* Get the tree viewer's id. This viewer id is used by
* viewer extension to define columns and filters.
*
* @return This viewer's id or null.
*/
protected abstract String getViewerId();
/**
* Returns the number of levels to auto expand.
* If the method returns <code>0</code>, no auto expansion will happen
*
* @return The number of levels to auto expand or <code>0</code>.
*/
protected int getAutoExpandLevel() {
return 2;
}
/**
* Whether to use a decorating label provider.
*/
protected boolean useLabelDecorator() {
return false;
}
/**
* Creates the tree viewer layout data instance.
*
* @param viewer The tree viewer. Must not be <code>null</code>.
* @return The tree viewer layout data instance.
*/
protected Object doCreateTreeViewerLayoutData(TreeViewer viewer) {
GridData data = new GridData(GridData.FILL_BOTH);
data.widthHint = 0;
data.heightHint = 0;
return data;
}
/**
* Creates the tree viewer label provider instance.
*
* @param parentTreeControl The parent tree control instance. Must not be <code>null</code>.
* @param viewer The tree viewer. Must not be <code>null</code>.,
*
* @return The tree viewer label provider instance.
*/
protected ILabelProvider doCreateTreeViewerLabelProvider(AbstractTreeControl parentTreeControl, TreeViewer viewer) {
TreeViewerLabelProvider labelProvider = new TreeViewerLabelProvider(parentTreeControl, viewer);
if (useLabelDecorator()) {
IWorkbench workbench = PlatformUI.getWorkbench();
IDecoratorManager manager = workbench.getDecoratorManager();
ILabelDecorator decorator = manager.getLabelDecorator();
return new TreeViewerDecoratingLabelProvider(viewer, labelProvider,decorator);
}
return labelProvider;
}
/**
* Creates the tree viewer content provider instance.
*
* @param viewer The tree viewer. Must not be <code>null</code>.
* @return The tree viewer content provider instance.
*/
protected abstract ITreeContentProvider doCreateTreeViewerContentProvider(TreeViewer viewer);
/**
* Creates the tree viewer comparator instance.
*
* @param parentTreeControl The parent tree control. Must not be <code>null</code>.
* @param viewer The tree viewer. Must not be <code>null</code>.
*
* @return The tree viewer comparator instance or <code>null</code> to turn off sorting.
*/
protected ViewerComparator doCreateTreeViewerComparator(AbstractTreeControl parentTreeControl, TreeViewer viewer) {
Assert.isNotNull(parentTreeControl);
Assert.isNotNull(viewer);
return new TreeControlSorter(parentTreeControl);
}
/**
* Creates a new selection changed listener instance.
*
* @param viewer The tree viewer. Must not be <code>null</code>.
* @return The selection changed listener instance.
*/
protected abstract ISelectionChangedListener doCreateTreeViewerSelectionChangedListener(TreeViewer viewer);
/**
* Create the context menu and toolbar groups.
*
* @param viewer The tree viewer instance. Must not be <code>null</code>.
*/
protected void doCreateContributionItems(TreeViewer viewer) {
Assert.isNotNull(viewer);
// Create the menu manager
manager = new MenuManager("#PopupMenu"); //$NON-NLS-1$
// Attach the menu listener
manager.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(IMenuManager manager) {
manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
}
});
// All items are removed when menu is closing
manager.setRemoveAllWhenShown(true);
// Associated with the tree
viewer.getTree().setMenu(manager.createContextMenu(viewer.getTree()));
}
/**
* Get the context menu manager.
*
* @return the context menu manager.
*/
public MenuManager getContextMenuManager() {
return manager;
}
/**
* Create the toolbar items to be added to the toolbar. Override
* to add the wanted toolbar items.
* <p>
* <b>Note:</b> The toolbar items are added from left to right.
*
* @param toolbarManager The toolbar to add the toolbar items too. Must not be <code>null</code>.
*/
public void createToolbarContributionItems(IToolBarManager toolbarManager) {
toolbarManager.insertAfter("group.additions.control", new CollapseAllAction(this)); //$NON-NLS-1$
configFilterAction = new ConfigFilterAction(this);
toolbarManager.insertAfter("group.additions.control", configFilterAction); //$NON-NLS-1$
}
/**
* Get the current filter descriptors of this viewer.
*
* @return The filter descriptors of this viewer.
*/
public FilterDescriptor[] getFilterDescriptors() {
return filterDescriptors != null ? Arrays.copyOf(filterDescriptors, filterDescriptors.length) : new FilterDescriptor[0];
}
/**
* Get the current viewer columns of this viewer.
*
* @return The current viewer columns.
*/
public ColumnDescriptor[] getViewerColumns() {
return columnDescriptors != null ? Arrays.copyOf(columnDescriptors, columnDescriptors.length) : new ColumnDescriptor[0];
}
/**
* Returns the viewer instance.
*
* @return The viewer instance or <code>null</code>.
*/
public Viewer getViewer() {
return viewer;
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
*/
@Override
public Object getAdapter(Class adapter) {
if (Viewer.class.isAssignableFrom(adapter)) {
// We have to double check if our real viewer is assignable to
// the requested Viewer class.
Viewer viewer = getViewer();
if (!adapter.isAssignableFrom(viewer.getClass())) {
viewer = null;
}
return viewer;
}
return super.getAdapter(adapter);
}
/*
* (non-Javadoc)
* @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse.swt.events.SelectionEvent)
*/
@Override
public void widgetSelected(SelectionEvent e) {
Assert.isTrue(e.getSource() instanceof TreeColumn);
TreeColumn treeColumn = (TreeColumn) e.getSource();
ColumnDescriptor column = (ColumnDescriptor) treeColumn.getData();
if (column != null) {
viewer.getTree().setSortColumn(treeColumn);
column.setAscending(!column.isAscending());
viewer.getTree().setSortDirection(column.isAscending() ? SWT.UP : SWT.DOWN);
Object[] expandedElements = viewer.getExpandedElements();
viewer.refresh();
viewer.setExpandedElements(expandedElements);
}
}
/*
* (non-Javadoc)
* @see org.eclipse.swt.events.SelectionListener#widgetDefaultSelected(org.eclipse.swt.events.SelectionEvent)
*/
@Override
public void widgetDefaultSelected(SelectionEvent e) {
}
/**
* Adds the given tree control input changed listener.
*
* @param listener The tree control input changed listener.
*/
public final void addInputChangedListener(ITreeControlInputChangedListener listener) {
if (listener != null) inputChangedListeners.add(listener);
}
/**
* Removes the given tree control input changed listener.
*
* @param listener The tree control input changed listener.
*/
public final void removeInputChangedListener(ITreeControlInputChangedListener listener) {
if (listener != null) inputChangedListeners.remove(listener);
}
}