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