Merge commit 'ee020e75a5de0c1e54557db79f77edc50e05ea04'
[fanfix.git] / src / be / nikiroo / utils / ui / DelayWorker.java
diff --git a/src/be/nikiroo/utils/ui/DelayWorker.java b/src/be/nikiroo/utils/ui/DelayWorker.java
new file mode 100644 (file)
index 0000000..3618b89
--- /dev/null
@@ -0,0 +1,220 @@
+package be.nikiroo.utils.ui;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeSet;
+
+import javax.swing.SwingWorker;
+
+/**
+ * This class helps you delay some graphical actions and execute the most recent
+ * ones when under contention.
+ * <p>
+ * How does it work?
+ * <ul>
+ * <li>it takes an ID and an associated {@link SwingWorker} and will call
+ * {@link SwingWorker#execute()} after a small delay (see
+ * {@link DelayWorker#DelayWorker(int)})</li>
+ * <li>if a second call to {@link DelayWorker#delay(String, SwingWorker)} comes
+ * with the same ID before the first one is done, it will be put on a waiting
+ * queue</li>
+ * <li>if a third call still with the same ID comes, its associated worker will
+ * <b>replace</b> the one in the queue (only one worker per ID in the queue,
+ * always the latest one)</li>
+ * <li>when the first worker is done, it will check the waiting queue and
+ * execute that latest worker if any</li>
+ * </ul>
+ * 
+ * @author niki
+ *
+ */
+@SuppressWarnings("rawtypes")
+public class DelayWorker {
+       private Map<String, SwingWorker> lazyEnCours;
+       private Object lazyEnCoursLock;
+
+       private TreeSet<String> wip;
+
+       private Object waiter;
+
+       private boolean cont;
+       private boolean paused;
+       private Thread loop;
+
+       /**
+        * Create a new {@link DelayWorker} with the given delay (in milliseconds)
+        * before each drain of the queue.
+        * 
+        * @param delayMs
+        *            the delay in milliseconds (can be 0, cannot be negative)
+        */
+       public DelayWorker(final int delayMs) {
+               if (delayMs < 0) {
+                       throw new IllegalArgumentException(
+                                       "A waiting delay cannot be negative");
+               }
+
+               lazyEnCours = new HashMap<String, SwingWorker>();
+               lazyEnCoursLock = new Object();
+               wip = new TreeSet<String>();
+               waiter = new Object();
+               cont = true;
+               paused = false;
+
+               loop = new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               while (cont) {
+                                       try {
+                                               Thread.sleep(delayMs);
+                                       } catch (InterruptedException e) {
+                                       }
+
+                                       Map<String, SwingWorker> workers = new HashMap<String, SwingWorker>();
+                                       synchronized (lazyEnCoursLock) {
+                                               for (String key : new ArrayList<String>(
+                                                               lazyEnCours.keySet())) {
+                                                       if (!wip.contains(key)) {
+                                                               workers.put(key, lazyEnCours.remove(key));
+                                                       }
+                                               }
+                                       }
+
+                                       for (final String key : workers.keySet()) {
+                                               SwingWorker worker = workers.get(key);
+
+                                               synchronized (lazyEnCoursLock) {
+                                                       wip.add(key);
+                                               }
+
+                                               worker.addPropertyChangeListener(
+                                                               new PropertyChangeListener() {
+                                                                       @Override
+                                                                       public void propertyChange(
+                                                                                       PropertyChangeEvent evt) {
+                                                                               synchronized (lazyEnCoursLock) {
+                                                                                       wip.remove(key);
+                                                                               }
+                                                                               wakeup();
+                                                                       }
+                                                               });
+
+                                               // Start it, at last
+                                               worker.execute();
+                                       }
+
+                                       synchronized (waiter) {
+                                               do {
+                                                       try {
+                                                               if (cont)
+                                                                       waiter.wait();
+                                                       } catch (InterruptedException e) {
+                                                       }
+                                               } while (cont && paused);
+                                       }
+                               }
+                       }
+               });
+
+               loop.setDaemon(true);
+               loop.setName("Loop for DelayWorker");
+       }
+
+       /**
+        * Start the internal loop that will drain the processing queue. <b>MUST
+        * NOT</b> be started twice (but see {@link DelayWorker#pause()} and
+        * {@link DelayWorker#resume()} instead).
+        */
+       public void start() {
+               loop.start();
+       }
+
+       /**
+        * Pause the system until {@link DelayWorker#resume()} is called -- note
+        * that it will still continue on the processes currently scheduled to run,
+        * but will pause after that.
+        * <p>
+        * Can be called even if already paused, will just do nothing in that
+        * context.
+        */
+       public void pause() {
+               paused = true;
+       }
+
+       /**
+        * Check if the {@link DelayWorker} is currently paused.
+        * 
+        * @return TRUE if it is
+        */
+       public boolean isPaused() {
+               return paused;
+       }
+
+       /**
+        * Resume the system after a pause.
+        * <p>
+        * Can be called even if already running, will just do nothing in that
+        * context.
+        */
+       public void resume() {
+               synchronized (waiter) {
+                       paused = false;
+                       wakeup();
+               }
+       }
+
+       /**
+        * Stop the system.
+        * <p>
+        * Note: this is final, you <b>MUST NOT</b> call {@link DelayWorker#start()}
+        * a second time (but see {@link DelayWorker#pause()} and
+        * {@link DelayWorker#resume()} instead).
+        */
+       public void stop() {
+               synchronized (waiter) {
+                       cont = false;
+                       wakeup();
+               }
+       }
+
+       /**
+        * Clear all the processes that were put on the queue but not yet scheduled
+        * to be executed -- note that it will still continue on the processes
+        * currently scheduled to run.
+        */
+       public void clear() {
+               synchronized (lazyEnCoursLock) {
+                       lazyEnCours.clear();
+                       wip.clear();
+               }
+       }
+
+       /**
+        * Put a new process in the delay queue.
+        * 
+        * @param id
+        *            the ID of this process (if you want to skip workers when they
+        *            are superseded by a new one, you need to use the same ID key)
+        * @param worker
+        *            the process to delay
+        */
+       public void delay(final String id, final SwingWorker worker) {
+               synchronized (lazyEnCoursLock) {
+                       lazyEnCours.put(id, worker);
+               }
+
+               wakeup();
+       }
+
+       /**
+        * Wake up the loop thread.
+        */
+       private void wakeup() {
+               synchronized (waiter) {
+                       waiter.notifyAll();
+               }
+       }
+}