/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.gui.workflow.editor.properties; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.Serializable; import java.lang.reflect.Field; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.gef.commands.Command; import org.eclipse.gef.commands.CommandStack; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Spinner; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; import de.rcenvironment.core.component.model.configuration.api.ConfigurationDescription; import de.rcenvironment.core.component.model.endpoint.api.EndpointDescription; import de.rcenvironment.core.component.workflow.model.api.WorkflowNode; import de.rcenvironment.core.component.workflow.model.api.WorkflowNodeUtil; import de.rcenvironment.core.component.workflow.model.spi.ComponentInstanceProperties; import de.rcenvironment.core.datamodel.api.DataType; import de.rcenvironment.core.gui.workflow.parts.WorkflowNodePart; /** * Abstract base class for implementing a property editor for a workflow node. * * To implement a new <code>WorkflowNodePropertySection}</code>: * <ol> * <li>Derive a new section from this class.</li> * <li>Implement the {@link #createCompositeContents(Composite, TabbedPropertySheetPage)} method to create the GUI. Remember to use SWT * Forms and appropriate styles to meet the GUI standards. Tag {@link Control}s which are displaying certain configuration properties with * the {@link Control#setData(String, Object)} method, using {@link #CONTROL_PROPERTY_KEY} as key and the property key as value.</li> * <li>Create a controller class implementing {@link Controller} or deriving from {@link DefaultController}. Implement the controller * functionality which should be reacting on certain GUI-events in the appropriate {@link Controller} function. To integrate changes in the * Undo/Redo stack, implement any changes as instances of {@link WorkflowNodeCommand} and hand them to the * {@link #execute(WorkflowNodeCommand)} method. Within {@link WorkflowNodeCommand}s the {@link WorkflowNode} can be retrieved via * {@link WorkflowNodeCommand#getWorkflowNode()}. The methods {@link #setProperty(String, Serializable)}, * {@link #setProperty(String, Serializable, Serializable)} and {@link #editProperty(String)} are convenience functions that integrate into * the Undo/Redo stack. Overwrite the {@link #createController()} method to return an instance of your {@link Controller} implementation. * </li> * <li>As changes to the model can be undone via the {@link CommandStack}, the GUI needs to be synchronized with changes to the underlying * model. This has to be realized in a custom {@link Synchronizer}, which should ideally be derived from {@link DefaultSynchronizer}. * Overwrite the {@link #createSynchronizer()} method to return an instance of your {@link Synchronizer} implementation.</li> * <li>All updates of the GUI, which reflect change events in the model, should happen through a {@link Updater} instance. If functionality * above those of the {@link DefaultUpdater} is required this class has to be derived and extended. Overwrite the {@link #createUpdater()} * method to return an instance of your {@link Updater} implementation.</li> * </ol> * * @author Heinrich Wendel * @author Christian Weiss * @author Doreen Seider * @author Markus Kunde */ public abstract class WorkflowNodePropertySection extends WorkflowPropertySection implements WorkflowNodeCommand.Executor { /** The key of {@link Control} data fields identifying the managed workflow node property. */ public static final String CONTROL_PROPERTY_KEY = "property.control"; /** The key of {@link Button} data fields identifying the associated enum type. */ public static final String ENUM_TYPE_KEY = "property.enum.type"; /** The key of {@link Button} data fields identifying the associated enum value. */ public static final String ENUM_VALUE_KEY = "property.enum.value"; protected final Log logger = LogFactory.getLog(this.getClass()); protected WorkflowNode node; private final Map<String, Map<Enum<?>, Set<Control>>> enumGroups = new HashMap<String, Map<Enum<?>, Set<Control>>>(); private ComponentInstanceProperties modelBindingTarget; private ComponentInstanceProperties lastRefreshConfiguration; private EditConfigurationValueCommand openEditCommand; private Composite composite; private final Controller controller = createController(); private final Synchronizer synchronizer = createSynchronizer(); private final SynchronizerAdapter synchronizerAdapter = new SynchronizerAdapter(); private final Updater updater = createUpdater(); @Override public void setInput(final IWorkbenchPart part, final ISelection selection) { final Object firstSelectionElement = ((IStructuredSelection) selection).getFirstElement(); final WorkflowNodePart workflowNodePart = (WorkflowNodePart) firstSelectionElement; final WorkflowNode workflowNode = (WorkflowNode) workflowNodePart.getModel(); if (getPart() == null || !getPart().equals(part) || node == null || !node.equals(workflowNode)) { super.setInput(part, selection); setWorkflowNodeBase(workflowNode); } } protected Composite getComposite() { return composite; } private void setWorkflowNodeBase(final WorkflowNode workflowNode) { tearDownModelBindingBase(); this.node = workflowNode; initializeModelBindingBase(); setWorkflowNode(workflowNode); } /** * Invoked, after a new input {@link WorkflowNode} has been set and the model binding is initialized. * * @param workflowNode the new input {@link WorkflowNode} */ protected void setWorkflowNode(final WorkflowNode workflowNode) { /* empty default implementation */ } private void initializeModelBindingBase() { if (modelBindingTarget == null) { modelBindingTarget = getConfiguration(); modelBindingTarget.addPropertyChangeListener(synchronizerAdapter); afterInitializingModelBinding(); } } /** * Invoked after the model binding for the base {@link WorkflowNodePropertySection} has been initialized. * */ protected void afterInitializingModelBinding() { /* empty default implementation */ } @Override public void dispose() { tearDownModelBindingBase(); super.dispose(); } private void tearDownModelBindingBase() { if (modelBindingTarget != null) { RuntimeException exception = null; try { beforeTearingDownModelBinding(); } catch (RuntimeException e) { exception = e; } modelBindingTarget.removePropertyChangeListener(synchronizerAdapter); modelBindingTarget = null; if (exception != null) { throw new RuntimeException("Tearing down model binding failed in derived class:", exception); } } } /** * Invoked before the model binding for the base {@link WorkflowNodePropertySection} is teared down. * */ protected void beforeTearingDownModelBinding() { /* empty default implementation */ } @Override /* * #createCompositeContent(Composite, TabbedPropertySheet) should be used to benefit from Controller, Synchronizer and Updater. */ @Deprecated // see comment above public void createControls(final Composite parent, final TabbedPropertySheetPage aTabbedPropertySheetPage) { super.createControls(parent, aTabbedPropertySheetPage); composite = parent; createCompositeContent(composite, aTabbedPropertySheetPage); initializeController(); initializeEnumGroups(); } protected void createCompositeContent(final Composite parent, final TabbedPropertySheetPage aTabbedPropertySheetPage) { /* empty default implementation */ } protected final Controller getController() { return controller; } protected Controller createController() { return new DefaultController(); } protected void initializeController() { final Controller controller2 = getController(); if (composite != null && controller2 != null) { initializeController(controller2, composite); } } protected void initializeController(final Controller controller2, final Composite parent) { for (final Control control : parent.getChildren()) { final String property = (String) control.getData(CONTROL_PROPERTY_KEY); if (control instanceof CTabFolder) { CTabFolder tabFolder = (CTabFolder) control; for (CTabItem item : tabFolder.getItems()) { if (item.getControl() != null) { if (item.getControl() instanceof Composite) { initializeController(controller2, (Composite) item.getControl()); } } } } else if (control instanceof Composite) { initializeController(controller2, (Composite) control); } final boolean activeControl = control instanceof Button; if (activeControl || property != null) { control.addFocusListener(controller2); control.addKeyListener(controller2); if (control instanceof Button) { ((Button) control).addSelectionListener(controller2); } else if (control instanceof Text) { ((Text) control).addSelectionListener(controller2); ((Text) control).addModifyListener(controller2); } else if (control instanceof StyledText) { ((StyledText) control).addSelectionListener(controller2); ((StyledText) control).addModifyListener(controller2); } else if (control instanceof Combo) { ((Combo) control).addSelectionListener(controller2); } else if (control instanceof CCombo) { ((CCombo) control).addSelectionListener(controller2); ((CCombo) control).addModifyListener(controller2); } else if (control instanceof Spinner) { ((Spinner) control).addSelectionListener(controller2); ((Spinner) control).addModifyListener(controller2); } } } } protected void initializeEnumGroups() { if (composite != null) { initializeEnumGroups(composite); } } protected void initializeEnumGroups(final Composite composite2) { for (final Control control : composite2.getChildren()) { if (control instanceof Composite) { initializeEnumGroups((Composite) control); } final String property = (String) control.getData(CONTROL_PROPERTY_KEY); @SuppressWarnings("unchecked") final Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) control.getData(ENUM_TYPE_KEY); final Enum<?> enumValue = (Enum<?>) control.getData(ENUM_VALUE_KEY); if (property != null && enumType != null && enumValue != null) { if (!enumType.isAssignableFrom(enumValue.getClass())) { throw new RuntimeException(); } if (!enumGroups.containsKey(property)) { enumGroups.put(property, new HashMap<Enum<?>, Set<Control>>()); } final Map<Enum<?>, Set<Control>> enumGroup = enumGroups.get(property); if (!enumGroup.containsKey(enumValue)) { enumGroup.put(enumValue, new HashSet<Control>()); } enumGroup.get(enumValue).add(control); } } } protected boolean isEnumControl(final Control control) { final String property = (String) control.getData(CONTROL_PROPERTY_KEY); @SuppressWarnings("unchecked") final Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) control.getData(ENUM_TYPE_KEY); final Enum<?> enumValue = (Enum<?>) control.getData(ENUM_VALUE_KEY); boolean result = control instanceof Button && (control.getStyle() & SWT.RADIO) != 0; result &= property != null && enumType != null && enumValue != null; return result; } protected final Synchronizer getSynchronizer() { return synchronizer; } protected Synchronizer createSynchronizer() { return new DefaultSynchronizer(); } protected final Updater getUpdater() { return updater; } protected Updater createUpdater() { return new DefaultUpdater(); } protected void addPropertyChangeListener(PropertyChangeListener listener) { node.addPropertyChangeListener(listener); } protected void removePropertyChangeListener(PropertyChangeListener listener) { node.removePropertyChangeListener(listener); } protected ComponentInstanceProperties getReadableConfiguration() { return node; } /** * Returns the readable configuration. * * @return {@link ComponentInstanceProperties} */ public ComponentInstanceProperties getConfiguration() { if (getCommandStack() == null || node == null) { throw new IllegalStateException("Property input not set"); } return node; } protected boolean hasInputs() { return WorkflowNodeUtil.hasInputs(node); } protected boolean hasOutputs() { return WorkflowNodeUtil.hasOutputs(node); } protected boolean hasInputs(DataType type) { return WorkflowNodeUtil.hasInputs(node, type); } protected boolean hasOutputs(DataType type) { return WorkflowNodeUtil.hasOutputs(node, type); } protected Set<EndpointDescription> getInputs() { return WorkflowNodeUtil.getInputs(node); } protected Set<EndpointDescription> getInputs(DataType type) { return WorkflowNodeUtil.getInputsByDataType(node, type); } protected Set<EndpointDescription> getOutputs() { return WorkflowNodeUtil.getOutputs(node); } protected Set<EndpointDescription> getOutputs(DataType type) { return WorkflowNodeUtil.getOutputs(node, type); } protected boolean isPropertySet(final String key) { return WorkflowNodeUtil.isConfigurationValueSet(node, key); } protected String getProperty(final String key) { if (node != null) { String s = WorkflowNodeUtil.getConfigurationValue(node, key); return s; } else { return null; } } protected void setProperty(final String key, final String value) { final String newValue = WorkflowNodeUtil.getConfigurationValue(node, key); setProperty(key, newValue, value); } protected void setProperty(final String key, final String oldValue, final String newValue) { if ((oldValue != null && !oldValue.equals(newValue)) || (oldValue == null && oldValue != newValue)) { final WorkflowNodeCommand command = new SetConfigurationValueCommand(key, oldValue, newValue); execute(command); } } protected EditConfigurationValueCommand editProperty(final String key) { final String oldValue = WorkflowNodeUtil.getConfigurationValue(node, key); final EditConfigurationValueCommand command = new EditConfigurationValueCommand(key, oldValue); execute(command); return command; } /** * If the current node is not the node where the command should be executed, this method must be called. * * @param workflowNode to execute the command from. * @param command to execute. */ public void execute(WorkflowNode workflowNode, WorkflowNodeCommand command) { if (openEditCommand != null) { openEditCommand.finishEditing(); openEditCommand = null; } command.setCommandStack(getCommandStack()); command.setWorkflowNode(workflowNode); command.initialize(); if (command.canExecute()) { getCommandStack().execute(new NodeCommandWrapper(command)); if (command instanceof EditConfigurationValueCommand) { openEditCommand = (EditConfigurationValueCommand) command; } } } @Override public void execute(final WorkflowNodeCommand command) { execute(node, command); } @Override public final void refresh() { /* * Caching the configuration the refresh was executed for the last time, avoids executing the refresh twice. */ if (lastRefreshConfiguration == null || lastRefreshConfiguration != getConfiguration()) { refreshSection(); lastRefreshConfiguration = getConfiguration(); } } protected void refreshSection() { if (composite != null) { refreshComposite(composite); } } protected void refreshComposite(final Composite composite2) { if (composite2.isDisposed()) { return; } for (final Control control : composite2.getChildren()) { if (control.isDisposed()) { continue; } if (control instanceof Composite) { refreshComposite((Composite) control); } final String propertyKey = (String) control.getData(CONTROL_PROPERTY_KEY); if (propertyKey == null) { continue; } final String propertyValue = getProperty(propertyKey); getUpdater().initializeControl(control, propertyKey, propertyValue); } } private static boolean isBooleanButton(final Control button) { return button instanceof Button && (button.getStyle() & SWT.CHECK) != 0 || (button.getStyle() & SWT.TOGGLE) != 0 || (button.getStyle() & SWT.RADIO) != 0; } /** * A wrapper class to wrap {@link WorkflowNodeCommand}s in GEF {@link Command}s. * * @author Christian Weiss */ private static final class NodeCommandWrapper extends WorkflowPropertySection.CommandWrapper { /** The backing command, invokations are forwarded to. */ private final WorkflowNodeCommand command; private NodeCommandWrapper(final WorkflowNodeCommand command) { super(command); this.command = command; } @Override public String getLabel() { return command.getLabel(); } } /** * {@link WorkflowNodeCommand} to change the value of a property in the backing <code>ComponentInstanceConfiguration</code>. * * @author Christian Weiss */ protected static class SetConfigurationValueCommand extends AbstractWorkflowNodeCommand { private final String key; private final String oldValue; private final String newValue; public SetConfigurationValueCommand(final String key, final String oldValue, final String newValue) { this.key = key; this.oldValue = oldValue; this.newValue = newValue; } @Override public void execute2() { ConfigurationDescription configDesc = getProperties().getConfigurationDescription(); configDesc.setConfigurationValue(key, newValue); } @Override public void undo2() { ConfigurationDescription configDesc = getProperties().getConfigurationDescription(); configDesc.setConfigurationValue(key, oldValue); } } /** * {@link WorkflowNodeCommand} to change the value of a property in the backing <code>ComponentInstanceConfiguration</code> through * editing. * * @author Christian Weiss */ protected static final class EditConfigurationValueCommand extends AbstractWorkflowNodeCommand { private final String key; private final String oldValue; private String newValue; private boolean editable = true; private EditConfigurationValueCommand(final String key, final String oldValue) { this(key, oldValue, oldValue); } private EditConfigurationValueCommand(final String key, final String oldValue, final String newValue) { this.key = key; this.oldValue = oldValue; this.newValue = newValue; } public boolean isEditable() { return editable; } public void finishEditing() { if (editable) { editable = false; ConfigurationDescription configDesc = getProperties().getConfigurationDescription(); configDesc.setConfigurationValue(key, newValue); } } public String getKey() { return key; } public String getOldValue() { return oldValue; } public String getNewValue() { return newValue; } public void setNewValue(final String newValue) { if (!editable) { throw new IllegalStateException(); } this.newValue = newValue; } @Override public void execute2() { if (!editable) { ConfigurationDescription configDesc = getProperties().getConfigurationDescription(); // execute methods needs to set new value to be able to restore changes upon redo configDesc.setConfigurationValue(key, newValue); } } @Override public void undo2() { // perform the changes first if (editable) { finishEditing(); } ConfigurationDescription configDesc = getProperties().getConfigurationDescription(); configDesc.setConfigurationValue(key, oldValue); } } /** * Controller interface. Needs to be implemented by controllers which want to use the * {@link WorkflowNodePropertySection#initializeController(Controller, Composite)} method to link the controller to {@link Control} * components which are tagged with the {@link WorkflowNodePropertySection#CONTROL_PROPERTY_KEY}, indicating that they are displaying * certain properties. * * <p> * <b>Remember</b>, to only commit changes to the model in the {@link Controller} and not changes to the GUI. The reactions to changes * in the model must be implemented in a {@link Synchronizer}. * </p> * * @author Christian Weiss */ protected interface Controller extends SelectionListener, FocusListener, KeyListener, ModifyListener { } /** * Default implementation of a {@link Controller}. * * Implements some core functionality needed by controller components for {@link WorkflowNodePropertySection}s. * * @author Christian Weiss */ protected class DefaultController implements Controller { protected EditConfigurationValueCommand editCommand; @Override public void widgetDefaultSelected(final SelectionEvent event) {} @Override public final void widgetSelected(final SelectionEvent event) { final Object source = event.getSource(); if (source instanceof Control) { final Control control = (Control) source; widgetSelected(event, control); final String property = (String) control.getData(CONTROL_PROPERTY_KEY); if (property != null) { widgetSelected(event, control, property); } final Enum<?> enumValue = (Enum<?>) control.getData(ENUM_VALUE_KEY); if (enumValue != null) { widgetSelected(event, control, property, enumValue); } } } protected void widgetSelected(final SelectionEvent event, final Control source) { /* empty default implementation */ } protected void widgetSelected(final SelectionEvent event, final Control source, final String property) { if (source instanceof Button) { final Button button = (Button) source; if (isBooleanButton(button)) { final boolean selected = button.getSelection(); setProperty(property, String.valueOf(selected)); } } else if (source instanceof Spinner) { final Spinner spinner = (Spinner) source; final Integer spinnerValue = spinner.getSelection(); if (getProperty(property) != null) { final Integer propertyValue = Integer.valueOf(getProperty(property)); if (spinnerValue != null && !spinnerValue.equals(propertyValue)) { setProperty(property, String.valueOf(spinnerValue)); } } else if (spinnerValue != null) { setProperty(property, String.valueOf(spinnerValue)); } } } protected void widgetSelected(final SelectionEvent event, final Control source, final String property, final Object value) { if (value instanceof Enum) { final Enum<?> enumValue = (Enum<?>) value; setProperty(property, enumValue.name()); } } @Override public void focusGained(final FocusEvent event) {} /** * * {@inheritDoc} * * Functionality: * <ul> * <li>Finishes an open 'edit session' for a property encapsulated in a {@link EditConfigurationValueCommand}.</li> * </ul> * * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent) */ @Override public void focusLost(final FocusEvent event) { if (editCommand != null) { editCommand.finishEditing(); editCommand = null; } } /** * {@inheritDoc} * * Functionality: * <ul> * <li>Editing in a {@link Text} control starts or continues an open 'edit session' for a property encapsulated in a * {@link EditConfigurationValueCommand}.</li> * </ul> * * @see org.eclipse.swt.events.ModifyListener#modifyText(org.eclipse.swt.events.ModifyEvent) */ @Override public void modifyText(final ModifyEvent event) { final Object source = event.getSource(); if (source instanceof Control) { final Control control = (Control) source; final String property = (String) control.getData(CONTROL_PROPERTY_KEY); if (source instanceof Text && property != null) { final Text text = (Text) source; final String textContent = text.getText(); final String propertyContent = getProperty(property); if (!textContent.equals(propertyContent)) { if (editCommand == null || !editCommand.isEditable()) { editCommand = editProperty(property); } editCommand.setNewValue(textContent); } } else if (source instanceof StyledText && property != null) { final StyledText text = (StyledText) source; final String textContent = text.getText(); final String propertyContent = getProperty(property); if (!textContent.equals(propertyContent)) { if (editCommand == null || !editCommand.isEditable()) { editCommand = editProperty(property); } editCommand.setNewValue(textContent); } } else if (source instanceof CCombo && property != null) { final CCombo text = (CCombo) source; final String textContent = text.getText(); final String propertyContent = getProperty(property); if (!textContent.equals(propertyContent)) { if (editCommand == null || !editCommand.isEditable()) { editCommand = editProperty(property); } editCommand.setNewValue(textContent); } } } } /** * {@inheritDoc} * * Functionality: * <ul> * <li>Pressing the ENTER key in a non-SWT.MULTI {@link Text} control forces a traversal.</li> * </ul> * * @see org.eclipse.swt.events.KeyListener#keyPressed(org.eclipse.swt.events.KeyEvent) */ @Override public void keyPressed(final KeyEvent event) { final Object source = event.getSource(); // TAB on enter in single line text fields if (source instanceof Text) { final Text text = (Text) source; if (isCarriageReturn(event) && !isMultiLineText(text)) { text.traverse(SWT.TRAVERSE_TAB_NEXT); } } else if (source instanceof StyledText) { final StyledText text = (StyledText) source; if (isCarriageReturn(event) && !isMultiLineText(text)) { text.traverse(SWT.TRAVERSE_TAB_NEXT); } } else if (source instanceof CCombo) { final CCombo text = (CCombo) source; if (isCarriageReturn(event)) { text.traverse(SWT.TRAVERSE_TAB_NEXT); } } } protected boolean isCarriageReturn(final KeyEvent event) { return event.keyCode == SWT.CR || event.keyCode == SWT.KEYPAD_CR; } protected boolean isMultiLineText(final Text text) { return (text.getStyle() & SWT.MULTI) != 0; } protected boolean isMultiLineText(final StyledText text) { return (text.getStyle() & SWT.MULTI) != 0; } @Override public void keyReleased(final KeyEvent event) {} /** * Replaces the current selection or cursor position in a {@link Text} control with the specified replacement <code>String</code>. * * @param text the {@link Text} control with selected text or cursor position * @param replacement the replacement <code>String</code> */ protected void replace(final Text text, final String replacement) { // default method of Text does not reset the selection or focus the control again // text.insert(replacement); final String textValue = text.getText(); final Point selection = text.getSelection(); // replace selected text part with replacement string final String newValue = textValue.substring(0, selection.x) + replacement + textValue.substring(selection.y); final String property = (String) text.getData(CONTROL_PROPERTY_KEY); if (!newValue.equals(textValue)) { if (property != null) { if (editCommand == null) { editCommand = editProperty(property); } editCommand.setNewValue(newValue); } text.setText(newValue); final int newX; final int newY; if (selection.x != selection.y) { newX = selection.x; newY = selection.x + replacement.length(); } else { newX = selection.x + replacement.length(); newY = newX; } text.setSelection(newX, newY); } } protected String getProperty(final String key) { /* * If the edit command is open and uncommitted return the value in the editor. */ if (editCommand != null && editCommand.isEditable() && key.equals(editCommand.getKey())) { return editCommand.getNewValue(); } return WorkflowNodePropertySection.this.getProperty(key); } protected EditConfigurationValueCommand editProperty(final String key) { return WorkflowNodePropertySection.this.editProperty(key); } } /** * Adapter to listen to events in the backing model and translate it to events in the {@link Synchronizer}. * * @author Christian Weiss */ private final class SynchronizerAdapter implements PropertyChangeListener { @Override public void propertyChange(final PropertyChangeEvent event) { final String propertyNameValue = event.getPropertyName(); final Matcher propertiesPatternMatcher = WorkflowNode.PROPERTIES_PATTERN.matcher(propertyNameValue); if (propertiesPatternMatcher.matches()) { final String propertyName = propertiesPatternMatcher.group(1); synchronizer.handlePropertyChange(propertyName, (String) event.getNewValue(), (String) event.getOldValue()); } } } /** * Listener class responsible for keeping the GUI in sync with the model. * * <p> * The <code>Synchronizer</code> gets registered at the model to listen to change events (properties & channels) and executes * appropriate actions to reflect those changes in the GUI. * </p> * * <p> * The integration of a <code>Synchronizer</code> is as follows: * <ul> * <li>A {@link SynchronizerAdapter} gets registered to the backing model (a {@link ReadableComponentInstanceConfiguration}. This * adapter filters events and converts the non-filtered to invocations of the {@link Synchronizer} instance created via * {@link WorkflowNodePropertySection#createSynchronizer()}.</li> * <li>The {@link Synchronizer} receives those filtered events via its custom interface and reacts through updating the GUI * appropriately. The default implementation {@link DefaultSynchronizer} forwards updates to the {@link Updater} instance created via * {@link WorkflowNodePropertySection#createUpdater()}.</li> * </ul> * </p> * * @author Christian Weiss */ public interface Synchronizer { /** * React on the change of a property. * * @param propertyName the key of the property * @param newValue the new value of the property * @param oldValue the old value of the property */ void handlePropertyChange(final String propertyName, final String newValue, final String oldValue); } /** * Default implementation of a {@link Synchronizer}, forwarding all updates to the {@link Updater}. * * <p> * It is adviced to derive from this class and call the super class implementation as the very first thing in overwritten methods. * </p> * * @author Christian Weiss */ protected class DefaultSynchronizer implements Synchronizer { @Override public void handlePropertyChange(final String propertyName, final String newValue, final String oldValue) { final Composite compositeInst = getComposite(); if (compositeInst != null) { recursePropertyChange(compositeInst, propertyName, newValue, oldValue); } } protected void recursePropertyChange(final Composite compositeInst, final String key, final String newValue, final String oldValue) { for (final Control control : compositeInst.getChildren()) { if (control instanceof Composite) { recursePropertyChange((Composite) control, key, newValue, oldValue); } final String linkedKey = (String) control.getData(CONTROL_PROPERTY_KEY); if (linkedKey != null && linkedKey.equals(key)) { handlePropertyChange(control, key, newValue, oldValue); } } } protected void handlePropertyChange(final Control control, final String key, final String newValue, final String oldValue) { getUpdater().updateControl(control, key, newValue, oldValue); } } /** * Interface for handlers updating the UI. * * @author Christian Weiss */ protected interface Updater { /** * Initializes the {@link Control} which is linked to the property. * * @param control the linked {@link Control} * @param propertyName the name of the property * @param value the value to display */ void initializeControl(final Control control, final String propertyName, final String value); /** * Updates the {@link Control} which is linked to the property. * * @param control the linked {@link Control} * @param propertyName the name of the property * @param newValue the value to display * @param oldValue the old value which should not be displayed anymore */ void updateControl(final Control control, final String propertyName, final String newValue, final String oldValue); } /** * Default {@link Updater} implementation of the handler to update the UI. * * @author Christian Weiss */ protected class DefaultUpdater implements Updater { /** * {@inheritDoc} * * <p> * The default implementation delegates to {@link #updateControl(Control, String, Serializable, Serializable)} with 'null' as * oldValue. * </p> * * @see de.rcenvironment.core.gui.workflow.editor. * properties.WorkflowNodePropertySection.Updater#initializeControl(org.eclipse.swt.widgets.Control, java.lang.String, * java.io.Serializable) */ @Override public void initializeControl(final Control control, final String propertyName, final String value) { updateControl(control, propertyName, value, null); } @Override public void updateControl(final Control control, final String propertyName, final String newValue, final String oldValue) { /* * Text inputs are only set, if the value is a String - otherwise a formatter should be used in a custom Updater. */ if (control instanceof Text && (newValue == null || newValue instanceof String)) { final Text textControl = (Text) control; final String valueOrDefault = valueOrDefault(newValue, ""); if (!valueOrDefault.equals(textControl.getText())) { textControl.setText(valueOrDefault); } /* * Text inputs are only set, if the value is a String - otherwise a formatter should be used in a custom Updater. */ } else if (control instanceof StyledText && (newValue == null || newValue instanceof String)) { final StyledText textControl = (StyledText) control; final String valueOrDefault = valueOrDefault(newValue, ""); if (!valueOrDefault.equals(textControl.getText())) { textControl.setText(valueOrDefault); } /* * Label outputs are set to the String value of the value. */ } else if (control instanceof Label) { final Label labelControl = (Label) control; final String valueOrDefault = stringValue(control, newValue, ""); if (!valueOrDefault.equals(labelControl.getText())) { labelControl.setText(valueOrDefault); } /* * Button inputs which are are of style CHECK or TOGGLE are set to the selection-state, if the value is of type Boolean. */ } else if (control instanceof Button && isBooleanButton(control)) { final Button buttonControl = (Button) control; final String valueOrDefault = valueOrDefault(newValue, Boolean.FALSE.toString()); if (!valueOrDefault.equals(buttonControl.getSelection())) { buttonControl.setSelection(Boolean.valueOf(valueOrDefault)); } } else if (control instanceof Spinner) { final Spinner spinnerControl = (Spinner) control; final String valueOrDefault = stringValue(control, newValue, "0"); if (!valueOrDefault.equals(spinnerControl.getSelection())) { spinnerControl.setSelection(Integer.valueOf(valueOrDefault)); } } else if (control instanceof Button && isEnumControl(control)) { @SuppressWarnings("unchecked") final Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) control.getData(ENUM_TYPE_KEY); final Enum<?> enumValue = (Enum<?>) control.getData(ENUM_VALUE_KEY); final Enum<?> newEnumValue; newEnumValue = getEnum(enumType, newValue); final boolean isSelected = enumValue.equals(newEnumValue); ((Button) control).setSelection(isSelected); } else if (control instanceof CCombo && (newValue == null || newValue instanceof String)) { CCombo combobox = (CCombo) control; final String valueOrDefault = valueOrDefault(newValue, ""); if (!valueOrDefault.equals(combobox.getText())) { combobox.setText(valueOrDefault); } } else if (control instanceof Combo && (newValue == null || newValue instanceof String)) { Combo combobox = (Combo) control; final String valueOrDefault = valueOrDefault(newValue, ""); if (!valueOrDefault.equals(combobox.getText())) { combobox.setText(valueOrDefault); } } } private Enum<?> getEnum(final Class<? extends Enum<?>> enumType, final String name) { Enum<?> result = null; for (final Field field : enumType.getFields()) { if (field.isEnumConstant()) { try { final Enum<?> enumValue = (Enum<?>) field.get(enumType); if (enumValue.name().equals(name)) { result = enumValue; break; } } catch (final IllegalAccessException e) { throw new RuntimeException(e); } } } return result; } private String stringValue(final Control control, final String value, final String defaultValue) { final String result; if (value != null) { result = value.toString(); } else { result = defaultValue; } return result; } protected String valueOrDefault(final String value, final String defaultValue) { final String result; if (value != null) { result = value; } else { result = defaultValue; } return result; } } /** * Layout composite to allow for adapting the composite content to use optimal space in the containing <code>ScrolledComposite</code>. * * <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 */ public 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; public LayoutComposite(final Composite parent) { this(parent, SWT.NONE | SWT.TRANSPARENT); } public LayoutComposite(final Composite parent, final int style) { super(parent, style); final FillLayout layout = new FillLayout(); layout.marginHeight = 0; layout.marginWidth = 0; layout.spacing = 0; setLayout(layout); } @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; } result.x = 0; return result; } } }