JAL-4125 Additions to JvOptionPane and QuitHandler with StructureViewerBase handling...
[jalview.git] / src / jalview / gui / StructureViewerBase.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import java.awt.Color;
24 import java.awt.Component;
25 import java.awt.event.ActionEvent;
26 import java.awt.event.ActionListener;
27 import java.awt.event.ItemEvent;
28 import java.awt.event.ItemListener;
29 import java.beans.PropertyVetoException;
30 import java.io.BufferedReader;
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.FileReader;
34 import java.io.IOException;
35 import java.io.PrintWriter;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Random;
39 import java.util.Vector;
40 import java.util.concurrent.Callable;
41 import java.util.concurrent.ExecutorService;
42 import java.util.concurrent.TimeUnit;
43
44 import javax.swing.ButtonGroup;
45 import javax.swing.JCheckBoxMenuItem;
46 import javax.swing.JMenu;
47 import javax.swing.JMenuItem;
48 import javax.swing.JRadioButtonMenuItem;
49 import javax.swing.event.MenuEvent;
50 import javax.swing.event.MenuListener;
51
52 import jalview.api.AlignmentViewPanel;
53 import jalview.bin.Cache;
54 import jalview.bin.Console;
55 import jalview.datamodel.AlignmentI;
56 import jalview.datamodel.PDBEntry;
57 import jalview.datamodel.SequenceI;
58 import jalview.gui.JalviewColourChooser.ColourChooserListener;
59 import jalview.gui.StructureViewer.ViewerType;
60 import jalview.gui.ViewSelectionMenu.ViewSetProvider;
61 import jalview.io.DataSourceType;
62 import jalview.io.JalviewFileChooser;
63 import jalview.io.JalviewFileView;
64 import jalview.jbgui.GStructureViewer;
65 import jalview.schemes.ColourSchemeI;
66 import jalview.schemes.ColourSchemes;
67 import jalview.structure.StructureMapping;
68 import jalview.structures.models.AAStructureBindingModel;
69 import jalview.util.BrowserLauncher;
70 import jalview.util.MessageManager;
71 import jalview.ws.dbsources.EBIAlfaFold;
72 import jalview.ws.dbsources.Pdb;
73 import jalview.ws.utils.UrlDownloadClient;
74
75 /**
76  * Base class with common functionality for JMol, Chimera or other structure
77  * viewers.
78  * 
79  * @author gmcarstairs
80  *
81  */
82 public abstract class StructureViewerBase extends GStructureViewer
83         implements Runnable, ViewSetProvider
84 {
85   /*
86    * names for colour options (additional to Jalview colour schemes)
87    */
88   enum ViewerColour
89   {
90     BySequence, ByChain, ChargeCysteine, ByViewer
91   }
92
93   /**
94    * list of sequenceSet ids associated with the view
95    */
96   protected List<String> _aps = new ArrayList<>();
97
98   /**
99    * list of alignment panels to use for superposition
100    */
101   protected Vector<AlignmentViewPanel> _alignwith = new Vector<>();
102
103   /**
104    * list of alignment panels that are used for colouring structures by aligned
105    * sequences
106    */
107   protected Vector<AlignmentViewPanel> _colourwith = new Vector<>();
108
109   private String viewId = null;
110
111   private AlignmentPanel ap;
112
113   protected boolean alignAddedStructures = false;
114
115   protected volatile boolean _started = false;
116
117   protected volatile boolean addingStructures = false;
118
119   protected Thread worker = null;
120
121   protected boolean allChainsSelected = false;
122
123   protected JMenu viewSelectionMenu;
124
125   /**
126    * set after sequence colouring has been applied for this structure viewer.
127    * used to determine if the final sequence/structure mapping has been
128    * determined
129    */
130   protected volatile boolean seqColoursApplied = false;
131
132   private IProgressIndicator progressBar = null;
133
134   private Random random = new Random();
135
136   /**
137    * Default constructor
138    */
139   public StructureViewerBase()
140   {
141     super();
142     setFrameIcon(null);
143   }
144
145   /**
146    * @return true if added structures should be aligned to existing one(s)
147    */
148   @Override
149   public boolean isAlignAddedStructures()
150   {
151     return alignAddedStructures;
152   }
153
154   /**
155    * 
156    * @param true
157    *          if added structures should be aligned to existing one(s)
158    */
159   @Override
160   public void setAlignAddedStructures(boolean alignAdded)
161   {
162     alignAddedStructures = alignAdded;
163   }
164
165   /**
166    * called by the binding model to indicate when adding structures is happening
167    * or has been completed
168    * 
169    * @param addingStructures
170    */
171   public synchronized void setAddingStructures(boolean addingStructures)
172   {
173     this.addingStructures = addingStructures;
174   }
175
176   /**
177    * 
178    * @param ap2
179    * @return true if this Jmol instance is linked with the given alignPanel
180    */
181   public boolean isLinkedWith(AlignmentPanel ap2)
182   {
183     return _aps.contains(ap2.av.getSequenceSetId());
184   }
185
186   public boolean isUsedforaligment(AlignmentViewPanel ap2)
187   {
188
189     return (_alignwith != null) && _alignwith.contains(ap2);
190   }
191
192   @Override
193   public boolean isUsedForColourBy(AlignmentViewPanel ap2)
194   {
195     return (_colourwith != null) && _colourwith.contains(ap2);
196   }
197
198   /**
199    * 
200    * @return TRUE if the view is NOT being coloured by the alignment colours.
201    */
202   public boolean isColouredByViewer()
203   {
204     return !getBinding().isColourBySequence();
205   }
206
207   public String getViewId()
208   {
209     if (viewId == null)
210     {
211       viewId = System.currentTimeMillis() + "." + this.hashCode();
212     }
213     return viewId;
214   }
215
216   protected void setViewId(String viewId)
217   {
218     this.viewId = viewId;
219   }
220
221   protected void buildActionMenu()
222   {
223     if (_alignwith == null)
224     {
225       _alignwith = new Vector<>();
226     }
227     if (_alignwith.size() == 0 && ap != null)
228     {
229       _alignwith.add(ap);
230     }
231     ;
232     // TODO: refactor to allow concrete classes to register buttons for adding
233     // here
234     // currently have to override to add buttons back in after they are cleared
235     // in this loop
236     for (Component c : viewerActionMenu.getMenuComponents())
237     {
238       if (c != alignStructs)
239       {
240         viewerActionMenu.remove((JMenuItem) c);
241       }
242     }
243   }
244
245   @Override
246   public AlignmentPanel getAlignmentPanel()
247   {
248     return ap;
249   }
250
251   protected void setAlignmentPanel(AlignmentPanel alp)
252   {
253     this.ap = alp;
254   }
255
256   @Override
257   public AlignmentPanel[] getAllAlignmentPanels()
258   {
259     AlignmentPanel[] t, list = new AlignmentPanel[0];
260     for (String setid : _aps)
261     {
262       AlignmentPanel[] panels = PaintRefresher.getAssociatedPanels(setid);
263       if (panels != null)
264       {
265         t = new AlignmentPanel[list.length + panels.length];
266         System.arraycopy(list, 0, t, 0, list.length);
267         System.arraycopy(panels, 0, t, list.length, panels.length);
268         list = t;
269       }
270     }
271
272     return list;
273   }
274
275   /**
276    * set the primary alignmentPanel reference and add another alignPanel to the
277    * list of ones to use for colouring and aligning
278    * 
279    * @param nap
280    */
281   public void addAlignmentPanel(AlignmentPanel nap)
282   {
283     if (getAlignmentPanel() == null)
284     {
285       setAlignmentPanel(nap);
286     }
287     if (!_aps.contains(nap.av.getSequenceSetId()))
288     {
289       _aps.add(nap.av.getSequenceSetId());
290     }
291   }
292
293   /**
294    * remove any references held to the given alignment panel
295    * 
296    * @param nap
297    */
298   @Override
299   public void removeAlignmentPanel(AlignmentViewPanel nap)
300   {
301     try
302     {
303       _alignwith.remove(nap);
304       _colourwith.remove(nap);
305       if (getAlignmentPanel() == nap)
306       {
307         setAlignmentPanel(null);
308         for (AlignmentPanel aps : getAllAlignmentPanels())
309         {
310           if (aps != nap)
311           {
312             setAlignmentPanel(aps);
313             break;
314           }
315         }
316       }
317     } catch (Exception ex)
318     {
319     }
320     if (getAlignmentPanel() != null)
321     {
322       buildActionMenu();
323     }
324   }
325
326   public void useAlignmentPanelForSuperposition(AlignmentPanel nap)
327   {
328     addAlignmentPanel(nap);
329     if (!_alignwith.contains(nap))
330     {
331       _alignwith.add(nap);
332     }
333   }
334
335   public void excludeAlignmentPanelForSuperposition(AlignmentPanel nap)
336   {
337     if (_alignwith.contains(nap))
338     {
339       _alignwith.remove(nap);
340     }
341   }
342
343   public void useAlignmentPanelForColourbyseq(AlignmentPanel nap,
344           boolean enableColourBySeq)
345   {
346     useAlignmentPanelForColourbyseq(nap);
347     getBinding().setColourBySequence(enableColourBySeq);
348     seqColour.setSelected(enableColourBySeq);
349     viewerColour.setSelected(!enableColourBySeq);
350   }
351
352   public void useAlignmentPanelForColourbyseq(AlignmentPanel nap)
353   {
354     addAlignmentPanel(nap);
355     if (!_colourwith.contains(nap))
356     {
357       _colourwith.add(nap);
358     }
359   }
360
361   public void excludeAlignmentPanelForColourbyseq(AlignmentPanel nap)
362   {
363     if (_colourwith.contains(nap))
364     {
365       _colourwith.remove(nap);
366     }
367   }
368
369   public abstract ViewerType getViewerType();
370
371   /**
372    * add a new structure (with associated sequences and chains) to this viewer,
373    * retrieving it if necessary first.
374    * 
375    * @param pdbentry
376    * @param seqs
377    * @param chains
378    * @param align
379    *          if true, new structure(s) will be aligned using associated
380    *          alignment
381    * @param alignFrame
382    */
383   protected void addStructure(final PDBEntry pdbentry,
384           final SequenceI[] seqs, final String[] chains,
385           final IProgressIndicator alignFrame)
386   {
387     if (pdbentry.getFile() == null)
388     {
389       if (worker != null && worker.isAlive())
390       {
391         // a retrieval is in progress, wait around and add ourselves to the
392         // queue.
393         new Thread(new Runnable()
394         {
395           @Override
396           public void run()
397           {
398             while (worker != null && worker.isAlive() && _started)
399             {
400               try
401               {
402                 Thread.sleep(100 + ((int) Math.random() * 100));
403
404               } catch (Exception e)
405               {
406               }
407             }
408             // and call ourselves again.
409             addStructure(pdbentry, seqs, chains, alignFrame);
410           }
411         }).start();
412         return;
413       }
414     }
415     // otherwise, start adding the structure.
416     getBinding().addSequenceAndChain(new PDBEntry[] { pdbentry },
417             new SequenceI[][]
418             { seqs }, new String[][] { chains });
419     addingStructures = true;
420     _started = false;
421     worker = new Thread(this);
422     worker.start();
423     return;
424   }
425
426   protected boolean hasPdbId(String pdbId)
427   {
428     return getBinding().hasPdbId(pdbId);
429   }
430
431   /**
432    * Returns a list of any viewer of the instantiated type. The list is
433    * restricted to those linked to the given alignment panel if it is not null.
434    */
435   protected List<StructureViewerBase> getViewersFor(AlignmentPanel alp)
436   {
437     return Desktop.instance.getStructureViewers(alp, this.getClass());
438   }
439
440   @Override
441   public void addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
442           String[] chains, final AlignmentViewPanel apanel, String pdbId)
443   {
444     /*
445      * JAL-1742 exclude view with this structure already mapped (don't offer
446      * to align chain B to chain A of the same structure); code may defend
447      * against this possibility before we reach here
448      */
449     if (hasPdbId(pdbId))
450     {
451       return;
452     }
453     AlignmentPanel alignPanel = (AlignmentPanel) apanel; // Implementation error
454                                                          // if this
455     // cast fails
456     useAlignmentPanelForSuperposition(alignPanel);
457     addStructure(pdbentry, seq, chains, alignPanel.alignFrame);
458   }
459
460   /**
461    * Adds mappings for the given sequences to an already opened PDB structure,
462    * and updates any viewers that have the PDB file
463    * 
464    * @param seq
465    * @param chains
466    * @param apanel
467    * @param pdbFilename
468    */
469   public void addSequenceMappingsToStructure(SequenceI[] seq,
470           String[] chains, final AlignmentViewPanel alpanel,
471           String pdbFilename)
472   {
473     AlignmentPanel apanel = (AlignmentPanel) alpanel;
474
475     // TODO : Fix multiple seq to one chain issue here.
476     /*
477      * create the mappings
478      */
479     apanel.getStructureSelectionManager().setMapping(seq, chains,
480             pdbFilename, DataSourceType.FILE, getProgressIndicator());
481
482     /*
483      * alert the FeatureRenderer to show new (PDB RESNUM) features
484      */
485     if (apanel.getSeqPanel().seqCanvas.fr != null)
486     {
487       apanel.getSeqPanel().seqCanvas.fr.featuresAdded();
488       // note - we don't do a refresh for structure here because we do it
489       // explicitly for all panels later on
490       apanel.paintAlignment(true, false);
491     }
492
493     /*
494      * add the sequences to any other viewers (of the same type) for this pdb
495      * file
496      */
497     // JBPNOTE: this looks like a binding routine, rather than a gui routine
498     for (StructureViewerBase viewer : getViewersFor(null))
499     {
500       AAStructureBindingModel bindingModel = viewer.getBinding();
501       for (int pe = 0; pe < bindingModel.getPdbCount(); pe++)
502       {
503         if (bindingModel.getPdbEntry(pe).getFile().equals(pdbFilename))
504         {
505           bindingModel.addSequence(pe, seq);
506           viewer.addAlignmentPanel(apanel);
507           /*
508            * add it to the set of alignments used for colouring structure by
509            * sequence
510            */
511           viewer.useAlignmentPanelForColourbyseq(apanel);
512           viewer.buildActionMenu();
513           apanel.getStructureSelectionManager()
514                   .sequenceColoursChanged(apanel);
515           break;
516         }
517       }
518     }
519   }
520
521   @Override
522   public boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
523           final AlignmentViewPanel apanel, String pdbId)
524   {
525     String alreadyMapped = apanel.getStructureSelectionManager()
526             .alreadyMappedToFile(pdbId);
527
528     if (alreadyMapped == null)
529     {
530       return false;
531     }
532
533     addSequenceMappingsToStructure(seq, chains, apanel, alreadyMapped);
534     return true;
535   }
536
537   void setChainMenuItems(List<String> chainNames)
538   {
539     chainMenu.removeAll();
540     if (chainNames == null || chainNames.isEmpty())
541     {
542       return;
543     }
544     JMenuItem menuItem = new JMenuItem(
545             MessageManager.getString("label.all"));
546     menuItem.addActionListener(new ActionListener()
547     {
548       @Override
549       public void actionPerformed(ActionEvent evt)
550       {
551         allChainsSelected = true;
552         for (int i = 0; i < chainMenu.getItemCount(); i++)
553         {
554           if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
555           {
556             ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
557           }
558         }
559         showSelectedChains();
560         allChainsSelected = false;
561       }
562     });
563
564     chainMenu.add(menuItem);
565
566     for (String chain : chainNames)
567     {
568       menuItem = new JCheckBoxMenuItem(chain, true);
569       menuItem.addItemListener(new ItemListener()
570       {
571         @Override
572         public void itemStateChanged(ItemEvent evt)
573         {
574           if (!allChainsSelected)
575           {
576             showSelectedChains();
577           }
578         }
579       });
580
581       chainMenu.add(menuItem);
582     }
583   }
584
585   /**
586    * Action on selecting one of Jalview's registered colour schemes
587    */
588   @Override
589   public void changeColour_actionPerformed(String colourSchemeName)
590   {
591     AlignmentI al = getAlignmentPanel().av.getAlignment();
592     ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(
593             colourSchemeName, getAlignmentPanel().av, al, null);
594     getBinding().colourByJalviewColourScheme(cs);
595   }
596
597   /**
598    * Builds the colour menu
599    */
600   protected void buildColourMenu()
601   {
602     colourMenu.removeAll();
603     AlignmentI al = getAlignmentPanel().av.getAlignment();
604
605     /*
606      * add colour by sequence, by chain, by charge and cysteine
607      */
608     colourMenu.add(seqColour);
609     colourMenu.add(chainColour);
610     colourMenu.add(chargeColour);
611     chargeColour.setEnabled(!al.isNucleotide());
612
613     /*
614      * add all 'simple' (per-residue) colour schemes registered to Jalview
615      */
616     ButtonGroup itemGroup = ColourMenuHelper.addMenuItems(colourMenu, this,
617             al, true);
618
619     /*
620      * add 'colour by viewer' (menu item text is set in subclasses)
621      */
622     viewerColour.setSelected(false);
623     viewerColour.addActionListener(new ActionListener()
624     {
625       @Override
626       public void actionPerformed(ActionEvent actionEvent)
627       {
628         viewerColour_actionPerformed();
629       }
630     });
631     colourMenu.add(viewerColour);
632
633     /*
634      * add 'set background colour'
635      */
636     JMenuItem backGround = new JMenuItem();
637     backGround
638             .setText(MessageManager.getString("action.background_colour"));
639     backGround.addActionListener(new ActionListener()
640     {
641       @Override
642       public void actionPerformed(ActionEvent actionEvent)
643       {
644         background_actionPerformed();
645       }
646     });
647     colourMenu.add(backGround);
648
649     /*
650      * add colour buttons to a group so their selection is
651      * mutually exclusive (background colour is a separate option)
652      */
653     itemGroup.add(seqColour);
654     itemGroup.add(chainColour);
655     itemGroup.add(chargeColour);
656     itemGroup.add(viewerColour);
657   }
658
659   /**
660    * Construct menu items
661    */
662   protected void initMenus()
663   {
664     AAStructureBindingModel binding = getBinding();
665
666     seqColour = new JRadioButtonMenuItem();
667     seqColour.setText(MessageManager.getString("action.by_sequence"));
668     seqColour.setName(ViewerColour.BySequence.name());
669     seqColour.setSelected(binding.isColourBySequence());
670     seqColour.addActionListener(new ActionListener()
671     {
672       @Override
673       public void actionPerformed(ActionEvent actionEvent)
674       {
675         seqColour_actionPerformed();
676       }
677     });
678
679     chainColour = new JRadioButtonMenuItem();
680     chainColour.setText(MessageManager.getString("action.by_chain"));
681     chainColour.setName(ViewerColour.ByChain.name());
682     chainColour.addActionListener(new ActionListener()
683     {
684       @Override
685       public void actionPerformed(ActionEvent actionEvent)
686       {
687         chainColour_actionPerformed();
688       }
689     });
690
691     chargeColour = new JRadioButtonMenuItem();
692     chargeColour.setText(MessageManager.getString("label.charge_cysteine"));
693     chargeColour.setName(ViewerColour.ChargeCysteine.name());
694     chargeColour.addActionListener(new ActionListener()
695     {
696       @Override
697       public void actionPerformed(ActionEvent actionEvent)
698       {
699         chargeColour_actionPerformed();
700       }
701     });
702
703     viewerColour = new JRadioButtonMenuItem();
704     viewerColour
705             .setText(MessageManager.getString("label.colour_with_viewer"));
706     viewerColour.setToolTipText(MessageManager
707             .getString("label.let_viewer_manage_structure_colours"));
708     viewerColour.setName(ViewerColour.ByViewer.name());
709     viewerColour.setSelected(!binding.isColourBySequence());
710
711     if (_colourwith == null)
712     {
713       _colourwith = new Vector<>();
714     }
715     if (_alignwith == null)
716     {
717       _alignwith = new Vector<>();
718     }
719
720     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
721             MessageManager.getString("label.colour_by"), this, _colourwith,
722             new ItemListener()
723             {
724               @Override
725               public void itemStateChanged(ItemEvent e)
726               {
727                 if (!seqColour.isSelected())
728                 {
729                   seqColour.doClick();
730                 }
731                 else
732                 {
733                   // update the viewer display now.
734                   seqColour_actionPerformed();
735                 }
736               }
737             });
738     viewMenu.add(seqColourBy);
739
740     final ItemListener handler = new ItemListener()
741     {
742       @Override
743       public void itemStateChanged(ItemEvent e)
744       {
745         if (_alignwith.isEmpty())
746         {
747           alignStructs.setEnabled(false);
748           alignStructs.setToolTipText(null);
749         }
750         else
751         {
752           alignStructs.setEnabled(true);
753           alignStructs.setToolTipText(MessageManager.formatMessage(
754                   "label.align_structures_using_linked_alignment_views",
755                   _alignwith.size()));
756         }
757       }
758     };
759     viewSelectionMenu = new ViewSelectionMenu(
760             MessageManager.getString("label.superpose_with"), this,
761             _alignwith, handler);
762     handler.itemStateChanged(null);
763     viewerActionMenu.add(viewSelectionMenu, 0);
764     viewerActionMenu.addMenuListener(new MenuListener()
765     {
766       @Override
767       public void menuSelected(MenuEvent e)
768       {
769         handler.itemStateChanged(null);
770       }
771
772       @Override
773       public void menuDeselected(MenuEvent e)
774       {
775       }
776
777       @Override
778       public void menuCanceled(MenuEvent e)
779       {
780       }
781     });
782
783     viewerActionMenu.setText(getViewerName());
784     helpItem.setText(MessageManager.formatMessage("label.viewer_help",
785             getViewerName()));
786
787     buildColourMenu();
788   }
789
790   /**
791    * Sends commands to the structure viewer to superimpose structures based on
792    * currently associated alignments. May optionally return an error message for
793    * the operation.
794    */
795   @Override
796   protected String alignStructsWithAllAlignPanels()
797   {
798     if (getAlignmentPanel() == null)
799     {
800       return null;
801     }
802
803     if (_alignwith.size() == 0)
804     {
805       _alignwith.add(getAlignmentPanel());
806     }
807
808     String reply = null;
809     try
810     {
811       reply = getBinding().superposeStructures(_alignwith);
812       if (reply != null && !reply.isEmpty())
813       {
814         String text = MessageManager
815                 .formatMessage("error.superposition_failed", reply);
816         statusBar.setText(text);
817       }
818     } catch (Exception e)
819     {
820       StringBuffer sp = new StringBuffer();
821       for (AlignmentViewPanel alignPanel : _alignwith)
822       {
823         sp.append("'" + alignPanel.getViewName() + "' ");
824       }
825       Console.info("Couldn't align structures with the " + sp.toString()
826               + "associated alignment panels.", e);
827     }
828     return reply;
829   }
830
831   /**
832    * Opens a colour chooser dialog, and applies the chosen colour to the
833    * background of the structure viewer
834    */
835   @Override
836   public void background_actionPerformed()
837   {
838     String ttl = MessageManager.getString("label.select_background_colour");
839     ColourChooserListener listener = new ColourChooserListener()
840     {
841       @Override
842       public void colourSelected(Color c)
843       {
844         getBinding().setBackgroundColour(c);
845       }
846     };
847     JalviewColourChooser.showColourChooser(this, ttl, null, listener);
848   }
849
850   @Override
851   public void viewerColour_actionPerformed()
852   {
853     if (viewerColour.isSelected())
854     {
855       // disable automatic sequence colouring.
856       getBinding().setColourBySequence(false);
857     }
858   }
859
860   @Override
861   public void chainColour_actionPerformed()
862   {
863     chainColour.setSelected(true);
864     getBinding().colourByChain();
865   }
866
867   @Override
868   public void chargeColour_actionPerformed()
869   {
870     chargeColour.setSelected(true);
871     getBinding().colourByCharge();
872   }
873
874   @Override
875   public void seqColour_actionPerformed()
876   {
877     AAStructureBindingModel binding = getBinding();
878     binding.setColourBySequence(seqColour.isSelected());
879     if (_colourwith == null)
880     {
881       _colourwith = new Vector<>();
882     }
883     if (binding.isColourBySequence())
884     {
885       if (!binding.isLoadingFromArchive())
886       {
887         if (_colourwith.size() == 0 && getAlignmentPanel() != null)
888         {
889           // Make the currently displayed alignment panel the associated view
890           _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
891         }
892       }
893       // Set the colour using the current view for the associated alignframe
894       for (AlignmentViewPanel alignPanel : _colourwith)
895       {
896         binding.colourBySequence(alignPanel);
897       }
898       seqColoursApplied = true;
899     }
900   }
901
902   @Override
903   public void pdbFile_actionPerformed()
904   {
905     // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
906     JalviewFileChooser chooser = new JalviewFileChooser(
907             Cache.getProperty("LAST_DIRECTORY"));
908
909     chooser.setFileView(new JalviewFileView());
910     chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
911     chooser.setToolTipText(MessageManager.getString("action.save"));
912
913     int value = chooser.showSaveDialog(this);
914
915     if (value == JalviewFileChooser.APPROVE_OPTION)
916     {
917       BufferedReader in = null;
918       try
919       {
920         // TODO: cope with multiple PDB files in view
921         in = new BufferedReader(
922                 new FileReader(getBinding().getStructureFiles()[0]));
923         File outFile = chooser.getSelectedFile();
924
925         PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
926         String data;
927         while ((data = in.readLine()) != null)
928         {
929           if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
930           {
931             out.println(data);
932           }
933         }
934         out.close();
935       } catch (Exception ex)
936       {
937         ex.printStackTrace();
938       } finally
939       {
940         if (in != null)
941         {
942           try
943           {
944             in.close();
945           } catch (IOException e)
946           {
947             // ignore
948           }
949         }
950       }
951     }
952   }
953
954   @Override
955   public void viewMapping_actionPerformed()
956   {
957     CutAndPasteTransfer cap = new CutAndPasteTransfer();
958     try
959     {
960       cap.appendText(getBinding().printMappings());
961     } catch (OutOfMemoryError e)
962     {
963       new OOMWarning(
964               "composing sequence-structure alignments for display in text box.",
965               e);
966       cap.dispose();
967       return;
968     }
969     Desktop.addInternalFrame(cap,
970             MessageManager.getString("label.pdb_sequence_mapping"), 550,
971             600);
972   }
973
974   protected abstract String getViewerName();
975
976   /**
977    * Configures the title and menu items of the viewer panel.
978    */
979   @Override
980   public void updateTitleAndMenus()
981   {
982     AAStructureBindingModel binding = getBinding();
983     if (binding.hasFileLoadingError())
984     {
985       repaint();
986       return;
987     }
988     setChainMenuItems(binding.getChainNames());
989
990     this.setTitle(binding.getViewerTitle(getViewerName(), true));
991
992     /*
993      * enable 'Superpose with' if more than one mapped structure
994      */
995     viewSelectionMenu.setEnabled(false);
996     if (getBinding().getMappedStructureCount() > 1
997             && getBinding().getSequence().length > 1)
998     {
999       viewSelectionMenu.setEnabled(true);
1000     }
1001
1002     /*
1003      * Show action menu if it has any enabled items
1004      */
1005     viewerActionMenu.setVisible(false);
1006     for (int i = 0; i < viewerActionMenu.getItemCount(); i++)
1007     {
1008       if (viewerActionMenu.getItem(i).isEnabled())
1009       {
1010         viewerActionMenu.setVisible(true);
1011         break;
1012       }
1013     }
1014
1015     if (!binding.isLoadingFromArchive())
1016     {
1017       seqColour_actionPerformed();
1018     }
1019   }
1020
1021   @Override
1022   public String toString()
1023   {
1024     return getTitle();
1025   }
1026
1027   @Override
1028   public boolean hasMapping()
1029   {
1030     if (worker != null && (addingStructures || _started))
1031     {
1032       return false;
1033     }
1034     if (getBinding() == null)
1035     {
1036       if (_aps == null || _aps.size() == 0)
1037       {
1038         // viewer has been closed, but we did at some point run.
1039         return true;
1040       }
1041       return false;
1042     }
1043     String[] pdbids = getBinding().getStructureFiles();
1044     if (pdbids == null)
1045     {
1046       return false;
1047     }
1048     int p = 0;
1049     for (String pdbid : pdbids)
1050     {
1051       StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
1052       if (sm != null && sm.length > 0 && sm[0] != null)
1053       {
1054         p++;
1055       }
1056     }
1057     // only return true if there is a mapping for every structure file we have
1058     // loaded
1059     if (p == 0 || p != pdbids.length)
1060     {
1061       return false;
1062     }
1063     // and that coloring has been applied
1064     return seqColoursApplied;
1065   }
1066
1067   @Override
1068   public void raiseViewer()
1069   {
1070     toFront();
1071   }
1072
1073   @Override
1074   public long startProgressBar(String msg)
1075   {
1076     // TODO would rather have startProgress/stopProgress as the
1077     // IProgressIndicator interface
1078     long tm = random.nextLong();
1079     if (progressBar != null)
1080     {
1081       progressBar.setProgressBar(msg, tm);
1082     }
1083     return tm;
1084   }
1085
1086   @Override
1087   public void stopProgressBar(String msg, long handle)
1088   {
1089     if (progressBar != null)
1090     {
1091       progressBar.setProgressBar(msg, handle);
1092     }
1093   }
1094
1095   protected IProgressIndicator getProgressIndicator()
1096   {
1097     return progressBar;
1098   }
1099
1100   protected void setProgressIndicator(IProgressIndicator pi)
1101   {
1102     progressBar = pi;
1103   }
1104
1105   public void setProgressMessage(String message, long id)
1106   {
1107     if (progressBar != null)
1108     {
1109       progressBar.setProgressBar(message, id);
1110     }
1111   }
1112
1113   @Override
1114   public void showConsole(boolean show)
1115   {
1116     // default does nothing
1117   }
1118
1119   /**
1120    * Show only the selected chain(s) in the viewer
1121    */
1122   protected void showSelectedChains()
1123   {
1124     List<String> toshow = new ArrayList<>();
1125     for (int i = 0; i < chainMenu.getItemCount(); i++)
1126     {
1127       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
1128       {
1129         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
1130         if (item.isSelected())
1131         {
1132           toshow.add(item.getText());
1133         }
1134       }
1135     }
1136     getBinding().showChains(toshow);
1137   }
1138
1139   /**
1140    * Tries to fetch a PDB file and save to a temporary local file. Returns the
1141    * saved file path if successful, or null if not.
1142    * 
1143    * @param processingEntry
1144    * @return
1145    */
1146   protected String fetchPdbFile(PDBEntry processingEntry)
1147   {
1148     String filePath = null;
1149     Pdb pdbclient = new Pdb();
1150     EBIAlfaFold afclient = new EBIAlfaFold();
1151     AlignmentI pdbseq = null;
1152     String pdbid = processingEntry.getId();
1153     long handle = System.currentTimeMillis()
1154             + Thread.currentThread().hashCode();
1155
1156     /*
1157      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
1158      */
1159     String msg = MessageManager.formatMessage("status.fetching_pdb",
1160             new Object[]
1161             { pdbid });
1162     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1163     // long hdl = startProgressBar(MessageManager.formatMessage(
1164     // "status.fetching_pdb", new Object[]
1165     // { pdbid }));
1166     try
1167     {
1168       if (afclient.isValidReference(pdbid))
1169       {
1170         pdbseq = afclient.getSequenceRecords(pdbid,
1171                 processingEntry.getRetrievalUrl());
1172       }
1173       else
1174       {
1175         if (processingEntry.hasRetrievalUrl())
1176         {
1177           String safePDBId = java.net.URLEncoder.encode(pdbid, "UTF-8")
1178                   .replace("%", "__");
1179
1180           // retrieve from URL to new local tmpfile
1181           File tmpFile = File.createTempFile(safePDBId,
1182                   "." + (PDBEntry.Type.MMCIF.toString().equals(
1183                           processingEntry.getType().toString()) ? "cif"
1184                                   : "pdb"));
1185           String fromUrl = processingEntry.getRetrievalUrl();
1186           UrlDownloadClient.download(fromUrl, tmpFile);
1187
1188           // may not need this check ?
1189           String file = tmpFile.getAbsolutePath();
1190           if (file != null)
1191           {
1192             pdbseq = EBIAlfaFold.importDownloadedStructureFromUrl(fromUrl,
1193                     tmpFile, pdbid, null, null, null);
1194           }
1195         }
1196         else
1197         {
1198           pdbseq = pdbclient.getSequenceRecords(pdbid);
1199         }
1200       }
1201     } catch (Exception e)
1202     {
1203       System.err.println(
1204               "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
1205     } finally
1206     {
1207       msg = pdbid + " " + MessageManager.getString("label.state_completed");
1208       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1209       // stopProgressBar(msg, hdl);
1210     }
1211     /*
1212      * If PDB data were saved and are not invalid (empty alignment), return the
1213      * file path.
1214      */
1215     if (pdbseq != null && pdbseq.getHeight() > 0)
1216     {
1217       // just use the file name from the first sequence's first PDBEntry
1218       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
1219               .elementAt(0).getFile()).getAbsolutePath();
1220       processingEntry.setFile(filePath);
1221     }
1222     return filePath;
1223   }
1224
1225   /**
1226    * If supported, saves the state of the structure viewer to a temporary file
1227    * and returns the file, else returns null
1228    * 
1229    * @return
1230    */
1231   public File saveSession()
1232   {
1233     if (getBinding() == null)
1234     {
1235       return null;
1236     }
1237     File session = getBinding().saveSession();
1238     long l = session.length();
1239     int wait = 50;
1240     do
1241     {
1242       try
1243       {
1244         Thread.sleep(5);
1245       } catch (InterruptedException e)
1246       {
1247       }
1248       long nextl = session.length();
1249       if (nextl != l)
1250       {
1251         wait = 50;
1252         l = nextl;
1253       }
1254     } while (--wait > 0);
1255     return session;
1256   }
1257
1258   /**
1259    * Close down this instance of Jalview's Chimera viewer, giving the user the
1260    * option to close the associated Chimera window (process). They may wish to
1261    * keep it open until they have had an opportunity to save any work.
1262    * 
1263    * @param forceClose
1264    *          if true, close any linked Chimera process; if false, prompt first
1265    */
1266   @Override
1267   public void closeViewer(boolean forceClose)
1268   {
1269     AAStructureBindingModel binding = getBinding();
1270     if (binding != null && binding.isViewerRunning())
1271     {
1272       if (!forceClose)
1273       {
1274         String viewerName = getViewerName();
1275
1276         int confirm = JvOptionPane.CANCEL_OPTION;
1277         String prompt = MessageManager
1278                 .formatMessage("label.confirm_close_viewer", new Object[]
1279                 { binding.getViewerTitle(viewerName, false), viewerName });
1280         prompt = JvSwingUtils.wrapTooltip(true, prompt);
1281         String title = MessageManager.getString("label.close_viewer");
1282         confirm = showCloseDialog(title, prompt);
1283
1284         /*
1285          * abort closure if user hits escape or Cancel
1286          */
1287         if (confirm == JvOptionPane.CANCEL_OPTION
1288                 || confirm == JvOptionPane.CLOSED_OPTION)
1289         {
1290           // abort possible quit handling if CANCEL chosen
1291           if (confirm == JvOptionPane.CANCEL_OPTION)
1292           {
1293             try
1294             {
1295               // this is a bit futile
1296               this.setClosed(false);
1297             } catch (PropertyVetoException e)
1298             {
1299             }
1300             QuitHandler.abortQuit();
1301           }
1302           return;
1303         }
1304         forceClose = confirm == JvOptionPane.YES_OPTION;
1305       }
1306     }
1307     if (binding != null)
1308     {
1309       binding.closeViewer(forceClose);
1310     }
1311     setAlignmentPanel(null);
1312     _aps.clear();
1313     _alignwith.clear();
1314     _colourwith.clear();
1315     // TODO: check for memory leaks where instance isn't finalised because jmb
1316     // holds a reference to the window
1317     // jmb = null;
1318     dispose();
1319   }
1320
1321   private int showCloseDialog(final String title, final String prompt)
1322   {
1323     confirmResponse = JvOptionPane.CANCEL_OPTION;
1324
1325     if (QuitHandler.quitting())
1326     {
1327
1328       Callable<Void> yesCall = () -> {
1329         Console.debug("YES");
1330         confirmResponse = JvOptionPane.YES_OPTION;
1331         return null;
1332       };
1333       Callable<Void> noCall = () -> {
1334         Console.debug("NO");
1335         confirmResponse = JvOptionPane.NO_OPTION;
1336         return null;
1337       };
1338       Callable<Void> cancelCall = () -> {
1339         Console.debug("CANCEL");
1340         confirmResponse = JvOptionPane.CANCEL_OPTION;
1341         return null;
1342       };
1343       Callable<Void>[] calls = new Callable[] { yesCall, noCall,
1344           cancelCall };
1345       String cancelQuit = MessageManager.getString("action.cancel_quit");
1346       String[] buttonsText = { MessageManager.getString("action.yes"),
1347           MessageManager.getString("action.no"), cancelQuit };
1348       JvOptionPane dialog = JvOptionPane.frameDialog(prompt,
1349               MessageManager.getString("label.close_viewer"),
1350               JvOptionPane.WARNING_MESSAGE, buttonsText, cancelQuit, calls,
1351               false);
1352       // wait for response
1353       ExecutorService executor = dialog.getExecutor();
1354       executor.shutdown();
1355       try
1356       {
1357         Console.debug("### executor.awaitTermination() starting");
1358         executor.awaitTermination(60, TimeUnit.SECONDS);
1359         Console.debug("### executor.awaitTermination() finished");
1360       } catch (InterruptedException e)
1361       {
1362       }
1363     }
1364     else
1365     {
1366       confirmResponse = JvOptionPane.showConfirmDialog(this, prompt,
1367               MessageManager.getString("label.close_viewer"),
1368               JvOptionPane.YES_NO_CANCEL_OPTION,
1369               JvOptionPane.WARNING_MESSAGE);
1370     }
1371     return confirmResponse;
1372   }
1373
1374   @Override
1375   public void showHelp_actionPerformed()
1376   {
1377     /*
1378     try
1379     {
1380     */
1381     String url = getBinding().getHelpURL();
1382     if (url != null)
1383     {
1384       BrowserLauncher.openURL(url);
1385     }
1386     /* 
1387     }
1388     catch (IOException ex)
1389     {
1390       System.err
1391               .println("Show " + getViewerName() + " failed with: "
1392                       + ex.getMessage());
1393     }
1394     */
1395   }
1396
1397   @Override
1398   public boolean hasViewerActionsMenu()
1399   {
1400     return viewerActionMenu != null && viewerActionMenu.isEnabled()
1401             && viewerActionMenu.getItemCount() > 0
1402             && viewerActionMenu.isVisible();
1403   }
1404
1405   private static int confirmResponse = 0;
1406 }