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