AsyncSwingWorker setpaused adjustment
[jalview.git] / src / javajs / async / AsyncSwingWorker.java
1 package javajs.async;
2
3 import java.awt.Component;
4
5 import javax.swing.ProgressMonitor;
6 import javax.swing.SwingUtilities;
7 import javax.swing.SwingWorker;
8
9 import javajs.async.SwingJSUtils.StateHelper;
10 import javajs.async.SwingJSUtils.StateMachine;
11
12 /**
13  * v. 2020.06.03 
14  *
15  * Executes synchronous or asynchronous tasks using a SwingWorker in Java or
16  * JavaScript, equivalently.
17  * 
18  * Unlike a standard SwingWorker, AsyncSwingWorker may itself be asynchronous.
19  * For example, it might load a file asynchronously, or carry out a background
20  * process in JavaScript much like one might be done in Java, but with only a
21  * single thread.
22  * 
23  * Whereas a standard SwingWorker would execute done() long before the
24  * asynchronous task completed, this class will wait until progress has been
25  * asynchronously set greater or equal to its max value or the task is canceled
26  * before executing that method.
27  * 
28  * Three methods must be supplied by the subclass:
29  * 
30  * void initAsync()
31  * 
32  * int doInBackgroundAsync(int progress)
33  * 
34  * void doneAsync()
35  * 
36  * Both initAsync() and doneAsync() are technically optional - they may be
37  * empty. doInBackgroundAsync(), however, is the key method where, like
38  * SwingWorker's doInBackground, the main work is done. The supplied progress
39  * parameter reminds the subclass of where it is at, and the return value allows
40  * the subclass to update the progress field in both the SwingWorker and the
41  * ProgressMonitor.
42  * 
43  * If it is desired to run the AsyncSwingWorker synchronously, call the
44  * executeSynchronously() method rather than execute(). Never call
45  * SwingWorker.run().
46  * 
47  * Note that doInBackgroundAsync runs on the Java AWT event queue. This means
48  * that, unlike a true SwingWorker, it will run in event-queue sequence, after
49  * anything that that method itself adds to the queue. This is what SwingWorker itself
50  * does with its done() signal. 
51  * 
52  * If doInBackgroundAsync has tasks that are time intensive, the thing to do is to
53  * 
54  * (a) pause this worker by setting the value of progress for the NEXT step:
55  * 
56  *    setProgressAsync(n);
57  *    
58  * (b) pause the timer so that when doInBackgroundAsync returns, the timer is not fired:
59  * 
60  *    setPaused(true);
61  * 
62  * (c) start your process as new Thread, which bypasses the AWT EventQueue:
63  * 
64  *    new Thread(Runnable).start();
65  *    
66  * (d) have your thread, when it is done, return control to this worker:
67  * 
68  *    setPaused(false);
69  *    
70  * This final call restarts the worker with the currently specified progress value.
71  * 
72  * @author hansonr
73  *
74  */
75 public abstract class AsyncSwingWorker extends SwingWorker<Void, Void> implements StateMachine {
76
77
78   // PropertyChangeEvent getPropertyName()
79   
80   private static final String PROPERTY_STATE = "state";
81   private static final String PROPERTY_PAUSE = "pause";
82   
83   // PropertyChangeEvent getNewValue()
84   
85   public static final String STARTED_ASYNC = "STARTED_ASYNC";
86   public static final String STARTED_SYNC = "STARTED_SYNC";
87   
88   public static final String DONE_ASYNC = "DONE_ASYNC";
89   public static final String CANCELED_ASYNC = "CANCELED_ASYNC";
90   
91   public static final String PAUSED = "PAUSED";
92   public static final String RESUMED = "RESUMED";
93
94   protected int progressAsync;
95
96   /**
97    * Override to provide initial tasks.
98    */
99   abstract public void initAsync();
100
101   /**
102    * Given the last progress, do some portion of the task that the SwingWorker
103    * would do in the background, and return the new progress. returning max or
104    * above will complete the task.
105    * 
106    * @param progress
107    * @return new progress
108    */
109   abstract public int doInBackgroundAsync(int progress);
110
111   /**
112    * Do something when the task is finished or canceled.
113    * 
114    */
115   abstract public void doneAsync();
116
117   protected ProgressMonitor progressMonitor;
118
119   protected int delayMillis;
120   protected String note;
121   protected int min;
122   protected int max;
123   protected int progressPercent;
124
125   protected boolean isAsync;
126   private Exception exception;
127
128   /**
129    * Construct an asynchronous SwingWorker task that optionally will display a
130    * ProgressMonitor. Progress also can be monitored by adding a
131    * PropertyChangeListener to the AsyncSwingWorker and looking for the "progress"
132    * event, just the same as for a standard SwingWorker.
133    * 
134    * @param owner       optional owner for the ProgressMonitor, typically a JFrame
135    *                    or JDialog.
136    * 
137    * @param title       A non-null title indicates we want to use a
138    *                    ProgressMonitor with that title line.
139    * 
140    * @param delayMillis A positive number indicating the delay we want before
141    *                    executions, during which progress will be reported.
142    * 
143    * @param min         The first progress value. No range limit.
144    * 
145    * @param max         The last progress value. No range limit; may be greater
146    *                    than min.
147    * 
148    */
149   public AsyncSwingWorker(Component owner, String title, int delayMillis, int min, int max) {
150     if (title != null && delayMillis > 0) {
151       progressMonitor = new ProgressMonitor(owner, title, "", Math.min(min, max), Math.max(min, max));
152       progressMonitor.setProgress(Math.min(min, max)); // displays monitor
153     }
154     this.delayMillis = Math.max(0, delayMillis);
155     this.isAsync = (delayMillis > 0);
156
157     this.min = min;
158     this.max = max;
159   }
160
161   public void executeAsync() {
162     firePropertyChange(PROPERTY_STATE, null, STARTED_ASYNC);
163     super.execute();
164   }
165
166   public void executeSynchronously() {
167     firePropertyChange(PROPERTY_STATE, null, STARTED_SYNC);
168     isAsync = false;
169     delayMillis = 0;
170     try {
171       doInBackground();
172     } catch (Exception e) {
173       exception = e;
174       e.printStackTrace();
175       cancelAsync();
176     }
177   }
178
179   public Exception getException() {
180     return exception;
181   }
182
183   public int getMinimum() {
184     return min;
185   }
186
187   public void setMinimum(int min) {
188     this.min = min;
189     if (progressMonitor != null) {
190       progressMonitor.setMinimum(min);
191     }
192   }
193
194   public int getMaximum() {
195     return max;
196   }
197
198   public void setMaximum(int max) {
199     if (progressMonitor != null) {
200       progressMonitor.setMaximum(max);
201     }
202     this.max = max;
203   }
204
205   public int getProgressPercent() {
206     return progressPercent;
207   }
208
209   public void setNote(String note) {
210     this.note = note;
211     if (progressMonitor != null) {
212       progressMonitor.setNote(note);
213     }
214   }
215
216   /**
217    * Cancel the asynchronous process.
218    * 
219    */
220   public void cancelAsync() {
221     helper.interrupt();
222   }
223
224   /**
225    * Check to see if the asynchronous process has been canceled.
226    *
227    * @return true if StateHelper is not alive anymore
228    * 
229    */
230   public boolean isCanceledAsync() {
231     return !helper.isAlive();
232   }
233
234   /**
235    * Check to see if the asynchronous process is completely done.
236    * 
237    * @return true only if the StateMachine is at STATE_DONE
238    * 
239    */
240   public boolean isDoneAsync() {
241     return helper.getState() == STATE_DONE;
242   }
243
244   /**
245    * Override to set a more informed note for the ProcessMonitor.
246    * 
247    * @param progress
248    * @return
249    */
250   public String getNote(int progress) {
251     return String.format("Completed %d%%.\n", progress);
252   }
253
254   /**
255    * Retrieve the last note delivered by the ProcessMonitor.
256    * 
257    * @return
258    */
259   public String getNote() {
260     return note;
261   }
262
263   public int getProgressAsync() {
264     return progressAsync;
265   }
266
267   /**
268    * Set the [min,max] progress safely.
269    * 
270    * SwingWorker only allows progress between 0 and 100. This method safely
271    * translates [min,max] to [0,100].
272    * 
273    * @param n
274    */
275   public void setProgressAsync(int n) {
276     n = (max > min ? Math.max(min, Math.min(n, max)) : Math.max(max, Math.min(n, min)));
277     progressAsync = n;
278     n = (n - min) * 100 / (max - min);
279     n = (n < 0 ? 0 : n > 100 ? 100 : n);
280     progressPercent = n;
281   }
282
283   ///// the StateMachine /////
284
285   private final static int STATE_INIT = 0;
286   private final static int STATE_LOOP = 1;
287   private final static int STATE_WAIT = 2;
288   private final static int STATE_DONE = 99;
289   
290   private StateHelper helper;
291
292   protected StateHelper getHelper() {
293     return helper;
294   }
295
296   private boolean isPaused;
297
298   protected void setPaused(boolean tf) {
299     isPaused = tf;
300     firePropertyChange(PROPERTY_PAUSE, null, (tf ? PAUSED : RESUMED));
301     if (!tf)
302       stateLoop();
303   }
304
305   protected boolean isPaused() {
306     return isPaused;
307   }
308
309   /**
310    * The StateMachine's main loop.
311    * 
312    * Note that a return from this method will exit doInBackground, trigger the
313    * isDone() state on the underying worker, and scheduling its done() for
314    * execution on the AWTEventQueue.
315    *
316    * Since this happens essentially immediately, it is unlikely that
317    * SwingWorker.isCancelled() will ever be true. Thus, the SwingWorker task
318    * itself won't be cancelable in Java or in JavaScript, since its
319    * doInBackground() method is officially complete, and isDone() is true well
320    * before we are "really" done. FutureTask will not set isCancelled() true once
321    * the task has run.
322    * 
323    * We are using an asynchronous task specifically because we want to have the
324    * opportunity for the ProgressMonitor to report in JavaScript. We will have to
325    * cancel our task and report progress explicitly using our own methods.
326    * 
327    */
328   @Override
329   public boolean stateLoop() {
330     while (helper.isAlive() && !isPaused) {
331       switch (helper.getState()) {
332       case STATE_INIT:
333         setProgressAsync(min);
334         initAsync();
335         helper.setState(STATE_WAIT);
336         continue;
337       case STATE_LOOP:
338         if (checkCanceled()) {
339           helper.setState(STATE_DONE);
340           firePropertyChange(PROPERTY_STATE, null, CANCELED_ASYNC);
341         } else {
342           int ret = doInBackgroundAsync(progressAsync);         
343           if (!helper.isAlive() || isPaused) {
344             continue;
345           }
346           progressAsync = ret;
347           setProgressAsync(progressAsync);
348           setNote(getNote(progressAsync));
349           setProgress(progressPercent);
350           if (progressMonitor != null) {
351             progressMonitor.setProgress(max > min ? progressAsync : max + min - progressAsync);
352           }
353           helper.setState(progressAsync == max ? STATE_DONE : STATE_WAIT);
354         }
355         continue;
356       case STATE_WAIT:
357         // meaning "sleep" and then "loop"
358         helper.setState(STATE_LOOP);
359         helper.sleep(delayMillis);
360         return true;
361       default:
362       case STATE_DONE:
363         stopProgressMonitor();
364         // Put the doneAsync() method on the AWTEventQueue
365         // just as for SwingWorker.done().
366         if (isAsync) {
367           SwingUtilities.invokeLater(doneRunnable);
368         } else {
369           doneRunnable.run();
370         }
371
372         return false;
373       }
374     }
375     if (!helper.isAlive()) {
376       stopProgressMonitor();
377     }
378     return false;
379   }
380
381   private void stopProgressMonitor() {
382     if (progressMonitor != null) {
383       progressMonitor.close();
384       progressMonitor = null;
385     }
386   }
387
388   private Runnable doneRunnable = new Runnable() {
389     @Override
390     public void run() {
391       doneAsync();
392       firePropertyChange(PROPERTY_STATE, null, DONE_ASYNC);
393     }
394
395   };
396
397   private boolean checkCanceled() {
398     if (isMonitorCanceled() || isCancelled()) {
399       helper.interrupt();
400       return true;
401     }
402     return false;
403   }
404
405   //// final SwingWorker methods not to be used by subclasses ////
406
407   private boolean isMonitorCanceled() {
408     return (progressMonitor != null && progressMonitor.isCanceled());
409   }
410
411   /**
412    * see SwingWorker, made final here.
413    * 
414    */
415   @Override
416   final protected Void doInBackground() throws Exception {
417     helper = new StateHelper(this);
418     setProgressAsync(min);
419     helper.next(STATE_INIT);
420     return null;
421   }
422
423   /**
424    * see SwingWorker, made final here. Nothing to do.
425    * 
426    */
427   @Override
428   final public void done() {
429   }
430
431 }