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