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