more laf options, jdoc
[fanfix.git] / ui / DelayWorker.java
1 package be.nikiroo.utils.ui;
2
3 import java.beans.PropertyChangeEvent;
4 import java.beans.PropertyChangeListener;
5 import java.util.ArrayList;
6 import java.util.HashMap;
7 import java.util.Map;
8 import java.util.TreeSet;
9
10 import javax.swing.SwingWorker;
11
12 /**
13 * This class helps you delay some graphical actions and execute the most recent
14 * ones when under contention.
15 * <p>
16 * How does it work?
17 * <ul>
18 * <li>it takes an ID and an associated {@link SwingWorker} and will call
19 * {@link SwingWorker#execute()} after a small delay (see
20 * {@link DelayWorker#DelayWorker(int)})</li>
21 * <li>if a second call to {@link DelayWorker#delay(String, SwingWorker)} comes
22 * with the same ID before the first one is done, it will be put on a waiting
23 * queue</li>
24 * <li>if a third call still with the same ID comes, its associated worker will
25 * <b>replace</b> the one in the queue (only one worker per ID in the queue,
26 * always the latest one)</li>
27 * <li>when the first worker is done, it will check the waiting queue and
28 * execute that latest worker if any</li>
29 * </ul>
30 *
31 * @author niki
32 *
33 */
34 @SuppressWarnings("rawtypes")
35 public class DelayWorker {
36 private Map<String, SwingWorker> lazyEnCours;
37 private Object lazyEnCoursLock;
38
39 private TreeSet<String> wip;
40
41 private Object waiter;
42
43 private boolean cont;
44 private boolean paused;
45 private Thread loop;
46
47 /**
48 * Create a new {@link DelayWorker} with the given delay (in milliseconds)
49 * before each drain of the queue.
50 *
51 * @param delayMs
52 * the delay in milliseconds (can be 0, cannot be negative)
53 */
54 public DelayWorker(final int delayMs) {
55 if (delayMs < 0) {
56 throw new IllegalArgumentException(
57 "A waiting delay cannot be negative");
58 }
59
60 lazyEnCours = new HashMap<String, SwingWorker>();
61 lazyEnCoursLock = new Object();
62 wip = new TreeSet<String>();
63 waiter = new Object();
64 cont = true;
65 paused = false;
66
67 loop = new Thread(new Runnable() {
68 @Override
69 public void run() {
70 while (cont) {
71 try {
72 Thread.sleep(delayMs);
73 } catch (InterruptedException e) {
74 }
75
76 Map<String, SwingWorker> workers = new HashMap<String, SwingWorker>();
77 synchronized (lazyEnCoursLock) {
78 for (String key : new ArrayList<String>(
79 lazyEnCours.keySet())) {
80 if (!wip.contains(key)) {
81 workers.put(key, lazyEnCours.remove(key));
82 }
83 }
84 }
85
86 for (final String key : workers.keySet()) {
87 SwingWorker worker = workers.get(key);
88
89 synchronized (lazyEnCoursLock) {
90 wip.add(key);
91 }
92
93 worker.addPropertyChangeListener(
94 new PropertyChangeListener() {
95 @Override
96 public void propertyChange(
97 PropertyChangeEvent evt) {
98 synchronized (lazyEnCoursLock) {
99 wip.remove(key);
100 }
101 wakeup();
102 }
103 });
104
105 // Start it, at last
106 worker.execute();
107 }
108
109 synchronized (waiter) {
110 do {
111 try {
112 if (cont)
113 waiter.wait();
114 } catch (InterruptedException e) {
115 }
116 } while (cont && paused);
117 }
118 }
119 }
120 });
121
122 loop.setDaemon(true);
123 loop.setName("Loop for DelayWorker");
124 }
125
126 /**
127 * Start the internal loop that will drain the processing queue. <b>MUST
128 * NOT</b> be started twice (but see {@link DelayWorker#pause()} and
129 * {@link DelayWorker#resume()} instead).
130 */
131 public void start() {
132 loop.start();
133 }
134
135 /**
136 * Pause the system until {@link DelayWorker#resume()} is called -- note
137 * that it will still continue on the processes currently scheduled to run,
138 * but will pause after that.
139 * <p>
140 * Can be called even if already paused, will just do nothing in that
141 * context.
142 */
143 public void pause() {
144 paused = true;
145 }
146
147 /**
148 * Check if the {@link DelayWorker} is currently paused.
149 *
150 * @return TRUE if it is
151 */
152 public boolean isPaused() {
153 return paused;
154 }
155
156 /**
157 * Resume the system after a pause.
158 * <p>
159 * Can be called even if already running, will just do nothing in that
160 * context.
161 */
162 public void resume() {
163 synchronized (waiter) {
164 paused = false;
165 wakeup();
166 }
167 }
168
169 /**
170 * Stop the system.
171 * <p>
172 * Note: this is final, you <b>MUST NOT</b> call {@link DelayWorker#start()}
173 * a second time (but see {@link DelayWorker#pause()} and
174 * {@link DelayWorker#resume()} instead).
175 */
176 public void stop() {
177 synchronized (waiter) {
178 cont = false;
179 wakeup();
180 }
181 }
182
183 /**
184 * Clear all the processes that were put on the queue but not yet scheduled
185 * to be executed -- note that it will still continue on the processes
186 * currently scheduled to run.
187 */
188 public void clear() {
189 synchronized (lazyEnCoursLock) {
190 lazyEnCours.clear();
191 wip.clear();
192 }
193 }
194
195 /**
196 * Put a new process in the delay queue.
197 *
198 * @param id
199 * the ID of this process (if you want to skip workers when they
200 * are superseded by a new one, you need to use the same ID key)
201 * @param worker
202 * the process to delay
203 */
204 public void delay(String id, SwingWorker worker) {
205 synchronized (lazyEnCoursLock) {
206 lazyEnCours.put(id, worker);
207 }
208
209 wakeup();
210 }
211
212 /**
213 * Wake up the loop thread.
214 */
215 private void wakeup() {
216 synchronized (waiter) {
217 waiter.notifyAll();
218 }
219 }
220 }