Adding SwingJS interface and utility classes
[jalview.git] / src / javajs / async / SwingJSUtils.java
1 package javajs.async;
2
3 import java.awt.Component;
4 import java.awt.Dimension;
5 import java.awt.Graphics;
6 import java.awt.Image;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.io.BufferedInputStream;
10 import java.io.BufferedReader;
11 import java.io.ByteArrayInputStream;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.io.InputStreamReader;
15 import java.util.stream.Collectors;
16
17 import javax.imageio.ImageIO;
18 import javax.swing.Timer;
19
20 /**
21  * A set of generally useful SwingJS-related methods. Includes:
22  * 
23  * alternatives to using getCodeBase() for loading resources, due to issues in
24  * Eclipse setting that incorrectly (but no problem in JavaScript)
25  * 
26  * 
27  * 
28  * @author hansonr
29  *
30  */
31 public class SwingJSUtils {
32         /**
33          * Set the dimension for the applet prior to j2sApplet's call to 
34          * run the applet. Must be used to create a static field:
35          * 
36          * <code>
37          *   private static Dimension dim = 
38          * </code>
39          * 
40          * 
41          * Then, if it is desired also to have Java also set this, add
42          * 
43          *  if (dim != null) setSize(dim);  
44          *  
45          *  to the applet's init() method.
46          * 
47          * @param w
48          * @param h
49          * @return the Dimension
50          * 
51          * @author hansonr
52          */
53         public static Dimension setDim(int w, int h) {
54                 String baseURI = (/** @j2sNative document.body.baseURI || */
55                 null);
56                 boolean isTest = (baseURI == null || baseURI.indexOf("_applet.html") >= 0);
57                 if (!isTest)
58                         return null;
59                 /**
60                  * @j2sNative
61                  * 
62                  *                      J2S.thisApplet.__Info.width = w; J2S.thisApplet.__Info.height = h;
63                  */
64                 return new Dimension(w, h);
65         }
66
67         /**
68          * Reliably load a resource of a specific type from the code directory
69          * 
70          * adaptable - here we are returning an image or a string
71          * 
72          * @param cl       the classname of the object to return (Image.class,
73          *                 String.class) null for InputStream
74          * @param filename
75          * @return
76          * 
77          * @author hansonr
78          */
79         public static Object getResource(Class<?> baseClass, String filename, Class<?> cl) {
80                 System.out.println("mpUtils.SwingJSUtils.getResource " + baseClass.getCanonicalName() + " " + filename);
81                 InputStream is = baseClass.getResourceAsStream(filename);
82                 if (cl == Image.class) {
83                         try {
84                                 return ImageIO.read(is);
85                         } catch (IOException e) {
86                                 e.printStackTrace();
87                         }
88                 } else if (cl == String.class) {
89                         return new BufferedReader(new InputStreamReader(is)).lines().collect(Collectors.joining("\n"));
90                 }
91                 return is;
92         }
93
94         /**
95          * Pre-fetch images during the static entry of the class. This should provide
96          * plenty of clock ticks, since the file transfer is synchronous, and all we are
97          * waiting for is the DOM image object to initialize.
98          * 
99          * @param cl
100          * @param images
101          * @param root
102          * @param nImages
103          * @param ext
104          */
105         public static void loadImagesStatic(Class<?> cl, Image[] images, String root, String ext, int nImages) {
106                 for (int i = nImages; --i >= 0;) {
107
108                         // Bild laden und beim MediaTracker registrieren
109                         // MediaTracker ladekontrolle = new MediaTracker(this);
110
111                         // BH SwingJS -- adding generally useful method for loading data
112                         // avoiding the use of getCodeBase(), which for some reason does not work in
113                         // Eclipse.
114
115                         images[i] = (Image) getResource(cl, root + i + "." + ext, Image.class);
116 //                      /**
117 //                       * @j2sNative
118 //                       * $("body").append(images[i]._imgNode);
119 //                       * 
120 //                       */
121 //                        ladekontrolle.addImage(scharf[i],i);
122                         // Warten , bis Bild ganz geladen ist
123
124 //                        try {ladekontrolle.waitForID(i);}
125 //                        catch (InterruptedException e)
126 //                           {}
127                 }
128         }
129
130         /**
131          * Fill an array with images based on a String[] listing
132          * @param cl reference class
133          * @param root  optional root path, ending in "/"
134          * @param names source file names
135          * @param images  array to fill
136          */
137         public static void loadImagesStatic(Class<?> cl, String root, String[] names, Image[] images) {
138                 for (int i = names.length; --i >= 0;) {
139                         images[i] = (Image) getResource(cl, root + names[i], Image.class);
140                 }
141         }
142
143         /**
144          * Eclipse-friendly image getting
145          * 
146          * @param c
147          * @param fileName
148          * @return
149          */
150         public static Image getImage(Component c, String fileName) {
151                 return getImage(c.getClass(), fileName);
152         }
153
154         /**
155          * Eclipse-friendly image getting
156          * 
157          * @param c
158          * @param fileName
159          * @return
160          */
161         public static Image getImage(Class<?> c, String fileName) {
162                 return (Image) getResource(c, fileName, Image.class);
163         }
164
165         /**
166          * Clear the component graphic. BH added this for JavaScript because changing
167          * the browser zoom can change the size of the canvas for unknown reasons.
168          * 
169          * @param c
170          */
171         public static void clearComponent(Component c) {
172                 Graphics gc = c.getGraphics();
173                 gc.clearRect(0, 0, c.getWidth(), c.getHeight());
174                 gc.dispose();
175         }
176
177         
178         /**
179          * A simple interface to the machine loop, generally of the form 
180          * <code>
181          *   public boolean stateLoop() {
182          *   while (stateHepler.isAlive()) {
183          *     switch (stateHelper.getState()) {
184          *     case STATE_XXX:
185          *        ...
186          *        return stateHelper.delayState(100,STATE_YYY);
187          *     case STATE_YYY:
188          *        ...
189          *        stateHelper.setState(STATE_ZZZ);
190          *        continue;
191          *     case STATE_ZZZ:
192          *        ...
193          *        return stateHelper.delayAction(100, MY_ID, "myCommand", myListener, STATE_XXX);        *   
194          *     case STATE_DONE:
195          *        ...
196          *        stateHelper.interrupt();
197          *        return false;
198          *     }
199          *     return true;
200          *   }
201          *   return false;
202          *   }
203          * </code>
204          * @author hansonr
205          *
206          */
207         public interface StateMachine {
208
209                 public boolean stateLoop();
210
211         }
212         /**
213          * StateHelper is a class that facilitates moving from an asychronous multithreaded model to a state-oriented model of programming
214          * for SwingJS animations and other asynchronous business.
215          *  
216          * @author hansonr
217          *
218          */
219         public static class StateHelper {
220                 
221                 public static final int UNCHANGED = Integer.MIN_VALUE;
222
223                 private StateMachine machine;
224                 private int state;
225                 private int level;
226                 
227                 private boolean interrupted;
228                 
229
230                 public StateHelper(StateMachine machine) {
231                         this.machine = machine;
232                 }
233
234                 public void interrupt() {
235                         interrupted = true;
236                 }
237                 
238                 public boolean isInterrupted() {
239                         return interrupted;
240                 }
241                 
242                 public boolean isAlive() {
243                         return !interrupted;
244                 }
245                 
246                 public void restart() {
247                         interrupted = false;
248                 }
249                 
250                 public void setState(int state) {
251                         this.state = this.stateNext = state;
252                 }
253
254                 public int getState() {
255                         return state;
256                 }
257
258                 public void setLevel(int level) {
259                         this.level = this.levelNext = level;
260                 }
261
262                 public int getLevel() {
263                         return level;
264                 }
265                 
266                 public void setNextState(int next) {
267                         stateNext = next; 
268                 }
269                 
270                 public int getNextState() {
271                         return stateNext;
272                 }
273
274                 public int getNextLevel() {
275                         return levelNext;
276                 }
277
278                 public void setNextLevel(int next) {
279                         levelNext = next; 
280                 }
281
282                 /** 
283                  * 
284                  * NOTE: this method must remain private; it is accessed via p$1
285                  * 
286                  * @return
287                  */
288                 private boolean nextState() {
289                         return next(stateNext, levelNext);
290                 }
291                 /**
292                  * Set the state and run machine.stateLoop().
293                  * 
294                  * @param state something meaningful to the machine
295                  * 
296                  * @return not interrupted
297                  * 
298                  * @author Bob Hanson hansonr@stolaf.edu
299                  */
300                 public boolean next(int state) {
301                         return next(state, 0);
302                 }
303                 
304                 /**
305                  * Set the state and level, and then run machine.stateLoop(). Driven directly or via delayedState or delayedAction
306                  * 
307                  * @param state something meaningful to the machine
308                  * @param level something meaningful to the machine
309                  * 
310                  * @return not interrupted
311                  * 
312                  * @author Bob Hanson hansonr@stolaf.edu
313                  */
314                 public boolean next(int state, int level) {
315                         return nextStatePriv(this, state, level);
316                 }
317
318                 private static boolean nextStatePriv(Object oThis, int state, int level) {
319                         StateHelper me = (StateHelper) oThis;
320                         if (me.interrupted)
321                                 return false;
322                         if (level != UNCHANGED)
323                                 me.level = level;
324                         if (state != UNCHANGED)
325                                 me.state = state;
326                         return me.machine.stateLoop();
327                 }
328
329                 /**
330                  * After the given number of milliseseconds, set the new state and run the machines stateLoop with unchanged level
331                  * 
332                  * @param ms the number of milliseconds to delay; 0 to execute synchronously             * 
333                  * @param stateNext  the next state to run
334                  * @return not interrupted
335                  * 
336                  * @author Bob Hanson hansonr@stolaf.edu
337                  */
338                 public boolean delayedState(int ms, int stateNext) {
339                         return delayedState(ms, stateNext, level);
340                 }
341
342                 private Timer stateTimer;
343
344                 private int stateNext;
345                 private int levelNext;
346                 
347                 /**
348                  * After the given number of milliseseconds, set the new state and level, and
349                  * run the machines stateLoop
350                  * 
351                  * @param ms        the number of milliseconds to delay; 0 to execute
352                  *                  synchronously *
353                  * @param stateNext the next state
354                  * @param levelNext the next level
355                  * @return not interrupted
356                  * 
357                  * @author Bob Hanson hansonr@stolaf.edu
358                  */
359                 
360                 public boolean delayedState(int ms, int stateNext, int levelNext) {
361                         if (interrupted)
362                                 return false;
363                         if (ms == 0)
364                                 return next(stateNext, levelNext);
365                         if (stateNext != UNCHANGED)
366                                 this.stateNext = stateNext;
367                         if (levelNext != UNCHANGED)
368                                 this.levelNext = levelNext;
369                         
370                         /**
371                          * @j2sNative
372                          * var me = this;
373                          * setTimeout(function(){
374                          *  p$1.nextState.apply(me, []);
375                          * },ms);
376                          */
377                         {
378                                 // Java only
379
380                                 if (stateTimer == null) {
381                                         stateTimer = new Timer(ms, new ActionListener() {
382                                                 @Override
383                                                 public void actionPerformed(ActionEvent e) {
384                                                         nextState();
385                                                 }
386
387                                         });
388                                         stateTimer.setRepeats(false);
389                                         stateTimer.start();
390                                 } else {
391                                         stateTimer.restart();
392                                 }
393                         }
394                         return true;
395                 }
396
397                 /**
398                  * Fire an actionPerformed event after a given number of milliseconds
399                  * 
400                  * @param ms       delay milliseconds. if 0, then this action will be called
401                  *                 synchronously
402                  * @param id       id for this event, possibly ACTION_PERFORMED (1001), but not
403                  *                 necessarily
404                  * @param command  key for ActionEvent.getCommand()
405                  * @param listener ActionListener to be called.
406                  * @return not interrupted
407                  * 
408                  * @author Bob Hanson hansonr@stolaf.edu
409                  */
410                 public boolean delayedAction(int ms, int id, String command, ActionListener listener) {
411                         return delayedAction(ms, id, command, listener, UNCHANGED, UNCHANGED);
412                 }
413
414                 /**
415                  * Fire an actionPerformed event after a given number of milliseconds
416                  * 
417                  * @param ms       delay milliseconds. if 0, then this action will be called
418                  *                 synchronously
419                  * @param id       id for this event, possibly ACTION_PERFORMED (1001), but not
420                  *                 necessarily
421                  * @param command  key for ActionEvent.getCommand()
422                  * @param listener ActionListener to be called.
423                  * 
424                  * @param state    the next state to go to after this listener is called; UNCHANGED to let the listener take care of this.
425                  * 
426                  * @return not interrupted
427                  * 
428                  * @author Bob Hanson hansonr@stolaf.edu
429                  */
430                 public boolean delayedAction(int ms, int id, String command, ActionListener listener, int state) {
431                         return delayedAction(ms, id, command, listener, state, UNCHANGED);
432                 }               
433                 
434                 /**
435                  * Fire an actionPerformed event after a given number of milliseconds. Setting BOTH stateNext and levelNext to UNCHANGED (Integer.MIN_VALUE)
436                  * allows the listener to handle continuing the loop.
437                  * 
438                  * @param ms       delay milliseconds. if 0, then this action will be called
439                  *                 synchronously
440                  * @param id       id for this event, possibly ACTION_PERFORMED (1001), but not
441                  *                 necessarily
442                  * @param command  key for ActionEvent.getCommand()
443                  * @param listener ActionListener to be called.
444                  * @param stateNext  state to run after the event is processed by the listener, or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle this.
445                  * @param levelNext  level to run after the event is processed by the listener, or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle this.
446                  * @return not interrupted
447                  * 
448                  * @author Bob Hanson hansonr@stolaf.edu
449                  */
450                 public boolean delayedAction(int ms, int id, String command, ActionListener listener, int stateNext, int levelNext) {
451                         if (interrupted)
452                                 return false; 
453                         ActionEvent event = new ActionEvent(this, id, command);
454                         if (ms == 0) {
455                                 listener.actionPerformed(event);
456                                 return (stateNext == UNCHANGED && levelNext == UNCHANGED || nextStatePriv(this, stateNext == UNCHANGED ? state : stateNext, levelNext == UNCHANGED ? level : levelNext));
457                         }
458                         
459                         StateHelper me = this;
460                         
461                         Timer timer = new Timer(ms, id == ActionEvent.ACTION_PERFORMED ? listener : new ActionListener() {
462                                 @Override
463                                 public void actionPerformed(ActionEvent e) {
464                                         if (!interrupted)
465                                                 listener.actionPerformed(event);
466                                         if (!interrupted && (stateNext != UNCHANGED || levelNext != UNCHANGED))
467                                                 nextStatePriv(me, stateNext == UNCHANGED ? state : stateNext, levelNext == UNCHANGED ? level : levelNext);
468                                 }
469                                 
470                         });
471                         timer.setRepeats(false);
472                         timer.start();
473                         return true;
474                 }
475
476                 public static void delayedRun(int ms, Runnable runnable) {
477                         new StateHelper(null).delayedRun(ms, runnable, UNCHANGED, UNCHANGED);
478                 }
479
480                 
481                 /**
482                  * Fire an actionPerformed event after a given number of milliseconds. Setting
483                  * BOTH stateNext and levelNext to UNCHANGED (Integer.MIN_VALUE) allows the
484                  * listener to handle continuing the loop.
485                  * 
486                  * @param ms        delay milliseconds. if 0, then this action will be called
487                  *                  synchronously
488                  * @param id        id for this event, possibly ACTION_PERFORMED (1001), but not
489                  *                  necessarily
490                  * @param command   key for ActionEvent.getCommand()
491                  * @param listener  ActionListener to be called.
492                  * @param stateNext state to run after the event is processed by the listener,
493                  *                  or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle
494                  *                  this.
495                  * @param levelNext level to run after the event is processed by the listener,
496                  *                  or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle
497                  *                  this.
498                  * @return not interrupted
499                  * 
500                  * @author Bob Hanson hansonr@stolaf.edu
501                  */
502                 public boolean delayedRun(int ms, Runnable runnable, int stateNext, int levelNext) {
503                         if (interrupted)
504                                 return false;
505                         if (ms == 0) {
506                                 return (stateNext == UNCHANGED && levelNext == UNCHANGED || nextStateIfUnchanged(this, runnable,
507                                                 stateNext == UNCHANGED ? state : stateNext, levelNext == UNCHANGED ? level : levelNext));
508                         }
509                         StateHelper me = this;
510                         /**
511                          * @j2sNative
512                          * 
513                          * setTimeout(function() {
514                          * 
515                          *    me.nextStateIfUnchanged$O$O$I$I.apply(me, [me, runnable, stateNext, levelNext]);
516                          * 
517                          * },ms);
518                          */
519                         {
520                                 Timer timer = new Timer(ms, new ActionListener() {
521                                         @Override
522                                         public void actionPerformed(ActionEvent e) {
523                                                 nextStateIfUnchanged(me, runnable, stateNext, levelNext);
524                                         }
525
526                                 });
527                                 timer.setRepeats(false);
528                                 timer.start();
529                         }
530                         return true;
531                 }
532
533                 protected boolean nextStateIfUnchanged(Object oThis, Object runnable, int stateNext, int levelNext) {
534                         StateHelper me = (StateHelper)(oThis);
535                         if (!me.interrupted)
536                                 ((Runnable) runnable).run();
537                         if (!me.interrupted && (stateNext != UNCHANGED || levelNext != UNCHANGED))
538                                 nextStatePriv(oThis, stateNext == UNCHANGED ? me.state : stateNext,
539                                                 levelNext == UNCHANGED ? me.level : levelNext);
540                         return true;
541                 }
542
543                 /**
544                  * sleep and then execute the next state
545                  * @param ms
546                  */
547                 public void sleep(int ms) {
548                         int next = stateNext;
549                         delayedState(ms, next);
550                 }
551         }
552         
553         /**
554          * open a "url-like" input stream
555          * @param base
556          * @param fileName
557          * @return
558          */
559         public static BufferedInputStream openStream(Class<?> base, String fileName) {
560                 String s = (String) getResource(base, fileName, String.class);
561         return new BufferedInputStream(new ByteArrayInputStream(s.getBytes()));
562         }
563
564
565         public static class Performance {
566
567                 public final static int TIME_RESET = 0;
568
569                 public final static int TIME_MARK = 1;
570
571                 public static final int TIME_SET = 2;
572
573                 public static final int TIME_GET = 3;
574
575                 public static long time, mark, set, duration;
576
577                 /**
578                  * typical usage:
579                  * 
580                  * Performance.timeCheck(null, Platform.TIME_MARK);
581                  * 
582                  * ...
583                  * 
584                  * Performance.timeCheck("some message", Platform.TIME_MARK);
585                  * 
586                  * reset...[set/mark]n...get  (total time) (time spent between set and mark)
587                  * 
588                  * set...get   (total time) (time spent between set and get)
589                  * 
590                  * long t0 = now(0); ........ ; dt = now(t0); (time since t0)e
591                  * 
592                  * @param msg
593                  * @param mode
594                  */
595                 public static void timeCheck(String msg, int mode) {
596                         msg = timeCheckStr(msg, mode);
597                         if (msg != null)
598                                 System.err.println(msg);
599                 }
600
601                 public static long now(long t) {
602                         return System.currentTimeMillis() - t;
603                 }
604                 
605                 public static String timeCheckStr(String msg, int mode) {
606                         long t = System.currentTimeMillis();
607                         switch (mode) {
608                         case TIME_RESET:
609                                 time = mark = t;
610                                 duration = set = 0;
611                                 if (msg != null) {
612                                         return ("Platform: timer reset\t\t\t" + msg);
613                                 }
614                                 break;
615                         case TIME_SET:
616                                 if (time == 0)
617                                         time = t;
618                                 set = t;
619                                 break;
620                         case TIME_MARK:
621                                 if (set > 0) {
622                                         // total time between set/mark points
623                                         duration += (t - set);
624                                 } else {
625                                         if (time == 0) {
626                                                 time = mark = t;
627                                         }
628                                         if (msg != null) {
629                                                 long m0 = mark;
630                                                 mark = t;
631                                                 return ("Platform: timer mark\t" + ((t - time) / 1000f) + "\t" + ((t - m0) / 1000f) + "\t"
632                                                                 + msg);
633                                         }
634                                         mark = t;
635                                 }
636                                 break;
637                         case TIME_GET:
638                                 if (msg != null) {
639                                         if (mark < set)
640                                                 duration = t - set;
641                                         return ("Platform: timer get\t" + ((t - time) / 1000f) + "\t" + ((duration) / 1000f) + "\t" + msg);
642                                 }
643                                 set = 0;
644                                 break;
645                         }
646                         return null;
647                 }
648
649         }
650
651 }