Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 92bff539cf79fff0648260b51543cdb68f36be89 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
/*
 * Copyright (c) 2014, 2016 CEA, Christian W. Damus, 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:
 *   Christian W. Damus (CEA) - Initial API and implementation
 *   Christian W. Damus - bug 485220
 *
 */
package org.eclipse.papyrus.infra.ui.editor;

import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.common.ui.URIEditorInput;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.transaction.util.TransactionUtil;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.papyrus.infra.core.resource.ModelSet;
import org.eclipse.papyrus.infra.core.services.ServiceException;
import org.eclipse.papyrus.infra.tools.util.CoreExecutors;
import org.eclipse.papyrus.infra.tools.util.PlatformHelper;
import org.eclipse.papyrus.infra.ui.Activator;
import org.eclipse.papyrus.infra.ui.editor.reload.IEditorReloadListener;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.ide.IDE;


/**
 * An {@linkplain IAdaptable adapter protocol} for editors that know how to internally
 * reload themselves without disturbing the workbench window's perspective layout.
 */
public interface IReloadableEditor {

	/**
	 * Reloads me in-place in the perspective layout.
	 *
	 * @param triggeringResources
	 *            the resources that have changed in some way, triggering re-load
	 * @param reason
	 *            the reason why the re-load is being requested
	 * @param dirtyPolicy
	 *            how the client would like to handle the case of a dirty editor
	 *
	 * @throws CoreException
	 *             on any failure to unload, reload, or whatever
	 */
	void reloadEditor(Collection<? extends Resource> triggeringResources, ReloadReason reason, DirtyPolicy dirtyPolicy) throws CoreException;

	void addEditorReloadListener(IEditorReloadListener listener);

	void removeEditorReloadListener(IEditorReloadListener listener);

	/**
	 * An enumeration of the reason why some resources that an editor has loaded are triggering a re-load (or close).
	 */
	enum ReloadReason {
		/** Resources have changed in persistent storage. */
		RESOURCES_CHANGED,
		/** Resources have been deleted from persistent storage. */
		RESOURCES_DELETED;

		/**
		 * Queries whether, under ordinary circumstances, the editor should attempt to re-load to pick up changes in its dependent resources.
		 *
		 * @param triggeringResources
		 *            the resources triggering re-load (or close)
		 *
		 * @return whether the editor should re-load
		 */
		public boolean shouldReload(Collection<? extends Resource> triggeringResources) {
			return this != RESOURCES_DELETED;
		}
	}

	/**
	 * An enumeration of policies that clients may request to govern what to do with the editor before re-load (or close) if it should happen to be
	 * dirty. Note that editors are free to honour the requested policy or not, according to their needs.
	 */
	enum DirtyPolicy {
		/**
		 * Save the editor without prompting.
		 */
		SAVE,
		/**
		 * Do not save the editor; just discard pending changes and re-load (or close).
		 */
		DO_NOT_SAVE,
		/**
		 * Do not re-load (or close) the editor; just keep pending changes and deal with conflicts later.
		 */
		IGNORE,
		/**
		 * Prompt the user to inquire whether to save, discard pending changes, or not re-load (or close) at all.
		 * Note that the user prompt must always result in one of the other policies being actually applied.
		 */
		PROMPT_TO_SAVE {

			@Override
			public DirtyPolicy resolve(IEditorPart editor, final Collection<? extends Resource> triggeringResources, final ReloadReason reason) throws CoreException {
				final boolean dirty = editor.isDirty();

				if (!dirty) {
					if (reason.shouldReload(triggeringResources)) {
						// Just re-load it. Simple
						return DO_NOT_SAVE;
					} else if (isPrincipalResourceAffected(editor, triggeringResources)) {
						// Just close it. Also simple
						return DO_NOT_SAVE;
					}
				}

				final String editorName = getEditorName(editor);

				final boolean allReadOnly = allReadOnly(triggeringResources);
				final String promptTitle;
				final String promptIntro;
				final String saveOption;
				final String dontSaveOption;
				final String ignoreOption = "Ignore";

				switch (reason) {
				case RESOURCES_DELETED:
					promptTitle = "Resources Deleted";
					promptIntro = NLS.bind("Some resources used by \"{0}\" have been deleted.", editorName);
					saveOption = "Save and Close";
					dontSaveOption = "Close Editor";
					break;
				default:
					promptTitle = "Resources Changed";
					promptIntro = NLS.bind("Some resources used by \"{0}\" have changed.", editorName);
					saveOption = "Save and Re-open";
					dontSaveOption = "Re-open Editor";
					break;
				}

				Callable<DirtyPolicy> result;

				if (allReadOnly) {
					// Only read-only models have changed. We (most likely) won't save them within this current editor. As they are already loaded, we can just continue.
					result = new Callable<DirtyPolicy>() {

						@Override
						public DirtyPolicy call() {
							Shell parentShell = Display.getCurrent().getActiveShell();

							final String message;
							final String[] options;
							if (dirty) {
								message = promptIntro + " Note: all these resources are loaded in read-only mode and won't be overridden if you choose to save. Unsaved changes will be lost.";
								options = new String[] { saveOption, dontSaveOption, ignoreOption };
							} else {
								message = promptIntro;
								options = new String[] { dontSaveOption, ignoreOption };
							}

							final MessageDialog dialog = new MessageDialog(parentShell, promptTitle, null, message, MessageDialog.WARNING, options, 0) {

								@Override
								protected void setShellStyle(int newShellStyle) {
									super.setShellStyle(newShellStyle | SWT.SHEET);
								}
							};
							final int answer = dialog.open();

							DirtyPolicy result;

							if (answer == SWT.DEFAULT) {
								// User hit Esc or dismissed the dialog with the window manager button. Ignore
								result = IGNORE;
							} else if (dirty) {
								result = values()[answer];
							} else {
								result = values()[answer + 1]; // Account for the missing "Save and Xxx" option
							}

							return result;
						}
					};
				} else {
					// At least one read-write resource has changed. Potential conflicts.
					result = new Callable<DirtyPolicy>() {

						@Override
						public DirtyPolicy call() {
							DirtyPolicy result = IGNORE;

							final Shell parentShell = Display.getCurrent().getActiveShell();
							final String action = reason.shouldReload(triggeringResources) ? "re-open" : "close";
							final String message;

							if (dirty) {
								message = promptIntro + NLS.bind(" Do you wish to {0} the current editor? Unsaved changes will be lost.", action);
							} else {
								message = promptIntro + NLS.bind(" Do you wish to {0} the current editor?", action);
							}

							final String[] options = { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL };
							final MessageDialog dialog = new MessageDialog(parentShell, promptTitle, null, message, MessageDialog.WARNING, options, 0) {

								@Override
								protected void setShellStyle(int newShellStyle) {
									super.setShellStyle(newShellStyle | SWT.SHEET);
								}
							};
							if (dialog.open() == 0) {
								result = DO_NOT_SAVE;
							}

							return result;
						}
					};
				}

				try {
					return CoreExecutors.getUIExecutorService().syncCall(result);
				} catch (ExecutionException e) {
					throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to determine dirty policy for editor re-load.", e));
				} catch (InterruptedException e) {
					throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Interrupted in determining dirty policy for editor re-load.", e));
				}
			}
		};

		/**
		 * Queries the default dirty policy currently in effect. The default-default is {@link #PROMPT_TO_SAVE}.
		 *
		 * @return the default policy
		 */
		public static DirtyPolicy getDefault() {
			return PROMPT_TO_SAVE;
		}

		/**
		 * Resolves me to a specific actionable policy, based on the resources that are triggering re-load (or close) and the reason.
		 *
		 * @param editor
		 *            the editor to be re-loaded
		 * @param triggeringResources
		 *            the resources (possibly an empty collection) that have changed
		 * @param reloadReason
		 *            the reason why re-load (or close) is triggered
		 *
		 * @return the specific policy to implement in re-loading the editor
		 *
		 * @throws CoreException
		 *             on failure to resolve the specific policy
		 */
		public DirtyPolicy resolve(IEditorPart editor, Collection<? extends Resource> triggeringResources, ReloadReason reason) throws CoreException {
			return this;
		}

		String getEditorName(IEditorPart editor) {
			ModelSet modelSet = getModelSet(editor);
			return (modelSet == null) ? editor.getTitle() : modelSet.getURIWithoutExtension().lastSegment();
		}

		private ModelSet getModelSet(IEditorPart editor) {
			ModelSet result = null;

			if (editor instanceof IMultiDiagramEditor) {
				try {
					result = ((IMultiDiagramEditor) editor).getServicesRegistry().getService(ModelSet.class);
				} catch (ServiceException e) {
					// No problem. We have a fall-back
					Activator.log.error(e);
				}
			}

			return result;
		}

		boolean isPrincipalResourceAffected(IEditorPart editor, Collection<? extends Resource> triggeringResources) {
			boolean result = false;

			ModelSet modelSet = getModelSet(editor);
			if (modelSet != null) {
				URI principalURI = modelSet.getURIWithoutExtension();
				for (Resource next : triggeringResources) {
					if (next.getURI().trimFileExtension().equals(principalURI)) {
						result = true;
						break;
					}
				}
			} else {
				URI principalURI = getURI(editor.getEditorInput());
				if (principalURI != null) {
					for (Resource next : triggeringResources) {
						if (next.getURI().equals(principalURI)) {
							result = true;
							break;
						}
					}
				}
			}

			return result;
		}

		private URI getURI(IEditorInput input) {
			URI result = null;

			if (input instanceof URIEditorInput) {
				result = ((URIEditorInput) input).getURI();
			} else if (input instanceof IURIEditorInput) {
				result = URI.createURI(((IURIEditorInput) input).getURI().toString());
			}

			return result;
		}

		protected boolean allReadOnly(Collection<? extends Resource> resources) {
			for (Resource resource : resources) {
				EditingDomain domain = TransactionUtil.getEditingDomain(resource);
				if ((domain == null) || !domain.isReadOnly(resource)) {
					return false;
				}
			}

			return true;
		}
	}

	/**
	 * A convenience adapter for editors that don't actually know how to reload themselves in place.
	 * It simply closes the editor and then opens it again on the original input.
	 */
	class Adapter implements IReloadableEditor {

		private final IEditorPart editor;

		public Adapter(IEditorPart editor) {
			super();

			this.editor = editor;
		}

		public static IReloadableEditor getAdapter(IMultiDiagramEditor editor) {
			return PlatformHelper.getAdapter(editor, IReloadableEditor.class, () -> new Adapter(editor));
		}

		@Override
		public void reloadEditor(Collection<? extends Resource> triggeringResources, ReloadReason reason, DirtyPolicy dirtyPolicy) throws CoreException {
			final IWorkbenchPage page = editor.getSite().getPage();
			final IEditorInput currentInput = editor.getEditorInput();

			final Display display = editor.getSite().getShell().getDisplay();

			final String editorId = editor.getSite().getId();

			final DirtyPolicy action = dirtyPolicy.resolve(editor, triggeringResources, reason);
			final boolean save = action == DirtyPolicy.SAVE;

			if (save && editor.isDirty()) {
				editor.doSave(new NullProgressMonitor());
			}

			if (action != DirtyPolicy.IGNORE) {
				page.closeEditor(editor, save);

				// If resources were deleted, we close and don't re-open
				if (reason.shouldReload(triggeringResources)) {
					display.asyncExec(new Runnable() {

						@Override
						public void run() {
							try {
								IDE.openEditor(page, currentInput, editorId);
							} catch (PartInitException ex) {
								Activator.log.error(ex);
							}
						}
					});
				}
			}
		}

		@Override
		public void addEditorReloadListener(IEditorReloadListener listener) {
			// Don't need to track these listeners because I never properly reload an editor
		}

		@Override
		public void removeEditorReloadListener(IEditorReloadListener listener) {
			// Don't need to track these listeners because I never properly reload an editor
		}
	}
}

Back to the top