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);
+ }
+
+
+}