JAL-3446 test for simpler key mask operation
[jalview.git] / src / jalview / util / Platform.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.util;
22
23 import java.awt.Component;
24 import java.awt.Dimension;
25 import java.awt.GraphicsEnvironment;
26 import java.awt.Toolkit;
27 import java.awt.event.KeyEvent;
28 import java.awt.event.MouseEvent;
29 import java.io.BufferedReader;
30 import java.io.File;
31 import java.io.FileOutputStream;
32 import java.io.FileReader;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.InputStreamReader;
36 import java.io.Reader;
37 import java.lang.reflect.Method;
38 import java.net.URL;
39 import java.util.Properties;
40 import java.util.logging.ConsoleHandler;
41 import java.util.logging.Level;
42 import java.util.logging.Logger;
43
44 import javax.swing.SwingUtilities;
45
46 import org.json.simple.parser.JSONParser;
47 import org.json.simple.parser.ParseException;
48
49 import com.stevesoft.pat.Regex;
50
51 import jalview.javascript.json.JSON;
52 import swingjs.api.JSUtilI;
53
54 /**
55  * System platform information used by Applet and Application
56  * 
57  * @author Jim Procter
58  */
59
60 public class Platform
61 {
62
63   private static boolean isJS = /** @j2sNative true || */
64           false;
65
66   private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
67           isWin = null;
68
69   private static Boolean isHeadless = null;
70
71   private static swingjs.api.JSUtilI jsutil;
72   
73
74   static
75   {
76     if (isJS)
77     {
78       try
79       {
80         jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
81       } catch (InstantiationException | IllegalAccessException
82               | ClassNotFoundException e)
83       {
84         e.printStackTrace();
85       }
86     }
87   }
88   // private static Boolean isHeadless = null;
89
90   /**
91    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
92    * 
93    * @return
94    */
95   public static boolean isMac()
96   {
97     return (isMac == null
98             ? (isMac = (System.getProperty("os.name").indexOf("Mac") >= 0))
99             : isMac);
100   }
101
102   public static int SHIFT_DOWN_MASK = KeyEvent.SHIFT_DOWN_MASK;
103
104   public static int ALT_DOWN_MASK = KeyEvent.ALT_DOWN_MASK;
105
106   public static int SHORTCUT_KEY_MASK = (Platform.isMac() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK);
107  
108   static
109   {
110     if (!GraphicsEnvironment.isHeadless())
111     {
112       try
113       {
114
115         Class<? extends Toolkit> tk = Toolkit.getDefaultToolkit().getClass();
116         Method method = tk.getMethod("getMenuShortcutKeyMaskEx");
117         if (method == null)
118           method = tk.getMethod("getMenuShortcutKeyMask");
119         SHORTCUT_KEY_MASK = ((int) method.invoke(tk, new Object[0]));
120         if (SHORTCUT_KEY_MASK <= 0xF)
121         {
122           // shift this into the extended region (was Java 8)
123           SHORTCUT_KEY_MASK = SHORTCUT_KEY_MASK << 6;
124         }
125       } catch (Exception e)
126       {
127       }
128     }
129   }
130
131   /**
132    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
133    * 
134    * @return
135    */
136   public static boolean isWin()
137   {
138     return (isWin == null
139             ? (isWin = (System.getProperty("os.name").indexOf("Win") >= 0))
140             : isWin);
141   }
142
143   /**
144    * 
145    * @return true if HTML5 JavaScript
146    */
147   public static boolean isJS()
148   {
149     return isJS;
150   }
151
152   /**
153    * sorry folks - Macs really are different
154    * 
155    * BH: disabled for SwingJS -- will need to check key-press issues
156    * 
157    * @return true if we do things in a special way.
158    */
159   public static boolean isAMacAndNotJS()
160   {
161     return (isNoJSMac == null ? (isNoJSMac = !isJS && isMac()) : isNoJSMac);
162   }
163
164   /**
165    * Check if we are on a Microsoft plaform...
166    * 
167    * @return true if we have to cope with another platform variation
168    */
169   public static boolean isWindowsAndNotJS()
170   {
171     return (isNoJSWin == null ? (isNoJSWin = !isJS && isWin()) : isNoJSWin);
172   }
173
174   /**
175    *
176    * @return true if we are running in non-interactive no UI mode
177    */
178   public static boolean isHeadless()
179   {
180     if (isHeadless == null)
181     {
182       isHeadless = "true".equals(System.getProperty("java.awt.headless"));
183     }
184     return isHeadless;
185   }
186
187   /**
188    * 
189    * @return nominal maximum command line length for this platform
190    */
191   public static int getMaxCommandLineLength()
192   {
193     // TODO: determine nominal limits for most platforms.
194     return 2046; // this is the max length for a windows NT system.
195   }
196
197   /**
198    * Answers the input with every backslash replaced with a double backslash (an
199    * 'escaped' single backslash)
200    * 
201    * @param s
202    * @return
203    */
204   public static String escapeBackslashes(String s)
205   {
206     return s == null ? null : s.replace("\\", "\\\\");
207   }
208
209   /**
210    * Answers true if the mouse event has Meta-down (Command key on Mac) or
211    * Ctrl-down (on other o/s). Note this answers _false_ if the Ctrl key is
212    * pressed instead of the Meta/Cmd key on Mac. To test for Ctrl-pressed on
213    * Mac, you can use e.isPopupTrigger().
214    * 
215    * @param e
216    * @return
217    */
218   public static boolean isControlDown(MouseEvent e)
219   {
220     return isControlDown(e, isMac());
221   }
222
223   /**
224    * Overloaded version of method (to allow unit testing)
225    * 
226    * @param e
227    * @param aMac
228    * @return
229    */
230   protected static boolean isControlDown(MouseEvent e, boolean aMac)
231   {
232     
233     System.out.println(e.isPopupTrigger() 
234             + " " + ((SHORTCUT_KEY_MASK & e.getModifiersEx()) != 0) 
235             + " " + e.isControlDown());
236     return (aMac ? !e.isPopupTrigger()
237             && (SHORTCUT_KEY_MASK & e.getModifiersEx()) != 0
238             : e.isControlDown());
239     }
240
241   // BH: I don't know about that previous method. Here is what SwingJS uses.
242   // Notice the distinction in mouse events. (BUTTON3_MASK == META)
243   //
244   // private static boolean isPopupTrigger(int id, int mods, boolean isWin) {
245   // boolean rt = ((mods & InputEvent.BUTTON3_MASK) != 0);
246   // if (isWin) {
247   // if (id != MouseEvent.MOUSE_RELEASED)
248   // return false;
249   ////
250   //// // Oddly, Windows returns InputEvent.META_DOWN_MASK on release, though
251   //// // BUTTON3_DOWN_MASK for pressed. So here we just accept both.
252   ////
253   //// actually, we can use XXX_MASK, not XXX_DOWN_MASK and avoid this issue,
254   // because
255   //// J2S adds the appropriate extended (0x3FC0) and simple (0x3F) modifiers.
256   ////
257   // return rt;
258   // } else {
259   // // mac, linux, unix
260   // if (id != MouseEvent.MOUSE_PRESSED)
261   // return false;
262   // boolean lt = ((mods & InputEvent.BUTTON1_MASK) != 0);
263   // boolean ctrl = ((mods & InputEvent.CTRL_MASK) != 0);
264   // return rt || (ctrl && lt);
265   // }
266   // }
267   //
268
269   /**
270    * Windows (not Mac, Linux, or Unix) and right button to test for the
271    * right-mouse pressed event in Windows that would have opened a menu or a
272    * Mac.
273    * 
274    * @param e
275    * @return
276    */
277   public static boolean isWinRightButton(MouseEvent e)
278   {
279     // was !isAMac(), but that is true also for Linux and Unix and JS,
280
281     return isWin() && SwingUtilities.isRightMouseButton(e);
282   }
283
284   /**
285    * Windows (not Mac, Linux, or Unix) and middle button -- for mouse wheeling
286    * without pressing the button.
287    * 
288    * @param e
289    * @return
290    */
291   public static boolean isWinMiddleButton(MouseEvent e)
292   {
293     // was !isAMac(), but that is true also for Linux and Unix and JS
294     return isWin() && SwingUtilities.isMiddleMouseButton(e);
295   }
296
297   public static boolean allowMnemonics()
298   {
299     return !isMac();
300   }
301
302   public final static int TIME_RESET = 0;
303
304   public final static int TIME_MARK = 1;
305
306   public static final int TIME_SET = 2;
307
308   public static final int TIME_GET = 3;
309
310   public static long time, mark, set, duration;
311
312   /**
313    * typical usage:
314    * 
315    * Platform.timeCheck(null, Platform.TIME_MARK);
316    * 
317    * ...
318    * 
319    * Platform.timeCheck("some message", Platform.TIME_MARK);
320    * 
321    * reset...[set/mark]n...get
322    * 
323    * @param msg
324    * @param mode
325    */
326   public static void timeCheck(String msg, int mode)
327   {
328     long t = System.currentTimeMillis();
329     switch (mode)
330     {
331     case TIME_RESET:
332       time = mark = t;
333       duration = 0;
334       if (msg != null)
335       {
336         System.err.println("Platform: timer reset\t\t\t" + msg);
337       }
338       break;
339     case TIME_MARK:
340       if (set > 0)
341       {
342         // total time between set/mark points
343         duration += (t - set);
344       }
345       else
346       {
347         if (time == 0)
348         {
349           time = mark = t;
350         }
351         if (msg != null)
352         {
353           System.err.println("Platform: timer mark\t" + ((t - time) / 1000f)
354                   + "\t" + ((t - mark) / 1000f) + "\t" + msg);
355         }
356         mark = t;
357       }
358       break;
359     case TIME_SET:
360       set = t;
361       break;
362     case TIME_GET:
363       if (msg != null)
364       {
365         System.err.println("Platform: timer dur\t" + ((t - time) / 1000f)
366                 + "\t" + ((duration) / 1000f) + "\t" + msg);
367       }
368       set = 0;
369       break;
370     }
371   }
372
373   public static void cacheFileData(String path, Object data)
374   {
375     if (isJS && data != null)
376     {
377       jsutil.cachePathData(path, data);
378     }
379   }
380
381   public static void cacheFileData(File file)
382   {
383     if (isJS)
384     {
385       byte[] data = Platform.getFileBytes(file);
386       {
387         if (data != null)
388         {
389           cacheFileData(file.toString(), data);
390         }
391       }
392     }
393   }
394
395   public static byte[] getFileBytes(File f)
396   {
397     return (isJS && f != null ? jsutil.getBytes(f) : null);
398   }
399
400   public static byte[] getFileAsBytes(String fileStr)
401   {
402     if (isJS && fileStr != null)
403     {
404       byte[] bytes = (byte[]) jsutil.getFile(fileStr, false);
405       cacheFileData(fileStr, bytes);
406       return bytes;
407     }
408     return null;
409   }
410
411   public static String getFileAsString(String url)
412   {
413     if (isJS && url != null)
414     {
415       String ret = (String) jsutil.getFile(url, true);
416       cacheFileData(url, ret);
417       return ret;
418     }
419     return null;
420   }
421
422   public static boolean setFileBytes(File f, String urlstring)
423   {
424     if (isJS && f != null && urlstring != null)
425     {
426       @SuppressWarnings("unused")
427       byte[] bytes = getFileAsBytes(urlstring);
428       jsutil.setFileBytes(f, bytes);
429       return true;
430     }
431     return false;
432   }
433
434   public static void addJ2SBinaryType(String ext)
435   {
436     if (isJS)
437     {
438       jsutil.addBinaryFileType(ext);
439     }
440   }
441
442   /**
443    * Encode the URI using JavaScript encodeURIComponent
444    * 
445    * @param value
446    * @return encoded value
447    */
448   public static String encodeURI(String value)
449   {
450     /**
451      * @j2sNative value = encodeURIComponent(value);
452      */
453     return value;
454   }
455
456   /**
457    * Open the URL using a simple window call if this is JavaScript
458    * 
459    * @param url
460    * @return true if window has been opened
461    */
462   public static boolean openURL(String url) throws IOException
463   {
464     if (!isJS())
465     {
466       return false;
467     }
468     /**
469      * @j2sNative
470      * 
471      * 
472      *            window.open(url);
473      */
474     return true;
475   }
476
477   public static String getUniqueAppletID()
478   {
479     return (isJS ? (String) jsutil.getAppletAttribute("_uniqueId") : null);
480   }
481
482   /**
483    * Read the Info block for this applet.
484    * 
485    * @param prefix
486    *          "jalview_"
487    * @param p
488    * @return unique id for this applet
489    */
490   public static void readInfoProperties(String prefix, Properties p)
491   {
492     if (isJS)
493     {
494       String id = getUniqueAppletID();
495
496       String key = "";
497       String value = "";
498       @SuppressWarnings("unused")
499       Object info = jsutil.getAppletAttribute("__Info");
500       /**
501        * @j2sNative for (key in info) { value = info[key];
502        */
503
504       if (key.indexOf(prefix) == 0)
505       {
506         System.out.println("Platform id=" + id + " reading Info." + key
507                 + " = " + value);
508         p.put(key, value);
509
510       }
511
512       /**
513        * @j2sNative }
514        */
515     }
516   }
517
518   public static void setAjaxJSON(URL url)
519   {
520     if (isJS())
521     {
522       JSON.setAjax(url);
523     }
524   }
525
526   public static Object parseJSON(InputStream response)
527           throws IOException, ParseException
528   {
529     if (isJS())
530     {
531       return JSON.parse(response);
532     }
533
534     BufferedReader br = null;
535     try
536     {
537       br = new BufferedReader(new InputStreamReader(response, "UTF-8"));
538       return new JSONParser().parse(br);
539     } finally
540     {
541       if (br != null)
542       {
543         try
544         {
545           br.close();
546         } catch (IOException e)
547         {
548           // ignore
549         }
550       }
551     }
552   }
553
554   public static Object parseJSON(String json) throws ParseException
555   {
556     return (isJS() ? JSON.parse(json) : new JSONParser().parse(json));
557   }
558
559   public static Object parseJSON(Reader r)
560           throws IOException, ParseException
561   {
562     if (r == null)
563     {
564       return null;
565     }
566
567     if (!isJS())
568     {
569       return new JSONParser().parse(r);
570     }
571     // Using a file reader is not currently supported in SwingJS JavaScript
572
573     if (r instanceof FileReader)
574     {
575       throw new IOException(
576               "StringJS does not support FileReader parsing for JSON -- but it could...");
577     }
578     return JSON.parse(r);
579   }
580
581   /**
582    * Dump the input stream to an output file.
583    * 
584    * @param is
585    * @param outFile
586    * @throws IOException
587    *           if the file cannot be created or there is a problem reading the
588    *           input stream.
589    */
590   public static void streamToFile(InputStream is, File outFile)
591           throws IOException
592   {
593
594     if (isJS)
595     {
596       jsutil.setFileBytes(outFile, is);
597       return;
598     }
599
600     FileOutputStream fio = new FileOutputStream(outFile);
601     try
602     {
603       byte[] bb = new byte[32 * 1024];
604       int l;
605       while ((l = is.read(bb)) > 0)
606       {
607         fio.write(bb, 0, l);
608       }
609     } finally
610     {
611       fio.close();
612     }
613   }
614
615   /**
616    * Add a known domain that implements access-control-allow-origin:*
617    * 
618    * These should be reviewed periodically.
619    * 
620    * @param domain
621    *          for a service that is not allowing ajax
622    * 
623    * @author hansonr@stolaf.edu
624    * 
625    */
626   public static void addJ2SDirectDatabaseCall(String domain)
627   {
628
629     if (isJS)
630     {
631       jsutil.addDirectDatabaseCall(domain);
632
633       System.out.println(
634               "Platform adding known access-control-allow-origin * for domain "
635                       + domain);
636     }
637
638   }
639
640   public static void getURLCommandArguments()
641   {
642
643     /**
644      * Retrieve the first query field as command arguments to Jalview. Include
645      * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's __Info.args
646      * element to this value.
647      * 
648      * @j2sNative var a =
649      *            decodeURI((document.location.href.replace("&","?").split("?j2s")[0]
650      *            + "?").split("?")[1].split("#")[0]); a &&
651      *            (J2S.thisApplet.__Info.args = a.split(" "));
652      */
653
654   }
655
656   /**
657    * A (case sensitive) file path comparator that ignores the difference between
658    * / and \
659    * 
660    * @param path1
661    * @param path2
662    * @return
663    */
664   public static boolean pathEquals(String path1, String path2)
665   {
666     if (path1 == null)
667     {
668       return path2 == null;
669     }
670     if (path2 == null)
671     {
672       return false;
673     }
674     String p1 = path1.replace('\\', '/');
675     String p2 = path2.replace('\\', '/');
676     return p1.equals(p2);
677   }
678
679   ///////////// JAL-3253 Applet additions //////////////
680
681   /**
682    * Retrieve the object's embedded size from a div's style on a page if
683    * embedded in SwingJS.
684    * 
685    * @param frame
686    *          JFrame or JInternalFrame
687    * @param defaultWidth
688    *          use -1 to return null (no default size)
689    * @param defaultHeight
690    * @return the embedded dimensions or null (no default size or not embedded)
691    */
692   public static Dimension getDimIfEmbedded(Component frame,
693           int defaultWidth, int defaultHeight)
694   {
695     Dimension d = null;
696     if (isJS)
697     {
698       d = (Dimension) getEmbeddedAttribute(frame, "dim");
699     }
700     return (d == null && defaultWidth >= 0
701             ? new Dimension(defaultWidth, defaultHeight)
702             : d);
703
704   }
705
706   public static Regex newRegex(String regex)
707   {
708     return newRegex(regex, null);
709   }
710
711   public static Regex newRegex(String searchString, String replaceString)
712   {
713     ensureRegex();
714     return (replaceString == null ? new Regex(searchString)
715             : new Regex(searchString, replaceString));
716   }
717
718   public static Regex newRegexPerl(String code)
719   {
720     ensureRegex();
721     return Regex.perlCode(code);
722   }
723
724   /**
725    * Initialize Java debug logging. A representative sample -- adapt as desired.
726    */
727   public static void startJavaLogging()
728   {
729     /**
730      * @j2sIgnore
731      */
732     {
733       logClass("java.awt.EventDispatchThread", "java.awt.EventQueue",
734               "java.awt.Component", "java.awt.focus.Component",
735               "java.awt.event.Component",
736               "java.awt.focus.DefaultKeyboardFocusManager");
737     }
738   }
739
740   /**
741    * Initiate Java logging for a given class. Only for Java, not JavaScript;
742    * Allows debugging of complex event processing.
743    * 
744    * @param className
745    */
746   public static void logClass(String... classNames)
747   {
748     /**
749      * @j2sIgnore
750      * 
751      * 
752      */
753     {
754       Logger rootLogger = Logger.getLogger("");
755       rootLogger.setLevel(Level.ALL);
756       ConsoleHandler consoleHandler = new ConsoleHandler();
757       consoleHandler.setLevel(Level.ALL);
758       for (int i = classNames.length; --i >= 0;)
759       {
760         Logger logger = Logger.getLogger(classNames[i]);
761         logger.setLevel(Level.ALL);
762         logger.addHandler(consoleHandler);
763       }
764     }
765   }
766
767   /**
768    * load a resource -- probably a core file -- if and only if a particular
769    * class has not been instantialized. We use a String here because if we used
770    * a .class object, that reference itself would simply load the class, and we
771    * want the core package to include that as well.
772    * 
773    * @param resourcePath
774    * @param className
775    */
776   public static void loadStaticResource(String resourcePath,
777           String className)
778   {
779     if (isJS)
780     {
781       jsutil.loadResourceIfClassUnknown(resourcePath, className);
782     }
783   }
784
785   public static void ensureRegex()
786   {
787     if (isJS)
788     {
789       loadStaticResource("core/core_stevesoft.z.js",
790               "com.stevesoft.pat.Regex");
791     }
792   }
793
794   /**
795    * Set the "app" property of the HTML5 applet object, for example,
796    * "testApplet.app", to point to the Jalview instance. This will be the object
797    * that page developers use that is similar to the original Java applet object
798    * that was accessed via LiveConnect.
799    * 
800    * @param j
801    */
802   public static void setAppClass(Object j)
803   {
804     if (isJS)
805     {
806       jsutil.setAppletAttribute("app", j);
807     }
808   }
809
810   /**
811    *
812    * If this frame ia embedded in a web page, return a known type.
813    * 
814    * @param frame
815    *          a JFrame or JInternalFrame
816    * @param type
817    * @return null if frame is not embedded.
818    */
819   public static Object getEmbeddedAttribute(Component frame, String type)
820   {
821     return (isJS ? jsutil.getEmbeddedAttribute(frame, type) : null);
822   }
823
824   public static void stackTrace()
825   {
826     try
827     {
828       throw new NullPointerException();
829     } catch (Exception e)
830     {
831       e.printStackTrace();
832     }
833
834   }
835
836   public static URL getDocumentBase()
837   {
838     return (isJS ? jsutil.getDocumentBase() : null);
839   }
840
841   public static URL getCodeBase()
842   {
843     return (isJS ? jsutil.getCodeBase() : null);
844   }
845
846 }