DelayWorker: now delays on work done, not on work scheduled
authorNiki Roo <niki@nikiroo.be>
Thu, 9 Apr 2020 16:10:33 +0000 (18:10 +0200)
committerNiki Roo <niki@nikiroo.be>
Thu, 9 Apr 2020 16:10:33 +0000 (18:10 +0200)
src/be/nikiroo/fanfix_swing/gui/utils/DelayWorker.java

index 19a9b5cd015723f5c95fb35bb7b128c44496b566..6d630c5da9cf6b7c5443d4c814465275d7764f8d 100644 (file)
@@ -1,26 +1,65 @@
 package be.nikiroo.fanfix_swing.gui.utils;
 
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
 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;
@@ -34,13 +73,36 @@ public class DelayWorker {
                                        } catch (InterruptedException e) {
                                        }
 
-                                       List<SwingWorker> workers;
+                                       Map<String, SwingWorker> workers = new HashMap<String, SwingWorker>();
                                        synchronized (lazyEnCoursLock) {
-                                               workers = new LinkedList<SwingWorker>(
-                                                               lazyEnCours.values());
-                                               lazyEnCours.clear();
+                                               for (String key : new ArrayList<String>(
+                                                               lazyEnCours.keySet())) {
+                                                       if (!wip.contains(key)) {
+                                                               workers.put(key, lazyEnCours.remove(key));
+                                                       }
+                                               }
                                        }
-                                       for (SwingWorker worker : workers) {
+
+                                       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();
                                        }
 
@@ -56,23 +118,47 @@ public class DelayWorker {
                                }
                        }
                });
+
                loop.setDaemon(true);
                loop.setName("Loop for DelayWorker");
        }
 
-       // twice = not legal
+       /**
+        * 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;
@@ -80,6 +166,13 @@ public class DelayWorker {
                }
        }
 
+       /**
+        * 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;
@@ -87,13 +180,28 @@ public class DelayWorker {
                }
        }
 
+       /**
+        * 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();
                }
        }
 
-       public <T, V> void delay(final String id, final SwingWorker<T, V> worker) {
+       /**
+        * 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);
                }
@@ -101,6 +209,9 @@ public class DelayWorker {
                wakeup();
        }
 
+       /**
+        * Wake up the loop thread.
+        */
        private void wakeup() {
                synchronized (waiter) {
                        waiter.notifyAll();