[519033] Do not refresh the SMV viewer after every change notification
[EMFCompare2.git] / plugins / org.eclipse.emf.compare.ide.ui / src / org / eclipse / emf / compare / ide / ui / internal / structuremergeviewer / EMFCompareStructureMergeViewerContentProvider.java
blobe8947e8a278829fd49afa0db51765bfcc89896e6
1 /*******************************************************************************
2 * Copyright (c) 2012, 2016 Obeo and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 * Obeo - initial API and implementation
10 * Michael Borkowski - public CallbackType visibility for testing
11 * Stefan Dirix - bug 473985
12 *******************************************************************************/
13 package org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer;
15 import static com.google.common.base.Predicates.instanceOf;
16 import static com.google.common.collect.Iterables.toArray;
17 import static com.google.common.collect.Iterables.transform;
19 import com.google.common.base.Function;
20 import com.google.common.base.Optional;
21 import com.google.common.base.Predicates;
22 import com.google.common.collect.Iterables;
23 import com.google.common.collect.Iterators;
24 import com.google.common.collect.Lists;
26 import java.util.Arrays;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.concurrent.CopyOnWriteArrayList;
30 import java.util.concurrent.locks.ReentrantLock;
32 import org.eclipse.compare.structuremergeviewer.ICompareInput;
33 import org.eclipse.core.runtime.IProgressMonitor;
34 import org.eclipse.core.runtime.jobs.IJobChangeEvent;
35 import org.eclipse.core.runtime.jobs.IJobChangeListener;
36 import org.eclipse.core.runtime.jobs.ISchedulingRule;
37 import org.eclipse.core.runtime.jobs.Job;
38 import org.eclipse.emf.common.notify.Adapter;
39 import org.eclipse.emf.common.notify.AdapterFactory;
40 import org.eclipse.emf.common.notify.Notification;
41 import org.eclipse.emf.common.notify.Notifier;
42 import org.eclipse.emf.compare.Comparison;
43 import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIMessages;
44 import org.eclipse.emf.compare.ide.ui.internal.treecontentmanager.EMFCompareDeferredTreeContentManager;
45 import org.eclipse.emf.compare.ide.ui.internal.treecontentmanager.EMFCompareDeferredTreeContentManagerUtil;
46 import org.eclipse.emf.compare.rcp.ui.internal.util.SWTUtil;
47 import org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroupProvider2;
48 import org.eclipse.emf.edit.provider.IViewerNotification;
49 import org.eclipse.emf.edit.provider.ViewerNotification;
50 import org.eclipse.emf.edit.tree.TreeNode;
51 import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
52 import org.eclipse.emf.edit.ui.provider.NotifyChangedToViewerRefresh;
53 import org.eclipse.jface.resource.ImageDescriptor;
54 import org.eclipse.jface.viewers.AbstractTreeViewer;
55 import org.eclipse.jface.viewers.IContentProvider;
56 import org.eclipse.ui.progress.DeferredTreeContentManager;
57 import org.eclipse.ui.progress.IDeferredWorkbenchAdapter;
58 import org.eclipse.ui.progress.IElementCollector;
59 import org.eclipse.ui.progress.PendingUpdateAdapter;
61 /**
62 * Specialized AdapterFactoryContentProvider for the emf compare structure merge viewer.
63 * <p>
64 * <i>This class is not intended to be used outside of its package. It has been set to public for testing
65 * purpose only.</i>
66 * </p>
68 * @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a>
70 public class EMFCompareStructureMergeViewerContentProvider extends AdapterFactoryContentProvider implements IJobChangeListener {
72 /**
73 * Class to listen the state of the content provider.
75 * @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
77 static class FetchListener {
79 /**
80 * This method is called when the content provider starts fetching new elements in an external Job.
82 public void startFetching() {
86 /**
87 * This method is called when the content provider has finished fetching elements and has removed the
88 * {@link PendingUpdateAdapter} from the tree.
90 public void doneFetching() {
95 /**
96 * Callback holder used to defer the run of a callback in a specific thread.
98 * @see {@link #callback}
99 * @see EMFCompareStructureMergeViewerContentProvider#runWhenReady(CallbackType, Runnable)
101 private static class CallbackHolder {
103 private final Runnable callback;
105 private final CallbackType callbackType;
107 public CallbackHolder(Runnable callback, CallbackType callbackType) {
108 super();
109 this.callback = callback;
110 this.callbackType = callbackType;
113 private Runnable getCallback() {
114 return callback;
117 private CallbackType getType() {
118 return callbackType;
124 * {@link IDeferredWorkbenchAdapter} using this content provider to fetch children.
126 * @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
128 private static class EMFCompareStructureMergeViewerContentProviderDeferredAdapter implements IDeferredWorkbenchAdapter {
130 private final EMFCompareStructureMergeViewerContentProvider contentProvider;
132 public EMFCompareStructureMergeViewerContentProviderDeferredAdapter(
133 EMFCompareStructureMergeViewerContentProvider contentProvider) {
134 super();
135 this.contentProvider = contentProvider;
139 * {@inheritDoc}
141 * @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getChildren(Object)}
143 public Object[] getChildren(Object o) {
144 // Not used
145 return null;
149 * {@inheritDoc}
151 * @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getImageDescriptor(Object)}
153 public ImageDescriptor getImageDescriptor(Object object) {
154 // Not used
155 return null;
159 * {@inheritDoc}
161 * @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getLabel(Object)}
163 public String getLabel(Object o) {
164 return EMFCompareIDEUIMessages.getString(
165 "EMFCompareStructureMergeViewerContentProvider.deferredWorkbenchAdapter.label"); //$NON-NLS-1$
169 * {@inheritDoc}
171 * @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getParent(Object)}
173 public Object getParent(Object o) {
174 // Not used
175 return null;
179 * {@inheritDoc}
181 * @see {IDeferredWorkbenchAdapter
182 * {@link IDeferredWorkbenchAdapter#fetchDeferredChildren(Object object, IElementCollector collector, IProgressMonitor monitor)}
184 public void fetchDeferredChildren(Object object, IElementCollector collector,
185 IProgressMonitor monitor) {
186 if (!monitor.isCanceled()) {
187 if (object instanceof CompareInputAdapter) {
188 Notifier target = ((Adapter)object).getTarget();
189 Object[] children = contentProvider.getChildren(target);
190 collector.add(children, monitor);
197 * {@inheritDoc}
199 * @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#isContainer()}
201 public boolean isContainer() {
202 // Not used
203 return true;
207 * {@inheritDoc}
209 * @see {IDeferredWorkbenchAdapter{@link #getRule(Object)}
211 public ISchedulingRule getRule(Object object) {
212 // Not used
213 return null;
218 /** {@link DeferredTreeContentManager} use to fetch groups in a external {@link Job}. */
219 private final EMFCompareDeferredTreeContentManager contentManagerAdapter;
221 /** Holds true if this content provider is currently fetching children. */
222 private boolean isFetchingGroup;
224 /** Will protect R/W of {@link #pending} and {@link #isFetchingGroup}. */
225 private final ReentrantLock lock;
227 /** Object listening the status of this object. */
228 private final List<FetchListener> listeners;
230 /** List of current callbacks. Callbacks are only run once. */
231 private List<CallbackHolder> callbacks;
233 /** Pending object displayed in the tree. */
234 private Object[] pending;
237 * Constructs the content provider with the appropriate adapter factory.
239 * @param adapterFactory
240 * The adapter factory used to construct the content provider.
242 public EMFCompareStructureMergeViewerContentProvider(AdapterFactory adapterFactory,
243 AbstractTreeViewer viewer) {
244 super(adapterFactory);
245 contentManagerAdapter = EMFCompareDeferredTreeContentManagerUtil
246 .createEMFDeferredTreeContentManager(viewer);
247 contentManagerAdapter.addUpdateCompleteListener(this);
248 lock = new ReentrantLock();
249 listeners = new CopyOnWriteArrayList<EMFCompareStructureMergeViewerContentProvider.FetchListener>();
250 callbacks = new CopyOnWriteArrayList<EMFCompareStructureMergeViewerContentProvider.CallbackHolder>();
255 * {@inheritDoc}
257 * @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#getParent(Object object)
259 @Override
260 public Object getParent(Object element) {
261 final Object ret;
262 if (element instanceof CompareInputAdapter) {
263 Object parentNode = super.getParent(((Adapter)element).getTarget());
264 if (parentNode instanceof TreeNode) {
265 final Optional<Adapter> cia = Iterators.tryFind(((TreeNode)parentNode).eAdapters().iterator(),
266 instanceOf(CompareInputAdapter.class));
267 if (cia.isPresent()) {
268 ret = cia.get();
269 } else {
270 ret = parentNode;
272 } else {
273 ret = parentNode;
275 } else if (element instanceof ICompareInput) {
276 ret = null;
277 } else {
278 ret = super.getParent(element);
280 return ret;
284 * Enum used for better readability of the method
285 * {@link EMFCompareStructureMergeViewerContentProvider#runWhenReady(CallbackType, Runnable)}.
287 * @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
289 // public for testing
290 public static enum CallbackType {
291 /** Run the runnable in the UI thread synchronously. */
292 IN_UI_SYNC,
293 /** Run the runnable in the UI thread asynchronously. */
294 IN_UI_ASYNC,
295 /** Run the runnable in the current thread. */
296 IN_CURRENT_THREAD
300 * Run the given runnable in the specified thread when then content provider is ready. It can be run
301 * directly if the content provider is not fecthing or during a callback when the content provider is done
302 * fetching.
304 * @param type
305 * of thread to run the {@link Runnable} inside.
306 * @param runnable
307 * to run
309 public void runWhenReady(CallbackType type, final Runnable runnable) {
310 // Prevents adding a callback if another thread set this content provider as not fetching.
311 lock.lock();
312 try {
313 if (isFetchingGroup) {
314 callbacks.add(new CallbackHolder(runnable, type));
315 } else {
316 run(runnable, type);
318 } finally {
319 lock.unlock();
324 * Runs a callback in the related thread.
326 * @param callback
327 * to run.
328 * @param type
329 * of thread.
331 private void run(Runnable callback, CallbackType type) {
332 switch (type) {
333 case IN_UI_SYNC:
334 SWTUtil.safeSyncExec(callback);
335 break;
336 case IN_UI_ASYNC:
337 SWTUtil.safeAsyncExec(callback);
338 break;
339 default:
340 callback.run();
341 break;
346 * Adds a listener to this content provider.
348 * @param listener
349 * to add
350 * @return
352 public boolean addFetchingListener(FetchListener listener) {
353 return listeners.add(listener);
357 * Removes a listener to this content provider.
359 * @param listener
360 * to remove
361 * @return
363 public boolean removeFetchingListener(FetchListener listener) {
364 return listeners.remove(listener);
368 * {@inheritDoc}
370 * @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#hasChildren(Object object)
372 @Override
373 public final boolean hasChildren(Object element) {
374 final boolean ret;
375 if (element instanceof CompareInputAdapter) {
376 ret = super.hasChildren(((Adapter)element).getTarget());
377 } else if (element instanceof ICompareInput) {
378 ret = false;
379 } else {
380 ret = super.hasChildren(element);
382 return ret;
386 * {@inheritDoc}
388 * @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#getChildren(java.lang.Object)
390 @Override
391 public final Object[] getChildren(Object element) {
392 Object[] children;
393 if (element instanceof CompareInputAdapter) {
394 children = getCompareInputAdapterChildren((CompareInputAdapter)element);
395 } else if (element instanceof ICompareInput) {
396 children = new Object[] {};
397 } else {
398 children = super.getChildren(element);
401 final Object[] compareInputChildren;
402 // Avoid NPE.
403 if (children == null) {
404 children = new Object[] {};
406 // Do not adapt if it's a pending updater
407 if (!Iterables.all(Arrays.asList(children), Predicates.instanceOf(PendingUpdateAdapter.class))) {
408 Iterable<?> compareInputs = adapt(children, getAdapterFactory(), ICompareInput.class);
409 compareInputChildren = toArray(compareInputs, Object.class);
410 } else {
411 compareInputChildren = children;
413 return compareInputChildren;
417 * Returns a {@link PendingUpdateAdapter} while a Job is fetching the children for this object or the
418 * children if they have already been fetched.
419 * <p>
420 * When the job is finished it will autamically replace the {@link PendingUpdateAdapter} by the fetched
421 * children. The fetched children will be stored under the TreeItem holding the input object or at the
422 * root of the tree if the input object match the input of the tree viewer.
423 * </p>
425 * @param compareInputAdapter
426 * @return
428 private Object[] getCompareInputAdapterChildren(CompareInputAdapter compareInputAdapter) {
429 Notifier target = compareInputAdapter.getTarget();
430 if (target instanceof TreeNode) {
431 TreeNode treeNode = (TreeNode)target;
432 if (treeNode.getData() instanceof Comparison) {
433 IDifferenceGroupProvider2 groupProvider2 = getGroupProvider2(treeNode);
434 // Handles the first initialisation of the groups.
435 lock.lock();
436 try {
437 if (groupProvider2 != null && !groupProvider2.groupsAreBuilt()) {
438 return deferReturnChildren(compareInputAdapter);
440 } finally {
441 lock.unlock();
445 return super.getChildren(compareInputAdapter.getTarget());
448 private Object[] deferReturnChildren(CompareInputAdapter compareInputAdapter) {
449 if (!isFetchingGroup) {
450 isFetchingGroup = true;
452 * Notifies listeners that the content provider starts fetching here and not in
453 * EMFCompareStructureMergeViewerContentProvider#aboutToRun() since it is only notified on the
454 * "clear pending updater" job events and not on the "fetching children" job events.
455 * @see org.eclipse.ui.progress.DeferredTreeContentManager.runClearPlaceholderJob(
456 * PendingUpdateAdapter)
458 for (FetchListener callback : listeners) {
459 callback.startFetching();
461 compareInputAdapter.setDeferredAdapter(
462 new EMFCompareStructureMergeViewerContentProviderDeferredAdapter(this));
463 pending = contentManagerAdapter.getChildren(compareInputAdapter);
465 return pending;
468 private IDifferenceGroupProvider2 getGroupProvider2(TreeNode treeNode) {
469 IDifferenceGroupProvider2 result = null;
470 Optional<Adapter> searchResult = Iterables.tryFind(treeNode.eAdapters(),
471 Predicates.instanceOf(IDifferenceGroupProvider2.class));
472 if (searchResult.isPresent()) {
473 return (IDifferenceGroupProvider2)searchResult.get();
475 return result;
479 * {@inheritDoc}
481 * @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#getElements(Object object)
483 @Override
484 public Object[] getElements(Object element) {
485 return getChildren(element);
489 * {@inheritDoc}
491 * @see IContentProvider#dispose()
493 @Override
494 public void dispose() {
495 super.dispose();
496 contentManagerAdapter.removeUpdateCompleteListener(this);
497 listeners.clear();
501 * This implementation specializes the EMF implementation to ensure that if more than 30 notifications
502 * arrive, the viewer is simply refreshed rather than processing each update separately. This is
503 * especially important for when undo is invoked in editor and there are many things to be undone.
504 * <p/>
505 * {@inheritDoc}
507 * @see IContentProvider#dispose()
509 @Override
510 public void notifyChanged(Notification notification) {
511 if (viewer != null && viewer.getControl() != null && !viewer.getControl().isDisposed()) {
512 if (notification instanceof IViewerNotification) {
513 if (viewerRefresh == null) {
514 viewerRefresh = new ViewerRefresh(viewer) {
515 int count = 0;
517 @Override
518 public synchronized boolean addNotification(IViewerNotification notification) {
519 if (super.addNotification(notification)) {
520 count = 0;
521 return true;
524 // When there are more than 30 notifications, it's probably cheaper to simply
525 // refresh the overall view.
526 if (count > 30) {
527 super.addNotification(new ViewerNotification(notification, null, true, true));
530 ++count;
531 return false;
536 if (viewerRefresh.addNotification((IViewerNotification)notification)) {
537 viewer.getControl().getDisplay().asyncExec(viewerRefresh);
539 } else {
540 NotifyChangedToViewerRefresh.handleNotifyChanged(viewer, notification.getNotifier(),
541 notification.getEventType(), notification.getFeature(), notification.getOldValue(),
542 notification.getNewValue(), notification.getPosition());
548 * Adapts each elements of the the given <code>iterable</code> to the given <code>type</code> by using the
549 * given <code>adapterFactory</code>.
551 * @param <T>
552 * the type of returned elements.
553 * @param iterable
554 * the iterable to transform.
555 * @param adapterFactory
556 * the {@link AdapterFactory} used to adapt elements.
557 * @param type
558 * the target type of adapted elements.
559 * @return an iterable with element of type <code>type</code>.
561 private static Iterable<?> adapt(Iterable<?> iterable, final AdapterFactory adapterFactory,
562 final Class<?> type) {
563 Function<Object, Object> adaptFunction = new Function<Object, Object>() {
564 public Object apply(Object input) {
565 Object ret = adapterFactory.adapt(input, type);
566 if (ret == null) {
567 return input;
569 return ret;
572 return transform(iterable, adaptFunction);
576 * Adapts each elements of the the given <code>array</code> to the given <code>type</code> by using the
577 * given <code>adapterFactory</code>.
579 * @param <T>
580 * the type of returned elements.
581 * @param iterable
582 * the array to transform.
583 * @param adapterFactory
584 * the {@link AdapterFactory} used to adapt elements.
585 * @param type
586 * the target type of adapted elements
587 * @return an iterable with element of type <code>type</code>.
589 private static Iterable<?> adapt(Object[] iterable, final AdapterFactory adapterFactory,
590 final Class<?> type) {
591 return adapt(Arrays.asList(iterable), adapterFactory, type);
595 * {@inheritDoc}
597 * @see IJobChangeListener{@link #aboutToRun(IJobChangeEvent)
599 public void aboutToRun(IJobChangeEvent event) {
601 * Nothing to do here since it has already been done in
602 * EMFCompareStructureMergeViewerContentProvider#getCompareInputAdapterChildren(CompareInputAdapter
603 * compareInputAdapter)
608 * {@inheritDoc}
610 * @see IJobChangeListener#awake(IJobChangeEvent)
612 public void awake(IJobChangeEvent event) {
613 // Nothing to do
617 * {@inheritDoc}
619 * @see IJobChangeListener#done(IJobChangeEvent)
621 public void done(IJobChangeEvent event) {
622 if (event.getResult().isOK()) {
623 // Prevents running callbacks while another thread add a new callback or another thread launch a
624 // fetching job.
625 lock.lock();
626 try {
627 if (isFetchingGroup) {
628 isFetchingGroup = false;
629 pending = null;
630 for (FetchListener listener : listeners) {
631 listener.doneFetching();
632 // If the listener starts to fetch again then stop notifying listeners and wait for
633 // the content provider to be ready before re-starting.
634 if (isFetchingGroup) {
635 return;
639 final Iterator<CallbackHolder> callbacksIterator = callbacks.iterator();
641 while (callbacksIterator.hasNext()) {
642 CallbackHolder callbackHolder = callbacksIterator.next();
643 run(callbackHolder.getCallback(), callbackHolder.getType());
644 // If the callback has started to fetch again the stop running callbacks and wait for
645 // the content provider to be ready.
646 if (isFetchingGroup) {
647 List<CallbackHolder> remainingCallBack = Lists.newArrayList(callbacksIterator);
648 callbacks = new CopyOnWriteArrayList<EMFCompareStructureMergeViewerContentProvider.CallbackHolder>(
649 remainingCallBack);
650 return;
654 callbacks.clear();
656 } finally {
657 lock.unlock();
663 * {@inheritDoc}
665 * @see IJobChangeListener#running(IJobChangeEvent)
667 public void running(IJobChangeEvent event) {
668 // Nothing to do
672 * {@inheritDoc}
674 * @see IJobChangeListener#scheduled(IJobChangeEvent)
676 public void scheduled(IJobChangeEvent event) {
677 // Nothing to do
681 * {@inheritDoc}
683 * @see IJobChangeListener#sleeping(IJobChangeEvent)
685 public void sleeping(IJobChangeEvent event) {
686 // Nothing to do