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