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