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