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