Bug 570852: [UI-Viewers] Add BasicCustomViewer

Change-Id: I03fc7b2cbc489183ae974dcd13b1a462343b2a1d
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/ScheduledDisplayRunnable.java b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/ScheduledDisplayRunnable.java
new file mode 100644
index 0000000..676725e
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/ScheduledDisplayRunnable.java
@@ -0,0 +1,192 @@
+/*=============================================================================#
+ # Copyright (c) 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ecommons.ui.swt;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.widgets.Display;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+
+@NonNullByDefault
+public abstract class ScheduledDisplayRunnable implements Runnable {
+	
+	
+	public static ScheduledDisplayRunnable adapt(final Runnable runnable, final Display display) {
+		return new ScheduledDisplayRunnable(display) {
+			@Override
+			protected void execute() {
+				runnable.run();
+			}
+			
+		};
+	}
+	
+	
+	private static final int IDLE= 0;
+	private static final int STOPPED= -1;
+	
+	private static final byte NOT_SCHEDULED= 0;
+	private static final byte DIRECT_SCHEDULED= 1;
+	private static final byte TIMED_SCHEDULED= 2;
+	
+	private static final int PREV_NANOS_TOL= 2_000;
+	private static final int SCHEDULE_NANOS_TOL= 2_000_000;
+	
+	
+	private final Display display;
+	
+	/** {@link #STOPPED}, {@link #IDLE}, > 0: scheduledPower */
+	private int state= IDLE;
+	/** {@link #NOT_SCHEDULED}, {@link #DIRECT_SCHEDULED}, {@link #TIMED_SCHEDULED} */
+	private byte scheduled= NOT_SCHEDULED;
+	private long scheduledTime;
+	
+	
+	public ScheduledDisplayRunnable(final Display display) {
+		this.display= nonNullAssert(display);
+	}
+	
+	
+	public void scheduleFor(final long nanoTime, final int power) {
+		if (power <= 0) {
+			throw new IllegalArgumentException();
+		}
+		synchronized (this) {
+			try {
+				if (this.state == STOPPED) {
+					return;
+				}
+				final long prevDiff= (this.state > IDLE) ? nanoTime - this.scheduledTime : -PREV_NANOS_TOL;
+				if (power < this.state
+						|| (power == this.state && prevDiff <= 0) ) {
+					return;
+				}
+				this.state= power;
+				this.scheduledTime= nanoTime;
+				if (this.scheduled == DIRECT_SCHEDULED
+						|| (this.scheduled == TIMED_SCHEDULED && prevDiff > -PREV_NANOS_TOL) ) {
+					return;
+				}
+				
+				final long scheduleDiff= nanoTime - System.nanoTime();
+				if (scheduleDiff > SCHEDULE_NANOS_TOL && Thread.currentThread() == this.display.getThread()) {
+					this.scheduled= TIMED_SCHEDULED;
+					this.display.timerExec((int)(scheduleDiff / 1_000_000), this);
+				}
+				else {
+					this.scheduled= DIRECT_SCHEDULED;
+					this.display.asyncExec(this);
+				}
+			}
+			catch (final RuntimeException e) {
+				handleScheduleException(e);
+			}
+		}
+	}
+	
+	public void scheduleFor(final long nanoTime) {
+		scheduleFor(nanoTime, 1);
+	}
+	
+	public void scheduleForNow(final int power) {
+		scheduleFor(System.nanoTime(), power);
+	}
+	
+	public void scheduleWithDelay(final long delay, final TimeUnit unit, final int power) {
+		if (delay < 0) {
+			throw new IllegalArgumentException();
+		}
+		scheduleFor(System.nanoTime() + unit.toNanos(delay), power);
+	}
+	
+	public void scheduleWithDelay(final long delay, final TimeUnit unit) {
+		if (delay < 0) {
+			throw new IllegalArgumentException();
+		}
+		scheduleFor(System.nanoTime() + unit.toNanos(delay), 1);
+	}
+	
+	public void runNow() {
+		final boolean inRealm;
+		try {
+			inRealm= (Thread.currentThread() == this.display.getThread());
+		}
+		catch (final RuntimeException e) {
+			handleScheduleException(e);
+			return;
+		}
+		if (inRealm) {
+			synchronized (this) {
+				if (this.state == STOPPED) {
+					return;
+				}
+				this.state= IDLE;
+			}
+			execute();
+		}
+		else {
+			scheduleFor(System.nanoTime(), Integer.MAX_VALUE);
+		}
+	}
+	
+	public synchronized void cancel() {
+		if (this.state == STOPPED) {
+			return;
+		}
+		this.state= IDLE;
+	}
+	
+	public synchronized void stop() {
+		this.state= STOPPED;
+	}
+	
+	@Override
+	public final void run() {
+		synchronized (this) {
+			this.scheduled= NOT_SCHEDULED;
+			if (this.state <= IDLE) {
+				return;
+			}
+			final long scheduleDiff= this.scheduledTime - System.nanoTime();
+			if (scheduleDiff > SCHEDULE_NANOS_TOL) {
+				this.scheduled= TIMED_SCHEDULED;
+				this.display.timerExec((int)(scheduleDiff / 1_000_000L), this);
+				return;
+			}
+			this.state= IDLE;
+		}
+		execute();
+	}
+	
+	
+	protected abstract void execute();
+	
+	
+	protected void handleScheduleException(final RuntimeException e) throws RuntimeException {
+		if (e instanceof SWTException && ((SWTException)e).code == SWT.ERROR_DEVICE_DISPOSED) {
+			stop();
+		}
+		else {
+			throw e;
+		}
+	}
+	
+}
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/viewers/BasicCustomViewer.java b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/viewers/BasicCustomViewer.java
new file mode 100644
index 0000000..4c69b33
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/viewers/BasicCustomViewer.java
@@ -0,0 +1,277 @@
+/*=============================================================================#
+ # Copyright (c) 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ecommons.ui.viewers;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullLateInit;
+
+import org.eclipse.jface.util.OpenStrategy;
+import org.eclipse.jface.util.SafeRunnable;
+import org.eclipse.jface.viewers.IPostSelectionProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Display;
+
+import org.eclipse.statet.jcommons.collections.CopyOnWriteIdentityListSet;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ecommons.ui.swt.ScheduledDisplayRunnable;
+
+
+@NonNullByDefault
+public abstract class BasicCustomViewer<TSelection extends ISelection> extends Viewer
+		implements IPostSelectionProvider {
+	
+	
+	public static enum UpdateType {
+		DIRECT,
+		DEFAULT,
+		DEFAULT_DELAYED_POST;
+	}
+	
+	
+	private final Display display;
+	
+	private TSelection selection;
+	
+	private final CopyOnWriteIdentityListSet<ISelectionChangedListener> selectionListeners=
+			new CopyOnWriteIdentityListSet<>();
+	
+	private final CopyOnWriteIdentityListSet<ISelectionChangedListener> postSelectionListeners=
+			new CopyOnWriteIdentityListSet<>();
+	
+	private long updateSelectionDefaultAsyncDelayNanos;
+	private SelectionRunnable updateSelectionRunnable= nonNullLateInit();
+	private PostSelectionRunnable postSelectionRunnable= nonNullLateInit();
+	
+	
+	public BasicCustomViewer(final Display display, final TSelection initalSelection) {
+		this.display= display;
+		
+		this.selection= initalSelection;
+	}
+	
+	
+	protected boolean isDisposed() {
+		return getControl().isDisposed();
+	}
+	
+	
+	@Override
+	public TSelection getSelection() {
+		return this.selection;
+	}
+	
+	@Override
+	public void setSelection(final ISelection selection, final boolean reveal) {
+		final long t= System.nanoTime();
+		this.selection= (TSelection)selection;
+		
+		handleSelectionChanged(t, UpdateType.DIRECT);
+	}
+	
+	
+//-- AbstractPostSelectionProvider ----
+	
+	@Override
+	public void addSelectionChangedListener(final ISelectionChangedListener listener) {
+		this.selectionListeners.add(listener);
+	}
+	
+	@Override
+	public void removeSelectionChangedListener(final ISelectionChangedListener listener) {
+		this.selectionListeners.remove(listener);
+	}
+	
+	
+	@Override
+	public void addPostSelectionChangedListener(final ISelectionChangedListener listener) {
+		this.postSelectionListeners.add(listener);
+	}
+	
+	@Override
+	public void removePostSelectionChangedListener(final ISelectionChangedListener listener) {
+		this.postSelectionListeners.remove(listener);
+	}
+	
+	
+	@Override
+	protected void fireSelectionChanged(final SelectionChangedEvent event) {
+		for (final ISelectionChangedListener listener : this.selectionListeners) {
+			SafeRunnable.run(() -> listener.selectionChanged(event));
+		}
+	}
+	
+	protected void firePostSelectionChanged(final SelectionChangedEvent event) {
+		for (final ISelectionChangedListener listener : this.postSelectionListeners) {
+			SafeRunnable.run(() -> listener.selectionChanged(event));
+		}
+	}
+	
+	
+//-- Selection Controller ----
+	
+	private class SelectionRunnable extends ScheduledDisplayRunnable {
+		
+		private final Object updateExchLock= new Object();
+		
+		private int updateFlags;
+		
+		private @Nullable UpdateType updateRequest= null;
+		private long updateRequestStamp;
+		
+		
+		public SelectionRunnable() {
+			super(BasicCustomViewer.this.display);
+		}
+		
+		public void schedule(final int flags, final long stamp,
+				final UpdateType updateType, final boolean async) {
+			final long scheduleTime;
+			synchronized (this.updateExchLock) {
+				this.updateFlags|= flags;
+				
+				if (updateType == UpdateType.DIRECT) {
+					this.updateRequest= UpdateType.DIRECT;
+					this.updateRequestStamp= stamp;
+					scheduleTime= stamp;
+				}
+				else if (this.updateRequest != UpdateType.DEFAULT) {
+					this.updateRequest= updateType;
+					this.updateRequestStamp= stamp;
+					scheduleTime= (async) ?
+							stamp + BasicCustomViewer.this.updateSelectionDefaultAsyncDelayNanos :
+							stamp;
+				}
+				else {
+					return;
+				}
+			}
+			if (async) {
+				scheduleFor(scheduleTime);
+			}
+			else {
+				runNow();
+			}
+		}
+		
+		@Override
+		protected void execute() {
+			final UpdateType type;
+			final long stamp;
+			final int flags;
+			synchronized (this.updateExchLock) {
+				flags= this.updateFlags;
+				this.updateFlags= 0;
+				
+				type= this.updateRequest;
+				this.updateRequest= null;
+				stamp= this.updateRequestStamp;
+			}
+			if (type == null || isDisposed()) {
+				return;
+			}
+			
+			updateSelection(type, stamp, flags);
+		}
+		
+	}
+	
+	private class PostSelectionRunnable extends ScheduledDisplayRunnable {
+		
+		private @Nullable SelectionChangedEvent event;
+		
+		public PostSelectionRunnable() {
+			super(BasicCustomViewer.this.display);
+		}
+		
+		public void schedule(final SelectionChangedEvent event, final long stamp,
+				final UpdateType updateType) {
+			this.event= event;
+			if (updateType == UpdateType.DEFAULT_DELAYED_POST) {
+				scheduleFor(stamp + OpenStrategy.getPostSelectionDelay(), 1);
+			}
+			else {
+				runNow();
+			}
+		}
+		
+		@Override
+		public void cancel() {
+			super.cancel();
+			this.event= null;
+		}
+		
+		@Override
+		protected void execute() {
+			final SelectionChangedEvent event= this.event;
+			this.event= null;
+			if (event == null || isDisposed()) {
+				return;
+			}
+			firePostSelectionChanged(event);
+		}
+		
+	}
+	
+	
+	protected void initSelectionController(final TSelection initialSelection,
+			final int updateDefaultAsyncDelayNanos) {
+		this.selection= initialSelection;
+		this.updateSelectionDefaultAsyncDelayNanos= updateDefaultAsyncDelayNanos * 1_000_000L;
+		this.updateSelectionRunnable= new SelectionRunnable();
+		this.postSelectionRunnable= new PostSelectionRunnable();
+	}
+	
+	/**
+	 * Initiates the refresh of the selection (fetched from widget).
+	 * 
+	 * @param flags viewer specific flags passed to {@link #fetchSelectionFromWidget(int, ISelection)}
+	 * @param type the type of the selection update
+	 * @param async if the update should be executed asynchronously ({@code true}, or synchronously
+	 *     ({@code false}
+	 */
+	protected void refreshSelection(final int flags, final UpdateType type, final boolean async) {
+		final long stamp= System.nanoTime();
+		this.updateSelectionRunnable.schedule(flags, stamp, type, async);
+	}
+	
+	protected @Nullable TSelection fetchSelectionFromWidget(final int flags,
+			final TSelection prevSelection) {
+		return null;
+	}
+	
+	private void updateSelection(final UpdateType type, final long stamp, final int flags) {
+		final TSelection newSelection= fetchSelectionFromWidget(flags, this.selection);
+		if (newSelection == null || newSelection.equals(this.selection)) {
+			return;
+		}
+		
+		this.selection= newSelection;
+		
+		handleSelectionChanged(stamp, type);
+	}
+	
+	private void handleSelectionChanged(final long stamp, final UpdateType type) {
+		final SelectionChangedEvent event= new SelectionChangedEvent(this, this.selection);
+		fireSelectionChanged(event);
+		
+		this.postSelectionRunnable.schedule(event, stamp, type);
+	}
+	
+	
+}