/*
* 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();
}
}
}
}