JAL-4186 Turn JvOptionPane Callable<Void> handlers back into Runnable for jalviewjs...
[jalview.git] / src / jalview / gui / Desktop.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.gui;
22
23 import java.awt.BorderLayout;
24 import java.awt.Color;
25 import java.awt.Dimension;
26 import java.awt.FontMetrics;
27 import java.awt.Graphics;
28 import java.awt.Graphics2D;
29 import java.awt.GridLayout;
30 import java.awt.Point;
31 import java.awt.Rectangle;
32 import java.awt.Toolkit;
33 import java.awt.Window;
34 import java.awt.datatransfer.Clipboard;
35 import java.awt.datatransfer.ClipboardOwner;
36 import java.awt.datatransfer.DataFlavor;
37 import java.awt.datatransfer.Transferable;
38 import java.awt.dnd.DnDConstants;
39 import java.awt.dnd.DropTargetDragEvent;
40 import java.awt.dnd.DropTargetDropEvent;
41 import java.awt.dnd.DropTargetEvent;
42 import java.awt.dnd.DropTargetListener;
43 import java.awt.event.ActionEvent;
44 import java.awt.event.ActionListener;
45 import java.awt.event.InputEvent;
46 import java.awt.event.KeyEvent;
47 import java.awt.event.MouseAdapter;
48 import java.awt.event.MouseEvent;
49 import java.awt.event.WindowAdapter;
50 import java.awt.event.WindowEvent;
51 import java.awt.geom.AffineTransform;
52 import java.beans.PropertyChangeEvent;
53 import java.beans.PropertyChangeListener;
54 import java.io.File;
55 import java.io.FileWriter;
56 import java.io.IOException;
57 import java.lang.reflect.Field;
58 import java.net.URL;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.HashMap;
62 import java.util.Hashtable;
63 import java.util.List;
64 import java.util.ListIterator;
65 import java.util.Locale;
66 import java.util.Vector;
67 import java.util.concurrent.ExecutorService;
68 import java.util.concurrent.Executors;
69 import java.util.concurrent.Semaphore;
70
71 import javax.swing.AbstractAction;
72 import javax.swing.Action;
73 import javax.swing.ActionMap;
74 import javax.swing.Box;
75 import javax.swing.BoxLayout;
76 import javax.swing.DefaultDesktopManager;
77 import javax.swing.DesktopManager;
78 import javax.swing.InputMap;
79 import javax.swing.JButton;
80 import javax.swing.JCheckBox;
81 import javax.swing.JComboBox;
82 import javax.swing.JComponent;
83 import javax.swing.JDesktopPane;
84 import javax.swing.JFrame;
85 import javax.swing.JInternalFrame;
86 import javax.swing.JLabel;
87 import javax.swing.JMenuItem;
88 import javax.swing.JPanel;
89 import javax.swing.JPopupMenu;
90 import javax.swing.JProgressBar;
91 import javax.swing.JTextField;
92 import javax.swing.KeyStroke;
93 import javax.swing.SwingUtilities;
94 import javax.swing.WindowConstants;
95 import javax.swing.event.HyperlinkEvent;
96 import javax.swing.event.HyperlinkEvent.EventType;
97 import javax.swing.event.InternalFrameAdapter;
98 import javax.swing.event.InternalFrameEvent;
99
100 import org.stackoverflowusers.file.WindowsShortcut;
101
102 import jalview.api.AlignViewportI;
103 import jalview.api.AlignmentViewPanel;
104 import jalview.api.structures.JalviewStructureDisplayI;
105 import jalview.bin.Cache;
106 import jalview.bin.Jalview;
107 import jalview.datamodel.Alignment;
108 import jalview.datamodel.HiddenColumns;
109 import jalview.datamodel.Sequence;
110 import jalview.datamodel.SequenceI;
111 import jalview.gui.ImageExporter.ImageWriterI;
112 import jalview.gui.QuitHandler.QResponse;
113 import jalview.io.BackupFiles;
114 import jalview.io.DataSourceType;
115 import jalview.io.FileFormat;
116 import jalview.io.FileFormatException;
117 import jalview.io.FileFormatI;
118 import jalview.io.FileFormats;
119 import jalview.io.FileLoader;
120 import jalview.io.FormatAdapter;
121 import jalview.io.IdentifyFile;
122 import jalview.io.JalviewFileChooser;
123 import jalview.io.JalviewFileView;
124 import jalview.jbgui.GSplitFrame;
125 import jalview.jbgui.GStructureViewer;
126 import jalview.project.Jalview2XML;
127 import jalview.structure.StructureSelectionManager;
128 import jalview.urls.IdOrgSettings;
129 import jalview.util.BrowserLauncher;
130 import jalview.util.ChannelProperties;
131 import jalview.util.ImageMaker.TYPE;
132 import jalview.util.LaunchUtils;
133 import jalview.util.MessageManager;
134 import jalview.util.Platform;
135 import jalview.util.ShortcutKeyMaskExWrapper;
136 import jalview.util.UrlConstants;
137 import jalview.viewmodel.AlignmentViewport;
138 import jalview.ws.params.ParamManager;
139 import jalview.ws.utils.UrlDownloadClient;
140
141 /**
142  * Jalview Desktop
143  * 
144  * 
145  * @author $author$
146  * @version $Revision: 1.155 $
147  */
148 public class Desktop extends jalview.jbgui.GDesktop
149         implements DropTargetListener, ClipboardOwner, IProgressIndicator,
150         jalview.api.StructureSelectionManagerProvider
151 {
152   private static final String CITATION;
153   static
154   {
155     URL bg_logo_url = ChannelProperties.getImageURL(
156             "bg_logo." + String.valueOf(SplashScreen.logoSize));
157     URL uod_logo_url = ChannelProperties.getImageURL(
158             "uod_banner." + String.valueOf(SplashScreen.logoSize));
159     boolean logo = (bg_logo_url != null || uod_logo_url != null);
160     StringBuilder sb = new StringBuilder();
161     sb.append(
162             "<br><br>Jalview is free software released under GPLv3.<br><br>Development is managed by The Barton Group, University of Dundee, Scotland, UK.");
163     if (logo)
164     {
165       sb.append("<br>");
166     }
167     sb.append(bg_logo_url == null ? ""
168             : "<img alt=\"Barton Group logo\" src=\""
169                     + bg_logo_url.toString() + "\">");
170     sb.append(uod_logo_url == null ? ""
171             : "&nbsp;<img alt=\"University of Dundee shield\" src=\""
172                     + uod_logo_url.toString() + "\">");
173     sb.append(
174             "<br><br>For help, see <a href=\"https://www.jalview.org/faq\">www.jalview.org/faq</a> and join <a href=\"https://discourse.jalview.org\">discourse.jalview.org</a>");
175     sb.append("<br><br>If  you use Jalview, please cite:"
176             + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
177             + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
178             + "<br>Bioinformatics <a href=\"https://doi.org/10.1093/bioinformatics/btp033\">doi: 10.1093/bioinformatics/btp033</a>");
179     CITATION = sb.toString();
180   }
181
182   private static final String DEFAULT_AUTHORS = "The Jalview Authors (See AUTHORS file for current list)";
183
184   private static int DEFAULT_MIN_WIDTH = 300;
185
186   private static int DEFAULT_MIN_HEIGHT = 250;
187
188   private static int ALIGN_FRAME_DEFAULT_MIN_WIDTH = 600;
189
190   private static int ALIGN_FRAME_DEFAULT_MIN_HEIGHT = 70;
191
192   private static final String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES";
193
194   public static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT";
195
196   public static HashMap<String, FileWriter> savingFiles = new HashMap<String, FileWriter>();
197
198   private static int DRAG_MODE = JDesktopPane.OUTLINE_DRAG_MODE;
199
200   public static void setLiveDragMode(boolean b)
201   {
202     DRAG_MODE = b ? JDesktopPane.LIVE_DRAG_MODE
203             : JDesktopPane.OUTLINE_DRAG_MODE;
204     if (desktop != null)
205       desktop.setDragMode(DRAG_MODE);
206   }
207
208   private JalviewChangeSupport changeSupport = new JalviewChangeSupport();
209
210   public static boolean nosplash = false;
211
212   /**
213    * news reader - null if it was never started.
214    */
215   private BlogReader jvnews = null;
216
217   private File projectFile;
218
219   /**
220    * @param listener
221    * @see jalview.gui.JalviewChangeSupport#addJalviewPropertyChangeListener(java.beans.PropertyChangeListener)
222    */
223   public void addJalviewPropertyChangeListener(
224           PropertyChangeListener listener)
225   {
226     changeSupport.addJalviewPropertyChangeListener(listener);
227   }
228
229   /**
230    * @param propertyName
231    * @param listener
232    * @see jalview.gui.JalviewChangeSupport#addJalviewPropertyChangeListener(java.lang.String,
233    *      java.beans.PropertyChangeListener)
234    */
235   public void addJalviewPropertyChangeListener(String propertyName,
236           PropertyChangeListener listener)
237   {
238     changeSupport.addJalviewPropertyChangeListener(propertyName, listener);
239   }
240
241   /**
242    * @param propertyName
243    * @param listener
244    * @see jalview.gui.JalviewChangeSupport#removeJalviewPropertyChangeListener(java.lang.String,
245    *      java.beans.PropertyChangeListener)
246    */
247   public void removeJalviewPropertyChangeListener(String propertyName,
248           PropertyChangeListener listener)
249   {
250     changeSupport.removeJalviewPropertyChangeListener(propertyName,
251             listener);
252   }
253
254   /** Singleton Desktop instance */
255   public static Desktop instance;
256
257   public static MyDesktopPane desktop;
258
259   public static MyDesktopPane getDesktop()
260   {
261     // BH 2018 could use currentThread() here as a reference to a
262     // Hashtable<Thread, MyDesktopPane> in JavaScript
263     return desktop;
264   }
265
266   static int openFrameCount = 0;
267
268   static final int xOffset = 30;
269
270   static final int yOffset = 30;
271
272   public static jalview.ws.jws1.Discoverer discoverer;
273
274   public static Object[] jalviewClipboard;
275
276   public static boolean internalCopy = false;
277
278   static int fileLoadingCount = 0;
279
280   class MyDesktopManager implements DesktopManager
281   {
282
283     private DesktopManager delegate;
284
285     public MyDesktopManager(DesktopManager delegate)
286     {
287       this.delegate = delegate;
288     }
289
290     @Override
291     public void activateFrame(JInternalFrame f)
292     {
293       try
294       {
295         delegate.activateFrame(f);
296       } catch (NullPointerException npe)
297       {
298         Point p = getMousePosition();
299         instance.showPasteMenu(p.x, p.y);
300       }
301     }
302
303     @Override
304     public void beginDraggingFrame(JComponent f)
305     {
306       delegate.beginDraggingFrame(f);
307     }
308
309     @Override
310     public void beginResizingFrame(JComponent f, int direction)
311     {
312       delegate.beginResizingFrame(f, direction);
313     }
314
315     @Override
316     public void closeFrame(JInternalFrame f)
317     {
318       delegate.closeFrame(f);
319     }
320
321     @Override
322     public void deactivateFrame(JInternalFrame f)
323     {
324       delegate.deactivateFrame(f);
325     }
326
327     @Override
328     public void deiconifyFrame(JInternalFrame f)
329     {
330       delegate.deiconifyFrame(f);
331     }
332
333     @Override
334     public void dragFrame(JComponent f, int newX, int newY)
335     {
336       if (newY < 0)
337       {
338         newY = 0;
339       }
340       delegate.dragFrame(f, newX, newY);
341     }
342
343     @Override
344     public void endDraggingFrame(JComponent f)
345     {
346       delegate.endDraggingFrame(f);
347       desktop.repaint();
348     }
349
350     @Override
351     public void endResizingFrame(JComponent f)
352     {
353       delegate.endResizingFrame(f);
354       desktop.repaint();
355     }
356
357     @Override
358     public void iconifyFrame(JInternalFrame f)
359     {
360       delegate.iconifyFrame(f);
361     }
362
363     @Override
364     public void maximizeFrame(JInternalFrame f)
365     {
366       delegate.maximizeFrame(f);
367     }
368
369     @Override
370     public void minimizeFrame(JInternalFrame f)
371     {
372       delegate.minimizeFrame(f);
373     }
374
375     @Override
376     public void openFrame(JInternalFrame f)
377     {
378       delegate.openFrame(f);
379     }
380
381     @Override
382     public void resizeFrame(JComponent f, int newX, int newY, int newWidth,
383             int newHeight)
384     {
385       if (newY < 0)
386       {
387         newY = 0;
388       }
389       delegate.resizeFrame(f, newX, newY, newWidth, newHeight);
390     }
391
392     @Override
393     public void setBoundsForFrame(JComponent f, int newX, int newY,
394             int newWidth, int newHeight)
395     {
396       delegate.setBoundsForFrame(f, newX, newY, newWidth, newHeight);
397     }
398
399     // All other methods, simply delegate
400
401   }
402
403   /**
404    * Creates a new Desktop object.
405    */
406   public Desktop()
407   {
408     super();
409     /**
410      * A note to implementors. It is ESSENTIAL that any activities that might
411      * block are spawned off as threads rather than waited for during this
412      * constructor.
413      */
414     instance = this;
415
416     doConfigureStructurePrefs();
417     setTitle(ChannelProperties.getProperty("app_name") + " "
418             + Cache.getProperty("VERSION"));
419
420     /**
421      * Set taskbar "grouped windows" name for linux desktops (works in GNOME and
422      * KDE). This uses sun.awt.X11.XToolkit.awtAppClassName which is not
423      * officially documented or guaranteed to exist, so we access it via
424      * reflection. There appear to be unfathomable criteria about what this
425      * string can contain, and it if doesn't meet those criteria then "java"
426      * (KDE) or "jalview-bin-Jalview" (GNOME) is used. "Jalview", "Jalview
427      * Develop" and "Jalview Test" seem okay, but "Jalview non-release" does
428      * not. The reflection access may generate a warning: WARNING: An illegal
429      * reflective access operation has occurred WARNING: Illegal reflective
430      * access by jalview.gui.Desktop () to field
431      * sun.awt.X11.XToolkit.awtAppClassName which I don't think can be avoided.
432      */
433     if (Platform.isLinux())
434     {
435       if (LaunchUtils.getJavaVersion() >= 11)
436       {
437         /*
438          * Send this message to stderr as the warning that follows (due to
439          * reflection) also goes to stderr.
440          */
441         System.err.println(
442                 "Linux platform only! You may have the following warning next: \"WARNING: An illegal reflective access operation has occurred\"\nThis is expected and cannot be avoided, sorry about that.");
443       }
444       try
445       {
446         Toolkit xToolkit = Toolkit.getDefaultToolkit();
447         Field[] declaredFields = xToolkit.getClass().getDeclaredFields();
448         Field awtAppClassNameField = null;
449
450         if (Arrays.stream(declaredFields)
451                 .anyMatch(f -> f.getName().equals("awtAppClassName")))
452         {
453           awtAppClassNameField = xToolkit.getClass()
454                   .getDeclaredField("awtAppClassName");
455         }
456
457         String title = ChannelProperties.getProperty("app_name");
458         if (awtAppClassNameField != null)
459         {
460           awtAppClassNameField.setAccessible(true);
461           awtAppClassNameField.set(xToolkit, title);
462         }
463         else
464         {
465           jalview.bin.Console.debug("XToolkit: awtAppClassName not found");
466         }
467       } catch (Exception e)
468       {
469         jalview.bin.Console.debug("Error setting awtAppClassName");
470         jalview.bin.Console.trace(Cache.getStackTraceString(e));
471       }
472     }
473
474     setIconImages(ChannelProperties.getIconList());
475
476     // override quit handling when GUI OS close [X] button pressed
477     this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
478     addWindowListener(new WindowAdapter()
479     {
480       @Override
481       public void windowClosing(WindowEvent ev)
482       {
483         QuitHandler.QResponse ret = desktopQuit(true, true); // ui, disposeFlag
484       }
485     });
486
487     boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false);
488
489     boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", false);
490     desktop = new MyDesktopPane(selmemusage);
491
492     showMemusage.setSelected(selmemusage);
493     desktop.setBackground(Color.white);
494
495     getContentPane().setLayout(new BorderLayout());
496     // alternate config - have scrollbars - see notes in JAL-153
497     // JScrollPane sp = new JScrollPane();
498     // sp.getViewport().setView(desktop);
499     // getContentPane().add(sp, BorderLayout.CENTER);
500
501     // BH 2018 - just an experiment to try unclipped JInternalFrames.
502     if (Platform.isJS())
503     {
504       getRootPane().putClientProperty("swingjs.overflow.hidden", "false");
505     }
506
507     getContentPane().add(desktop, BorderLayout.CENTER);
508     desktop.setDragMode(DRAG_MODE);
509
510     // This line prevents Windows Look&Feel resizing all new windows to maximum
511     // if previous window was maximised
512     desktop.setDesktopManager(new MyDesktopManager(
513             Platform.isJS() ? desktop.getDesktopManager()
514                     : new DefaultDesktopManager()));
515     /*
516     (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
517             : Platform.isAMacAndNotJS()
518                     ? new AquaInternalFrameManager(
519                             desktop.getDesktopManager())
520                     : desktop.getDesktopManager())));
521                     */
522
523     Rectangle dims = getLastKnownDimensions("");
524     if (dims != null)
525     {
526       setBounds(dims);
527     }
528     else
529     {
530       Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
531       int xPos = Math.max(5, (screenSize.width - 900) / 2);
532       int yPos = Math.max(5, (screenSize.height - 650) / 2);
533       setBounds(xPos, yPos, 900, 650);
534     }
535
536     if (!Platform.isJS())
537     /**
538      * Java only
539      * 
540      * @j2sIgnore
541      */
542     {
543       jconsole = new Console(this, showjconsole);
544       jconsole.setHeader(Cache.getVersionDetailsForConsole());
545       showConsole(showjconsole);
546
547       showNews.setVisible(false);
548
549       experimentalFeatures.setSelected(showExperimental());
550
551       getIdentifiersOrgData();
552
553       checkURLLinks();
554
555       // Spawn a thread that shows the splashscreen
556       if (!nosplash)
557       {
558         SwingUtilities.invokeLater(new Runnable()
559         {
560           @Override
561           public void run()
562           {
563             new SplashScreen(true);
564           }
565         });
566       }
567
568       // Thread off a new instance of the file chooser - this reduces the time
569       // it
570       // takes to open it later on.
571       new Thread(new Runnable()
572       {
573         @Override
574         public void run()
575         {
576           jalview.bin.Console.debug("Filechooser init thread started.");
577           String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
578           JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"),
579                   fileFormat);
580           jalview.bin.Console.debug("Filechooser init thread finished.");
581         }
582       }).start();
583       // Add the service change listener
584       changeSupport.addJalviewPropertyChangeListener("services",
585               new PropertyChangeListener()
586               {
587
588                 @Override
589                 public void propertyChange(PropertyChangeEvent evt)
590                 {
591                   jalview.bin.Console
592                           .debug("Firing service changed event for "
593                                   + evt.getNewValue());
594                   JalviewServicesChanged(evt);
595                 }
596               });
597     }
598
599     this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
600
601     MouseAdapter ma;
602     this.addMouseListener(ma = new MouseAdapter()
603     {
604       @Override
605       public void mousePressed(MouseEvent evt)
606       {
607         if (evt.isPopupTrigger()) // Mac
608         {
609           showPasteMenu(evt.getX(), evt.getY());
610         }
611       }
612
613       @Override
614       public void mouseReleased(MouseEvent evt)
615       {
616         if (evt.isPopupTrigger()) // Windows
617         {
618           showPasteMenu(evt.getX(), evt.getY());
619         }
620       }
621     });
622     desktop.addMouseListener(ma);
623   }
624
625   /**
626    * Answers true if user preferences to enable experimental features is True
627    * (on), else false
628    * 
629    * @return
630    */
631   public boolean showExperimental()
632   {
633     String experimental = Cache.getDefault(EXPERIMENTAL_FEATURES,
634             Boolean.FALSE.toString());
635     return Boolean.valueOf(experimental).booleanValue();
636   }
637
638   public void doConfigureStructurePrefs()
639   {
640     // configure services
641     StructureSelectionManager ssm = StructureSelectionManager
642             .getStructureSelectionManager(this);
643     if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
644     {
645       ssm.setAddTempFacAnnot(
646               Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
647       ssm.setProcessSecondaryStructure(
648               Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
649       // JAL-3915 - RNAView is no longer an option so this has no effect
650       ssm.setSecStructServices(
651               Cache.getDefault(Preferences.USE_RNAVIEW, false));
652     }
653     else
654     {
655       ssm.setAddTempFacAnnot(false);
656       ssm.setProcessSecondaryStructure(false);
657       ssm.setSecStructServices(false);
658     }
659   }
660
661   public void checkForNews()
662   {
663     final Desktop me = this;
664     // Thread off the news reader, in case there are connection problems.
665     new Thread(new Runnable()
666     {
667       @Override
668       public void run()
669       {
670         jalview.bin.Console.debug("Starting news thread.");
671         jvnews = new BlogReader(me);
672         showNews.setVisible(true);
673         jalview.bin.Console.debug("Completed news thread.");
674       }
675     }).start();
676   }
677
678   public void getIdentifiersOrgData()
679   {
680     if (Cache.getProperty("NOIDENTIFIERSSERVICE") == null)
681     {// Thread off the identifiers fetcher
682       new Thread(new Runnable()
683       {
684         @Override
685         public void run()
686         {
687           jalview.bin.Console
688                   .debug("Downloading data from identifiers.org");
689           try
690           {
691             UrlDownloadClient.download(IdOrgSettings.getUrl(),
692                     IdOrgSettings.getDownloadLocation());
693           } catch (IOException e)
694           {
695             jalview.bin.Console
696                     .debug("Exception downloading identifiers.org data"
697                             + e.getMessage());
698           }
699         }
700       }).start();
701       ;
702     }
703   }
704
705   @Override
706   protected void showNews_actionPerformed(ActionEvent e)
707   {
708     showNews(showNews.isSelected());
709   }
710
711   void showNews(boolean visible)
712   {
713     jalview.bin.Console.debug((visible ? "Showing" : "Hiding") + " news.");
714     showNews.setSelected(visible);
715     if (visible && !jvnews.isVisible())
716     {
717       new Thread(new Runnable()
718       {
719         @Override
720         public void run()
721         {
722           long now = System.currentTimeMillis();
723           Desktop.instance.setProgressBar(
724                   MessageManager.getString("status.refreshing_news"), now);
725           jvnews.refreshNews();
726           Desktop.instance.setProgressBar(null, now);
727           jvnews.showNews();
728         }
729       }).start();
730     }
731   }
732
733   /**
734    * recover the last known dimensions for a jalview window
735    * 
736    * @param windowName
737    *          - empty string is desktop, all other windows have unique prefix
738    * @return null or last known dimensions scaled to current geometry (if last
739    *         window geom was known)
740    */
741   Rectangle getLastKnownDimensions(String windowName)
742   {
743     // TODO: lock aspect ratio for scaling desktop Bug #0058199
744     Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
745     String x = Cache.getProperty(windowName + "SCREEN_X");
746     String y = Cache.getProperty(windowName + "SCREEN_Y");
747     String width = Cache.getProperty(windowName + "SCREEN_WIDTH");
748     String height = Cache.getProperty(windowName + "SCREEN_HEIGHT");
749     if ((x != null) && (y != null) && (width != null) && (height != null))
750     {
751       int ix = Integer.parseInt(x), iy = Integer.parseInt(y),
752               iw = Integer.parseInt(width), ih = Integer.parseInt(height);
753       if (Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
754       {
755         // attempt #1 - try to cope with change in screen geometry - this
756         // version doesn't preserve original jv aspect ratio.
757         // take ratio of current screen size vs original screen size.
758         double sw = ((1f * screenSize.width) / (1f * Integer
759                 .parseInt(Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
760         double sh = ((1f * screenSize.height) / (1f * Integer
761                 .parseInt(Cache.getProperty("SCREENGEOMETRY_HEIGHT"))));
762         // rescale the bounds depending upon the current screen geometry.
763         ix = (int) (ix * sw);
764         iw = (int) (iw * sw);
765         iy = (int) (iy * sh);
766         ih = (int) (ih * sh);
767         while (ix >= screenSize.width)
768         {
769           jalview.bin.Console.debug(
770                   "Window geometry location recall error: shifting horizontal to within screenbounds.");
771           ix -= screenSize.width;
772         }
773         while (iy >= screenSize.height)
774         {
775           jalview.bin.Console.debug(
776                   "Window geometry location recall error: shifting vertical to within screenbounds.");
777           iy -= screenSize.height;
778         }
779         jalview.bin.Console.debug(
780                 "Got last known dimensions for " + windowName + ": x:" + ix
781                         + " y:" + iy + " width:" + iw + " height:" + ih);
782       }
783       // return dimensions for new instance
784       return new Rectangle(ix, iy, iw, ih);
785     }
786     return null;
787   }
788
789   void showPasteMenu(int x, int y)
790   {
791     JPopupMenu popup = new JPopupMenu();
792     JMenuItem item = new JMenuItem(
793             MessageManager.getString("label.paste_new_window"));
794     item.addActionListener(new ActionListener()
795     {
796       @Override
797       public void actionPerformed(ActionEvent evt)
798       {
799         paste();
800       }
801     });
802
803     popup.add(item);
804     popup.show(this, x, y);
805   }
806
807   public void paste()
808   {
809     // quick patch for JAL-4150 - needs some more work and test coverage
810     // TODO - unify below and AlignFrame.paste()
811     // TODO - write tests and fix AlignFrame.paste() which doesn't track if
812     // clipboard has come from a different alignment window than the one where
813     // paste has been called! JAL-4151
814
815     if (Desktop.jalviewClipboard != null)
816     {
817       // The clipboard was filled from within Jalview, we must use the
818       // sequences
819       // And dataset from the copied alignment
820       SequenceI[] newseq = (SequenceI[]) Desktop.jalviewClipboard[0];
821       // be doubly sure that we create *new* sequence objects.
822       SequenceI[] sequences = new SequenceI[newseq.length];
823       for (int i = 0; i < newseq.length; i++)
824       {
825         sequences[i] = new Sequence(newseq[i]);
826       }
827       Alignment alignment = new Alignment(sequences);
828       // dataset is inherited
829       alignment.setDataset((Alignment) Desktop.jalviewClipboard[1]);
830       AlignFrame af = new AlignFrame(alignment, AlignFrame.DEFAULT_WIDTH,
831               AlignFrame.DEFAULT_HEIGHT);
832       String newtitle = new String("Copied sequences");
833
834       if (Desktop.jalviewClipboard[2] != null)
835       {
836         HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
837         af.viewport.setHiddenColumns(hc);
838       }
839
840       Desktop.addInternalFrame(af, newtitle, AlignFrame.DEFAULT_WIDTH,
841               AlignFrame.DEFAULT_HEIGHT);
842
843     }
844     else
845     {
846       try
847       {
848         Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
849         Transferable contents = c.getContents(this);
850
851         if (contents != null)
852         {
853           String file = (String) contents
854                   .getTransferData(DataFlavor.stringFlavor);
855
856           FileFormatI format = new IdentifyFile().identify(file,
857                   DataSourceType.PASTE);
858
859           new FileLoader().LoadFile(file, DataSourceType.PASTE, format);
860
861         }
862       } catch (Exception ex)
863       {
864         System.out.println(
865                 "Unable to paste alignment from system clipboard:\n" + ex);
866       }
867     }
868   }
869
870   /**
871    * Adds and opens the given frame to the desktop
872    * 
873    * @param frame
874    *          Frame to show
875    * @param title
876    *          Visible Title
877    * @param w
878    *          width
879    * @param h
880    *          height
881    */
882   public static synchronized void addInternalFrame(
883           final JInternalFrame frame, String title, int w, int h)
884   {
885     addInternalFrame(frame, title, true, w, h, true, false);
886   }
887
888   /**
889    * Add an internal frame to the Jalview desktop
890    * 
891    * @param frame
892    *          Frame to show
893    * @param title
894    *          Visible Title
895    * @param makeVisible
896    *          When true, display frame immediately, otherwise, caller must call
897    *          setVisible themselves.
898    * @param w
899    *          width
900    * @param h
901    *          height
902    */
903   public static synchronized void addInternalFrame(
904           final JInternalFrame frame, String title, boolean makeVisible,
905           int w, int h)
906   {
907     addInternalFrame(frame, title, makeVisible, w, h, true, false);
908   }
909
910   /**
911    * Add an internal frame to the Jalview desktop and make it visible
912    * 
913    * @param frame
914    *          Frame to show
915    * @param title
916    *          Visible Title
917    * @param w
918    *          width
919    * @param h
920    *          height
921    * @param resizable
922    *          Allow resize
923    */
924   public static synchronized void addInternalFrame(
925           final JInternalFrame frame, String title, int w, int h,
926           boolean resizable)
927   {
928     addInternalFrame(frame, title, true, w, h, resizable, false);
929   }
930
931   /**
932    * Add an internal frame to the Jalview desktop
933    * 
934    * @param frame
935    *          Frame to show
936    * @param title
937    *          Visible Title
938    * @param makeVisible
939    *          When true, display frame immediately, otherwise, caller must call
940    *          setVisible themselves.
941    * @param w
942    *          width
943    * @param h
944    *          height
945    * @param resizable
946    *          Allow resize
947    * @param ignoreMinSize
948    *          Do not set the default minimum size for frame
949    */
950   public static synchronized void addInternalFrame(
951           final JInternalFrame frame, String title, boolean makeVisible,
952           int w, int h, boolean resizable, boolean ignoreMinSize)
953   {
954
955     // TODO: allow callers to determine X and Y position of frame (eg. via
956     // bounds object).
957     // TODO: consider fixing method to update entries in the window submenu with
958     // the current window title
959
960     frame.setTitle(title);
961     if (frame.getWidth() < 1 || frame.getHeight() < 1)
962     {
963       frame.setSize(w, h);
964     }
965     // THIS IS A PUBLIC STATIC METHOD, SO IT MAY BE CALLED EVEN IN
966     // A HEADLESS STATE WHEN NO DESKTOP EXISTS. MUST RETURN
967     // IF JALVIEW IS RUNNING HEADLESS
968     // ///////////////////////////////////////////////
969     if (instance == null || (System.getProperty("java.awt.headless") != null
970             && System.getProperty("java.awt.headless").equals("true")))
971     {
972       return;
973     }
974
975     openFrameCount++;
976
977     if (!ignoreMinSize)
978     {
979       frame.setMinimumSize(
980               new Dimension(DEFAULT_MIN_WIDTH, DEFAULT_MIN_HEIGHT));
981
982       // Set default dimension for Alignment Frame window.
983       // The Alignment Frame window could be added from a number of places,
984       // hence,
985       // I did this here in order not to miss out on any Alignment frame.
986       if (frame instanceof AlignFrame)
987       {
988         frame.setMinimumSize(new Dimension(ALIGN_FRAME_DEFAULT_MIN_WIDTH,
989                 ALIGN_FRAME_DEFAULT_MIN_HEIGHT));
990       }
991     }
992
993     frame.setVisible(makeVisible);
994     frame.setClosable(true);
995     frame.setResizable(resizable);
996     frame.setMaximizable(resizable);
997     frame.setIconifiable(resizable);
998     frame.setOpaque(Platform.isJS());
999
1000     if (frame.getX() < 1 && frame.getY() < 1)
1001     {
1002       frame.setLocation(xOffset * openFrameCount,
1003               yOffset * ((openFrameCount - 1) % 10) + yOffset);
1004     }
1005
1006     /*
1007      * add an entry for the new frame in the Window menu (and remove it when the
1008      * frame is closed)
1009      */
1010     final JMenuItem menuItem = new JMenuItem(title);
1011     frame.addInternalFrameListener(new InternalFrameAdapter()
1012     {
1013       @Override
1014       public void internalFrameActivated(InternalFrameEvent evt)
1015       {
1016         JInternalFrame itf = desktop.getSelectedFrame();
1017         if (itf != null)
1018         {
1019           if (itf instanceof AlignFrame)
1020           {
1021             Jalview.setCurrentAlignFrame((AlignFrame) itf);
1022           }
1023           itf.requestFocus();
1024         }
1025       }
1026
1027       @Override
1028       public void internalFrameClosed(InternalFrameEvent evt)
1029       {
1030         PaintRefresher.RemoveComponent(frame);
1031
1032         /*
1033          * defensive check to prevent frames being added half off the window
1034          */
1035         if (openFrameCount > 0)
1036         {
1037           openFrameCount--;
1038         }
1039
1040         /*
1041          * ensure no reference to alignFrame retained by menu item listener
1042          */
1043         if (menuItem.getActionListeners().length > 0)
1044         {
1045           menuItem.removeActionListener(menuItem.getActionListeners()[0]);
1046         }
1047         windowMenu.remove(menuItem);
1048       }
1049     });
1050
1051     menuItem.addActionListener(new ActionListener()
1052     {
1053       @Override
1054       public void actionPerformed(ActionEvent e)
1055       {
1056         try
1057         {
1058           frame.setSelected(true);
1059           frame.setIcon(false);
1060         } catch (java.beans.PropertyVetoException ex)
1061         {
1062
1063         }
1064       }
1065     });
1066
1067     setKeyBindings(frame);
1068
1069     desktop.add(frame);
1070
1071     windowMenu.add(menuItem);
1072
1073     frame.toFront();
1074     try
1075     {
1076       frame.setSelected(true);
1077       frame.requestFocus();
1078     } catch (java.beans.PropertyVetoException ve)
1079     {
1080     } catch (java.lang.ClassCastException cex)
1081     {
1082       jalview.bin.Console.warn(
1083               "Squashed a possible GUI implementation error. If you can recreate this, please look at https://issues.jalview.org/browse/JAL-869",
1084               cex);
1085     }
1086   }
1087
1088   /**
1089    * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close
1090    * the window
1091    * 
1092    * @param frame
1093    */
1094   private static void setKeyBindings(JInternalFrame frame)
1095   {
1096     @SuppressWarnings("serial")
1097     final Action closeAction = new AbstractAction()
1098     {
1099       @Override
1100       public void actionPerformed(ActionEvent e)
1101       {
1102         frame.dispose();
1103       }
1104     };
1105
1106     /*
1107      * set up key bindings for Ctrl-W and Cmd-W, with the same (Close) action
1108      */
1109     KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
1110             InputEvent.CTRL_DOWN_MASK);
1111     KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
1112             ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
1113
1114     InputMap inputMap = frame
1115             .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
1116     String ctrlW = ctrlWKey.toString();
1117     inputMap.put(ctrlWKey, ctrlW);
1118     inputMap.put(cmdWKey, ctrlW);
1119
1120     ActionMap actionMap = frame.getActionMap();
1121     actionMap.put(ctrlW, closeAction);
1122   }
1123
1124   @Override
1125   public void lostOwnership(Clipboard clipboard, Transferable contents)
1126   {
1127     if (!internalCopy)
1128     {
1129       Desktop.jalviewClipboard = null;
1130     }
1131
1132     internalCopy = false;
1133   }
1134
1135   @Override
1136   public void dragEnter(DropTargetDragEvent evt)
1137   {
1138   }
1139
1140   @Override
1141   public void dragExit(DropTargetEvent evt)
1142   {
1143   }
1144
1145   @Override
1146   public void dragOver(DropTargetDragEvent evt)
1147   {
1148   }
1149
1150   @Override
1151   public void dropActionChanged(DropTargetDragEvent evt)
1152   {
1153   }
1154
1155   /**
1156    * DOCUMENT ME!
1157    * 
1158    * @param evt
1159    *          DOCUMENT ME!
1160    */
1161   @Override
1162   public void drop(DropTargetDropEvent evt)
1163   {
1164     boolean success = true;
1165     // JAL-1552 - acceptDrop required before getTransferable call for
1166     // Java's Transferable for native dnd
1167     evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
1168     Transferable t = evt.getTransferable();
1169     List<Object> files = new ArrayList<>();
1170     List<DataSourceType> protocols = new ArrayList<>();
1171
1172     try
1173     {
1174       Desktop.transferFromDropTarget(files, protocols, evt, t);
1175     } catch (Exception e)
1176     {
1177       e.printStackTrace();
1178       success = false;
1179     }
1180
1181     if (files != null)
1182     {
1183       try
1184       {
1185         for (int i = 0; i < files.size(); i++)
1186         {
1187           // BH 2018 File or String
1188           Object file = files.get(i);
1189           String fileName = file.toString();
1190           DataSourceType protocol = (protocols == null)
1191                   ? DataSourceType.FILE
1192                   : protocols.get(i);
1193           FileFormatI format = null;
1194
1195           if (fileName.endsWith(".jar"))
1196           {
1197             format = FileFormat.Jalview;
1198
1199           }
1200           else
1201           {
1202             format = new IdentifyFile().identify(file, protocol);
1203           }
1204           if (file instanceof File)
1205           {
1206             Platform.cacheFileData((File) file);
1207           }
1208           new FileLoader().LoadFile(null, file, protocol, format);
1209
1210         }
1211       } catch (Exception ex)
1212       {
1213         success = false;
1214       }
1215     }
1216     evt.dropComplete(success); // need this to ensure input focus is properly
1217                                // transfered to any new windows created
1218   }
1219
1220   /**
1221    * DOCUMENT ME!
1222    * 
1223    * @param e
1224    *          DOCUMENT ME!
1225    */
1226   @Override
1227   public void inputLocalFileMenuItem_actionPerformed(AlignViewport viewport)
1228   {
1229     String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
1230     JalviewFileChooser chooser = JalviewFileChooser.forRead(
1231             Cache.getProperty("LAST_DIRECTORY"), fileFormat,
1232             BackupFiles.getEnabled());
1233
1234     chooser.setFileView(new JalviewFileView());
1235     chooser.setDialogTitle(
1236             MessageManager.getString("label.open_local_file"));
1237     chooser.setToolTipText(MessageManager.getString("action.open"));
1238
1239     chooser.setResponseHandler(0, () -> {
1240       File selectedFile = chooser.getSelectedFile();
1241       Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
1242
1243       FileFormatI format = chooser.getSelectedFormat();
1244
1245       /*
1246        * Call IdentifyFile to verify the file contains what its extension implies.
1247        * Skip this step for dynamically added file formats, because IdentifyFile does
1248        * not know how to recognise them.
1249        */
1250       if (FileFormats.getInstance().isIdentifiable(format))
1251       {
1252         try
1253         {
1254           format = new IdentifyFile().identify(selectedFile,
1255                   DataSourceType.FILE);
1256         } catch (FileFormatException e)
1257         {
1258           // format = null; //??
1259         }
1260       }
1261
1262       new FileLoader().LoadFile(viewport, selectedFile, DataSourceType.FILE,
1263               format);
1264     });
1265     chooser.showOpenDialog(this);
1266   }
1267
1268   /**
1269    * Shows a dialog for input of a URL at which to retrieve alignment data
1270    * 
1271    * @param viewport
1272    */
1273   @Override
1274   public void inputURLMenuItem_actionPerformed(AlignViewport viewport)
1275   {
1276     // This construct allows us to have a wider textfield
1277     // for viewing
1278     JLabel label = new JLabel(
1279             MessageManager.getString("label.input_file_url"));
1280
1281     JPanel panel = new JPanel(new GridLayout(2, 1));
1282     panel.add(label);
1283
1284     /*
1285      * the URL to fetch is input in Java: an editable combobox with history JS:
1286      * (pending JAL-3038) a plain text field
1287      */
1288     JComponent history;
1289     String urlBase = "https://www.";
1290     if (Platform.isJS())
1291     {
1292       history = new JTextField(urlBase, 35);
1293     }
1294     else
1295     /**
1296      * Java only
1297      * 
1298      * @j2sIgnore
1299      */
1300     {
1301       JComboBox<String> asCombo = new JComboBox<>();
1302       asCombo.setPreferredSize(new Dimension(400, 20));
1303       asCombo.setEditable(true);
1304       asCombo.addItem(urlBase);
1305       String historyItems = Cache.getProperty("RECENT_URL");
1306       if (historyItems != null)
1307       {
1308         for (String token : historyItems.split("\\t"))
1309         {
1310           asCombo.addItem(token);
1311         }
1312       }
1313       history = asCombo;
1314     }
1315     panel.add(history);
1316
1317     Object[] options = new Object[] { MessageManager.getString("action.ok"),
1318         MessageManager.getString("action.cancel") };
1319     Runnable action = () -> {
1320       @SuppressWarnings("unchecked")
1321       String url = (history instanceof JTextField
1322               ? ((JTextField) history).getText()
1323               : ((JComboBox<String>) history).getEditor().getItem()
1324                       .toString().trim());
1325
1326       if (url.toLowerCase(Locale.ROOT).endsWith(".jar"))
1327       {
1328         if (viewport != null)
1329         {
1330           new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
1331                   FileFormat.Jalview);
1332         }
1333         else
1334         {
1335           new FileLoader().LoadFile(url, DataSourceType.URL,
1336                   FileFormat.Jalview);
1337         }
1338       }
1339       else
1340       {
1341         FileFormatI format = null;
1342         try
1343         {
1344           format = new IdentifyFile().identify(url, DataSourceType.URL);
1345         } catch (FileFormatException e)
1346         {
1347           // TODO revise error handling, distinguish between
1348           // URL not found and response not valid
1349         }
1350
1351         if (format == null)
1352         {
1353           String msg = MessageManager.formatMessage("label.couldnt_locate",
1354                   url);
1355           JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
1356                   MessageManager.getString("label.url_not_found"),
1357                   JvOptionPane.WARNING_MESSAGE);
1358           return;
1359         }
1360
1361         if (viewport != null)
1362         {
1363           new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
1364                   format);
1365         }
1366         else
1367         {
1368           new FileLoader().LoadFile(url, DataSourceType.URL, format);
1369         }
1370       }
1371     };
1372     String dialogOption = MessageManager
1373             .getString("label.input_alignment_from_url");
1374     JvOptionPane.newOptionDialog(desktop).setResponseHandler(0, action)
1375             .showInternalDialog(panel, dialogOption,
1376                     JvOptionPane.YES_NO_CANCEL_OPTION,
1377                     JvOptionPane.PLAIN_MESSAGE, null, options,
1378                     MessageManager.getString("action.ok"));
1379   }
1380
1381   /**
1382    * Opens the CutAndPaste window for the user to paste an alignment in to
1383    * 
1384    * @param viewPanel
1385    *          - if not null, the pasted alignment is added to the current
1386    *          alignment; if null, to a new alignment window
1387    */
1388   @Override
1389   public void inputTextboxMenuItem_actionPerformed(
1390           AlignmentViewPanel viewPanel)
1391   {
1392     CutAndPasteTransfer cap = new CutAndPasteTransfer();
1393     cap.setForInput(viewPanel);
1394     Desktop.addInternalFrame(cap,
1395             MessageManager.getString("label.cut_paste_alignmen_file"), true,
1396             600, 500);
1397   }
1398
1399   /*
1400    * Check with user and saving files before actually quitting
1401    */
1402   public void desktopQuit()
1403   {
1404     desktopQuit(true, false);
1405   }
1406
1407   public QuitHandler.QResponse desktopQuit(boolean ui, boolean disposeFlag)
1408   {
1409     final Runnable doDesktopQuit = () -> {
1410       Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
1411       Cache.setProperty("SCREENGEOMETRY_WIDTH", screen.width + "");
1412       Cache.setProperty("SCREENGEOMETRY_HEIGHT", screen.height + "");
1413       storeLastKnownDimensions("", new Rectangle(getBounds().x,
1414               getBounds().y, getWidth(), getHeight()));
1415
1416       if (jconsole != null)
1417       {
1418         storeLastKnownDimensions("JAVA_CONSOLE_", jconsole.getBounds());
1419         jconsole.stopConsole();
1420       }
1421
1422       if (jvnews != null)
1423       {
1424         storeLastKnownDimensions("JALVIEW_RSS_WINDOW_", jvnews.getBounds());
1425       }
1426
1427       // Frames should all close automatically. Keeping external
1428       // viewers open should already be decided by user.
1429       closeAll_actionPerformed(null);
1430
1431       // check for aborted quit
1432       if (QuitHandler.quitCancelled())
1433       {
1434         jalview.bin.Console.debug("Desktop aborting quit");
1435         return;
1436       }
1437
1438       if (dialogExecutor != null)
1439       {
1440         dialogExecutor.shutdownNow();
1441       }
1442
1443       if (groovyConsole != null)
1444       {
1445         // suppress a possible repeat prompt to save script
1446         groovyConsole.setDirty(false);
1447         groovyConsole.exit();
1448       }
1449
1450       if (QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT)
1451       {
1452         // note that shutdown hook will not be run
1453         jalview.bin.Console.debug("Force Quit selected by user");
1454         Runtime.getRuntime().halt(0);
1455       }
1456
1457       jalview.bin.Console.debug("Quit selected by user");
1458       if (disposeFlag)
1459       {
1460         instance.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
1461         // instance.dispose();
1462       }
1463       instance.quit();
1464     };
1465
1466     return QuitHandler.getQuitResponse(ui, doDesktopQuit, doDesktopQuit,
1467             QuitHandler.defaultCancelQuit);
1468   }
1469
1470   /**
1471    * Don't call this directly, use desktopQuit() above. Exits the program.
1472    */
1473   @Override
1474   public void quit()
1475   {
1476     // this will run the shutdownHook but QuitHandler.getQuitResponse() should
1477     // not run a second time if gotQuitResponse flag has been set (i.e. user
1478     // confirmed quit of some kind).
1479     Jalview.exit("Desktop exiting.", 0);
1480   }
1481
1482   private void storeLastKnownDimensions(String string, Rectangle jc)
1483   {
1484     jalview.bin.Console.debug("Storing last known dimensions for " + string
1485             + ": x:" + jc.x + " y:" + jc.y + " width:" + jc.width
1486             + " height:" + jc.height);
1487
1488     Cache.setProperty(string + "SCREEN_X", jc.x + "");
1489     Cache.setProperty(string + "SCREEN_Y", jc.y + "");
1490     Cache.setProperty(string + "SCREEN_WIDTH", jc.width + "");
1491     Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + "");
1492   }
1493
1494   /**
1495    * DOCUMENT ME!
1496    * 
1497    * @param e
1498    *          DOCUMENT ME!
1499    */
1500   @Override
1501   public void aboutMenuItem_actionPerformed(ActionEvent e)
1502   {
1503     new Thread(new Runnable()
1504     {
1505       @Override
1506       public void run()
1507       {
1508         new SplashScreen(false);
1509       }
1510     }).start();
1511   }
1512
1513   /**
1514    * Returns the html text for the About screen, including any available version
1515    * number, build details, author details and citation reference, but without
1516    * the enclosing {@code html} tags
1517    * 
1518    * @return
1519    */
1520   public String getAboutMessage()
1521   {
1522     StringBuilder message = new StringBuilder(1024);
1523     message.append("<div style=\"font-family: sans-serif;\">")
1524             .append("<h1><strong>Version: ")
1525             .append(Cache.getProperty("VERSION")).append("</strong></h1>")
1526             .append("<strong>Built: <em>")
1527             .append(Cache.getDefault("BUILD_DATE", "unknown"))
1528             .append("</em> from ").append(Cache.getBuildDetailsForSplash())
1529             .append("</strong>");
1530
1531     String latestVersion = Cache.getDefault("LATEST_VERSION", "Checking");
1532     if (latestVersion.equals("Checking"))
1533     {
1534       // JBP removed this message for 2.11: May be reinstated in future version
1535       // message.append("<br>...Checking latest version...</br>");
1536     }
1537     else if (!latestVersion.equals(Cache.getProperty("VERSION")))
1538     {
1539       boolean red = false;
1540       if (Cache.getProperty("VERSION").toLowerCase(Locale.ROOT)
1541               .indexOf("automated build") == -1)
1542       {
1543         red = true;
1544         // Displayed when code version and jnlp version do not match and code
1545         // version is not a development build
1546         message.append("<div style=\"color: #FF0000;font-style: bold;\">");
1547       }
1548
1549       message.append("<br>!! Version ")
1550               .append(Cache.getDefault("LATEST_VERSION", "..Checking.."))
1551               .append(" is available for download from ")
1552               .append(Cache.getDefault("www.jalview.org",
1553                       "https://www.jalview.org"))
1554               .append(" !!");
1555       if (red)
1556       {
1557         message.append("</div>");
1558       }
1559     }
1560     message.append("<br>Authors:  ");
1561     message.append(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS));
1562     message.append(CITATION);
1563
1564     message.append("</div>");
1565
1566     return message.toString();
1567   }
1568
1569   /**
1570    * Action on requesting Help documentation
1571    */
1572   @Override
1573   public void documentationMenuItem_actionPerformed()
1574   {
1575     try
1576     {
1577       if (Platform.isJS())
1578       {
1579         BrowserLauncher.openURL("https://www.jalview.org/help.html");
1580       }
1581       else
1582       /**
1583        * Java only
1584        * 
1585        * @j2sIgnore
1586        */
1587       {
1588         Help.showHelpWindow();
1589       }
1590     } catch (Exception ex)
1591     {
1592       System.err.println("Error opening help: " + ex.getMessage());
1593     }
1594   }
1595
1596   @Override
1597   public void closeAll_actionPerformed(ActionEvent e)
1598   {
1599     // TODO show a progress bar while closing?
1600     JInternalFrame[] frames = desktop.getAllFrames();
1601     for (int i = 0; i < frames.length; i++)
1602     {
1603       try
1604       {
1605         frames[i].setClosed(true);
1606       } catch (java.beans.PropertyVetoException ex)
1607       {
1608       }
1609     }
1610     Jalview.setCurrentAlignFrame(null);
1611     System.out.println("ALL CLOSED");
1612
1613     /*
1614      * reset state of singleton objects as appropriate (clear down session state
1615      * when all windows are closed)
1616      */
1617     StructureSelectionManager ssm = StructureSelectionManager
1618             .getStructureSelectionManager(this);
1619     if (ssm != null)
1620     {
1621       ssm.resetAll();
1622     }
1623   }
1624
1625   public int structureViewersStillRunningCount()
1626   {
1627     int count = 0;
1628     JInternalFrame[] frames = desktop.getAllFrames();
1629     for (int i = 0; i < frames.length; i++)
1630     {
1631       if (frames[i] != null
1632               && frames[i] instanceof JalviewStructureDisplayI)
1633       {
1634         if (((JalviewStructureDisplayI) frames[i]).stillRunning())
1635           count++;
1636       }
1637     }
1638     return count;
1639   }
1640
1641   @Override
1642   public void raiseRelated_actionPerformed(ActionEvent e)
1643   {
1644     reorderAssociatedWindows(false, false);
1645   }
1646
1647   @Override
1648   public void minimizeAssociated_actionPerformed(ActionEvent e)
1649   {
1650     reorderAssociatedWindows(true, false);
1651   }
1652
1653   void closeAssociatedWindows()
1654   {
1655     reorderAssociatedWindows(false, true);
1656   }
1657
1658   /*
1659    * (non-Javadoc)
1660    * 
1661    * @seejalview.jbgui.GDesktop#garbageCollect_actionPerformed(java.awt.event.
1662    * ActionEvent)
1663    */
1664   @Override
1665   protected void garbageCollect_actionPerformed(ActionEvent e)
1666   {
1667     // We simply collect the garbage
1668     jalview.bin.Console.debug("Collecting garbage...");
1669     System.gc();
1670     jalview.bin.Console.debug("Finished garbage collection.");
1671   }
1672
1673   /*
1674    * (non-Javadoc)
1675    * 
1676    * @see jalview.jbgui.GDesktop#showMemusage_actionPerformed(java.awt.event.
1677    * ActionEvent )
1678    */
1679   @Override
1680   protected void showMemusage_actionPerformed(ActionEvent e)
1681   {
1682     desktop.showMemoryUsage(showMemusage.isSelected());
1683   }
1684
1685   /*
1686    * (non-Javadoc)
1687    * 
1688    * @see
1689    * jalview.jbgui.GDesktop#showConsole_actionPerformed(java.awt.event.ActionEvent
1690    * )
1691    */
1692   @Override
1693   protected void showConsole_actionPerformed(ActionEvent e)
1694   {
1695     showConsole(showConsole.isSelected());
1696   }
1697
1698   Console jconsole = null;
1699
1700   /**
1701    * control whether the java console is visible or not
1702    * 
1703    * @param selected
1704    */
1705   void showConsole(boolean selected)
1706   {
1707     // TODO: decide if we should update properties file
1708     if (jconsole != null) // BH 2018
1709     {
1710       showConsole.setSelected(selected);
1711       Cache.setProperty("SHOW_JAVA_CONSOLE",
1712               Boolean.valueOf(selected).toString());
1713       jconsole.setVisible(selected);
1714     }
1715   }
1716
1717   void reorderAssociatedWindows(boolean minimize, boolean close)
1718   {
1719     JInternalFrame[] frames = desktop.getAllFrames();
1720     if (frames == null || frames.length < 1)
1721     {
1722       return;
1723     }
1724
1725     AlignmentViewport source = null, target = null;
1726     if (frames[0] instanceof AlignFrame)
1727     {
1728       source = ((AlignFrame) frames[0]).getCurrentView();
1729     }
1730     else if (frames[0] instanceof TreePanel)
1731     {
1732       source = ((TreePanel) frames[0]).getViewPort();
1733     }
1734     else if (frames[0] instanceof PCAPanel)
1735     {
1736       source = ((PCAPanel) frames[0]).av;
1737     }
1738     else if (frames[0].getContentPane() instanceof PairwiseAlignPanel)
1739     {
1740       source = ((PairwiseAlignPanel) frames[0].getContentPane()).av;
1741     }
1742
1743     if (source != null)
1744     {
1745       for (int i = 0; i < frames.length; i++)
1746       {
1747         target = null;
1748         if (frames[i] == null)
1749         {
1750           continue;
1751         }
1752         if (frames[i] instanceof AlignFrame)
1753         {
1754           target = ((AlignFrame) frames[i]).getCurrentView();
1755         }
1756         else if (frames[i] instanceof TreePanel)
1757         {
1758           target = ((TreePanel) frames[i]).getViewPort();
1759         }
1760         else if (frames[i] instanceof PCAPanel)
1761         {
1762           target = ((PCAPanel) frames[i]).av;
1763         }
1764         else if (frames[i].getContentPane() instanceof PairwiseAlignPanel)
1765         {
1766           target = ((PairwiseAlignPanel) frames[i].getContentPane()).av;
1767         }
1768
1769         if (source == target)
1770         {
1771           try
1772           {
1773             if (close)
1774             {
1775               frames[i].setClosed(true);
1776             }
1777             else
1778             {
1779               frames[i].setIcon(minimize);
1780               if (!minimize)
1781               {
1782                 frames[i].toFront();
1783               }
1784             }
1785
1786           } catch (java.beans.PropertyVetoException ex)
1787           {
1788           }
1789         }
1790       }
1791     }
1792   }
1793
1794   /**
1795    * DOCUMENT ME!
1796    * 
1797    * @param e
1798    *          DOCUMENT ME!
1799    */
1800   @Override
1801   protected void preferences_actionPerformed(ActionEvent e)
1802   {
1803     Preferences.openPreferences();
1804   }
1805
1806   /**
1807    * Prompts the user to choose a file and then saves the Jalview state as a
1808    * Jalview project file
1809    */
1810   @Override
1811   public void saveState_actionPerformed()
1812   {
1813     saveState_actionPerformed(false);
1814   }
1815
1816   public void saveState_actionPerformed(boolean saveAs)
1817   {
1818     java.io.File projectFile = getProjectFile();
1819     // autoSave indicates we already have a file and don't need to ask
1820     boolean autoSave = projectFile != null && !saveAs
1821             && BackupFiles.getEnabled();
1822
1823     // System.out.println("autoSave="+autoSave+", projectFile='"+projectFile+"',
1824     // saveAs="+saveAs+", Backups
1825     // "+(BackupFiles.getEnabled()?"enabled":"disabled"));
1826
1827     boolean approveSave = false;
1828     if (!autoSave)
1829     {
1830       JalviewFileChooser chooser = new JalviewFileChooser("jvp",
1831               "Jalview Project");
1832
1833       chooser.setFileView(new JalviewFileView());
1834       chooser.setDialogTitle(MessageManager.getString("label.save_state"));
1835
1836       int value = chooser.showSaveDialog(this);
1837
1838       if (value == JalviewFileChooser.APPROVE_OPTION)
1839       {
1840         projectFile = chooser.getSelectedFile();
1841         setProjectFile(projectFile);
1842         approveSave = true;
1843       }
1844     }
1845
1846     if (approveSave || autoSave)
1847     {
1848       final Desktop me = this;
1849       final java.io.File chosenFile = projectFile;
1850       new Thread(new Runnable()
1851       {
1852         @Override
1853         public void run()
1854         {
1855           // TODO: refactor to Jalview desktop session controller action.
1856           setProgressBar(MessageManager.formatMessage(
1857                   "label.saving_jalview_project", new Object[]
1858                   { chosenFile.getName() }), chosenFile.hashCode());
1859           Cache.setProperty("LAST_DIRECTORY", chosenFile.getParent());
1860           // TODO catch and handle errors for savestate
1861           // TODO prevent user from messing with the Desktop whilst we're saving
1862           try
1863           {
1864             boolean doBackup = BackupFiles.getEnabled();
1865             BackupFiles backupfiles = doBackup ? new BackupFiles(chosenFile)
1866                     : null;
1867
1868             new Jalview2XML().saveState(
1869                     doBackup ? backupfiles.getTempFile() : chosenFile);
1870
1871             if (doBackup)
1872             {
1873               backupfiles.setWriteSuccess(true);
1874               backupfiles.rollBackupsAndRenameTempFile();
1875             }
1876           } catch (OutOfMemoryError oom)
1877           {
1878             new OOMWarning("Whilst saving current state to "
1879                     + chosenFile.getName(), oom);
1880           } catch (Exception ex)
1881           {
1882             jalview.bin.Console.error("Problems whilst trying to save to "
1883                     + chosenFile.getName(), ex);
1884             JvOptionPane.showMessageDialog(me,
1885                     MessageManager.formatMessage(
1886                             "label.error_whilst_saving_current_state_to",
1887                             new Object[]
1888                             { chosenFile.getName() }),
1889                     MessageManager.getString("label.couldnt_save_project"),
1890                     JvOptionPane.WARNING_MESSAGE);
1891           }
1892           setProgressBar(null, chosenFile.hashCode());
1893         }
1894       }).start();
1895     }
1896   }
1897
1898   @Override
1899   public void saveAsState_actionPerformed(ActionEvent e)
1900   {
1901     saveState_actionPerformed(true);
1902   }
1903
1904   protected void setProjectFile(File choice)
1905   {
1906     this.projectFile = choice;
1907   }
1908
1909   public File getProjectFile()
1910   {
1911     return this.projectFile;
1912   }
1913
1914   /**
1915    * Shows a file chooser dialog and tries to read in the selected file as a
1916    * Jalview project
1917    */
1918   @Override
1919   public void loadState_actionPerformed()
1920   {
1921     final String[] suffix = new String[] { "jvp", "jar" };
1922     final String[] desc = new String[] { "Jalview Project",
1923         "Jalview Project (old)" };
1924     JalviewFileChooser chooser = new JalviewFileChooser(
1925             Cache.getProperty("LAST_DIRECTORY"), suffix, desc,
1926             "Jalview Project", true, BackupFiles.getEnabled()); // last two
1927                                                                 // booleans:
1928                                                                 // allFiles,
1929     // allowBackupFiles
1930     chooser.setFileView(new JalviewFileView());
1931     chooser.setDialogTitle(MessageManager.getString("label.restore_state"));
1932     chooser.setResponseHandler(0, () -> {
1933       File selectedFile = chooser.getSelectedFile();
1934       setProjectFile(selectedFile);
1935       String choice = selectedFile.getAbsolutePath();
1936       Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
1937       new Thread(new Runnable()
1938       {
1939         @Override
1940         public void run()
1941         {
1942           try
1943           {
1944             new Jalview2XML().loadJalviewAlign(selectedFile);
1945           } catch (OutOfMemoryError oom)
1946           {
1947             new OOMWarning("Whilst loading project from " + choice, oom);
1948           } catch (Exception ex)
1949           {
1950             jalview.bin.Console.error(
1951                     "Problems whilst loading project from " + choice, ex);
1952             JvOptionPane.showMessageDialog(Desktop.desktop,
1953                     MessageManager.formatMessage(
1954                             "label.error_whilst_loading_project_from",
1955                             new Object[]
1956                             { choice }),
1957                     MessageManager.getString("label.couldnt_load_project"),
1958                     JvOptionPane.WARNING_MESSAGE);
1959           }
1960         }
1961       }, "Project Loader").start();
1962     });
1963
1964     chooser.showOpenDialog(this);
1965   }
1966
1967   @Override
1968   public void inputSequence_actionPerformed(ActionEvent e)
1969   {
1970     new SequenceFetcher(this);
1971   }
1972
1973   JPanel progressPanel;
1974
1975   ArrayList<JPanel> fileLoadingPanels = new ArrayList<>();
1976
1977   public void startLoading(final Object fileName)
1978   {
1979     if (fileLoadingCount == 0)
1980     {
1981       fileLoadingPanels.add(addProgressPanel(MessageManager
1982               .formatMessage("label.loading_file", new Object[]
1983               { fileName })));
1984     }
1985     fileLoadingCount++;
1986   }
1987
1988   private JPanel addProgressPanel(String string)
1989   {
1990     if (progressPanel == null)
1991     {
1992       progressPanel = new JPanel(new GridLayout(1, 1));
1993       totalProgressCount = 0;
1994       instance.getContentPane().add(progressPanel, BorderLayout.SOUTH);
1995     }
1996     JPanel thisprogress = new JPanel(new BorderLayout(10, 5));
1997     JProgressBar progressBar = new JProgressBar();
1998     progressBar.setIndeterminate(true);
1999
2000     thisprogress.add(new JLabel(string), BorderLayout.WEST);
2001
2002     thisprogress.add(progressBar, BorderLayout.CENTER);
2003     progressPanel.add(thisprogress);
2004     ((GridLayout) progressPanel.getLayout()).setRows(
2005             ((GridLayout) progressPanel.getLayout()).getRows() + 1);
2006     ++totalProgressCount;
2007     instance.validate();
2008     return thisprogress;
2009   }
2010
2011   int totalProgressCount = 0;
2012
2013   private void removeProgressPanel(JPanel progbar)
2014   {
2015     if (progressPanel != null)
2016     {
2017       synchronized (progressPanel)
2018       {
2019         progressPanel.remove(progbar);
2020         GridLayout gl = (GridLayout) progressPanel.getLayout();
2021         gl.setRows(gl.getRows() - 1);
2022         if (--totalProgressCount < 1)
2023         {
2024           this.getContentPane().remove(progressPanel);
2025           progressPanel = null;
2026         }
2027       }
2028     }
2029     validate();
2030   }
2031
2032   public void stopLoading()
2033   {
2034     fileLoadingCount--;
2035     if (fileLoadingCount < 1)
2036     {
2037       while (fileLoadingPanels.size() > 0)
2038       {
2039         removeProgressPanel(fileLoadingPanels.remove(0));
2040       }
2041       fileLoadingPanels.clear();
2042       fileLoadingCount = 0;
2043     }
2044     validate();
2045   }
2046
2047   public static int getViewCount(String alignmentId)
2048   {
2049     AlignmentViewport[] aps = getViewports(alignmentId);
2050     return (aps == null) ? 0 : aps.length;
2051   }
2052
2053   /**
2054    * 
2055    * @param alignmentId
2056    *          - if null, all sets are returned
2057    * @return all AlignmentPanels concerning the alignmentId sequence set
2058    */
2059   public static AlignmentPanel[] getAlignmentPanels(String alignmentId)
2060   {
2061     if (Desktop.desktop == null)
2062     {
2063       // no frames created and in headless mode
2064       // TODO: verify that frames are recoverable when in headless mode
2065       return null;
2066     }
2067     List<AlignmentPanel> aps = new ArrayList<>();
2068     AlignFrame[] frames = getAlignFrames();
2069     if (frames == null)
2070     {
2071       return null;
2072     }
2073     for (AlignFrame af : frames)
2074     {
2075       for (AlignmentPanel ap : af.alignPanels)
2076       {
2077         if (alignmentId == null
2078                 || alignmentId.equals(ap.av.getSequenceSetId()))
2079         {
2080           aps.add(ap);
2081         }
2082       }
2083     }
2084     if (aps.size() == 0)
2085     {
2086       return null;
2087     }
2088     AlignmentPanel[] vap = aps.toArray(new AlignmentPanel[aps.size()]);
2089     return vap;
2090   }
2091
2092   /**
2093    * get all the viewports on an alignment.
2094    * 
2095    * @param sequenceSetId
2096    *          unique alignment id (may be null - all viewports returned in that
2097    *          case)
2098    * @return all viewports on the alignment bound to sequenceSetId
2099    */
2100   public static AlignmentViewport[] getViewports(String sequenceSetId)
2101   {
2102     List<AlignmentViewport> viewp = new ArrayList<>();
2103     if (desktop != null)
2104     {
2105       AlignFrame[] frames = Desktop.getAlignFrames();
2106
2107       for (AlignFrame afr : frames)
2108       {
2109         if (sequenceSetId == null || afr.getViewport().getSequenceSetId()
2110                 .equals(sequenceSetId))
2111         {
2112           if (afr.alignPanels != null)
2113           {
2114             for (AlignmentPanel ap : afr.alignPanels)
2115             {
2116               if (sequenceSetId == null
2117                       || sequenceSetId.equals(ap.av.getSequenceSetId()))
2118               {
2119                 viewp.add(ap.av);
2120               }
2121             }
2122           }
2123           else
2124           {
2125             viewp.add(afr.getViewport());
2126           }
2127         }
2128       }
2129       if (viewp.size() > 0)
2130       {
2131         return viewp.toArray(new AlignmentViewport[viewp.size()]);
2132       }
2133     }
2134     return null;
2135   }
2136
2137   /**
2138    * Explode the views in the given frame into separate AlignFrame
2139    * 
2140    * @param af
2141    */
2142   public static void explodeViews(AlignFrame af)
2143   {
2144     int size = af.alignPanels.size();
2145     if (size < 2)
2146     {
2147       return;
2148     }
2149
2150     // FIXME: ideally should use UI interface API
2151     FeatureSettings viewFeatureSettings = (af.featureSettings != null
2152             && af.featureSettings.isOpen()) ? af.featureSettings : null;
2153     Rectangle fsBounds = af.getFeatureSettingsGeometry();
2154     for (int i = 0; i < size; i++)
2155     {
2156       AlignmentPanel ap = af.alignPanels.get(i);
2157
2158       AlignFrame newaf = new AlignFrame(ap);
2159
2160       // transfer reference for existing feature settings to new alignFrame
2161       if (ap == af.alignPanel)
2162       {
2163         if (viewFeatureSettings != null && viewFeatureSettings.fr.ap == ap)
2164         {
2165           newaf.featureSettings = viewFeatureSettings;
2166         }
2167         newaf.setFeatureSettingsGeometry(fsBounds);
2168       }
2169
2170       /*
2171        * Restore the view's last exploded frame geometry if known. Multiple views from
2172        * one exploded frame share and restore the same (frame) position and size.
2173        */
2174       Rectangle geometry = ap.av.getExplodedGeometry();
2175       if (geometry != null)
2176       {
2177         newaf.setBounds(geometry);
2178       }
2179
2180       ap.av.setGatherViewsHere(false);
2181
2182       addInternalFrame(newaf, af.getTitle(), AlignFrame.DEFAULT_WIDTH,
2183               AlignFrame.DEFAULT_HEIGHT);
2184       // and materialise a new feature settings dialog instance for the new
2185       // alignframe
2186       // (closes the old as if 'OK' was pressed)
2187       if (ap == af.alignPanel && newaf.featureSettings != null
2188               && newaf.featureSettings.isOpen()
2189               && af.alignPanel.getAlignViewport().isShowSequenceFeatures())
2190       {
2191         newaf.showFeatureSettingsUI();
2192       }
2193     }
2194
2195     af.featureSettings = null;
2196     af.alignPanels.clear();
2197     af.closeMenuItem_actionPerformed(true);
2198
2199   }
2200
2201   /**
2202    * Gather expanded views (separate AlignFrame's) with the same sequence set
2203    * identifier back in to this frame as additional views, and close the
2204    * expanded views. Note the expanded frames may themselves have multiple
2205    * views. We take the lot.
2206    * 
2207    * @param source
2208    */
2209   public void gatherViews(AlignFrame source)
2210   {
2211     source.viewport.setGatherViewsHere(true);
2212     source.viewport.setExplodedGeometry(source.getBounds());
2213     JInternalFrame[] frames = desktop.getAllFrames();
2214     String viewId = source.viewport.getSequenceSetId();
2215     for (int t = 0; t < frames.length; t++)
2216     {
2217       if (frames[t] instanceof AlignFrame && frames[t] != source)
2218       {
2219         AlignFrame af = (AlignFrame) frames[t];
2220         boolean gatherThis = false;
2221         for (int a = 0; a < af.alignPanels.size(); a++)
2222         {
2223           AlignmentPanel ap = af.alignPanels.get(a);
2224           if (viewId.equals(ap.av.getSequenceSetId()))
2225           {
2226             gatherThis = true;
2227             ap.av.setGatherViewsHere(false);
2228             ap.av.setExplodedGeometry(af.getBounds());
2229             source.addAlignmentPanel(ap, false);
2230           }
2231         }
2232
2233         if (gatherThis)
2234         {
2235           if (af.featureSettings != null && af.featureSettings.isOpen())
2236           {
2237             if (source.featureSettings == null)
2238             {
2239               // preserve the feature settings geometry for this frame
2240               source.featureSettings = af.featureSettings;
2241               source.setFeatureSettingsGeometry(
2242                       af.getFeatureSettingsGeometry());
2243             }
2244             else
2245             {
2246               // close it and forget
2247               af.featureSettings.close();
2248             }
2249           }
2250           af.alignPanels.clear();
2251           af.closeMenuItem_actionPerformed(true);
2252         }
2253       }
2254     }
2255
2256     // refresh the feature setting UI for the source frame if it exists
2257     if (source.featureSettings != null && source.featureSettings.isOpen())
2258     {
2259       source.showFeatureSettingsUI();
2260     }
2261
2262   }
2263
2264   public JInternalFrame[] getAllFrames()
2265   {
2266     return desktop.getAllFrames();
2267   }
2268
2269   /**
2270    * Checks the given url to see if it gives a response indicating that the user
2271    * should be informed of a new questionnaire.
2272    * 
2273    * @param url
2274    */
2275   public void checkForQuestionnaire(String url)
2276   {
2277     UserQuestionnaireCheck jvq = new UserQuestionnaireCheck(url);
2278     // javax.swing.SwingUtilities.invokeLater(jvq);
2279     new Thread(jvq).start();
2280   }
2281
2282   public void checkURLLinks()
2283   {
2284     // Thread off the URL link checker
2285     addDialogThread(new Runnable()
2286     {
2287       @Override
2288       public void run()
2289       {
2290         if (Cache.getDefault("CHECKURLLINKS", true))
2291         {
2292           // check what the actual links are - if it's just the default don't
2293           // bother with the warning
2294           List<String> links = Preferences.sequenceUrlLinks
2295                   .getLinksForMenu();
2296
2297           // only need to check links if there is one with a
2298           // SEQUENCE_ID which is not the default EMBL_EBI link
2299           ListIterator<String> li = links.listIterator();
2300           boolean check = false;
2301           List<JLabel> urls = new ArrayList<>();
2302           while (li.hasNext())
2303           {
2304             String link = li.next();
2305             if (link.contains(jalview.util.UrlConstants.SEQUENCE_ID)
2306                     && !UrlConstants.isDefaultString(link))
2307             {
2308               check = true;
2309               int barPos = link.indexOf("|");
2310               String urlMsg = barPos == -1 ? link
2311                       : link.substring(0, barPos) + ": "
2312                               + link.substring(barPos + 1);
2313               urls.add(new JLabel(urlMsg));
2314             }
2315           }
2316           if (!check)
2317           {
2318             return;
2319           }
2320
2321           // ask user to check in case URL links use old style tokens
2322           // ($SEQUENCE_ID$ for sequence id _or_ accession id)
2323           JPanel msgPanel = new JPanel();
2324           msgPanel.setLayout(new BoxLayout(msgPanel, BoxLayout.PAGE_AXIS));
2325           msgPanel.add(Box.createVerticalGlue());
2326           JLabel msg = new JLabel(MessageManager
2327                   .getString("label.SEQUENCE_ID_for_DB_ACCESSION1"));
2328           JLabel msg2 = new JLabel(MessageManager
2329                   .getString("label.SEQUENCE_ID_for_DB_ACCESSION2"));
2330           msgPanel.add(msg);
2331           for (JLabel url : urls)
2332           {
2333             msgPanel.add(url);
2334           }
2335           msgPanel.add(msg2);
2336
2337           final JCheckBox jcb = new JCheckBox(
2338                   MessageManager.getString("label.do_not_display_again"));
2339           jcb.addActionListener(new ActionListener()
2340           {
2341             @Override
2342             public void actionPerformed(ActionEvent e)
2343             {
2344               // update Cache settings for "don't show this again"
2345               boolean showWarningAgain = !jcb.isSelected();
2346               Cache.setProperty("CHECKURLLINKS",
2347                       Boolean.valueOf(showWarningAgain).toString());
2348             }
2349           });
2350           msgPanel.add(jcb);
2351
2352           JvOptionPane.showMessageDialog(Desktop.desktop, msgPanel,
2353                   MessageManager
2354                           .getString("label.SEQUENCE_ID_no_longer_used"),
2355                   JvOptionPane.WARNING_MESSAGE);
2356         }
2357       }
2358     });
2359   }
2360
2361   /**
2362    * Proxy class for JDesktopPane which optionally displays the current memory
2363    * usage and highlights the desktop area with a red bar if free memory runs
2364    * low.
2365    * 
2366    * @author AMW
2367    */
2368   public class MyDesktopPane extends JDesktopPane implements Runnable
2369   {
2370     private static final float ONE_MB = 1048576f;
2371
2372     boolean showMemoryUsage = false;
2373
2374     Runtime runtime;
2375
2376     java.text.NumberFormat df;
2377
2378     float maxMemory, allocatedMemory, freeMemory, totalFreeMemory,
2379             percentUsage;
2380
2381     public MyDesktopPane(boolean showMemoryUsage)
2382     {
2383       showMemoryUsage(showMemoryUsage);
2384     }
2385
2386     public void showMemoryUsage(boolean showMemory)
2387     {
2388       this.showMemoryUsage = showMemory;
2389       if (showMemory)
2390       {
2391         Thread worker = new Thread(this);
2392         worker.start();
2393       }
2394       repaint();
2395     }
2396
2397     public boolean isShowMemoryUsage()
2398     {
2399       return showMemoryUsage;
2400     }
2401
2402     @Override
2403     public void run()
2404     {
2405       df = java.text.NumberFormat.getNumberInstance();
2406       df.setMaximumFractionDigits(2);
2407       runtime = Runtime.getRuntime();
2408
2409       while (showMemoryUsage)
2410       {
2411         try
2412         {
2413           maxMemory = runtime.maxMemory() / ONE_MB;
2414           allocatedMemory = runtime.totalMemory() / ONE_MB;
2415           freeMemory = runtime.freeMemory() / ONE_MB;
2416           totalFreeMemory = freeMemory + (maxMemory - allocatedMemory);
2417
2418           percentUsage = (totalFreeMemory / maxMemory) * 100;
2419
2420           // if (percentUsage < 20)
2421           {
2422             // border1 = BorderFactory.createMatteBorder(12, 12, 12, 12,
2423             // Color.red);
2424             // instance.set.setBorder(border1);
2425           }
2426           repaint();
2427           // sleep after showing usage
2428           Thread.sleep(3000);
2429         } catch (Exception ex)
2430         {
2431           ex.printStackTrace();
2432         }
2433       }
2434     }
2435
2436     @Override
2437     public void paintComponent(Graphics g)
2438     {
2439       if (showMemoryUsage && g != null && df != null)
2440       {
2441         if (percentUsage < 20)
2442         {
2443           g.setColor(Color.red);
2444         }
2445         FontMetrics fm = g.getFontMetrics();
2446         if (fm != null)
2447         {
2448           g.drawString(MessageManager.formatMessage("label.memory_stats",
2449                   new Object[]
2450                   { df.format(totalFreeMemory), df.format(maxMemory),
2451                       df.format(percentUsage) }),
2452                   10, getHeight() - fm.getHeight());
2453         }
2454       }
2455
2456       // output debug scale message. Important for jalview.bin.HiDPISettingTest2
2457       Desktop.debugScaleMessage(Desktop.getDesktop().getGraphics());
2458     }
2459   }
2460
2461   /**
2462    * Accessor method to quickly get all the AlignmentFrames loaded.
2463    * 
2464    * @return an array of AlignFrame, or null if none found
2465    */
2466   public static AlignFrame[] getAlignFrames()
2467   {
2468     if (Jalview.isHeadlessMode())
2469     {
2470       // Desktop.desktop is null in headless mode
2471       return new AlignFrame[] { Jalview.currentAlignFrame };
2472     }
2473
2474     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
2475
2476     if (frames == null)
2477     {
2478       return null;
2479     }
2480     List<AlignFrame> avp = new ArrayList<>();
2481     // REVERSE ORDER
2482     for (int i = frames.length - 1; i > -1; i--)
2483     {
2484       if (frames[i] instanceof AlignFrame)
2485       {
2486         avp.add((AlignFrame) frames[i]);
2487       }
2488       else if (frames[i] instanceof SplitFrame)
2489       {
2490         /*
2491          * Also check for a split frame containing an AlignFrame
2492          */
2493         GSplitFrame sf = (GSplitFrame) frames[i];
2494         if (sf.getTopFrame() instanceof AlignFrame)
2495         {
2496           avp.add((AlignFrame) sf.getTopFrame());
2497         }
2498         if (sf.getBottomFrame() instanceof AlignFrame)
2499         {
2500           avp.add((AlignFrame) sf.getBottomFrame());
2501         }
2502       }
2503     }
2504     if (avp.size() == 0)
2505     {
2506       return null;
2507     }
2508     AlignFrame afs[] = avp.toArray(new AlignFrame[avp.size()]);
2509     return afs;
2510   }
2511
2512   /**
2513    * Returns an array of any AppJmol frames in the Desktop (or null if none).
2514    * 
2515    * @return
2516    */
2517   public GStructureViewer[] getJmols()
2518   {
2519     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
2520
2521     if (frames == null)
2522     {
2523       return null;
2524     }
2525     List<GStructureViewer> avp = new ArrayList<>();
2526     // REVERSE ORDER
2527     for (int i = frames.length - 1; i > -1; i--)
2528     {
2529       if (frames[i] instanceof AppJmol)
2530       {
2531         GStructureViewer af = (GStructureViewer) frames[i];
2532         avp.add(af);
2533       }
2534     }
2535     if (avp.size() == 0)
2536     {
2537       return null;
2538     }
2539     GStructureViewer afs[] = avp.toArray(new GStructureViewer[avp.size()]);
2540     return afs;
2541   }
2542
2543   /**
2544    * Add Groovy Support to Jalview
2545    */
2546   @Override
2547   public void groovyShell_actionPerformed()
2548   {
2549     try
2550     {
2551       openGroovyConsole();
2552     } catch (Exception ex)
2553     {
2554       jalview.bin.Console.error("Groovy Shell Creation failed.", ex);
2555       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
2556
2557               MessageManager.getString("label.couldnt_create_groovy_shell"),
2558               MessageManager.getString("label.groovy_support_failed"),
2559               JvOptionPane.ERROR_MESSAGE);
2560     }
2561   }
2562
2563   /**
2564    * Open the Groovy console
2565    */
2566   void openGroovyConsole()
2567   {
2568     if (groovyConsole == null)
2569     {
2570       groovyConsole = new groovy.ui.Console();
2571       groovyConsole.setVariable("Jalview", this);
2572       groovyConsole.run();
2573
2574       /*
2575        * We allow only one console at a time, so that AlignFrame menu option
2576        * 'Calculate | Run Groovy script' is unambiguous. Disable 'Groovy Console', and
2577        * enable 'Run script', when the console is opened, and the reverse when it is
2578        * closed
2579        */
2580       Window window = (Window) groovyConsole.getFrame();
2581       window.addWindowListener(new WindowAdapter()
2582       {
2583         @Override
2584         public void windowClosed(WindowEvent e)
2585         {
2586           /*
2587            * rebind CMD-Q from Groovy Console to Jalview Quit
2588            */
2589           addQuitHandler();
2590           enableExecuteGroovy(false);
2591         }
2592       });
2593     }
2594
2595     /*
2596      * show Groovy console window (after close and reopen)
2597      */
2598     ((Window) groovyConsole.getFrame()).setVisible(true);
2599
2600     /*
2601      * if we got this far, enable 'Run Groovy' in AlignFrame menus and disable
2602      * opening a second console
2603      */
2604     enableExecuteGroovy(true);
2605   }
2606
2607   /**
2608    * Bind Ctrl/Cmd-Q to Quit - for reset as Groovy Console takes over this
2609    * binding when opened
2610    */
2611   protected void addQuitHandler()
2612   {
2613     getRootPane()
2614             .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2615                     KeyStroke
2616                             .getKeyStroke(KeyEvent.VK_Q,
2617                                     jalview.util.ShortcutKeyMaskExWrapper
2618                                             .getMenuShortcutKeyMaskEx()),
2619                     "Quit");
2620     getRootPane().getActionMap().put("Quit", new AbstractAction()
2621     {
2622       @Override
2623       public void actionPerformed(ActionEvent e)
2624       {
2625         desktopQuit();
2626       }
2627     });
2628   }
2629
2630   /**
2631    * Enable or disable 'Run Groovy script' in AlignFrame calculate menus
2632    * 
2633    * @param enabled
2634    *          true if Groovy console is open
2635    */
2636   public void enableExecuteGroovy(boolean enabled)
2637   {
2638     /*
2639      * disable opening a second Groovy console (or re-enable when the console is
2640      * closed)
2641      */
2642     groovyShell.setEnabled(!enabled);
2643
2644     AlignFrame[] alignFrames = getAlignFrames();
2645     if (alignFrames != null)
2646     {
2647       for (AlignFrame af : alignFrames)
2648       {
2649         af.setGroovyEnabled(enabled);
2650       }
2651     }
2652   }
2653
2654   /**
2655    * Progress bars managed by the IProgressIndicator method.
2656    */
2657   private Hashtable<Long, JPanel> progressBars;
2658
2659   private Hashtable<Long, IProgressIndicatorHandler> progressBarHandlers;
2660
2661   /*
2662    * (non-Javadoc)
2663    * 
2664    * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
2665    */
2666   @Override
2667   public void setProgressBar(String message, long id)
2668   {
2669     if (progressBars == null)
2670     {
2671       progressBars = new Hashtable<>();
2672       progressBarHandlers = new Hashtable<>();
2673     }
2674
2675     if (progressBars.get(Long.valueOf(id)) != null)
2676     {
2677       JPanel panel = progressBars.remove(Long.valueOf(id));
2678       if (progressBarHandlers.contains(Long.valueOf(id)))
2679       {
2680         progressBarHandlers.remove(Long.valueOf(id));
2681       }
2682       removeProgressPanel(panel);
2683     }
2684     else
2685     {
2686       progressBars.put(Long.valueOf(id), addProgressPanel(message));
2687     }
2688   }
2689
2690   /*
2691    * (non-Javadoc)
2692    * 
2693    * @see jalview.gui.IProgressIndicator#registerHandler(long,
2694    * jalview.gui.IProgressIndicatorHandler)
2695    */
2696   @Override
2697   public void registerHandler(final long id,
2698           final IProgressIndicatorHandler handler)
2699   {
2700     if (progressBarHandlers == null
2701             || !progressBars.containsKey(Long.valueOf(id)))
2702     {
2703       throw new Error(MessageManager.getString(
2704               "error.call_setprogressbar_before_registering_handler"));
2705     }
2706     progressBarHandlers.put(Long.valueOf(id), handler);
2707     final JPanel progressPanel = progressBars.get(Long.valueOf(id));
2708     if (handler.canCancel())
2709     {
2710       JButton cancel = new JButton(
2711               MessageManager.getString("action.cancel"));
2712       final IProgressIndicator us = this;
2713       cancel.addActionListener(new ActionListener()
2714       {
2715
2716         @Override
2717         public void actionPerformed(ActionEvent e)
2718         {
2719           handler.cancelActivity(id);
2720           us.setProgressBar(MessageManager
2721                   .formatMessage("label.cancelled_params", new Object[]
2722                   { ((JLabel) progressPanel.getComponent(0)).getText() }),
2723                   id);
2724         }
2725       });
2726       progressPanel.add(cancel, BorderLayout.EAST);
2727     }
2728   }
2729
2730   /**
2731    * 
2732    * @return true if any progress bars are still active
2733    */
2734   @Override
2735   public boolean operationInProgress()
2736   {
2737     if (progressBars != null && progressBars.size() > 0)
2738     {
2739       return true;
2740     }
2741     return false;
2742   }
2743
2744   /**
2745    * This will return the first AlignFrame holding the given viewport instance.
2746    * It will break if there are more than one AlignFrames viewing a particular
2747    * av.
2748    * 
2749    * @param viewport
2750    * @return alignFrame for viewport
2751    */
2752   public static AlignFrame getAlignFrameFor(AlignViewportI viewport)
2753   {
2754     if (desktop != null)
2755     {
2756       AlignmentPanel[] aps = getAlignmentPanels(
2757               viewport.getSequenceSetId());
2758       for (int panel = 0; aps != null && panel < aps.length; panel++)
2759       {
2760         if (aps[panel] != null && aps[panel].av == viewport)
2761         {
2762           return aps[panel].alignFrame;
2763         }
2764       }
2765     }
2766     return null;
2767   }
2768
2769   public VamsasApplication getVamsasApplication()
2770   {
2771     // TODO: JAL-3311 remove remaining code from Jalview relating to VAMSAS
2772     return null;
2773
2774   }
2775
2776   /**
2777    * flag set if jalview GUI is being operated programmatically
2778    */
2779   private boolean inBatchMode = false;
2780
2781   /**
2782    * check if jalview GUI is being operated programmatically
2783    * 
2784    * @return inBatchMode
2785    */
2786   public boolean isInBatchMode()
2787   {
2788     return inBatchMode;
2789   }
2790
2791   /**
2792    * set flag if jalview GUI is being operated programmatically
2793    * 
2794    * @param inBatchMode
2795    */
2796   public void setInBatchMode(boolean inBatchMode)
2797   {
2798     this.inBatchMode = inBatchMode;
2799   }
2800
2801   /**
2802    * start service discovery and wait till it is done
2803    */
2804   public void startServiceDiscovery()
2805   {
2806     startServiceDiscovery(false);
2807   }
2808
2809   /**
2810    * start service discovery threads - blocking or non-blocking
2811    * 
2812    * @param blocking
2813    */
2814   public void startServiceDiscovery(boolean blocking)
2815   {
2816     startServiceDiscovery(blocking, false);
2817   }
2818
2819   /**
2820    * start service discovery threads
2821    * 
2822    * @param blocking
2823    *          - false means call returns immediately
2824    * @param ignore_SHOW_JWS2_SERVICES_preference
2825    *          - when true JABA services are discovered regardless of user's JWS2
2826    *          discovery preference setting
2827    */
2828   public void startServiceDiscovery(boolean blocking,
2829           boolean ignore_SHOW_JWS2_SERVICES_preference)
2830   {
2831     boolean alive = true;
2832     Thread t0 = null, t1 = null, t2 = null;
2833     // JAL-940 - JALVIEW 1 services are now being EOLed as of JABA 2.1 release
2834     if (true)
2835     {
2836       // todo: changesupport handlers need to be transferred
2837       if (discoverer == null)
2838       {
2839         discoverer = new jalview.ws.jws1.Discoverer();
2840         // register PCS handler for desktop.
2841         discoverer.addPropertyChangeListener(changeSupport);
2842       }
2843       // JAL-940 - disabled JWS1 service configuration - always start discoverer
2844       // until we phase out completely
2845       (t0 = new Thread(discoverer)).start();
2846     }
2847
2848     if (ignore_SHOW_JWS2_SERVICES_preference
2849             || Cache.getDefault("SHOW_JWS2_SERVICES", true))
2850     {
2851       t2 = jalview.ws.jws2.Jws2Discoverer.getDiscoverer()
2852               .startDiscoverer(changeSupport);
2853     }
2854     Thread t3 = null;
2855     {
2856       // TODO: do rest service discovery
2857     }
2858     if (blocking)
2859     {
2860       while (alive)
2861       {
2862         try
2863         {
2864           Thread.sleep(15);
2865         } catch (Exception e)
2866         {
2867         }
2868         alive = (t1 != null && t1.isAlive()) || (t2 != null && t2.isAlive())
2869                 || (t3 != null && t3.isAlive())
2870                 || (t0 != null && t0.isAlive());
2871       }
2872     }
2873   }
2874
2875   /**
2876    * called to check if the service discovery process completed successfully.
2877    * 
2878    * @param evt
2879    */
2880   protected void JalviewServicesChanged(PropertyChangeEvent evt)
2881   {
2882     if (evt.getNewValue() == null || evt.getNewValue() instanceof Vector)
2883     {
2884       final String ermsg = jalview.ws.jws2.Jws2Discoverer.getDiscoverer()
2885               .getErrorMessages();
2886       if (ermsg != null)
2887       {
2888         if (Cache.getDefault("SHOW_WSDISCOVERY_ERRORS", true))
2889         {
2890           if (serviceChangedDialog == null)
2891           {
2892             // only run if we aren't already displaying one of these.
2893             addDialogThread(serviceChangedDialog = new Runnable()
2894             {
2895               @Override
2896               public void run()
2897               {
2898
2899                 /*
2900                  * JalviewDialog jd =new JalviewDialog() {
2901                  * 
2902                  * @Override protected void cancelPressed() { // TODO Auto-generated method stub
2903                  * 
2904                  * }@Override protected void okPressed() { // TODO Auto-generated method stub
2905                  * 
2906                  * }@Override protected void raiseClosed() { // TODO Auto-generated method stub
2907                  * 
2908                  * } }; jd.initDialogFrame(new JLabel("<html><table width=\"450\"><tr><td>" +
2909                  * ermsg +
2910                  * "<br/>It may be that you have invalid JABA URLs in your web service preferences,"
2911                  * + " or mis-configured HTTP proxy settings.<br/>" +
2912                  * "Check the <em>Connections</em> and <em>Web services</em> tab of the" +
2913                  * " Tools->Preferences dialog box to change them.</td></tr></table></html>" ),
2914                  * true, true, "Web Service Configuration Problem", 450, 400);
2915                  * 
2916                  * jd.waitForInput();
2917                  */
2918                 JvOptionPane.showConfirmDialog(Desktop.desktop,
2919                         new JLabel("<html><table width=\"450\"><tr><td>"
2920                                 + ermsg + "</td></tr></table>"
2921                                 + "<p>It may be that you have invalid JABA URLs<br/>in your web service preferences,"
2922                                 + "<br>or as a command-line argument, or mis-configured HTTP proxy settings.</p>"
2923                                 + "<p>Check the <em>Connections</em> and <em>Web services</em> tab<br/>of the"
2924                                 + " Tools->Preferences dialog box to change them.</p></html>"),
2925                         "Web Service Configuration Problem",
2926                         JvOptionPane.DEFAULT_OPTION,
2927                         JvOptionPane.ERROR_MESSAGE);
2928                 serviceChangedDialog = null;
2929
2930               }
2931             });
2932           }
2933         }
2934         else
2935         {
2936           jalview.bin.Console.error(
2937                   "Errors reported by JABA discovery service. Check web services preferences.\n"
2938                           + ermsg);
2939         }
2940       }
2941     }
2942   }
2943
2944   private Runnable serviceChangedDialog = null;
2945
2946   /**
2947    * start a thread to open a URL in the configured browser. Pops up a warning
2948    * dialog to the user if there is an exception when calling out to the browser
2949    * to open the URL.
2950    * 
2951    * @param url
2952    */
2953   public static void showUrl(final String url)
2954   {
2955     showUrl(url, Desktop.instance);
2956   }
2957
2958   /**
2959    * Like showUrl but allows progress handler to be specified
2960    * 
2961    * @param url
2962    * @param progress
2963    *          (null) or object implementing IProgressIndicator
2964    */
2965   public static void showUrl(final String url,
2966           final IProgressIndicator progress)
2967   {
2968     new Thread(new Runnable()
2969     {
2970       @Override
2971       public void run()
2972       {
2973         try
2974         {
2975           if (progress != null)
2976           {
2977             progress.setProgressBar(MessageManager
2978                     .formatMessage("status.opening_params", new Object[]
2979                     { url }), this.hashCode());
2980           }
2981           jalview.util.BrowserLauncher.openURL(url);
2982         } catch (Exception ex)
2983         {
2984           JvOptionPane.showInternalMessageDialog(Desktop.desktop,
2985                   MessageManager
2986                           .getString("label.web_browser_not_found_unix"),
2987                   MessageManager.getString("label.web_browser_not_found"),
2988                   JvOptionPane.WARNING_MESSAGE);
2989
2990           ex.printStackTrace();
2991         }
2992         if (progress != null)
2993         {
2994           progress.setProgressBar(null, this.hashCode());
2995         }
2996       }
2997     }).start();
2998   }
2999
3000   public static WsParamSetManager wsparamManager = null;
3001
3002   public static ParamManager getUserParameterStore()
3003   {
3004     if (wsparamManager == null)
3005     {
3006       wsparamManager = new WsParamSetManager();
3007     }
3008     return wsparamManager;
3009   }
3010
3011   /**
3012    * static hyperlink handler proxy method for use by Jalview's internal windows
3013    * 
3014    * @param e
3015    */
3016   public static void hyperlinkUpdate(HyperlinkEvent e)
3017   {
3018     if (e.getEventType() == EventType.ACTIVATED)
3019     {
3020       String url = null;
3021       try
3022       {
3023         url = e.getURL().toString();
3024         Desktop.showUrl(url);
3025       } catch (Exception x)
3026       {
3027         if (url != null)
3028         {
3029           jalview.bin.Console
3030                   .error("Couldn't handle string " + url + " as a URL.");
3031         }
3032         // ignore any exceptions due to dud links.
3033       }
3034
3035     }
3036   }
3037
3038   /**
3039    * single thread that handles display of dialogs to user.
3040    */
3041   ExecutorService dialogExecutor = Executors.newFixedThreadPool(3);
3042
3043   /**
3044    * flag indicating if dialogExecutor should try to acquire a permit
3045    */
3046   private volatile boolean dialogPause = true;
3047
3048   /**
3049    * pause the queue
3050    */
3051   private java.util.concurrent.Semaphore block = new Semaphore(0);
3052
3053   private static groovy.ui.Console groovyConsole;
3054
3055   /**
3056    * add another dialog thread to the queue
3057    * 
3058    * @param prompter
3059    */
3060   public void addDialogThread(final Runnable prompter)
3061   {
3062     dialogExecutor.submit(new Runnable()
3063     {
3064       @Override
3065       public void run()
3066       {
3067         if (dialogPause)
3068         {
3069           try
3070           {
3071             block.acquire();
3072           } catch (InterruptedException x)
3073           {
3074           }
3075         }
3076         if (instance == null)
3077         {
3078           return;
3079         }
3080         try
3081         {
3082           SwingUtilities.invokeAndWait(prompter);
3083         } catch (Exception q)
3084         {
3085           jalview.bin.Console.warn("Unexpected Exception in dialog thread.",
3086                   q);
3087         }
3088       }
3089     });
3090   }
3091
3092   public void startDialogQueue()
3093   {
3094     // set the flag so we don't pause waiting for another permit and semaphore
3095     // the current task to begin
3096     dialogPause = false;
3097     block.release();
3098   }
3099
3100   /**
3101    * Outputs an image of the desktop to file in EPS format, after prompting the
3102    * user for choice of Text or Lineart character rendering (unless a preference
3103    * has been set). The file name is generated as
3104    * 
3105    * <pre>
3106    * Jalview_snapshot_nnnnn.eps where nnnnn is the current timestamp in milliseconds
3107    * </pre>
3108    */
3109   @Override
3110   protected void snapShotWindow_actionPerformed(ActionEvent e)
3111   {
3112     // currently the menu option to do this is not shown
3113     invalidate();
3114
3115     int width = getWidth();
3116     int height = getHeight();
3117     File of = new File(
3118             "Jalview_snapshot_" + System.currentTimeMillis() + ".eps");
3119     ImageWriterI writer = new ImageWriterI()
3120     {
3121       @Override
3122       public void exportImage(Graphics g) throws Exception
3123       {
3124         paintAll(g);
3125         jalview.bin.Console.info("Successfully written snapshot to file "
3126                 + of.getAbsolutePath());
3127       }
3128     };
3129     String title = "View of desktop";
3130     ImageExporter exporter = new ImageExporter(writer, null, TYPE.EPS,
3131             title);
3132     exporter.doExport(of, this, width, height, title);
3133   }
3134
3135   /**
3136    * Explode the views in the given SplitFrame into separate SplitFrame windows.
3137    * This respects (remembers) any previous 'exploded geometry' i.e. the size
3138    * and location last time the view was expanded (if any). However it does not
3139    * remember the split pane divider location - this is set to match the
3140    * 'exploding' frame.
3141    * 
3142    * @param sf
3143    */
3144   public void explodeViews(SplitFrame sf)
3145   {
3146     AlignFrame oldTopFrame = (AlignFrame) sf.getTopFrame();
3147     AlignFrame oldBottomFrame = (AlignFrame) sf.getBottomFrame();
3148     List<? extends AlignmentViewPanel> topPanels = oldTopFrame
3149             .getAlignPanels();
3150     List<? extends AlignmentViewPanel> bottomPanels = oldBottomFrame
3151             .getAlignPanels();
3152     int viewCount = topPanels.size();
3153     if (viewCount < 2)
3154     {
3155       return;
3156     }
3157
3158     /*
3159      * Processing in reverse order works, forwards order leaves the first panels not
3160      * visible. I don't know why!
3161      */
3162     for (int i = viewCount - 1; i >= 0; i--)
3163     {
3164       /*
3165        * Make new top and bottom frames. These take over the respective AlignmentPanel
3166        * objects, including their AlignmentViewports, so the cdna/protein
3167        * relationships between the viewports is carried over to the new split frames.
3168        * 
3169        * explodedGeometry holds the (x, y) position of the previously exploded
3170        * SplitFrame, and the (width, height) of the AlignFrame component
3171        */
3172       AlignmentPanel topPanel = (AlignmentPanel) topPanels.get(i);
3173       AlignFrame newTopFrame = new AlignFrame(topPanel);
3174       newTopFrame.setSize(oldTopFrame.getSize());
3175       newTopFrame.setVisible(true);
3176       Rectangle geometry = ((AlignViewport) topPanel.getAlignViewport())
3177               .getExplodedGeometry();
3178       if (geometry != null)
3179       {
3180         newTopFrame.setSize(geometry.getSize());
3181       }
3182
3183       AlignmentPanel bottomPanel = (AlignmentPanel) bottomPanels.get(i);
3184       AlignFrame newBottomFrame = new AlignFrame(bottomPanel);
3185       newBottomFrame.setSize(oldBottomFrame.getSize());
3186       newBottomFrame.setVisible(true);
3187       geometry = ((AlignViewport) bottomPanel.getAlignViewport())
3188               .getExplodedGeometry();
3189       if (geometry != null)
3190       {
3191         newBottomFrame.setSize(geometry.getSize());
3192       }
3193
3194       topPanel.av.setGatherViewsHere(false);
3195       bottomPanel.av.setGatherViewsHere(false);
3196       JInternalFrame splitFrame = new SplitFrame(newTopFrame,
3197               newBottomFrame);
3198       if (geometry != null)
3199       {
3200         splitFrame.setLocation(geometry.getLocation());
3201       }
3202       Desktop.addInternalFrame(splitFrame, sf.getTitle(), -1, -1);
3203     }
3204
3205     /*
3206      * Clear references to the panels (now relocated in the new SplitFrames) before
3207      * closing the old SplitFrame.
3208      */
3209     topPanels.clear();
3210     bottomPanels.clear();
3211     sf.close();
3212   }
3213
3214   /**
3215    * Gather expanded split frames, sharing the same pairs of sequence set ids,
3216    * back into the given SplitFrame as additional views. Note that the gathered
3217    * frames may themselves have multiple views.
3218    * 
3219    * @param source
3220    */
3221   public void gatherViews(GSplitFrame source)
3222   {
3223     /*
3224      * special handling of explodedGeometry for a view within a SplitFrame: - it
3225      * holds the (x, y) position of the enclosing SplitFrame, and the (width,
3226      * height) of the AlignFrame component
3227      */
3228     AlignFrame myTopFrame = (AlignFrame) source.getTopFrame();
3229     AlignFrame myBottomFrame = (AlignFrame) source.getBottomFrame();
3230     myTopFrame.viewport.setExplodedGeometry(new Rectangle(source.getX(),
3231             source.getY(), myTopFrame.getWidth(), myTopFrame.getHeight()));
3232     myBottomFrame.viewport
3233             .setExplodedGeometry(new Rectangle(source.getX(), source.getY(),
3234                     myBottomFrame.getWidth(), myBottomFrame.getHeight()));
3235     myTopFrame.viewport.setGatherViewsHere(true);
3236     myBottomFrame.viewport.setGatherViewsHere(true);
3237     String topViewId = myTopFrame.viewport.getSequenceSetId();
3238     String bottomViewId = myBottomFrame.viewport.getSequenceSetId();
3239
3240     JInternalFrame[] frames = desktop.getAllFrames();
3241     for (JInternalFrame frame : frames)
3242     {
3243       if (frame instanceof SplitFrame && frame != source)
3244       {
3245         SplitFrame sf = (SplitFrame) frame;
3246         AlignFrame topFrame = (AlignFrame) sf.getTopFrame();
3247         AlignFrame bottomFrame = (AlignFrame) sf.getBottomFrame();
3248         boolean gatherThis = false;
3249         for (int a = 0; a < topFrame.alignPanels.size(); a++)
3250         {
3251           AlignmentPanel topPanel = topFrame.alignPanels.get(a);
3252           AlignmentPanel bottomPanel = bottomFrame.alignPanels.get(a);
3253           if (topViewId.equals(topPanel.av.getSequenceSetId())
3254                   && bottomViewId.equals(bottomPanel.av.getSequenceSetId()))
3255           {
3256             gatherThis = true;
3257             topPanel.av.setGatherViewsHere(false);
3258             bottomPanel.av.setGatherViewsHere(false);
3259             topPanel.av.setExplodedGeometry(
3260                     new Rectangle(sf.getLocation(), topFrame.getSize()));
3261             bottomPanel.av.setExplodedGeometry(
3262                     new Rectangle(sf.getLocation(), bottomFrame.getSize()));
3263             myTopFrame.addAlignmentPanel(topPanel, false);
3264             myBottomFrame.addAlignmentPanel(bottomPanel, false);
3265           }
3266         }
3267
3268         if (gatherThis)
3269         {
3270           topFrame.getAlignPanels().clear();
3271           bottomFrame.getAlignPanels().clear();
3272           sf.close();
3273         }
3274       }
3275     }
3276
3277     /*
3278      * The dust settles...give focus to the tab we did this from.
3279      */
3280     myTopFrame.setDisplayedView(myTopFrame.alignPanel);
3281   }
3282
3283   public static groovy.ui.Console getGroovyConsole()
3284   {
3285     return groovyConsole;
3286   }
3287
3288   /**
3289    * handles the payload of a drag and drop event.
3290    * 
3291    * TODO refactor to desktop utilities class
3292    * 
3293    * @param files
3294    *          - Data source strings extracted from the drop event
3295    * @param protocols
3296    *          - protocol for each data source extracted from the drop event
3297    * @param evt
3298    *          - the drop event
3299    * @param t
3300    *          - the payload from the drop event
3301    * @throws Exception
3302    */
3303   public static void transferFromDropTarget(List<Object> files,
3304           List<DataSourceType> protocols, DropTargetDropEvent evt,
3305           Transferable t) throws Exception
3306   {
3307
3308     DataFlavor uriListFlavor = new DataFlavor(
3309             "text/uri-list;class=java.lang.String"), urlFlavour = null;
3310     try
3311     {
3312       urlFlavour = new DataFlavor(
3313               "application/x-java-url; class=java.net.URL");
3314     } catch (ClassNotFoundException cfe)
3315     {
3316       jalview.bin.Console.debug("Couldn't instantiate the URL dataflavor.",
3317               cfe);
3318     }
3319
3320     if (urlFlavour != null && t.isDataFlavorSupported(urlFlavour))
3321     {
3322
3323       try
3324       {
3325         java.net.URL url = (URL) t.getTransferData(urlFlavour);
3326         // nb: java 8 osx bug https://bugs.openjdk.java.net/browse/JDK-8156099
3327         // means url may be null.
3328         if (url != null)
3329         {
3330           protocols.add(DataSourceType.URL);
3331           files.add(url.toString());
3332           jalview.bin.Console.debug("Drop handled as URL dataflavor "
3333                   + files.get(files.size() - 1));
3334           return;
3335         }
3336         else
3337         {
3338           if (Platform.isAMacAndNotJS())
3339           {
3340             System.err.println(
3341                     "Please ignore plist error - occurs due to problem with java 8 on OSX");
3342           }
3343         }
3344       } catch (Throwable ex)
3345       {
3346         jalview.bin.Console.debug("URL drop handler failed.", ex);
3347       }
3348     }
3349     if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor))
3350     {
3351       // Works on Windows and MacOSX
3352       jalview.bin.Console.debug("Drop handled as javaFileListFlavor");
3353       for (Object file : (List) t
3354               .getTransferData(DataFlavor.javaFileListFlavor))
3355       {
3356         files.add(file);
3357         protocols.add(DataSourceType.FILE);
3358       }
3359     }
3360     else
3361     {
3362       // Unix like behaviour
3363       boolean added = false;
3364       String data = null;
3365       if (t.isDataFlavorSupported(uriListFlavor))
3366       {
3367         jalview.bin.Console.debug("Drop handled as uriListFlavor");
3368         // This is used by Unix drag system
3369         data = (String) t.getTransferData(uriListFlavor);
3370       }
3371       if (data == null)
3372       {
3373         // fallback to text: workaround - on OSX where there's a JVM bug
3374         jalview.bin.Console
3375                 .debug("standard URIListFlavor failed. Trying text");
3376         // try text fallback
3377         DataFlavor textDf = new DataFlavor(
3378                 "text/plain;class=java.lang.String");
3379         if (t.isDataFlavorSupported(textDf))
3380         {
3381           data = (String) t.getTransferData(textDf);
3382         }
3383
3384         jalview.bin.Console.debug("Plain text drop content returned "
3385                 + (data == null ? "Null - failed" : data));
3386
3387       }
3388       if (data != null)
3389       {
3390         while (protocols.size() < files.size())
3391         {
3392           jalview.bin.Console.debug("Adding missing FILE protocol for "
3393                   + files.get(protocols.size()));
3394           protocols.add(DataSourceType.FILE);
3395         }
3396         for (java.util.StringTokenizer st = new java.util.StringTokenizer(
3397                 data, "\r\n"); st.hasMoreTokens();)
3398         {
3399           added = true;
3400           String s = st.nextToken();
3401           if (s.startsWith("#"))
3402           {
3403             // the line is a comment (as per the RFC 2483)
3404             continue;
3405           }
3406           java.net.URI uri = new java.net.URI(s);
3407           if (uri.getScheme().toLowerCase(Locale.ROOT).startsWith("http"))
3408           {
3409             protocols.add(DataSourceType.URL);
3410             files.add(uri.toString());
3411           }
3412           else
3413           {
3414             // otherwise preserve old behaviour: catch all for file objects
3415             java.io.File file = new java.io.File(uri);
3416             protocols.add(DataSourceType.FILE);
3417             files.add(file.toString());
3418           }
3419         }
3420       }
3421
3422       if (jalview.bin.Console.isDebugEnabled())
3423       {
3424         if (data == null || !added)
3425         {
3426
3427           if (t.getTransferDataFlavors() != null
3428                   && t.getTransferDataFlavors().length > 0)
3429           {
3430             jalview.bin.Console.debug(
3431                     "Couldn't resolve drop data. Here are the supported flavors:");
3432             for (DataFlavor fl : t.getTransferDataFlavors())
3433             {
3434               jalview.bin.Console.debug(
3435                       "Supported transfer dataflavor: " + fl.toString());
3436               Object df = t.getTransferData(fl);
3437               if (df != null)
3438               {
3439                 jalview.bin.Console.debug("Retrieves: " + df);
3440               }
3441               else
3442               {
3443                 jalview.bin.Console.debug("Retrieved nothing");
3444               }
3445             }
3446           }
3447           else
3448           {
3449             jalview.bin.Console
3450                     .debug("Couldn't resolve dataflavor for drop: "
3451                             + t.toString());
3452           }
3453         }
3454       }
3455     }
3456     if (Platform.isWindowsAndNotJS())
3457     {
3458       jalview.bin.Console
3459               .debug("Scanning dropped content for Windows Link Files");
3460
3461       // resolve any .lnk files in the file drop
3462       for (int f = 0; f < files.size(); f++)
3463       {
3464         String source = files.get(f).toString().toLowerCase(Locale.ROOT);
3465         if (protocols.get(f).equals(DataSourceType.FILE)
3466                 && (source.endsWith(".lnk") || source.endsWith(".url")
3467                         || source.endsWith(".site")))
3468         {
3469           try
3470           {
3471             Object obj = files.get(f);
3472             File lf = (obj instanceof File ? (File) obj
3473                     : new File((String) obj));
3474             // process link file to get a URL
3475             jalview.bin.Console.debug("Found potential link file: " + lf);
3476             WindowsShortcut wscfile = new WindowsShortcut(lf);
3477             String fullname = wscfile.getRealFilename();
3478             protocols.set(f, FormatAdapter.checkProtocol(fullname));
3479             files.set(f, fullname);
3480             jalview.bin.Console.debug("Parsed real filename " + fullname
3481                     + " to extract protocol: " + protocols.get(f));
3482           } catch (Exception ex)
3483           {
3484             jalview.bin.Console.error(
3485                     "Couldn't parse " + files.get(f) + " as a link file.",
3486                     ex);
3487           }
3488         }
3489       }
3490     }
3491   }
3492
3493   /**
3494    * Sets the Preferences property for experimental features to True or False
3495    * depending on the state of the controlling menu item
3496    */
3497   @Override
3498   protected void showExperimental_actionPerformed(boolean selected)
3499   {
3500     Cache.setProperty(EXPERIMENTAL_FEATURES, Boolean.toString(selected));
3501   }
3502
3503   /**
3504    * Answers a (possibly empty) list of any structure viewer frames (currently
3505    * for either Jmol or Chimera) which are currently open. This may optionally
3506    * be restricted to viewers of a specified class, or viewers linked to a
3507    * specified alignment panel.
3508    * 
3509    * @param apanel
3510    *          if not null, only return viewers linked to this panel
3511    * @param structureViewerClass
3512    *          if not null, only return viewers of this class
3513    * @return
3514    */
3515   public List<StructureViewerBase> getStructureViewers(
3516           AlignmentPanel apanel,
3517           Class<? extends StructureViewerBase> structureViewerClass)
3518   {
3519     List<StructureViewerBase> result = new ArrayList<>();
3520     JInternalFrame[] frames = Desktop.instance.getAllFrames();
3521
3522     for (JInternalFrame frame : frames)
3523     {
3524       if (frame instanceof StructureViewerBase)
3525       {
3526         if (structureViewerClass == null
3527                 || structureViewerClass.isInstance(frame))
3528         {
3529           if (apanel == null
3530                   || ((StructureViewerBase) frame).isLinkedWith(apanel))
3531           {
3532             result.add((StructureViewerBase) frame);
3533           }
3534         }
3535       }
3536     }
3537     return result;
3538   }
3539
3540   public static final String debugScaleMessage = "Desktop graphics transform scale=";
3541
3542   private static boolean debugScaleMessageDone = false;
3543
3544   public static void debugScaleMessage(Graphics g)
3545   {
3546     if (debugScaleMessageDone)
3547     {
3548       return;
3549     }
3550     // output used by tests to check HiDPI scaling settings in action
3551     try
3552     {
3553       Graphics2D gg = (Graphics2D) g;
3554       if (gg != null)
3555       {
3556         AffineTransform t = gg.getTransform();
3557         double scaleX = t.getScaleX();
3558         double scaleY = t.getScaleY();
3559         jalview.bin.Console.debug(debugScaleMessage + scaleX + " (X)");
3560         jalview.bin.Console.debug(debugScaleMessage + scaleY + " (Y)");
3561         debugScaleMessageDone = true;
3562       }
3563       else
3564       {
3565         jalview.bin.Console.debug("Desktop graphics null");
3566       }
3567     } catch (Exception e)
3568     {
3569       jalview.bin.Console.debug(Cache.getStackTraceString(e));
3570     }
3571   }
3572 }