blob: d9eb047b9199bf49d9452d88362993e9f9060844 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2016 Obeo 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:
* Obeo - initial API and implementation
* Simon Delisle - bug 495753
*******************************************************************************/
package org.eclipse.emf.compare.rcp.ui.internal.preferences.impl;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.PojoProperties;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.emf.compare.rcp.EMFCompareRCPPlugin;
import org.eclipse.emf.compare.rcp.internal.extension.IItemDescriptor;
import org.eclipse.emf.compare.rcp.internal.extension.IItemRegistry;
import org.eclipse.emf.compare.rcp.internal.extension.impl.ItemUtil;
import org.eclipse.emf.compare.rcp.ui.internal.EMFCompareRCPUIMessages;
import org.eclipse.emf.compare.rcp.ui.internal.configuration.ui.AbstractConfigurationUI;
import org.eclipse.emf.compare.rcp.ui.internal.configuration.ui.IConfigurationUIFactory;
import org.eclipse.emf.compare.rcp.ui.internal.preferences.DataHolder;
import org.eclipse.jface.databinding.viewers.IViewerObservableSet;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.preferences.ScopedPreferenceStore;
import org.osgi.service.prefs.Preferences;
/**
* A User interface that holds a viewer and satellites elements.
* <p>
* This viewer can have a satellite configuration composite reacting on selection. It displays a configuration
* UI for the current selection. It's requires a configuration UI registry.
* </p>
* <p>
* This viewer can have a satellite text field holding the description of the current selection. This field
* display the description for the current element.
* </p>
* <p>
* This class allows a user to select and check elements.
* </p>
* <p>
* It can also synchronize the state of checked element into a {@link DataHolder}
* </p>
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
*/
public final class InteractiveUIContent {
/** Width hint for configuration composite. */
private static final int WIDTH_HINT_CONFIG_COMPOSITE = 400;
/** Height hint for description composite. */
private static final int DESCRIPTION_COMPOSITE_HEIGHT_HINT = 50;
/** Width hint for description composite. */
private static final int DESCRIPTION_COMPOSITE_WIDTH_HINT = 400;
/** Text that will be updated with the description of the viewer. */
private final Label descriptionText;
/** Composite holding the viewer. */
private final Composite viewerCompsite;
/** Composite holding the configuration. This will react to the selection in the viewer. */
private final Composite configurationComposite;
/** Composite that is used when the selection has no registered configuration. */
private final Composite defaultComposite;
/** Viewer of {@link IItemDescriptor}. */
private CheckboxTableViewer viewer;
/** List of all {@link AbstractConfigurationUI} that are linked to this viewer. */
private final Map<String, AbstractConfigurationUI> configurators = new HashMap<String, AbstractConfigurationUI>();
/**
* Constructor.
*
* @param parent
* Composite parent holding this interactive content.
* @param hasDescription
* Set to true if this has a description label that react to viewer selection.
* @param hasConfiguration
* set to true if this has a configuration composite that react to viewer selection.
*/
private InteractiveUIContent(Composite parent, boolean hasDescription, boolean hasConfiguration) {
super();
Composite contentComposite = new Composite(parent, SWT.NONE);
final int numberOfColumns;
if (hasConfiguration) {
numberOfColumns = 2;
} else {
numberOfColumns = 1;
}
GridLayoutFactory.fillDefaults().numColumns(numberOfColumns).applyTo(contentComposite);
contentComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
// Engine chooser composite
this.viewerCompsite = new Composite(contentComposite, SWT.NONE);
GridLayoutFactory.fillDefaults().applyTo(viewerCompsite);
viewerCompsite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
if (hasConfiguration) {
// Config composite
this.configurationComposite = createConfigComposite(contentComposite);
// Init default composite.
defaultComposite = new Composite(configurationComposite, SWT.NONE);
defaultComposite.setLayout(new GridLayout(1, true));
Label text = new Label(defaultComposite, SWT.WRAP);
text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true, 1, 1));
text.setText(
EMFCompareRCPUIMessages.getString("InteractiveUIContent.defaultConfiguration.label")); //$NON-NLS-1$
} else {
this.configurationComposite = null;
this.defaultComposite = null;
}
if (hasDescription) {
// Descriptor engine Text
this.descriptionText = createDescriptionComposite(parent);
} else {
this.descriptionText = null;
}
}
/**
* Adds a configuration to this Interactive content.
*
* @param id
* Id of the item to configure
* @param configuratorfactory
* Factory for the configuration
* @param store
* Preference store that will hold this {@link IConfigurationUIFactory} value.
*/
public void addConfigurator(String id, IConfigurationUIFactory configuratorfactory,
IPreferenceStore store) {
AbstractConfigurationUI configurator = configuratorfactory.createUI(configurationComposite, SWT.NONE,
store);
configurators.put(id, configurator);
}
/**
* Checks one element in the viewer.
*
* @param descriptor
* element to check.
*/
public void checkElement(IItemDescriptor<?> descriptor) {
viewer.setCheckedElements(new Object[] {descriptor });
}
/**
* Checks multiple element in the viewer. (Only use if multiple selection is allowed)
*
* @param descriptors
* elements to check.
*/
public void checkElements(IItemDescriptor<?>[] descriptors) {
viewer.setCheckedElements(descriptors);
}
/**
* Creates the composite that will hold all configurations for a tab.
*
* @param composite
* Main composite
* @return Group that will hold configurations in a stack layout.
*/
private Group createConfigComposite(Composite composite) {
Group confComposite = new Group(composite, SWT.NONE);
confComposite.setText(
EMFCompareRCPUIMessages.getString("InteractiveUIContent.configurationComposite.label")); //$NON-NLS-1$
StackLayout layout = new StackLayout();
layout.marginHeight = 10;
layout.marginWidth = 5;
confComposite.setLayout(layout);
GridData layoutData = new GridData(SWT.BEGINNING, SWT.FILL, false, true);
layoutData.widthHint = WIDTH_HINT_CONFIG_COMPOSITE;
confComposite.setLayoutData(layoutData);
return confComposite;
}
/**
* Composite for description. This composite holds the text widget that will be updated with the current
* selection.
*
* @param composite
* Main composite.
* @return Label that will hold viewer selection description.
*/
private Label createDescriptionComposite(Composite composite) {
Group descriptionComposite = new Group(composite, SWT.NONE);
descriptionComposite.setText(
EMFCompareRCPUIMessages.getString("InteractiveUIContent.descriptionComposite.label")); //$NON-NLS-1$
descriptionComposite.setLayout(new GridLayout(1, false));
descriptionComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1));
Label engineDescriptionLabel = new Label(descriptionComposite, SWT.WRAP);
GridData layoutData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1);
layoutData.heightHint = DESCRIPTION_COMPOSITE_HEIGHT_HINT;
layoutData.widthHint = DESCRIPTION_COMPOSITE_WIDTH_HINT;
engineDescriptionLabel.setLayoutData(layoutData);
return engineDescriptionLabel;
}
/**
* @return A map of all configuration.
*/
public Map<String, AbstractConfigurationUI> getConfigurators() {
return ImmutableMap.copyOf(configurators);
}
/**
* Gets the viewer.
*
* @return The viewer.
*/
public CheckboxTableViewer getViewer() {
return viewer;
}
/**
* Returns the composite that will hold the viewer.
*
* @return The composite holding the viewer.
*/
private Composite getViewerComposite() {
return viewerCompsite;
}
/**
* Handles a selection in the viewer. Update related components.
*
* @param descriptor
* Item to select.
*/
public void select(IItemDescriptor<?> descriptor) {
// Update viewer
viewer.setSelection(new StructuredSelection(descriptor), true);
updateLinkedElements(descriptor);
}
/**
* Sets the viewer in the interactive content.
*
* @param inputViewer
* A {@link StructuredViewer} of {@link IItemDescriptor}
*/
public void setViewer(CheckboxTableViewer inputViewer) {
this.viewer = inputViewer;
if (configurationComposite != null) {
viewer.addSelectionChangedListener(new ConfigurationListener());
}
viewer.addSelectionChangedListener(new DescriptionListener());
}
/**
* Updates the linked element in this interactive content.
*
* @param descriptor
* Item used as input to get information for satellite elements.
*/
private void updateLinkedElements(IItemDescriptor<?> descriptor) {
// Update description
descriptionText.setText(descriptor.getDescription());
if (configurationComposite != null) {
updateConfigurationComposite(descriptor);
}
}
/**
* Updates the configuration composite.
*
* @param descriptor
* New descriptor.
*/
private void updateConfigurationComposite(IItemDescriptor<?> descriptor) {
StackLayout stackLayout = (StackLayout)configurationComposite.getLayout();
if (configurators.containsKey(descriptor.getID())) {
stackLayout.topControl = configurators.get(descriptor.getID());
} else {
stackLayout.topControl = defaultComposite;
}
configurationComposite.layout();
}
/**
* This listener updates the Data Holder.
* <p>
* With this listener, only one element can be checked at a time
* </p>
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
* @param <T>
* Type of item.
*/
private static final class SingleCheckListener<T> implements ICheckStateListener {
/** Data holder. */
private final DataHolder<T> dataObject;
/** Viewer. */
private final CheckboxTableViewer descriptorViewer;
/** Shell. */
private final Shell shell;
/**
* Constructor.
*
* @param dataObject
* Data holder.
* @param descriptorViewer
* Viewer
* @param shell
* Shell.
*/
private SingleCheckListener(DataHolder<T> dataObject, CheckboxTableViewer descriptorViewer,
Shell shell) {
this.dataObject = dataObject;
this.descriptorViewer = descriptorViewer;
this.shell = shell;
}
/**
* {@inheritDoc}
*/
public void checkStateChanged(CheckStateChangedEvent event) {
Object element = event.getElement();
if (event.getChecked()) {
if (element instanceof IItemDescriptor<?>) {
@SuppressWarnings("unchecked")
IItemDescriptor<T> descriptor = (IItemDescriptor<T>)element;
dataObject.setData(Collections.singleton(descriptor));
}
descriptorViewer.setCheckedElements(new Object[] {element });
} else {
// Prevent from nothing checked
if (descriptorViewer.getCheckedElements().length == 0) {
descriptorViewer.setCheckedElements(new Object[] {element });
MessageDialog.openWarning(shell,
EMFCompareRCPUIMessages
.getString("InteractiveUIContent.incorrectSelection.title"), //$NON-NLS-1$
EMFCompareRCPUIMessages
.getString("InteractiveUIContent.incorrectSelection.message")); //$NON-NLS-1$
}
}
}
}
/**
* Listener in charge of updating the configuration composite.
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
*/
private final class ConfigurationListener implements ISelectionChangedListener {
/**
* {@inheritDoc}
*/
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (selection instanceof IStructuredSelection) {
IStructuredSelection structSelection = (IStructuredSelection)selection;
Object selected = structSelection.getFirstElement();
if (selected instanceof IItemDescriptor<?>) {
updateLinkedElements((IItemDescriptor<?>)selected);
}
}
}
}
/**
* Listener used to update description text.
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
*/
private final class DescriptionListener implements ISelectionChangedListener {
/**
* {@inheritDoc}
*/
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (selection instanceof IStructuredSelection) {
IStructuredSelection structSelection = (IStructuredSelection)selection;
Object selected = structSelection.getFirstElement();
if (selected instanceof IItemDescriptor<?>) {
IItemDescriptor<?> desc = (IItemDescriptor<?>)selected;
String description = desc.getDescription();
descriptionText.setText(description);
}
}
}
}
/**
* Builder for an Interactive UI.
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
* @param <T>
* type of item in the viewer.
*/
public static class InteractiveUIBuilder<T> {
/** Holding composite of all the structure. */
private Composite parent;
/** Item registry holding input of the viewer. */
private IItemRegistry<T> registry;
/** Key of the preference node where the configuration for an item is stored. */
private String configurationNodeKey;
/** Configuration UI registry. */
private Map<String, IConfigurationUIFactory> configurationUIRegistry;
/** Set of elements to check by default. */
private Set<IItemDescriptor<T>> defaultCheck;
/** Element to select by default. */
private IItemDescriptor<T> defaultSelection;
/** Object holding data representing the current status of the interactive content. */
private DataHolder<T> dataHolder;
/** Set to true if the interactive content has a description field. */
private boolean hasDescription = true;
/** Set to true if this interactive content is synchronized with only one element at a time. */
private boolean isSimple = false;
/**
* Constructor.
*
* @param parent
* Holding composite of all the structure.
* @param registry
* Item registry holding input of the viewer.
*/
public InteractiveUIBuilder(Composite parent, IItemRegistry<T> registry) {
super();
this.parent = parent;
this.registry = registry;
}
/**
* Sets a dataHolder that will be synchronized with the checked element.
*
* @param newDataHolder
* DataHolder.
* @return {@link InteractiveUIBuilder}
*/
public InteractiveUIBuilder<T> setHoldingData(DataHolder<T> newDataHolder) {
this.dataHolder = newDataHolder;
return this;
}
/**
* Node key used to get the {@link Preferences} to retrieve {@link IConfigurationUIFactory}. See
* {@link ItemUtil#getConfigurationPreferenceNode(String, String)} (needed if a
* ConfigurationUIRegistry has been provided)
*
* @param key
* .
* @return {@link InteractiveUIBuilder}
*/
public InteractiveUIBuilder<T> setConfigurationNodeKey(String key) {
this.configurationNodeKey = key;
return this;
}
/**
* Registry of {@link IConfigurationUIFactory} used to fill the configuration composite.
*
* @param configuratorUIRegistry
* .
* @return {@link InteractiveUIBuilder}
*/
public InteractiveUIBuilder<T> setConfiguratorUIRegistry(
Map<String, IConfigurationUIFactory> configuratorUIRegistry) {
this.configurationUIRegistry = configuratorUIRegistry;
return this;
}
/**
* Sets the default element to check. (A singleton if "this" is set to simple
* {@link InteractiveUIBuilder#setSimple(boolean)}
*
* @param newDefaultCheck
* .
* @return InteractiveUIBuilder
*/
public InteractiveUIBuilder<T> setDefaultCheck(Set<IItemDescriptor<T>> newDefaultCheck) {
this.defaultCheck = newDefaultCheck;
return this;
}
/**
* Set the default element to select.
*
* @param newDefaultSelection
* .
* @return InteractiveUIBuilder
*/
public InteractiveUIBuilder<T> setDefaultSelection(IItemDescriptor<T> newDefaultSelection) {
this.defaultSelection = newDefaultSelection;
return this;
}
/**
* Set to true if "this" needs to create a description field.
*
* @param newHasDescription
* .
* @return {@link InteractiveUIBuilder}
*/
public InteractiveUIBuilder<T> setHasDescription(boolean newHasDescription) {
this.hasDescription = newHasDescription;
return this;
}
/**
* Set to true if the viewer can only have only one element checked at a time.
*
* @param newIsSimple
* .
* @return {@link InteractiveUIBuilder}
*/
public InteractiveUIBuilder<T> setSimple(boolean newIsSimple) {
this.isSimple = newIsSimple;
return this;
}
/**
* Build a new {@link InteractiveUI}.
*
* @return InteractiveUIContent
*/
public InteractiveUIContent build() {
// If simple only one element check at a time
Preconditions.checkArgument(!isSimple || defaultCheck == null || defaultCheck.size() == 1);
boolean hasConfiguration = configurationUIRegistry != null;
// If has a configuration composite then the key to retrieve the preference must be set up.
Preconditions.checkArgument(!hasConfiguration || configurationNodeKey != null);
final InteractiveUIContent interactiveUI = new InteractiveUIContent(parent, hasDescription,
hasConfiguration);
CheckboxTableViewer descriptorViewer = createViewer(interactiveUI);
if (hasConfiguration) {
createConfigurationComposite(interactiveUI);
}
setViewerInput(descriptorViewer);
// Init and bind data
bindAndInit(interactiveUI, descriptorViewer);
return interactiveUI;
}
/**
* Initializes the viewer.
*
* @param interactiveUI
* .
* @return CheckboxTableViewer
*/
private CheckboxTableViewer createViewer(final InteractiveUIContent interactiveUI) {
int style = SWT.BORDER | SWT.V_SCROLL | SWT.FULL_SELECTION;
if (isSimple) {
style = style | SWT.SINGLE;
}
CheckboxTableViewer descriptorViewer = CheckboxTableViewer
.newCheckList(interactiveUI.getViewerComposite(), style);
interactiveUI.setViewer(descriptorViewer);
descriptorViewer.setContentProvider(ArrayContentProvider.getInstance());
descriptorViewer.setLabelProvider(new ItemDescriptorLabelProvider());
GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1);
descriptorViewer.getControl().setLayoutData(gd);
return descriptorViewer;
}
/**
* Creates the configuration composite.
*
* @param interactiveUI
* .
*/
private void createConfigurationComposite(final InteractiveUIContent interactiveUI) {
// Init configuration elements
for (IItemDescriptor<T> item : registry.getItemDescriptors()) {
String itemId = item.getID();
IConfigurationUIFactory configuratorFactory = configurationUIRegistry.get(itemId);
if (configuratorFactory != null) {
// Preferences pref = ItemUtil.getConfigurationPreferenceNode(configurationNodeKey,
// itemId);
ScopedPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE,
EMFCompareRCPPlugin.PLUGIN_ID);
store.setSearchContexts(
new IScopeContext[] {InstanceScope.INSTANCE, ConfigurationScope.INSTANCE });
interactiveUI.addConfigurator(itemId, configuratorFactory, store);
}
}
}
/**
* Initializes and binds interactive content with the data holder value.
*
* @param interactiveUI
* .
* @param descriptorViewer
* .
*/
private void bindAndInit(final InteractiveUIContent interactiveUI,
CheckboxTableViewer descriptorViewer) {
if (defaultSelection != null) {
interactiveUI.select(defaultSelection);
}
if (isSimple) {
if (defaultCheck != null) {
IItemDescriptor<T> defaultCheckedElement = defaultCheck.iterator().next();
interactiveUI.checkElement(defaultCheckedElement);
if (dataHolder != null) {
dataHolder.setData(Collections.singleton(defaultCheckedElement));
}
}
descriptorViewer.addCheckStateListener(new SingleCheckListener<T>(dataHolder,
descriptorViewer, Display.getDefault().getActiveShell()));
} else {
if (dataHolder != null) {
if (defaultCheck != null) {
dataHolder.setData(defaultCheck);
}
// Bind data
bindMultipleData(interactiveUI.getViewer(), dataHolder);
}
}
}
/**
* Sets the viewer input.
*
* @param descriptorViewer
* .
*/
private void setViewerInput(CheckboxTableViewer descriptorViewer) {
List<IItemDescriptor<T>> itemDescriptors = registry.getItemDescriptors();
Collections.sort(itemDescriptors);
descriptorViewer.setInput(itemDescriptors);
}
/**
* Binds UI to data object.
*
* @param descriptorViewer
* .
* @param dataObject
* The data holder.
*/
private void bindMultipleData(CheckboxTableViewer descriptorViewer, final DataHolder<T> dataObject) {
DataBindingContext ctx = new DataBindingContext();
// Bind the button with the corresponding field in data
IViewerObservableSet target = ViewersObservables.observeCheckedElements(descriptorViewer,
IItemDescriptor.class);
IObservableSet model = PojoProperties.set(DataHolder.class, DataHolder.DATA_FIELD_NAME)
.observe(dataObject);
ctx.bindSet(target, model);
}
}
}