/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.gui.utils.common.configuration; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.LinkedList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IContributionItem; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IStatusLineManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.layout.TreeColumnLayout; import org.eclipse.jface.viewers.CellEditor; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.ICellEditorListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TextCellEditor; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.TreeEditor; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.TreeEvent; import org.eclipse.swt.events.TreeListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.Widget; import org.eclipse.ui.views.properties.IPropertyDescriptor; import org.eclipse.ui.views.properties.IPropertySheetEntry; import de.rcenvironment.core.gui.utils.common.configuration.ConfigurationViewerContentProvider.Element; import de.rcenvironment.core.gui.utils.common.configuration.ConfigurationViewerContentProvider.Leaf; /** * A viewer to represent a hierarchical property tree including means to change property values. * * @author Christian Weiss */ public class ConfigurationViewer extends TreeViewer { /** * The interface VisibilityAction extends the {@link IAction} interface with the capability to * hold the 'visibility' state of an action. * * @author Christian Weiss */ public interface VisibilityAction extends IAction { /** * Sets the visible. * * @param visible the new visible */ void setVisible(boolean visible); /** * Checks if is visible. * * @return true, if is visible */ boolean isVisible(); } private static final Log LOGGER = LogFactory.getLog(ConfigurationViewer.class); /** The default ratio of the property label column. */ private static final float DEFAULT_PROPERTY_COLUMN_RATIO = 0.3f; /** The Constant COLUMN_LABELS. */ private static final String[] COLUMN_LABELS = { Messages.propertyKey, Messages.propertyValue }; /** The index of the editable column. */ private static final int EDIT_COLUMN_INDEX = 1; private static final int MAX_EDITABLE_TEXT_LENGTH = 2048; /** The context menu items. */ private final List<Object> contextMenuItems = new LinkedList<Object>(); /** The tree. */ private final Tree tree; /** The composite used to facilitate dynamic resizing of the tree. */ private final LayoutComposite layoutComposite; /** The menu manager. */ private MenuManager menuManager; /** The tree editor. */ private final TreeEditor treeEditor; /** The cell editor. */ private CellEditor cellEditor; /** The editor listener. */ private ICellEditorListener editorListener; /** The status line manager for showing messages. */ private IStatusLineManager statusLineManager; private final PropertyChangeListener propertyChangeListener = new UpdatingPropertyChangeListener(); /** * Instantiates a new property viewer. * * @param parent the parent */ public ConfigurationViewer(final Composite parent) { this(parent, SWT.NONE); } /** * Instantiates a new property viewer. * * @param parent the parent * @param style the style */ public ConfigurationViewer(final Composite parent, final int style) { super(new LayoutComposite(parent, SWT.NONE), style | SWT.FULL_SELECTION | SWT.SINGLE | SWT.HIDE_SELECTION); tree = getTree(); this.layoutComposite = (LayoutComposite) tree.getParent(); // configure the widget tree.setLinesVisible(true); tree.setHeaderVisible(true); // configure the columns addColumns(); final TreeColumnLayout treeColumnLayout = new TreeColumnLayout(); this.layoutComposite.setLayout(treeColumnLayout); final Widget[] columns = tree.getColumns(); final int i100 = 100; final int minWidth = 200; final int column0weight = (int) (100.0f * DEFAULT_PROPERTY_COLUMN_RATIO); final int column0minWidth = minWidth * column0weight / i100; final int column1weight = i100 - column0weight; final int column1minWidth = minWidth - column0minWidth; treeColumnLayout.setColumnData(columns[0], new ColumnWeightData(column0weight, column0minWidth)); treeColumnLayout.setColumnData(columns[1], new ColumnWeightData(column1weight, column1minWidth)); // add our listeners to the widget hookControl(); // create a new tree editor treeEditor = new TreeEditor(tree); // // create the entry and editor listener // createEntryListener(); createEditorListener(); // createPropertyChangeListener(); // initialize the context menu manager initContextMenuManager(); } @Override protected void inputChanged(final Object input, final Object oldInput) { if (oldInput != null) { try { final Method removeMethod = oldInput.getClass().getMethod("removePropertyChangeListener", PropertyChangeListener.class); if (removeMethod != null) { removeMethod.invoke(oldInput, propertyChangeListener); } } catch (SecurityException e) { e = null; } catch (NoSuchMethodException e) { e = null; } catch (IllegalArgumentException e) { LOGGER.error(e); } catch (IllegalAccessException e) { LOGGER.error(e); } catch (InvocationTargetException e) { LOGGER.error(e); } } if (input != null) { try { final Method addMethod = input.getClass().getMethod("addPropertyChangeListener", PropertyChangeListener.class); if (addMethod != null) { addMethod.invoke(input, propertyChangeListener); } } catch (SecurityException e) { e = null; } catch (NoSuchMethodException e) { e = null; } catch (IllegalArgumentException e) { LOGGER.error(e); } catch (IllegalAccessException e) { LOGGER.error(e); } catch (InvocationTargetException e) { LOGGER.error(e); } } super.inputChanged(input, oldInput); } /** * Inits the context menu manager. */ private void initContextMenuManager() { menuManager = new MenuManager("#PopupMenu"); //$NON-NLS-1$ // clear the context menu before it is shown menuManager.setRemoveAllWhenShown(true); // rebuild the context menu before it is shown menuManager.addMenuListener(new IMenuListener() { /** * {@inheritDoc} * * @see org.eclipse.jface.action.IMenuListener#menuAboutToShow(org.eclipse.jface.action.IMenuManager) */ @Override public void menuAboutToShow(IMenuManager manager) { for (final Object itemObject : contextMenuItems) { if (itemObject instanceof IContributionItem) { final IContributionItem item = (IContributionItem) itemObject; menuManager.add(item); } else if (itemObject instanceof VisibilityAction) { final VisibilityAction item = (VisibilityAction) itemObject; if (item.isVisible()) { menuManager.add(item); } } else if (itemObject instanceof IAction) { final IAction item = (IAction) itemObject; menuManager.add(item); } else { throw new AssertionError(); } } } }); final Menu menu = menuManager.createContextMenu(tree); tree.setMenu(menu); } /** * Adds an {@link IAction} item to the context menu. * * @param item the item */ public void addContextMenuItem(final IAction item) { contextMenuItems.add(item); } /** * Adds an item {@link IContributionItem} to the context menu. * * @param item the item */ public void addContextMenuItem(final IContributionItem item) { contextMenuItems.add(item); } /** * Fire selection changed. * * @param event the event {@inheritDoc} * @see org.eclipse.jface.viewers.Viewer#fireSelectionChanged(org.eclipse.jface.viewers.SelectionChangedEvent) */ @Override protected void fireSelectionChanged(final SelectionChangedEvent event) { SelectionChangedEvent proceedEvent = event; final ISelection selection = event.getSelection(); if (selection instanceof IStructuredSelection) { final IStructuredSelection structuredSelection = (IStructuredSelection) selection; final Object element = structuredSelection .getFirstElement(); if (element instanceof ConfigurationViewerContentProvider.Element) { final Object value = ((ConfigurationViewerContentProvider.Element) element).getPropertyDescriptor(); if (value != null) { final IStructuredSelection replacementSelection = new StructuredSelection(value); final SelectionChangedEvent replacementEvent = new SelectionChangedEvent(event.getSelectionProvider(), replacementSelection); proceedEvent = replacementEvent; } else { proceedEvent = null; } } } if (proceedEvent != null) { super.fireSelectionChanged(proceedEvent); } } /** * Activate a cell editor for the given selected tree item. * * @param item the selected tree item */ private void activateCellEditor(TreeItem item) { // ensure the cell editor is visible tree.showSelection(); // Get the entry for this item final Element activeProperty = (Element) item.getData(); // Get the cell editor for the entry. // Note that the editor parent must be the Tree control cellEditor = activeProperty.createPropertyEditor(tree); if (cellEditor == null) { // unable to create the editor return; } boolean editable = true; // only set the value of the editor if a non-null value is set, otherwise // IllegalArgumentException would occur (thrown by JFace) final Object value = activeProperty.getValue(); if (value != null) { final Object editorValue; if (cellEditor instanceof TextCellEditor) { /* * Max length for TextCellEditor is for some reason 5632. As this is a heuristic * value, the editable length is truncated to a meaningful default. */ final String textValue = value.toString(); if (textValue.length() > MAX_EDITABLE_TEXT_LENGTH) { editable = false; } editorValue = textValue; } else { editorValue = value; } if (!editable) { return; } cellEditor.setValue(editorValue); } // activate the cell editor cellEditor.activate(); // if the cell editor has no control we can stop now Control control = cellEditor.getControl(); if (control == null) { cellEditor.deactivate(); cellEditor = null; return; } // add our editor listener cellEditor.addListener(editorListener); // set the layout of the tree editor to match the cell editor CellEditor.LayoutData layout = cellEditor.getLayoutData(); treeEditor.horizontalAlignment = layout.horizontalAlignment; treeEditor.grabHorizontal = layout.grabHorizontal; treeEditor.minimumWidth = layout.minimumWidth; treeEditor.setEditor(control, item, EDIT_COLUMN_INDEX); // set the error text from the cell editor setErrorMessage(cellEditor.getErrorMessage()); // give focus to the cell editor cellEditor.setFocus(); } /** * Add columns to the tree and set up the layout manager accordingly. */ private void addColumns() { // create the columns final TreeColumn[] columns = tree.getColumns(); for (int i = 0; i < COLUMN_LABELS.length; i++) { final String string = COLUMN_LABELS[i]; if (string != null) { final TreeColumn column; if (i < columns.length) { column = columns[i]; } else { column = new TreeColumn(tree, 0); } column.setText(string); /* * The tree must distribute the available space amongst the columns, thus the width * of the last column is determined by the layout and must not be changed by the * user. */ column.setResizable(i < COLUMN_LABELS.length - 1); } } } /** * Apply editor value. * * {@inheritDoc} * * @see org.eclipse.jface.viewers.ColumnViewer#applyEditorValue() */ @Override protected void applyEditorValue() { if (cellEditor == null) { return; } // Check if editor has a valid value if (!cellEditor.isValueValid()) { setErrorMessage(cellEditor.getErrorMessage()); return; } // get the element final TreeItem treeItem = treeEditor.getItem(); // treeItem can be null when view is opened if (treeItem == null || treeItem.isDisposed()) { return; } final Element property = (Element) treeItem.getData(); // get the new value final Object newValue; final IPropertyDescriptor propertyDescriptor = property.getPropertyDescriptor(); if (propertyDescriptor instanceof SelectionPropertyDescriptor) { newValue = ((SelectionPropertyDescriptor) propertyDescriptor).getValue((Integer) cellEditor.getValue()); } else { newValue = cellEditor.getValue(); } // get the old value final Object oldValue = property.getValue(); // if (property instanceof Leaf // && oldValue != newValue || (oldValue != null && !oldValue.equals(newValue))) { property.setValue(newValue); update(property, null); update(property.getParent(), null); } // cellEditor = null; } /** * Creates a new cell editor listener. */ private void createEditorListener() { editorListener = new ICellEditorListener() { @Override public void cancelEditor() { deactivateCellEditor(); } @Override public void editorValueChanged(boolean oldValidState, boolean newValidState) { // Do nothing } @Override public void applyEditorValue() { ConfigurationViewer.this.applyEditorValue(); } }; } /** * Deactivate the currently active cell editor. */ private void deactivateCellEditor() { treeEditor.setEditor(null, null, EDIT_COLUMN_INDEX); if (cellEditor != null) { cellEditor.deactivate(); // fireCellEditorDeactivated(cellEditor); cellEditor.removeListener(editorListener); cellEditor = null; } // clear any error message from the editor setErrorMessage(null); } /** * Sends out a selection changed event for the entry tree to all registered listeners. */ private void entrySelectionChanged() { SelectionChangedEvent changeEvent = new SelectionChangedEvent(this, getSelection()); fireSelectionChanged(changeEvent); } /** * Selection in the viewer occurred. Check if there is an active cell editor. If yes, deactivate * it and check if a new cell editor must be activated. * * @param selection the TreeItem that is selected */ private void handleSelect(TreeItem selection) { // deactivate the current cell editor if (cellEditor != null) { deactivateCellEditor(); } if (selection == null) { setMessage(null); setErrorMessage(null); } else { Object object = selection.getData(); if (object instanceof Element) { final Element activeProperty = (Element) object; setMessage(activeProperty.getDescription()); activateCellEditor(selection); } if (object instanceof IPropertySheetEntry) { // get the entry for this item IPropertySheetEntry activeEntry = (IPropertySheetEntry) object; // display the description for the item setMessage(activeEntry.getDescription()); // activate a cell editor on the selection activateCellEditor(selection); } } entrySelectionChanged(); } /** * Establish this viewer as a listener on the control. */ private void hookControl() { // Handle selections in the Tree // Part1: Double click only (allow traversal via keyboard without // activation tree.addSelectionListener(new SelectionAdapter() { /* * (non-Javadoc) * * @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse .swt.events. * SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { // The viewer only owns the status line when there is // no 'active' cell editor if (cellEditor == null || !cellEditor.isActivated()) { updateStatusLine(e.item); } } /* * (non-Javadoc) * * @see org.eclipse.swt.events.SelectionListener#widgetDefaultSelected * (org.eclipse.swt.events .SelectionEvent) */ @Override public void widgetDefaultSelected(SelectionEvent e) { if (e.item instanceof TreeItem) { handleSelect((TreeItem) e.item); } } }); // Part2: handle single click activation of cell editor tree.addMouseListener(new MouseAdapter() { @Override public void mouseDown(MouseEvent event) { // only activate if there is a cell editor Point pt = new Point(event.x, event.y); TreeItem item = tree.getItem(pt); if (item != null) { handleSelect(item); } } }); // Add a tree listener to expand and collapse which // allows for lazy creation of children tree.addTreeListener(new TreeListener() { @Override public void treeExpanded(final TreeEvent event) { handleTreeExpand(event); } @Override public void treeCollapsed(final TreeEvent event) { handleTreeCollapse(event); } }); // Refresh the tree when F5 pressed tree.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { if (e.character == SWT.ESC) { deactivateCellEditor(); } else if (e.keyCode == SWT.F5) { // The following will simulate a reselect setInput(getInput()); } } }); } /** * Update the status line based on the data of item. * * @param item the item */ protected void updateStatusLine(Widget item) { setMessage(null); setErrorMessage(null); } /** * Sets the error message to be displayed in the status line. * * @param errorMessage the message to be displayed, or <code>null</code> */ private void setErrorMessage(String errorMessage) { // show the error message if (statusLineManager != null) { statusLineManager.setErrorMessage(errorMessage); } } /** * Sets the message to be displayed in the status line. This message is displayed when there is * no error message. * * @param message the message to be displayed, or <code>null</code> */ private void setMessage(String message) { // show the message if (statusLineManager != null) { statusLineManager.setMessage(message); } } /** * Layout composite to allow for dynamic resizing of the tree. * * <p> * The calculation of the width of this <code>Composite</code> is based on the hints provided by * {@link #computeSize(int, int, boolean)} calls. Therefor such a hint is saved in a local * variable ({@link #widthHint}) and used whenever {@link SWT#DEFAULT} is used instead of a * meaningful width hint. * </p> * * @author Christian Weiss */ private static final class LayoutComposite extends Composite { /** * State memorizer used to ignore the first with hint. * <p> * The first width hint has to be ignored as the <code>ControlListener</code> gets * registered too late to get the first meaningful width hint. * </p> */ private boolean first = true; /** Buffer variable to store/remember the last meaningful width hint. */ private Integer widthHint = 0; LayoutComposite(Composite parent, int style) { super(parent, style); } @Override public Point computeSize(int wHint, int hHint, boolean changed) { /* * If a width hint is provided. >> Ignore, if it is the first one. >> Store, otherwise. */ if (wHint != SWT.DEFAULT) { if (!first) { this.widthHint = wHint; } first = false; } /* * Use the last meaningful width hint for size calculation. This way the (meaningful) * hint is used to calculate the table size and not the actual width of the columns. */ if (widthHint != null) { wHint = Math.min(widthHint, getClientArea().width); } final Point result = super.computeSize(wHint, hHint, changed); /* * Store the default (min) width of the tree, if this is the very first call using no * width hint. */ if (first && wHint == SWT.DEFAULT) { this.widthHint = result.x; } // System.err.printf("%d :: %d\n", wHint, hHint); // System.err.printf("%s :: %s :: %s\n", getClientArea(), getParent().getClientArea(), // result); result.x = 0; return result; } } /** * {@link PropertyChangeListener} to update the tree upon {@link PropertyChangeEvent}s. * * @author Christian Weiss */ public class UpdatingPropertyChangeListener implements PropertyChangeListener { @Override public void propertyChange(final PropertyChangeEvent event) { if (!tree.isDisposed()) { tree.update(); } } } }