diff options
4 files changed, 228 insertions, 47 deletions
diff --git a/plugins/org.eclipse.eef.ide.ui/plugin.properties b/plugins/org.eclipse.eef.ide.ui/plugin.properties index 07bb99c2a..33000c5e8 100644 --- a/plugins/org.eclipse.eef.ide.ui/plugin.properties +++ b/plugins/org.eclipse.eef.ide.ui/plugin.properties @@ -37,3 +37,8 @@ AbstractEEFWidgetLifecycleManager_noDescriptionAvailable=No description availabl AbstractEEFWidgetLifecycleManager_lockedByOther=This widget is locked by another user AbstractEEFWidgetLifecycleManager_lockedByMe=This widget is locked for other users AbstractEEFWidgetLifecycleManager_lockedNoWrite=This widget is locked because of a permission issue + +EEFTextLifecycleManager_conflictDialog_title = Model Changed During Edition +EEFTextLifecycleManager_conflictDialog_message = The model has changed in an incompatible way while you were editing this widget.\n\nNew value from the model: {0}\nEdited value: {1} +EEFTextLifecycleManager_conflictDialog_choiceNewModelValue = Use new value from the model +EEFTextLifecycleManager_conflictDialog_choiceLocalEditedValue = Keep the edited value diff --git a/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/Messages.java b/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/Messages.java index 8a930b27a..5ef270304 100644 --- a/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/Messages.java +++ b/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/Messages.java @@ -82,6 +82,18 @@ public final class Messages { @TranslatableMessage public static String AbstractEEFWidgetLifecycleManager_lockedNoWrite; + @TranslatableMessage + public static String EEFTextLifecycleManager_conflictDialog_title; + + @TranslatableMessage + public static String EEFTextLifecycleManager_conflictDialog_message; + + @TranslatableMessage + public static String EEFTextLifecycleManager_conflictDialog_choiceNewModelValue; + + @TranslatableMessage + public static String EEFTextLifecycleManager_conflictDialog_choiceLocalEditedValue; + // CHECKSTYLE:ON /** diff --git a/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/preferences/EEFPreferences.java b/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/preferences/EEFPreferences.java new file mode 100644 index 000000000..1e01dd0ce --- /dev/null +++ b/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/preferences/EEFPreferences.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 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.internal.preferences; + +import org.eclipse.core.runtime.preferences.DefaultScope; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.eef.ide.ui.internal.EEFIdeUiPlugin; +import org.eclipse.eef.ide.ui.internal.widgets.EEFTextLifecycleManager.ConflictResolutionMode; +import org.osgi.service.prefs.BackingStoreException; + +/** + * Preferences for the EEF UI. + * + * @author pcdavid + */ +public final class EEFPreferences { + /** + * The key for the text conflict resolution mode. + */ + public static final String TEXT_CONFLICT_RESOLUTION_MODE = "TEXT_CONFLICT_RESOLUTION_MODE"; //$NON-NLS-1$ + + /** + * The default value for text conflict resolution mode. + */ + public static final ConflictResolutionMode DEFAULT_TEXT_CONFLICT_RESOLUTION_MODE = ConflictResolutionMode.ASK_USER; + + /** + * The EEF Common preference scope. + */ + private static final IEclipsePreferences PREFERENCES_SCOPE = InstanceScope.INSTANCE.getNode(EEFIdeUiPlugin.PLUGIN_ID); + + /** + * The EEF Common default preference scope. + */ + private static final IEclipsePreferences DEFAULT_PREFERENCES_SCOPE = DefaultScope.INSTANCE.getNode(EEFIdeUiPlugin.PLUGIN_ID); + + /** + * The constructor. + */ + private EEFPreferences() { + // prevent instantiation + } + + /** + * Indicates how text conflicts should be resolved. + * + * @return the conflict resolution mode to be used. + */ + public static ConflictResolutionMode getTextConflictResolutionMode() { + String mode = PREFERENCES_SCOPE.get(TEXT_CONFLICT_RESOLUTION_MODE, null); + if (mode == null) { + // No explicit preference was set, use the configured default (from the default scope), or the hard-coded + // "default default" if none was set. + mode = DEFAULT_PREFERENCES_SCOPE.get(TEXT_CONFLICT_RESOLUTION_MODE, DEFAULT_TEXT_CONFLICT_RESOLUTION_MODE.name()); + } + try { + return ConflictResolutionMode.valueOf(mode); + } catch (@SuppressWarnings("unused") IllegalArgumentException iae) { + return DEFAULT_TEXT_CONFLICT_RESOLUTION_MODE; + } + } + + /** + * Sets the state of the debug mode. + * + * @param mode + * the conflict resolution mode to use. + */ + public static void setTextConflictResolutionMode(ConflictResolutionMode mode) { + if (mode == null) { + throw new IllegalArgumentException(); + } else { + PREFERENCES_SCOPE.put(TEXT_CONFLICT_RESOLUTION_MODE, mode.name()); + save(); + } + } + + /** + * Save the modification of the preference store. + */ + private static void save() { + try { + PREFERENCES_SCOPE.flush(); + } catch (BackingStoreException e) { + EEFIdeUiPlugin.getPlugin().error(e.getMessage(), e); + } + } +} diff --git a/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/widgets/EEFTextLifecycleManager.java b/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/widgets/EEFTextLifecycleManager.java index 1560e0d56..9c5994c58 100644 --- a/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/widgets/EEFTextLifecycleManager.java +++ b/plugins/org.eclipse.eef.ide.ui/src/org/eclipse/eef/ide/ui/internal/widgets/EEFTextLifecycleManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015, 2016 Obeo. + * Copyright (c) 2015, 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 @@ -31,6 +31,8 @@ import org.eclipse.eef.ide.ui.api.widgets.AbstractEEFWidgetLifecycleManager; import org.eclipse.eef.ide.ui.api.widgets.EEFStyleHelper; 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.Messages; +import org.eclipse.eef.ide.ui.internal.preferences.EEFPreferences; import org.eclipse.eef.ide.ui.internal.widgets.styles.EEFColor; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EcoreUtil; @@ -46,9 +48,11 @@ import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; import org.eclipse.ui.forms.widgets.FormToolkit; @@ -58,6 +62,25 @@ import org.eclipse.ui.forms.widgets.FormToolkit; * @author sbegaudeau */ public class EEFTextLifecycleManager extends AbstractEEFWidgetLifecycleManager { + /** + * The different ways an edition conflict can be resolved. Used by the default implementation of + * {@link EEFTextLifecycleManager#resolveEditionConflict(Shell, String, String, String)}. + */ + public static enum ConflictResolutionMode { + /** + * Use the version being edited in the widget, overriding the new version computed from the current model state. + */ + USE_LOCAL_VERSION, + /** + * Use the version computed from the current model state, replacing the text being edited by the user in the + * widget. + */ + USE_MODEL_VERSION, + /** + * Ask the user through a simple dialog which version to keep. + */ + ASK_USER + } /** * This constant is used in order to tell SWT that the text area should be 300px wide even if it is not useful. The @@ -67,12 +90,6 @@ public class EEFTextLifecycleManager extends AbstractEEFWidgetLifecycleManager { private static final int TEXT_AREA_WIDTH_HINT = 300; /** - * Feature flag to enable experimental workaround for the text recovery in case of concurrent refresh. - */ - private static final boolean FLAG_RECOVER_TEXT_INPUT = "true" //$NON-NLS-1$ - .equals(System.getProperty("org.eclipse.eef.experimental.recoverLostTextInput", "false")); //$NON-NLS-1$ //$NON-NLS-2$ - - /** * The description. */ private EEFTextDescription description; @@ -318,15 +335,9 @@ public class EEFTextLifecycleManager extends AbstractEEFWidgetLifecycleManager { this.focusListener = new FocusListener() { @Override public void focusLost(FocusEvent e) { - if (FLAG_RECOVER_TEXT_INPUT) { - if (!EEFTextLifecycleManager.this.lockedByOtherInProgress.get() && !EEFTextLifecycleManager.this.container.isRenderingInProgress() - && EEFTextLifecycleManager.this.isDirty) { - EEFTextLifecycleManager.this.updateValue(false); - } - } else { - if (!EEFTextLifecycleManager.this.container.isRenderingInProgress() && EEFTextLifecycleManager.this.isDirty) { - EEFTextLifecycleManager.this.updateValue(false); - } + if (!EEFTextLifecycleManager.this.lockedByOtherInProgress.get() && !EEFTextLifecycleManager.this.container.isRenderingInProgress() + && EEFTextLifecycleManager.this.isDirty) { + EEFTextLifecycleManager.this.updateValue(false); } } @@ -389,32 +400,14 @@ public class EEFTextLifecycleManager extends AbstractEEFWidgetLifecycleManager { if (m != null) { boolean resettingToPreviousReferenceValue = equals(newDisplayText[0], m.referenceValue); boolean userHasUncommitedInput = !equals(newDisplayText[0], m.userInput); - if (FLAG_RECOVER_TEXT_INPUT && m.appliesTo(EEFTextLifecycleManager.this) && userHasUncommitedInput) { + if (m.appliesTo(EEFTextLifecycleManager.this) && userHasUncommitedInput) { if (resettingToPreviousReferenceValue) { // Custom user input overrides resetting the same previous referenceValue. newDisplayText[0] = m.userInput; - } else { - // The new model state produces a different value than the one the user saw when he - // started - // editing. - String msg = "The model has changed in an incompatible way while you were editing this widget.\nNew value from the model: {0}\nEdited version: {1}"; //$NON-NLS-1$ - String[] choices = { "Use new value from the model", "Keep the edited version" }; //$NON-NLS-1$ //$NON-NLS-2$ - // Can not use MessageDialog.open(kind, parent, title, message, style, - // dialogButtonLabels) - // with Neon... - MessageDialog dialog = new MessageDialog(EEFTextLifecycleManager.this.text.getShell(), "Model Changed During Edition", //$NON-NLS-1$ - null, MessageFormat.format(msg, newDisplayText[0], m.userInput), MessageDialog.QUESTION, 0, choices); - switch (dialog.open()) { - case 0: - // Nothing to do, newDisplayText[0] is initialized with the value from the model. - break; - case 1: - // Override with user input. - newDisplayText[0] = m.userInput; - break; - default: - throw new IllegalStateException(); - } + } else if (!equals(m.userInput, newDisplayText[0])) { + // Conflict must be resolved somehow. + newDisplayText[0] = resolveEditionConflict(EEFTextLifecycleManager.this.text.getShell(), m.referenceValue, m.userInput, + newDisplayText[0]); } } Memento.remove(text); @@ -423,6 +416,84 @@ public class EEFTextLifecycleManager extends AbstractEEFWidgetLifecycleManager { } /** + * Handle conflicts between un-commited changes in the widget and concurrent changes in the model that produce a + * different value than the original one seen by the user. + * + * @param shell + * the shell to use if user interaction is needed. + * @param originalValue + * the original, common value, before the user started editing and before the concurrent model change + * produced a new text. + * @param localEditedVersion + * the value as edited by the user, and seen in the UI. + * @param newValueFromModel + * the new value produced from the new model state. + * @return the new value to use in the text field. + */ + protected String resolveEditionConflict(Shell shell, String originalValue, String localEditedVersion, final String newValueFromModel) { + String result; + switch (EEFPreferences.getTextConflictResolutionMode()) { + case USE_LOCAL_VERSION: + result = localEditedVersion; + break; + case USE_MODEL_VERSION: + result = newValueFromModel; + break; + case ASK_USER: + result = askUserToResolveConflict(shell, originalValue, localEditedVersion, newValueFromModel); + break; + default: + throw new IllegalStateException(); + } + return result; + } + + /** + * Open a simple dialog to inform the user of a conflict and ask him which version to keep. + * + * @param shell + * the shell to use if user interaction is needed. + * @param originalValue + * the original, common value, before the user started editing and before the concurrent model change + * produced a new text. + * @param localEditedVersion + * the value as edited by the user, and seen in the UI. + * @param newValueFromModel + * the new value produced from the new model state. + * @return the value chosen by the user. + */ + protected String askUserToResolveConflict(final Shell shell, String originalValue, final String localEditedVersion, + final String newValueFromModel) { + final String[] result = { localEditedVersion }; + // @formatter:off + final String[] choices = { + Messages.EEFTextLifecycleManager_conflictDialog_choiceNewModelValue, + Messages.EEFTextLifecycleManager_conflictDialog_choiceLocalEditedValue, + }; + // @formatter:on + shell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + Image img = shell.getDisplay().getSystemImage(SWT.ICON_QUESTION); + MessageDialog dialog = new MessageDialog(shell, Messages.EEFTextLifecycleManager_conflictDialog_title, img, + MessageFormat.format(Messages.EEFTextLifecycleManager_conflictDialog_message, newValueFromModel, localEditedVersion), + MessageDialog.QUESTION, 0, choices); + switch (dialog.open()) { + case 0: + result[0] = newValueFromModel; + break; + case 1: + result[0] = localEditedVersion; + break; + default: + throw new IllegalStateException(); + } + } + }); + return result[0]; + } + + /** * Tests two objects for equality, considering <code>null</code>. * * @param o1 @@ -516,15 +587,11 @@ public class EEFTextLifecycleManager extends AbstractEEFWidgetLifecycleManager { @Override protected void lockedByOther() { - if (FLAG_RECOVER_TEXT_INPUT) { - this.lockedByOtherInProgress.set(true); - try { - super.lockedByOther(); - } finally { - this.lockedByOtherInProgress.set(false); - } - } else { + this.lockedByOtherInProgress.set(true); + try { super.lockedByOther(); + } finally { + this.lockedByOtherInProgress.set(false); } } |