JAL-1933 update threshold on focus lost in text field
[jalview.git] / src / jalview / gui / ChimeraViewFrame.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 jalview.bin.Cache;
24 import jalview.datamodel.Alignment;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.ColumnSelection;
27 import jalview.datamodel.PDBEntry;
28 import jalview.datamodel.SequenceI;
29 import jalview.ext.rbvi.chimera.ChimeraCommands;
30 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
31 import jalview.gui.StructureViewer.ViewerType;
32 import jalview.io.DataSourceType;
33 import jalview.io.JalviewFileChooser;
34 import jalview.io.JalviewFileView;
35 import jalview.io.StructureFile;
36 import jalview.schemes.BuriedColourScheme;
37 import jalview.schemes.ColourSchemeI;
38 import jalview.schemes.HelixColourScheme;
39 import jalview.schemes.HydrophobicColourScheme;
40 import jalview.schemes.PurinePyrimidineColourScheme;
41 import jalview.schemes.StrandColourScheme;
42 import jalview.schemes.TaylorColourScheme;
43 import jalview.schemes.TurnColourScheme;
44 import jalview.schemes.ZappoColourScheme;
45 import jalview.structures.models.AAStructureBindingModel;
46 import jalview.util.MessageManager;
47 import jalview.util.Platform;
48 import jalview.ws.dbsources.Pdb;
49
50 import java.awt.event.ActionEvent;
51 import java.awt.event.ActionListener;
52 import java.awt.event.ItemEvent;
53 import java.awt.event.ItemListener;
54 import java.awt.event.MouseAdapter;
55 import java.awt.event.MouseEvent;
56 import java.io.BufferedReader;
57 import java.io.File;
58 import java.io.FileInputStream;
59 import java.io.FileOutputStream;
60 import java.io.FileReader;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.io.PrintWriter;
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.List;
67 import java.util.Random;
68 import java.util.Vector;
69
70 import javax.swing.JCheckBoxMenuItem;
71 import javax.swing.JColorChooser;
72 import javax.swing.JInternalFrame;
73 import javax.swing.JMenu;
74 import javax.swing.JMenuItem;
75 import javax.swing.JOptionPane;
76 import javax.swing.event.InternalFrameAdapter;
77 import javax.swing.event.InternalFrameEvent;
78 import javax.swing.event.MenuEvent;
79 import javax.swing.event.MenuListener;
80
81 /**
82  * GUI elements for handling an external chimera display
83  * 
84  * @author jprocter
85  *
86  */
87 public class ChimeraViewFrame extends StructureViewerBase
88 {
89   private JalviewChimeraBinding jmb;
90
91   private boolean allChainsSelected = false;
92
93   private IProgressIndicator progressBar = null;
94
95   /*
96    * Path to Chimera session file. This is set when an open Jalview/Chimera
97    * session is saved, or on restore from a Jalview project (if it holds the
98    * filename of any saved Chimera sessions).
99    */
100   private String chimeraSessionFile = null;
101
102   private Random random = new Random();
103
104   /**
105    * Initialise menu options.
106    */
107   private void initMenus()
108   {
109     viewerActionMenu.setText(MessageManager.getString("label.chimera"));
110     viewerColour.setText(MessageManager
111             .getString("label.colour_with_chimera"));
112     viewerColour.setToolTipText(MessageManager
113             .getString("label.let_chimera_manage_structure_colours"));
114     helpItem.setText(MessageManager.getString("label.chimera_help"));
115     seqColour.setSelected(jmb.isColourBySequence());
116     viewerColour.setSelected(!jmb.isColourBySequence());
117     if (_colourwith == null)
118     {
119       _colourwith = new Vector<AlignmentPanel>();
120     }
121     if (_alignwith == null)
122     {
123       _alignwith = new Vector<AlignmentPanel>();
124     }
125
126     // save As not yet implemented
127     savemenu.setVisible(false);
128
129     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
130             MessageManager.getString("label.colour_by"), this, _colourwith,
131             new ItemListener()
132             {
133               @Override
134               public void itemStateChanged(ItemEvent e)
135               {
136                 if (!seqColour.isSelected())
137                 {
138                   seqColour.doClick();
139                 }
140                 else
141                 {
142                   // update the Chimera display now.
143                   seqColour_actionPerformed(null);
144                 }
145               }
146             });
147     viewMenu.add(seqColourBy);
148     viewMenu.add(fitToWindow);
149
150     final ItemListener handler;
151     JMenu alpanels = new ViewSelectionMenu(
152             MessageManager.getString("label.superpose_with"), this,
153             _alignwith, handler = new ItemListener()
154             {
155               @Override
156               public void itemStateChanged(ItemEvent e)
157               {
158                 alignStructs.setEnabled(_alignwith.size() > 0);
159                 alignStructs.setToolTipText(MessageManager
160                         .formatMessage(
161                                 "label.align_structures_using_linked_alignment_views",
162                                 new Object[] { new Integer(_alignwith
163                                         .size()).toString() }));
164               }
165             });
166     handler.itemStateChanged(null);
167     viewerActionMenu.add(alpanels);
168     viewerActionMenu.addMenuListener(new MenuListener()
169     {
170
171       @Override
172       public void menuSelected(MenuEvent e)
173       {
174         handler.itemStateChanged(null);
175       }
176
177       @Override
178       public void menuDeselected(MenuEvent e)
179       {
180         // TODO Auto-generated method stub
181       }
182
183       @Override
184       public void menuCanceled(MenuEvent e)
185       {
186         // TODO Auto-generated method stub
187       }
188     });
189
190     JMenuItem writeFeatures = new JMenuItem(
191             MessageManager.getString("label.create_chimera_attributes"));
192     writeFeatures.setToolTipText(MessageManager
193             .getString("label.create_chimera_attributes_tip"));
194     writeFeatures.addActionListener(new ActionListener()
195     {
196       @Override
197       public void actionPerformed(ActionEvent e)
198       {
199         sendFeaturesToChimera();
200       }
201     });
202     viewerActionMenu.add(writeFeatures);
203
204     final JMenu fetchAttributes = new JMenu("Fetch Chimera attributes");
205     fetchAttributes
206             .setToolTipText("Copy Chimera attribute to Jalview feature");
207     fetchAttributes.addMouseListener(new MouseAdapter()
208     {
209
210       @Override
211       public void mouseEntered(MouseEvent e)
212       {
213         buildAttributesMenu(fetchAttributes);
214       }
215     });
216     viewerActionMenu.add(fetchAttributes);
217
218   }
219
220   /**
221    * Query Chimera for its residue attribute names and add them as items off the
222    * attributes menu
223    * 
224    * @param attributesMenu
225    */
226   protected void buildAttributesMenu(JMenu attributesMenu)
227   {
228     List<String> atts = jmb.sendChimeraCommand("list resattr", true);
229     if (atts == null)
230     {
231       return;
232     }
233     attributesMenu.removeAll();
234     Collections.sort(atts);
235     for (String att : atts)
236     {
237       final String attName = att.split(" ")[1];
238
239       /*
240        * ignore 'jv_*' attributes, as these are Jalview features that have
241        * been transferred to residue attributes in Chimera!
242        */
243       if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX))
244       {
245         JMenuItem menuItem = new JMenuItem(attName);
246         menuItem.addActionListener(new ActionListener()
247         {
248           @Override
249           public void actionPerformed(ActionEvent e)
250           {
251             getChimeraAttributes(attName);
252           }
253         });
254         attributesMenu.add(menuItem);
255       }
256     }
257   }
258
259   /**
260    * Read residues in Chimera with the given attribute name, and set as features
261    * on the corresponding sequence positions (if any)
262    * 
263    * @param attName
264    */
265   protected void getChimeraAttributes(String attName)
266   {
267     jmb.copyStructureAttributesToFeatures(attName, getAlignmentPanel());
268   }
269
270   /**
271    * Send a command to Chimera to create residue attributes for Jalview features
272    * <p>
273    * The syntax is: setattr r <attName> <attValue> <atomSpec>
274    * <p>
275    * For example: setattr r jv:chain "Ferredoxin-1, Chloroplastic" #0:94.A
276    */
277   protected void sendFeaturesToChimera()
278   {
279     jmb.sendFeaturesToViewer(getAlignmentPanel());
280   }
281
282   /**
283    * add a single PDB structure to a new or existing Chimera view
284    * 
285    * @param pdbentry
286    * @param seq
287    * @param chains
288    * @param ap
289    */
290   public ChimeraViewFrame(PDBEntry pdbentry, SequenceI[] seq,
291           String[] chains, final AlignmentPanel ap)
292   {
293     this();
294     String pdbId = pdbentry.getId();
295
296     /*
297      * If the PDB file is already loaded, the user may just choose to add to an
298      * existing viewer (or cancel)
299      */
300     if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
301     {
302       return;
303     }
304
305     /*
306      * Check if there are other Chimera views involving this alignment and give
307      * user the option to add and align this molecule to one of them (or cancel)
308      */
309     if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
310     {
311       return;
312     }
313
314     /*
315      * If the options above are declined or do not apply, show the structure in
316      * a new viewer
317      */
318     openNewChimera(ap, new PDBEntry[] { pdbentry },
319             new SequenceI[][] { seq });
320   }
321
322   /**
323    * Create a helper to manage progress bar display
324    */
325   protected void createProgressBar()
326   {
327     if (progressBar == null)
328     {
329       progressBar = new ProgressBar(statusPanel, statusBar);
330     }
331   }
332
333   /**
334    * Answers true if this viewer already involves the given PDB ID
335    */
336   @Override
337   protected boolean hasPdbId(String pdbId)
338   {
339     return jmb.hasPdbId(pdbId);
340   }
341
342   private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
343           SequenceI[][] seqs)
344   {
345     createProgressBar();
346     jmb = new JalviewChimeraBindingModel(this,
347             ap.getStructureSelectionManager(), pdbentrys, seqs, null);
348     addAlignmentPanel(ap);
349     useAlignmentPanelForColourbyseq(ap);
350     if (pdbentrys.length > 1)
351     {
352       alignAddedStructures = true;
353       useAlignmentPanelForSuperposition(ap);
354     }
355     jmb.setColourBySequence(true);
356     setSize(400, 400); // probably should be a configurable/dynamic default here
357     initMenus();
358
359     addingStructures = false;
360     worker = new Thread(this);
361     worker.start();
362
363     this.addInternalFrameListener(new InternalFrameAdapter()
364     {
365       @Override
366       public void internalFrameClosing(InternalFrameEvent internalFrameEvent)
367       {
368         closeViewer(false);
369       }
370     });
371
372   }
373
374   /**
375    * Create a new viewer from saved session state data including Chimera session
376    * file
377    * 
378    * @param chimeraSessionFile
379    * @param alignPanel
380    * @param pdbArray
381    * @param seqsArray
382    * @param colourByChimera
383    * @param colourBySequence
384    * @param newViewId
385    */
386   public ChimeraViewFrame(String chimeraSessionFile,
387           AlignmentPanel alignPanel, PDBEntry[] pdbArray,
388           SequenceI[][] seqsArray, boolean colourByChimera,
389           boolean colourBySequence, String newViewId)
390   {
391     this();
392     setViewId(newViewId);
393     this.chimeraSessionFile = chimeraSessionFile;
394     openNewChimera(alignPanel, pdbArray, seqsArray);
395     if (colourByChimera)
396     {
397       jmb.setColourBySequence(false);
398       seqColour.setSelected(false);
399       viewerColour.setSelected(true);
400     }
401     else if (colourBySequence)
402     {
403       jmb.setColourBySequence(true);
404       seqColour.setSelected(true);
405       viewerColour.setSelected(false);
406     }
407   }
408
409   /**
410    * create a new viewer containing several structures superimposed using the
411    * given alignPanel.
412    * 
413    * @param pe
414    * @param seqs
415    * @param ap
416    */
417   public ChimeraViewFrame(PDBEntry[] pe, SequenceI[][] seqs,
418           AlignmentPanel ap)
419   {
420     this();
421     openNewChimera(ap, pe, seqs);
422   }
423
424   /**
425    * Default constructor
426    */
427   public ChimeraViewFrame()
428   {
429     super();
430
431     /*
432      * closeViewer will decide whether or not to close this frame
433      * depending on whether user chooses to Cancel or not
434      */
435     setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
436   }
437
438   /**
439    * Returns a list of any Chimera viewers in the desktop. The list is
440    * restricted to those linked to the given alignment panel if it is not null.
441    */
442   @Override
443   protected List<StructureViewerBase> getViewersFor(AlignmentPanel ap)
444   {
445     List<StructureViewerBase> result = new ArrayList<StructureViewerBase>();
446     JInternalFrame[] frames = Desktop.instance.getAllFrames();
447
448     for (JInternalFrame frame : frames)
449     {
450       if (frame instanceof ChimeraViewFrame)
451       {
452         if (ap == null || ((StructureViewerBase) frame).isLinkedWith(ap))
453         {
454           result.add((StructureViewerBase) frame);
455         }
456       }
457     }
458     return result;
459   }
460
461   /**
462    * Launch Chimera. If we have a chimera session file name, send Chimera the
463    * command to open its saved session file.
464    */
465   void initChimera()
466   {
467     Desktop.addInternalFrame(this, jmb.getViewerTitle("Chimera", true),
468             getBounds().width, getBounds().height);
469
470     if (!jmb.launchChimera())
471     {
472       JvOptionPane.showMessageDialog(Desktop.desktop,
473               MessageManager.getString("label.chimera_failed"),
474               MessageManager.getString("label.error_loading_file"),
475               JvOptionPane.ERROR_MESSAGE);
476       this.dispose();
477       return;
478     }
479
480     if (this.chimeraSessionFile != null)
481     {
482       boolean opened = jmb.openSession(chimeraSessionFile);
483       if (!opened)
484       {
485         System.err
486                 .println("An error occurred opening Chimera session file "
487                         + chimeraSessionFile);
488       }
489     }
490
491     jmb.startChimeraListener();
492   }
493
494   /**
495    * If the list is not empty, add menu items for 'All' and each individual
496    * chain to the "View | Show Chain" sub-menu. Multiple selections are allowed.
497    * 
498    * @param chainNames
499    */
500   @Override
501   void setChainMenuItems(List<String> chainNames)
502   {
503     chainMenu.removeAll();
504     if (chainNames == null || chainNames.isEmpty())
505     {
506       return;
507     }
508     JMenuItem menuItem = new JMenuItem(
509             MessageManager.getString("label.all"));
510     menuItem.addActionListener(new ActionListener()
511     {
512       @Override
513       public void actionPerformed(ActionEvent evt)
514       {
515         allChainsSelected = true;
516         for (int i = 0; i < chainMenu.getItemCount(); i++)
517         {
518           if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
519           {
520             ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
521           }
522         }
523         showSelectedChains();
524         allChainsSelected = false;
525       }
526     });
527
528     chainMenu.add(menuItem);
529
530     for (String chainName : chainNames)
531     {
532       menuItem = new JCheckBoxMenuItem(chainName, true);
533       menuItem.addItemListener(new ItemListener()
534       {
535         @Override
536         public void itemStateChanged(ItemEvent evt)
537         {
538           if (!allChainsSelected)
539           {
540             showSelectedChains();
541           }
542         }
543       });
544
545       chainMenu.add(menuItem);
546     }
547   }
548
549   /**
550    * Show only the selected chain(s) in the viewer
551    */
552   @Override
553   void showSelectedChains()
554   {
555     List<String> toshow = new ArrayList<String>();
556     for (int i = 0; i < chainMenu.getItemCount(); i++)
557     {
558       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
559       {
560         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
561         if (item.isSelected())
562         {
563           toshow.add(item.getText());
564         }
565       }
566     }
567     jmb.showChains(toshow);
568   }
569
570   /**
571    * Close down this instance of Jalview's Chimera viewer, giving the user the
572    * option to close the associated Chimera window (process). They may wish to
573    * keep it open until they have had an opportunity to save any work.
574    * 
575    * @param closeChimera
576    *          if true, close any linked Chimera process; if false, prompt first
577    */
578   @Override
579   public void closeViewer(boolean closeChimera)
580   {
581     if (jmb != null && jmb.isChimeraRunning())
582     {
583       if (!closeChimera)
584       {
585         String prompt = MessageManager.formatMessage(
586                 "label.confirm_close_chimera",
587                 new Object[] { jmb.getViewerTitle("Chimera", false) });
588         prompt = JvSwingUtils.wrapTooltip(true, prompt);
589         int confirm = JvOptionPane.showConfirmDialog(this, prompt,
590                 MessageManager.getString("label.close_viewer"),
591                 JvOptionPane.YES_NO_CANCEL_OPTION);
592         /*
593          * abort closure if user hits escape or Cancel
594          */
595         if (confirm == JvOptionPane.CANCEL_OPTION
596                 || confirm == JvOptionPane.CLOSED_OPTION)
597         {
598           return;
599         }
600         closeChimera = confirm == JvOptionPane.YES_OPTION;
601       }
602       jmb.closeViewer(closeChimera);
603     }
604     setAlignmentPanel(null);
605     _aps.clear();
606     _alignwith.clear();
607     _colourwith.clear();
608     // TODO: check for memory leaks where instance isn't finalised because jmb
609     // holds a reference to the window
610     jmb = null;
611     dispose();
612   }
613
614   /**
615    * Open any newly added PDB structures in Chimera, having first fetched data
616    * from PDB (if not already saved).
617    */
618   @Override
619   public void run()
620   {
621     _started = true;
622     // todo - record which pdbids were successfully imported.
623     StringBuilder errormsgs = new StringBuilder(128);
624     StringBuilder files = new StringBuilder(128);
625     List<PDBEntry> filePDB = new ArrayList<PDBEntry>();
626     List<Integer> filePDBpos = new ArrayList<Integer>();
627     PDBEntry thePdbEntry = null;
628     StructureFile pdb = null;
629     try
630     {
631       String[] curfiles = jmb.getPdbFile(); // files currently in viewer
632       // TODO: replace with reference fetching/transfer code (validate PDBentry
633       // as a DBRef?)
634       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
635       {
636         String file = null;
637         thePdbEntry = jmb.getPdbEntry(pi);
638         if (thePdbEntry.getFile() == null)
639         {
640           /*
641            * Retrieve PDB data, save to file, attach to PDBEntry
642            */
643           file = fetchPdbFile(thePdbEntry);
644           if (file == null)
645           {
646             errormsgs.append("'" + thePdbEntry.getId() + "' ");
647           }
648         }
649         else
650         {
651           /*
652            * Got file already - ignore if already loaded in Chimera.
653            */
654           file = new File(thePdbEntry.getFile()).getAbsoluteFile()
655                   .getPath();
656           if (curfiles != null && curfiles.length > 0)
657           {
658             addingStructures = true; // already files loaded.
659             for (int c = 0; c < curfiles.length; c++)
660             {
661               if (curfiles[c].equals(file))
662               {
663                 file = null;
664                 break;
665               }
666             }
667           }
668         }
669         if (file != null)
670         {
671           filePDB.add(thePdbEntry);
672           filePDBpos.add(Integer.valueOf(pi));
673           files.append(" \"" + Platform.escapeString(file) + "\"");
674         }
675       }
676     } catch (OutOfMemoryError oomerror)
677     {
678       new OOMWarning("Retrieving PDB files: " + thePdbEntry.getId(),
679               oomerror);
680     } catch (Exception ex)
681     {
682       ex.printStackTrace();
683       errormsgs.append("When retrieving pdbfiles for '"
684               + thePdbEntry.getId() + "'");
685     }
686     if (errormsgs.length() > 0)
687     {
688
689       JvOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
690               .formatMessage("label.pdb_entries_couldnt_be_retrieved",
691                       new Object[] { errormsgs.toString() }),
692               MessageManager.getString("label.couldnt_load_file"),
693               JvOptionPane.ERROR_MESSAGE);
694     }
695
696     if (files.length() > 0)
697     {
698       jmb.setFinishedInit(false);
699       if (!addingStructures)
700       {
701         try
702         {
703           initChimera();
704         } catch (Exception ex)
705         {
706           Cache.log.error("Couldn't open Chimera viewer!", ex);
707         }
708       }
709       int num = -1;
710       for (PDBEntry pe : filePDB)
711       {
712         num++;
713         if (pe.getFile() != null)
714         {
715           try
716           {
717             int pos = filePDBpos.get(num).intValue();
718             long startTime = startProgressBar("Chimera "
719                     + MessageManager.getString("status.opening_file_for")
720                     + " " + pe.getId());
721             jmb.openFile(pe);
722             jmb.addSequence(pos, jmb.getSequence()[pos]);
723             File fl = new File(pe.getFile());
724             DataSourceType protocol = DataSourceType.URL;
725             try
726             {
727               if (fl.exists())
728               {
729                 protocol = DataSourceType.FILE;
730               }
731             } catch (Throwable e)
732             {
733             } finally
734             {
735               stopProgressBar("", startTime);
736             }
737             // Explicitly map to the filename used by Chimera ;
738             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
739                     jmb.getChains()[pos], pe.getFile(), protocol);
740             stashFoundChains(pdb, pe.getFile());
741           } catch (OutOfMemoryError oomerror)
742           {
743             new OOMWarning(
744                     "When trying to open and map structures from Chimera!",
745                     oomerror);
746           } catch (Exception ex)
747           {
748             Cache.log.error("Couldn't open " + pe.getFile()
749                     + " in Chimera viewer!", ex);
750           } finally
751           {
752             Cache.log.debug("File locations are " + files);
753           }
754         }
755       }
756
757       jmb.refreshGUI();
758       jmb.setFinishedInit(true);
759       jmb.setLoadingFromArchive(false);
760
761       // refresh the sequence colours for the new structure(s)
762       for (AlignmentPanel ap : _colourwith)
763       {
764         jmb.updateColours(ap);
765       }
766       // do superposition if asked to
767       if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
768       {
769         new Thread(new Runnable()
770         {
771           @Override
772           public void run()
773           {
774             alignStructs_withAllAlignPanels();
775           }
776         }).start();
777         alignAddedStructures = false;
778       }
779       addingStructures = false;
780     }
781     _started = false;
782     worker = null;
783   }
784
785   /**
786    * Fetch PDB data and save to a local file. Returns the full path to the file,
787    * or null if fetch fails.
788    * 
789    * @param processingEntry
790    * @return
791    * @throws Exception
792    */
793
794   private void stashFoundChains(StructureFile pdb, String file)
795   {
796     for (int i = 0; i < pdb.getChains().size(); i++)
797     {
798       String chid = new String(pdb.getId() + ":"
799               + pdb.getChains().elementAt(i).id);
800       jmb.getChainNames().add(chid);
801       jmb.getChainFile().put(chid, file);
802     }
803   }
804   private String fetchPdbFile(PDBEntry processingEntry) throws Exception
805   {
806     // FIXME: this is duplicated code with Jmol frame ?
807     String filePath = null;
808     Pdb pdbclient = new Pdb();
809     AlignmentI pdbseq = null;
810     String pdbid = processingEntry.getId();
811     long handle = System.currentTimeMillis()
812             + Thread.currentThread().hashCode();
813
814     /*
815      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
816      */
817     String msg = MessageManager.formatMessage("status.fetching_pdb",
818             new Object[] { pdbid });
819     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
820     // long hdl = startProgressBar(MessageManager.formatMessage(
821     // "status.fetching_pdb", new Object[]
822     // { pdbid }));
823     try
824     {
825       pdbseq = pdbclient.getSequenceRecords(pdbid);
826     } catch (OutOfMemoryError oomerror)
827     {
828       new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
829     } finally
830     {
831       msg = pdbid + " " + MessageManager.getString("label.state_completed");
832       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
833       // stopProgressBar(msg, hdl);
834     }
835     /*
836      * If PDB data were saved and are not invalid (empty alignment), return the
837      * file path.
838      */
839     if (pdbseq != null && pdbseq.getHeight() > 0)
840     {
841       // just use the file name from the first sequence's first PDBEntry
842       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
843               .elementAt(0).getFile()).getAbsolutePath();
844       processingEntry.setFile(filePath);
845     }
846     return filePath;
847   }
848
849   /**
850    * Convenience method to update the progress bar if there is one. Be sure to
851    * call stopProgressBar with the returned handle to remove the message.
852    * 
853    * @param msg
854    * @param handle
855    */
856   public long startProgressBar(String msg)
857   {
858     // TODO would rather have startProgress/stopProgress as the
859     // IProgressIndicator interface
860     long tm = random.nextLong();
861     if (progressBar != null)
862     {
863       progressBar.setProgressBar(msg, tm);
864     }
865     return tm;
866   }
867
868   /**
869    * End the progress bar with the specified handle, leaving a message (if not
870    * null) on the status bar
871    * 
872    * @param msg
873    * @param handle
874    */
875   public void stopProgressBar(String msg, long handle)
876   {
877     if (progressBar != null)
878     {
879       progressBar.setProgressBar(msg, handle);
880     }
881   }
882
883   @Override
884   public void pdbFile_actionPerformed(ActionEvent actionEvent)
885   {
886     JalviewFileChooser chooser = new JalviewFileChooser(
887             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
888
889     chooser.setFileView(new JalviewFileView());
890     chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
891     chooser.setToolTipText(MessageManager.getString("action.save"));
892
893     int value = chooser.showSaveDialog(this);
894
895     if (value == JalviewFileChooser.APPROVE_OPTION)
896     {
897       BufferedReader in = null;
898       try
899       {
900         // TODO: cope with multiple PDB files in view
901         in = new BufferedReader(new FileReader(jmb.getPdbFile()[0]));
902         File outFile = chooser.getSelectedFile();
903
904         PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
905         String data;
906         while ((data = in.readLine()) != null)
907         {
908           if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
909           {
910             out.println(data);
911           }
912         }
913         out.close();
914       } catch (Exception ex)
915       {
916         ex.printStackTrace();
917       } finally
918       {
919         if (in != null)
920         {
921           try
922           {
923             in.close();
924           } catch (IOException e)
925           {
926             e.printStackTrace();
927           }
928         }
929       }
930     }
931   }
932
933   @Override
934   public void viewMapping_actionPerformed(ActionEvent actionEvent)
935   {
936     jalview.gui.CutAndPasteTransfer cap = new jalview.gui.CutAndPasteTransfer();
937     try
938     {
939       cap.appendText(jmb.printMappings());
940     } catch (OutOfMemoryError e)
941     {
942       new OOMWarning(
943               "composing sequence-structure alignments for display in text box.",
944               e);
945       cap.dispose();
946       return;
947     }
948     jalview.gui.Desktop.addInternalFrame(cap,
949             MessageManager.getString("label.pdb_sequence_mapping"), 550,
950             600);
951   }
952
953   @Override
954   public void eps_actionPerformed(ActionEvent e)
955   {
956     throw new Error(
957             MessageManager
958                     .getString("error.eps_generation_not_implemented"));
959   }
960
961   @Override
962   public void png_actionPerformed(ActionEvent e)
963   {
964     throw new Error(
965             MessageManager
966                     .getString("error.png_generation_not_implemented"));
967   }
968
969   @Override
970   public void viewerColour_actionPerformed(ActionEvent actionEvent)
971   {
972     if (viewerColour.isSelected())
973     {
974       // disable automatic sequence colouring.
975       jmb.setColourBySequence(false);
976     }
977   }
978
979   @Override
980   public void seqColour_actionPerformed(ActionEvent actionEvent)
981   {
982     jmb.setColourBySequence(seqColour.isSelected());
983     if (_colourwith == null)
984     {
985       _colourwith = new Vector<AlignmentPanel>();
986     }
987     if (jmb.isColourBySequence())
988     {
989       if (!jmb.isLoadingFromArchive())
990       {
991         if (_colourwith.size() == 0 && getAlignmentPanel() != null)
992         {
993           // Make the currently displayed alignment panel the associated view
994           _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
995         }
996       }
997       // Set the colour using the current view for the associated alignframe
998       for (AlignmentPanel ap : _colourwith)
999       {
1000         jmb.colourBySequence(ap.av.isShowSequenceFeatures(), ap);
1001       }
1002     }
1003   }
1004
1005   @Override
1006   public void chainColour_actionPerformed(ActionEvent actionEvent)
1007   {
1008     chainColour.setSelected(true);
1009     jmb.colourByChain();
1010   }
1011
1012   @Override
1013   public void chargeColour_actionPerformed(ActionEvent actionEvent)
1014   {
1015     chargeColour.setSelected(true);
1016     jmb.colourByCharge();
1017   }
1018
1019   @Override
1020   public void zappoColour_actionPerformed(ActionEvent actionEvent)
1021   {
1022     zappoColour.setSelected(true);
1023     jmb.setJalviewColourScheme(new ZappoColourScheme());
1024   }
1025
1026   @Override
1027   public void taylorColour_actionPerformed(ActionEvent actionEvent)
1028   {
1029     taylorColour.setSelected(true);
1030     jmb.setJalviewColourScheme(new TaylorColourScheme());
1031   }
1032
1033   @Override
1034   public void hydroColour_actionPerformed(ActionEvent actionEvent)
1035   {
1036     hydroColour.setSelected(true);
1037     jmb.setJalviewColourScheme(new HydrophobicColourScheme());
1038   }
1039
1040   @Override
1041   public void helixColour_actionPerformed(ActionEvent actionEvent)
1042   {
1043     helixColour.setSelected(true);
1044     jmb.setJalviewColourScheme(new HelixColourScheme());
1045   }
1046
1047   @Override
1048   public void strandColour_actionPerformed(ActionEvent actionEvent)
1049   {
1050     strandColour.setSelected(true);
1051     jmb.setJalviewColourScheme(new StrandColourScheme());
1052   }
1053
1054   @Override
1055   public void turnColour_actionPerformed(ActionEvent actionEvent)
1056   {
1057     turnColour.setSelected(true);
1058     jmb.setJalviewColourScheme(new TurnColourScheme());
1059   }
1060
1061   @Override
1062   public void buriedColour_actionPerformed(ActionEvent actionEvent)
1063   {
1064     buriedColour.setSelected(true);
1065     jmb.setJalviewColourScheme(new BuriedColourScheme());
1066   }
1067
1068   @Override
1069   public void purinePyrimidineColour_actionPerformed(ActionEvent actionEvent)
1070   {
1071     setJalviewColourScheme(new PurinePyrimidineColourScheme());
1072   }
1073
1074   @Override
1075   public void userColour_actionPerformed(ActionEvent actionEvent)
1076   {
1077     userColour.setSelected(true);
1078     new UserDefinedColours(this, null);
1079   }
1080
1081   @Override
1082   public void backGround_actionPerformed(ActionEvent actionEvent)
1083   {
1084     java.awt.Color col = JColorChooser
1085             .showDialog(this, MessageManager
1086                     .getString("label.select_backgroud_colour"), null);
1087     if (col != null)
1088     {
1089       jmb.setBackgroundColour(col);
1090     }
1091   }
1092
1093   @Override
1094   public void showHelp_actionPerformed(ActionEvent actionEvent)
1095   {
1096     try
1097     {
1098       jalview.util.BrowserLauncher
1099               .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
1100     } catch (Exception ex)
1101     {
1102     }
1103   }
1104
1105   public void updateTitleAndMenus()
1106   {
1107     if (jmb.fileLoadingError != null && jmb.fileLoadingError.length() > 0)
1108     {
1109       repaint();
1110       return;
1111     }
1112     setChainMenuItems(jmb.getChainNames());
1113
1114     this.setTitle(jmb.getViewerTitle("Chimera", true));
1115     // if (jmb.getPdbFile().length > 1 && jmb.getSequence().length > 1)
1116     // {
1117       viewerActionMenu.setVisible(true);
1118     // }
1119     if (!jmb.isLoadingFromArchive())
1120     {
1121       seqColour_actionPerformed(null);
1122     }
1123   }
1124
1125   /*
1126    * (non-Javadoc)
1127    * 
1128    * @see
1129    * jalview.jbgui.GStructureViewer#alignStructs_actionPerformed(java.awt.event
1130    * .ActionEvent)
1131    */
1132   @Override
1133   protected void alignStructs_actionPerformed(ActionEvent actionEvent)
1134   {
1135     alignStructs_withAllAlignPanels();
1136   }
1137
1138   private void alignStructs_withAllAlignPanels()
1139   {
1140     if (getAlignmentPanel() == null)
1141     {
1142       return;
1143     }
1144
1145     if (_alignwith.size() == 0)
1146     {
1147       _alignwith.add(getAlignmentPanel());
1148     }
1149
1150     try
1151     {
1152       AlignmentI[] als = new Alignment[_alignwith.size()];
1153       ColumnSelection[] alc = new ColumnSelection[_alignwith.size()];
1154       int[] alm = new int[_alignwith.size()];
1155       int a = 0;
1156
1157       for (AlignmentPanel ap : _alignwith)
1158       {
1159         als[a] = ap.av.getAlignment();
1160         alm[a] = -1;
1161         alc[a++] = ap.av.getColumnSelection();
1162       }
1163       jmb.superposeStructures(als, alm, alc);
1164     } catch (Exception e)
1165     {
1166       StringBuffer sp = new StringBuffer();
1167       for (AlignmentPanel ap : _alignwith)
1168       {
1169         sp.append("'" + ap.alignFrame.getTitle() + "' ");
1170       }
1171       Cache.log.info("Couldn't align structures with the " + sp.toString()
1172               + "associated alignment panels.", e);
1173     }
1174   }
1175
1176   @Override
1177   public void setJalviewColourScheme(ColourSchemeI ucs)
1178   {
1179     jmb.setJalviewColourScheme(ucs);
1180
1181   }
1182
1183   /**
1184    * 
1185    * @param alignment
1186    * @return first alignment panel displaying given alignment, or the default
1187    *         alignment panel
1188    */
1189   public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment)
1190   {
1191     for (AlignmentPanel ap : getAllAlignmentPanels())
1192     {
1193       if (ap.av.getAlignment() == alignment)
1194       {
1195         return ap;
1196       }
1197     }
1198     return getAlignmentPanel();
1199   }
1200
1201   @Override
1202   public AAStructureBindingModel getBinding()
1203   {
1204     return jmb;
1205   }
1206
1207   /**
1208    * Ask Chimera to save its session to the designated file path, or to a
1209    * temporary file if the path is null. Returns the file path if successful,
1210    * else null.
1211    * 
1212    * @param filepath
1213    * @see getStateInfo
1214    */
1215   protected String saveSession(String filepath)
1216   {
1217     String pathUsed = filepath;
1218     try
1219     {
1220       if (pathUsed == null)
1221       {
1222         File tempFile = File.createTempFile("chimera", ".py");
1223         tempFile.deleteOnExit();
1224         pathUsed = tempFile.getPath();
1225       }
1226       boolean result = jmb.saveSession(pathUsed);
1227       if (result)
1228       {
1229         this.chimeraSessionFile = pathUsed;
1230         return pathUsed;
1231       }
1232     } catch (IOException e)
1233     {
1234     }
1235     return null;
1236   }
1237
1238   /**
1239    * Returns a string representing the state of the Chimera session. This is
1240    * done by requesting Chimera to save its session to a temporary file, then
1241    * reading the file contents. Returns an empty string on any error.
1242    */
1243   @Override
1244   public String getStateInfo()
1245   {
1246     String sessionFile = saveSession(null);
1247     if (sessionFile == null)
1248     {
1249       return "";
1250     }
1251     InputStream is = null;
1252     try
1253     {
1254       File f = new File(sessionFile);
1255       byte[] bytes = new byte[(int) f.length()];
1256       is = new FileInputStream(sessionFile);
1257       is.read(bytes);
1258       return new String(bytes);
1259     } catch (IOException e)
1260     {
1261       return "";
1262     } finally
1263     {
1264       if (is != null)
1265       {
1266         try
1267         {
1268           is.close();
1269         } catch (IOException e)
1270         {
1271           // ignore
1272         }
1273       }
1274     }
1275   }
1276
1277   @Override
1278   protected void fitToWindow_actionPerformed()
1279   {
1280     jmb.focusView();
1281   }
1282
1283   @Override
1284   public ViewerType getViewerType()
1285   {
1286     return ViewerType.CHIMERA;
1287   }
1288
1289   @Override
1290   protected AAStructureBindingModel getBindingModel()
1291   {
1292     return jmb;
1293   }
1294 }