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