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