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