package org.andork.tracker;
import java.util.ArrayList;
import java.util.List;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Tracker {
public static class FlushOptions {
public boolean finishSynchronously;
public boolean throwFirstError;
public FlushOptions() {
}
public FlushOptions finishSynchronously(boolean val) {
finishSynchronously = val;
return this;
}
public FlushOptions throwFirstError(boolean val) {
throwFirstError = val;
return this;
}
}
public static interface Runner {
void checkThread();
void setTimeout(Runnable r, int delay);
}
public static Tracker EDT = new Tracker(new Runner() {
@Override
public void checkThread() {
if (!SwingUtilities.isEventDispatchThread()) {
throw new TrackerException("Must be called from EDT");
}
}
@Override
public void setTimeout(Runnable r, int delay) {
if (delay <= 0) {
SwingUtilities.invokeLater(r);
} else {
Timer timer = new Timer(delay, e -> r.run());
timer.setRepeats(false);
timer.start();
}
}
});
static final ThreadLocal<Computation> currentComputation = new ThreadLocal<>();
public static Computation currentComputation() {
return currentComputation.get();
}
public static boolean isActive() {
return currentComputation.get() != null;
}
static void setCurrentComputation(Computation comp) {
currentComputation.set(comp);
}
final Runner runner;
final List<Computation> pendingComputations = new ArrayList<>();
final List<Runnable> afterFlushCallbacks = new ArrayList<>();
boolean inCompute = false;
boolean willFlush = false;
boolean inFlush = false;
boolean throwFirstError = false;
Tracker(Runner runner) {
this.runner = runner;
}
void addPendingComputation(Computation comp) {
pendingComputations.add(comp);
}
public void afterFlush(Runnable r) {
runner.checkThread();
afterFlushCallbacks.add(r);
requireFlush();
}
public Computation autorun(ComputeFunction r) throws Exception {
runner.checkThread();
Computation comp = new Computation(this, r);
comp.start();
if (isActive()) {
onInvalidate(() -> comp.stop());
}
return comp;
}
public Computation autorun(Runnable r) {
try {
return autorun(comp -> r.run());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void flush() {
flush(null);
}
public void flush(FlushOptions options) {
runFlush(new FlushOptions().finishSynchronously(true)
.throwFirstError(options == null ? false : options.throwFirstError));
}
boolean inCompute() {
return inCompute;
}
public void nonreactive(Runnable r) {
runner.checkThread();
Computation previous = currentComputation();
setCurrentComputation(null);
try {
r.run();
} finally {
setCurrentComputation(previous);
}
}
public void onInvalidate(Runnable r) {
if (!isActive()) {
throw new Error("Tracker.onInvalidate requires a currentComputation");
}
currentComputation().onInvalidate(r);
}
void requireFlush() {
runner.checkThread();
if (!willFlush) {
runner.setTimeout(this::runFlush, 0);
willFlush = true;
}
}
void runFlush() {
runFlush(null);
}
void runFlush(FlushOptions options) {
runner.checkThread();
if (inFlush) {
throw new TrackerException("Can't call Tracker.flush while flushing");
}
if (inCompute) {
throw new TrackerException("Can't flush inside Tracker.autorun");
}
if (options == null) {
options = new FlushOptions();
}
inFlush = true;
willFlush = true;
throwFirstError = options.throwFirstError;
int recomputedCount = 0;
boolean finishedTry = false;
try {
while (!pendingComputations.isEmpty() || !afterFlushCallbacks.isEmpty()) {
// recompute all pending computations
while (!pendingComputations.isEmpty()) {
Computation comp = pendingComputations.remove(0);
comp.recompute();
if (comp.needsRecompute()) {
pendingComputations.add(0, comp);
}
if (!options.finishSynchronously && ++recomputedCount > 1000) {
finishedTry = true;
return;
}
}
if (!afterFlushCallbacks.isEmpty()) {
// call one afterFlush callback, which may
// invalidate more computations
Runnable func = afterFlushCallbacks.remove(0);
try {
func.run();
} catch (Exception e) {
throwOrLog("afterFlush", e);
}
}
}
finishedTry = true;
} finally {
if (!finishedTry) {
// we're erroring due to throwFirstError being true. // 506
inFlush = false; // needed before calling `Tracker.flush()`
// again
// finish flushing
runFlush(new FlushOptions().finishSynchronously(options.finishSynchronously).throwFirstError(false));
}
willFlush = false;
inFlush = false;
if (!pendingComputations.isEmpty() || !afterFlushCallbacks.isEmpty()) {
// We're yielding because we ran a bunch of computations and we
// aren't required to finish synchronously, so we'd like to give
// the event loop a chance. We should flush again soon.
if (options.finishSynchronously) {
throw new TrackerException("still have work to do?");
}
runner.setTimeout(this::requireFlush, 10);
}
}
}
void setInCompute(boolean inCompute) {
this.inCompute = inCompute;
}
void throwOrLog(String from, Exception e) {
if (throwFirstError) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new TrackerException(from, e);
}
e.printStackTrace();
}
}