/******************************************************************************* * Copyright (c) 2016, 2017 Obeo. * 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 *******************************************************************************/ package org.eclipse.eef.ide.ui.api.widgets; import java.util.Collection; import java.util.Optional; import java.util.function.Consumer; import org.eclipse.eef.EEFDynamicMappingFor; import org.eclipse.eef.EEFDynamicMappingIf; import org.eclipse.eef.EEFGroupDescription; import org.eclipse.eef.EEFWidgetDescription; import org.eclipse.eef.EEFWidgetStyle; import org.eclipse.eef.common.api.utils.Util; import org.eclipse.eef.common.ui.api.EEFWidgetFactory; import org.eclipse.eef.common.ui.api.IEEFFormContainer; import org.eclipse.eef.core.api.EEFExpressionUtils; import org.eclipse.eef.core.api.EditingContextAdapter; import org.eclipse.eef.core.api.LockStatusChangeEvent; import org.eclipse.eef.core.api.LockStatusChangeEvent.LockStatus; import org.eclipse.eef.core.api.controllers.IEEFWidgetController; import org.eclipse.eef.core.api.utils.EvalFactory; import org.eclipse.eef.ide.ui.api.widgets.EEFStyleHelper.IEEFTextStyleCallback; import org.eclipse.eef.ide.ui.internal.EEFIdeUiPlugin; import org.eclipse.eef.ide.ui.internal.Icons; import org.eclipse.eef.ide.ui.internal.Messages; import org.eclipse.eef.ide.ui.internal.widgets.EEFStyledTextStyleCallback; import org.eclipse.eef.ide.ui.internal.widgets.styles.EEFColor; import org.eclipse.eef.ide.ui.internal.widgets.styles.EEFFont; import org.eclipse.emf.ecore.EObject; import org.eclipse.jface.fieldassist.ControlDecoration; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredViewer; import org.eclipse.sirius.common.interpreter.api.IInterpreter; import org.eclipse.sirius.common.interpreter.api.IVariableManager; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseTrackListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; /** * Parent of all the lifecycle managers. * * @author sbegaudeau */ public abstract class AbstractEEFWidgetLifecycleManager extends AbstractEEFLifecycleManager { /** * The number of pixel of the additional gap necessary to draw the validation marker. */ protected static final int VALIDATION_MARKER_OFFSET = 5; /** * The variable manager. */ protected IVariableManager variableManager; /** * The interpreter. */ protected IInterpreter interpreter; /** * The editing context adapter. */ protected EditingContextAdapter editingContextAdapter; /** * The label. */ protected StyledText label; /** * The help label. */ protected CLabel help; /** * The listener on the help. */ private MouseTrackListener mouseTrackListener; /** * The listener used to react to changes in the lock status of a semantic element. */ private Consumer<Collection<LockStatusChangeEvent>> lockStatusChangedListener; /** * The decorator used to indicate the permission on the validation widget. */ private ControlDecoration controlDecoration; /** * The constructor. * * @param variableManager * The variable manager * @param interpreter * The interpreter * @param editingContextAdapter * The editing context adapter */ public AbstractEEFWidgetLifecycleManager(IVariableManager variableManager, IInterpreter interpreter, EditingContextAdapter editingContextAdapter) { this.variableManager = variableManager; this.interpreter = interpreter; this.editingContextAdapter = editingContextAdapter; } /** * {@inheritDoc} * * @see org.eclipse.eef.ide.ui.api.widgets.AbstractEEFLifecycleManager#createControl(org.eclipse.swt.widgets.Composite, * org.eclipse.eef.common.ui.api.IEEFFormContainer) */ @Override public void createControl(Composite parent, IEEFFormContainer formContainer) { super.createControl(parent, formContainer); EEFWidgetFactory widgetFactory = formContainer.getWidgetFactory(); Composite composite = parent; // If we are in a group, we will always create a label (empty or not) for the 3 columns layout of the group. boolean isInGroup = this.isInGroup(); // Some widgets (like a checkbox) will not have a separated "label" widget for their label. Those widgets will // thus never create another widget expect in the group (for the layout). boolean needsSeparatedLabel = this.needSeparatedLabel(); // Finally if the label expression is blank, we will not create a label inside of a group (for the layout). boolean isBlankLabel = Util.isBlank(this.getWidgetDescription().getLabelExpression()); boolean needsLabel = isInGroup || (!isBlankLabel && needsSeparatedLabel); boolean needsHelp = isInGroup || !Util.isBlank(this.getWidgetDescription().getHelpExpression()); // If we are not in a group, we will create a composite to hold all the label and help of the widget if // necessary if (!isInGroup && (needsLabel || needsHelp)) { composite = widgetFactory.createComposite(parent); // We will only create the necessary number of columns for this "invisible" composite int numColumn = 1; if (needsLabel) { numColumn = numColumn + 1; } if (needsHelp) { numColumn = numColumn + 1; } GridLayout layout = new GridLayout(numColumn, false); composite.setLayout(layout); GridData layoutData = new GridData(GridData.FILL_HORIZONTAL); layoutData.horizontalSpan = 1; composite.setLayoutData(layoutData); } if (needsLabel) { this.label = widgetFactory.createStyledText(composite, SWT.NONE); this.label.setEditable(false); this.label.setEnabled(false); this.label.setLayoutData(new GridData(this.getLabelVerticalAlignment())); } if (needsHelp) { this.help = widgetFactory.createCLabel(composite, ""); //$NON-NLS-1$ if (!Util.isBlank(this.getWidgetDescription().getHelpExpression())) { this.help.setImage(EEFIdeUiPlugin.getPlugin().getImageRegistry().get(Icons.HELP)); this.help.setLayoutData(new GridData(this.getLabelVerticalAlignment())); this.help.setToolTipText(""); //$NON-NLS-1$ } } this.createMainControl(composite, formContainer); this.controlDecoration = new ControlDecoration(this.getValidationControl(), SWT.TOP | SWT.LEFT); this.checkLockStatus(); } /** * Checks the current lock status and make the user interface react to it. */ private void checkLockStatus() { Object self = this.variableManager.getVariables().get(EEFExpressionUtils.SELF); if (self instanceof EObject) { LockStatus status = this.editingContextAdapter.getLockStatus((EObject) self); this.handleLockStatus(status); } } /** * Indicates if the widget description is located directly under a group or if it is under a container. * * @return <code>true</code> if the widget description is directly under a group, <code>false</code> otherwise */ private boolean isInGroup() { EObject eContainer = this.getWidgetDescription().eContainer(); // Test if the widget description is in a dynamic mapping directly under a group if (eContainer instanceof EEFDynamicMappingIf && eContainer.eContainer() instanceof EEFDynamicMappingFor) { EEFDynamicMappingFor dynamicMappingFor = (EEFDynamicMappingFor) eContainer.eContainer(); return dynamicMappingFor.eContainer() instanceof EEFGroupDescription; } // Otherwise, let's test if it is directly under a group return eContainer instanceof EEFGroupDescription; } /** * Indicates if the widget should create a label widget for its label. * * @return <code>true</code> if a label should be created, <code>false</code> otherwise. */ protected boolean needSeparatedLabel() { return true; } /** * Returns the vertical alignment of the label of the widget. Use one of the following values: * <ul> * <li>GridData.VERTICAL_ALIGN_BEGINNING</li> * <li>GridData.VERTICAL_ALIGN_CENTER</li> * <li>GridData.VERTICAL_ALIGN_END</li> * </ul> * * @return The vertical alignment of the label of the widget */ protected int getLabelVerticalAlignment() { // By default, the label is aligned to the top of the widget return GridData.VERTICAL_ALIGN_BEGINNING; } /** * {@inheritDoc} * * @see org.eclipse.eef.ide.ui.api.widgets.AbstractEEFLifecycleManager#getController() */ @Override protected abstract IEEFWidgetController getController(); /** * Returns the description of the widget. * * @return The description of the widget */ protected abstract EEFWidgetDescription getWidgetDescription(); /** * Create the main control. * * @param parent * The composite parent * @param formContainer * The form container */ protected abstract void createMainControl(Composite parent, IEEFFormContainer formContainer); /** * {@inheritDoc} * * @see org.eclipse.eef.ide.ui.api.widgets.AbstractEEFLifecycleManager#aboutToBeShown() */ @Override public void aboutToBeShown() { super.aboutToBeShown(); this.getController().onNewLabel((value) -> { if (!label.isDisposed() && !(label.getText() != null && label.getText().equals(value))) { label.setText(Optional.ofNullable(value).orElse("")); //$NON-NLS-1$ } AbstractEEFWidgetLifecycleManager.this.setLabelFontStyle(); }); this.getController().onNewHelp((value) -> { if (help != null && !help.isDisposed() && !(help.getText() != null && help.getText().equals(value))) { help.setToolTipText(Optional.ofNullable(value).orElse(Messages.AbstractEEFWidgetLifecycleManager_noDescriptionAvailable)); } }); if (this.help != null) { this.mouseTrackListener = new MouseTrackListener() { @Override public void mouseHover(MouseEvent e) { // Defer the computation of the help message when the user hovers the Help label getController().computeHelp(); } @Override public void mouseExit(MouseEvent e) { // Nothing todo } @Override public void mouseEnter(MouseEvent e) { // Nothing todo } }; this.help.addMouseTrackListener(mouseTrackListener); } this.lockStatusChangedListener = (events) -> { Display.getDefault().asyncExec(() -> { events.stream().filter(event -> this.getWidgetSemanticElement().equals(event.getElement())) .forEach(event -> this.handleLockStatus(event.getStatus())); }); }; this.editingContextAdapter.addLockStatusChangedListener(this.lockStatusChangedListener); } /** * Handles the change in the lock status by switching the user interface to a "locked by me", "locked by other" or * "unlocked" state. * * @param status * The lock status */ private void handleLockStatus(LockStatus status) { if (status != null) { switch (status) { case LOCKED_BY_ME: AbstractEEFWidgetLifecycleManager.this.lockedByMe(); break; case LOCKED_BY_OTHER: AbstractEEFWidgetLifecycleManager.this.lockedByOther(); break; case LOCKED_PERMISSION: AbstractEEFWidgetLifecycleManager.this.lockedNoWrite(); break; case UNLOCKED: AbstractEEFWidgetLifecycleManager.this.unlocked(); break; default: AbstractEEFWidgetLifecycleManager.this.unlocked(); break; } } } /** * Returns the semantic element of the current widget. * * @return The semantic element of the current widget */ protected Object getWidgetSemanticElement() { return this.variableManager.getVariables().get(EEFExpressionUtils.SELF); } /** * Sets the appearance and behavior of the widget in order to indicate that the semantic element used by the widget * is currently locked by the current user. By default, it will only display a small green lock next to the * validation control. */ protected void lockedByMe() { if (this.controlDecoration.getControl() != null) { this.controlDecoration.hide(); this.controlDecoration.setDescriptionText(Messages.AbstractEEFWidgetLifecycleManager_lockedByMe); this.controlDecoration.setImage(EEFIdeUiPlugin.getPlugin().getImageRegistry().get(Icons.PERMISSION_GRANTED_TO_CURRENT_USER_EXCLUSIVELY)); this.controlDecoration.show(); } } /** * Sets the appearance and behavior of the widget in order to indicate that the semantic element used by the widget * is currently locked by another user. As a result, it will set the user interface in a disabled mode along with a * red lock next to the widget. */ protected void lockedByOther() { this.setEnabled(false); if (this.controlDecoration.getControl() != null) { this.controlDecoration.hide(); this.controlDecoration.setDescriptionText(Messages.AbstractEEFWidgetLifecycleManager_lockedByOther); this.controlDecoration.setImage(EEFIdeUiPlugin.getPlugin().getImageRegistry().get(Icons.PERMISSION_DENIED)); this.controlDecoration.show(); } } /** * Sets the appearance and behavior of the widget in order to indicate that the semantic element used by the widget * cannot be modified by the user. As a result, it will set the user interface in a disable mode with a grey lock * next to the widget. */ protected void lockedNoWrite() { this.setEnabled(false); if (this.controlDecoration.getControl() != null) { this.controlDecoration.hide(); this.controlDecoration.setDescriptionText(Messages.AbstractEEFWidgetLifecycleManager_lockedNoWrite); this.controlDecoration.setImage(EEFIdeUiPlugin.getPlugin().getImageRegistry().get(Icons.PERMISSION_NO_WRITE)); this.controlDecoration.show(); } } /** * Sets the appearance and behavior of the widget in order to indicate that the semantic element used by the widget * is currently unlocked. As a result, it will set back the widget to its default state. */ protected void unlocked() { this.setEnabled(this.isEnabled()); if (this.controlDecoration.getControl() != null) { this.controlDecoration.hide(); } } /** * Sets the enablement of the widget. * * @param isEnabled * <code>true</code> when the widget should have its default behavior, <code>false</code> when the widget * should be in a read only mode. */ protected abstract void setEnabled(boolean isEnabled); /** * Check if a widget is enabled. * * @return True if the widget should be enabled otherwise false. */ protected boolean isEnabled() { Boolean result = EvalFactory.of(interpreter, variableManager).logIfInvalidType(Boolean.class).defaultValue(Boolean.TRUE) .evaluate(getWidgetDescription().getIsEnabledExpression()); return result.booleanValue(); } /** * Set label font style. */ protected void setLabelFontStyle() { EEFStyleHelper styleHelper = this.getEEFStyleHelper(); EEFWidgetStyle style = styleHelper.getWidgetStyle(getWidgetDescription()); IEEFTextStyleCallback callback = new EEFStyledTextStyleCallback(this.label); if (style != null) { styleHelper.applyTextStyle(style.getLabelFontNameExpression(), style.getLabelFontSizeExpression(), style.getLabelFontStyleExpression(), this.label.getFont(), style.getLabelBackgroundColorExpression(), style.getLabelForegroundColorExpression(), callback); } else { // Set everything back to the default value callback.applyForegroundColor(new EEFColor((Color) null)); callback.applyBackgroundColor(new EEFColor((Color) null)); callback.applyFontStyle(false, false); callback.applyFont(new EEFFont(null, 0, 0) { @Override public Font getFont() { return null; } }); } } /** * Returns the style helper used to compute the style of the widget. * * @return The style helper */ protected EEFStyleHelper getEEFStyleHelper() { return new EEFStyleHelper(interpreter, variableManager); } /** * {@inheritDoc} * * @see org.eclipse.eef.ide.ui.api.widgets.AbstractEEFLifecycleManager#refresh() */ @Override public void refresh() { super.refresh(); this.checkLockStatus(); } /** * {@inheritDoc} * * @see org.eclipse.eef.ide.ui.api.widgets.AbstractEEFLifecycleManager#aboutToBeHidden() */ @Override public void aboutToBeHidden() { super.aboutToBeHidden(); if (this.help != null && !this.help.isDisposed()) { this.help.removeMouseTrackListener(mouseTrackListener); } this.getController().removeNewLabelConsumer(); this.editingContextAdapter.removeLockStatusChangedListener(this.lockStatusChangedListener); } /** * {@inheritDoc} * * @see org.eclipse.eef.ide.ui.api.widgets.IEEFLifecycleManager#dispose() */ @Override public void dispose() { EEFIdeUiPlugin.getPlugin().debug("AbstractEEFWidgetLifeCycleManager#dispose()"); //$NON-NLS-1$ } /** * Returns the <code>IStructuredSelection</code> of the specified viewer. * <p> * Backport of <code>StructuredViewer.getStructuredSelection()</code> which was introduced in JFace 3.11 (Mars) to * work with JFace 3.10 (Luna). * </p> * * @param viewer * the viewer. * @return IStructuredSelection * @throws ClassCastException * if the selection of the viewer is not an instance of IStructuredSelection */ protected IStructuredSelection getStructuredSelection(StructuredViewer viewer) throws ClassCastException { ISelection selection = viewer.getSelection(); if (selection instanceof IStructuredSelection) { return (IStructuredSelection) selection; } throw new ClassCastException(Messages.AbstractEEFWidgetLifecycleManager_invalidSelectionType); } }