JAL-2668 fix broken factory method for colour scheme
[jalview.git] / src / jalview / gui / TreePanel.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.analysis.AlignmentSorter;
24 import jalview.analysis.AverageDistanceTree;
25 import jalview.analysis.NJTree;
26 import jalview.analysis.TreeBuilder;
27 import jalview.analysis.TreeModel;
28 import jalview.analysis.scoremodels.ScoreModels;
29 import jalview.api.analysis.ScoreModelI;
30 import jalview.api.analysis.SimilarityParamsI;
31 import jalview.bin.Cache;
32 import jalview.commands.CommandI;
33 import jalview.commands.OrderCommand;
34 import jalview.datamodel.Alignment;
35 import jalview.datamodel.AlignmentI;
36 import jalview.datamodel.AlignmentView;
37 import jalview.datamodel.BinaryNode;
38 import jalview.datamodel.DBRefEntry;
39 import jalview.datamodel.HiddenColumns;
40 import jalview.datamodel.NodeTransformI;
41 import jalview.datamodel.SequenceFeature;
42 import jalview.datamodel.SequenceI;
43 import jalview.datamodel.SequenceNode;
44 import jalview.io.JalviewFileChooser;
45 import jalview.io.JalviewFileView;
46 import jalview.io.NewickFile;
47 import jalview.jbgui.GTreePanel;
48 import jalview.util.ImageMaker;
49 import jalview.util.MessageManager;
50 import jalview.viewmodel.AlignmentViewport;
51
52 import java.awt.Font;
53 import java.awt.Graphics;
54 import java.awt.event.ActionEvent;
55 import java.awt.event.ActionListener;
56 import java.awt.image.BufferedImage;
57 import java.beans.PropertyChangeEvent;
58 import java.beans.PropertyChangeListener;
59 import java.io.FileOutputStream;
60 import java.util.ArrayList;
61 import java.util.List;
62
63 import javax.imageio.ImageIO;
64 import javax.swing.ButtonGroup;
65 import javax.swing.JMenuItem;
66 import javax.swing.JRadioButtonMenuItem;
67 import javax.swing.event.InternalFrameAdapter;
68 import javax.swing.event.InternalFrameEvent;
69
70 import org.jibble.epsgraphics.EpsGraphics2D;
71
72 /**
73  * DOCUMENT ME!
74  * 
75  * @author $author$
76  * @version $Revision$
77  */
78 public class TreePanel extends GTreePanel
79 {
80   String treeType;
81
82   String scoreModelName; // if tree computed
83
84   String treeTitle; // if tree loaded
85
86   SimilarityParamsI similarityParams;
87
88   TreeCanvas treeCanvas;
89
90   TreeModel tree;
91
92   AlignViewport av;
93
94   /**
95    * Creates a new TreePanel object.
96    * 
97    * @param ap
98    * @param type
99    * @param modelName
100    * @param options
101    */
102   public TreePanel(AlignmentPanel ap, String type, String modelName,
103           SimilarityParamsI options)
104   {
105     super();
106     this.similarityParams = options;
107     initTreePanel(ap, type, modelName, null, null);
108
109     // We know this tree has distances. JBPNote TODO: prolly should add this as
110     // a userdefined default
111     // showDistances(true);
112   }
113
114   public TreePanel(AlignmentPanel alignPanel, NewickFile newtree,
115           String theTitle, AlignmentView inputData)
116   {
117     super();
118     this.treeTitle = theTitle;
119     initTreePanel(alignPanel, null, null, newtree, inputData);
120   }
121
122   public AlignmentI getAlignment()
123   {
124     return treeCanvas.av.getAlignment();
125   }
126
127   public AlignmentViewport getViewPort()
128   {
129     return treeCanvas.av;
130   }
131
132   void initTreePanel(AlignmentPanel ap, String type, String modelName,
133           NewickFile newTree, AlignmentView inputData)
134   {
135
136     av = ap.av;
137     this.treeType = type;
138     this.scoreModelName = modelName;
139
140     treeCanvas = new TreeCanvas(this, ap, scrollPane);
141     scrollPane.setViewportView(treeCanvas);
142
143     PaintRefresher.Register(this, ap.av.getSequenceSetId());
144
145     buildAssociatedViewMenu();
146
147     final PropertyChangeListener listener = addAlignmentListener();
148
149     /*
150      * remove listener when window is closed, so that this
151      * panel can be garbage collected
152      */
153     addInternalFrameListener(new InternalFrameAdapter()
154     {
155       @Override
156       public void internalFrameClosed(InternalFrameEvent evt)
157       {
158         if (av != null)
159         {
160           av.removePropertyChangeListener(listener);
161         }
162       }
163     });
164
165     TreeLoader tl = new TreeLoader(newTree, inputData);
166     tl.start();
167
168   }
169
170   /**
171    * @return
172    */
173   protected PropertyChangeListener addAlignmentListener()
174   {
175     final PropertyChangeListener listener = new PropertyChangeListener()
176     {
177       @Override
178       public void propertyChange(PropertyChangeEvent evt)
179       {
180         if (evt.getPropertyName().equals("alignment"))
181         {
182           if (tree == null)
183           {
184             System.out.println("tree is null");
185             // TODO: deal with case when a change event is received whilst a
186             // tree is still being calculated - should save reference for
187             // processing message later.
188             return;
189           }
190           if (evt.getNewValue() == null)
191           {
192             System.out.println(
193                     "new alignment sequences vector value is null");
194           }
195
196           tree.updatePlaceHolders((List<SequenceI>) evt.getNewValue());
197           treeCanvas.nameHash.clear(); // reset the mapping between canvas
198           // rectangles and leafnodes
199           repaint();
200         }
201       }
202     };
203     av.addPropertyChangeListener(listener);
204     return listener;
205   }
206
207   @Override
208   public void viewMenu_menuSelected()
209   {
210     buildAssociatedViewMenu();
211   }
212
213   void buildAssociatedViewMenu()
214   {
215     AlignmentPanel[] aps = PaintRefresher
216             .getAssociatedPanels(av.getSequenceSetId());
217     if (aps.length == 1 && treeCanvas.ap == aps[0])
218     {
219       associateLeavesMenu.setVisible(false);
220       return;
221     }
222
223     associateLeavesMenu.setVisible(true);
224
225     if ((viewMenu
226             .getItem(viewMenu.getItemCount() - 2) instanceof JMenuItem))
227     {
228       viewMenu.insertSeparator(viewMenu.getItemCount() - 1);
229     }
230
231     associateLeavesMenu.removeAll();
232
233     JRadioButtonMenuItem item;
234     ButtonGroup buttonGroup = new ButtonGroup();
235     int i, iSize = aps.length;
236     final TreePanel thisTreePanel = this;
237     for (i = 0; i < iSize; i++)
238     {
239       final AlignmentPanel ap = aps[i];
240       item = new JRadioButtonMenuItem(ap.av.viewName, ap == treeCanvas.ap);
241       buttonGroup.add(item);
242       item.addActionListener(new ActionListener()
243       {
244         @Override
245         public void actionPerformed(ActionEvent evt)
246         {
247           treeCanvas.applyToAllViews = false;
248           treeCanvas.ap = ap;
249           treeCanvas.av = ap.av;
250           PaintRefresher.Register(thisTreePanel, ap.av.getSequenceSetId());
251         }
252       });
253
254       associateLeavesMenu.add(item);
255     }
256
257     final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
258             MessageManager.getString("label.all_views"));
259     buttonGroup.add(itemf);
260     itemf.setSelected(treeCanvas.applyToAllViews);
261     itemf.addActionListener(new ActionListener()
262     {
263       @Override
264       public void actionPerformed(ActionEvent evt)
265       {
266         treeCanvas.applyToAllViews = itemf.isSelected();
267       }
268     });
269     associateLeavesMenu.add(itemf);
270
271   }
272
273   class TreeLoader extends Thread
274   {
275     private NewickFile newtree;
276
277     private AlignmentView odata = null;
278
279     public TreeLoader(NewickFile newickFile, AlignmentView inputData)
280     {
281       this.newtree = newickFile;
282       this.odata = inputData;
283
284       if (newickFile != null)
285       {
286         // Must be outside run(), as Jalview2XML tries to
287         // update distance/bootstrap visibility at the same time
288         showBootstrap(newickFile.HasBootstrap());
289         showDistances(newickFile.HasDistances());
290       }
291     }
292
293     @Override
294     public void run()
295     {
296
297       if (newtree != null)
298       {
299         tree = new TreeModel(av.getAlignment().getSequencesArray(), odata,
300                 newtree);
301         if (tree.getOriginalData() == null)
302         {
303           originalSeqData.setVisible(false);
304         }
305       }
306       else
307       {
308         ScoreModelI sm = ScoreModels.getInstance()
309                 .getScoreModel(scoreModelName, treeCanvas.ap);
310         TreeBuilder njtree = treeType.equals(TreeBuilder.NEIGHBOUR_JOINING)
311                 ? new NJTree(av, sm, similarityParams)
312                 : new AverageDistanceTree(av, sm, similarityParams);
313         tree = new TreeModel(njtree);
314         showDistances(true);
315       }
316
317       tree.reCount(tree.getTopNode());
318       tree.findHeight(tree.getTopNode());
319       treeCanvas.setTree(tree);
320       treeCanvas.repaint();
321       av.setCurrentTree(tree);
322       if (av.getSortByTree())
323       {
324         sortByTree_actionPerformed();
325       }
326     }
327   }
328
329   public void showDistances(boolean b)
330   {
331     treeCanvas.setShowDistances(b);
332     distanceMenu.setSelected(b);
333   }
334
335   public void showBootstrap(boolean b)
336   {
337     treeCanvas.setShowBootstrap(b);
338     bootstrapMenu.setSelected(b);
339   }
340
341   public void showPlaceholders(boolean b)
342   {
343     placeholdersMenu.setState(b);
344     treeCanvas.setMarkPlaceholders(b);
345   }
346
347   /**
348    * DOCUMENT ME!
349    * 
350    * @return DOCUMENT ME!
351    */
352   public TreeModel getTree()
353   {
354     return tree;
355   }
356
357   /**
358    * DOCUMENT ME!
359    * 
360    * @param e
361    *          DOCUMENT ME!
362    */
363   @Override
364   public void textbox_actionPerformed(ActionEvent e)
365   {
366     CutAndPasteTransfer cap = new CutAndPasteTransfer();
367
368     String newTitle = getPanelTitle();
369
370     NewickFile fout = new NewickFile(tree.getTopNode());
371     try
372     {
373       cap.setText(fout.print(tree.hasBootstrap(), tree.hasDistances(),
374               tree.hasRootDistance()));
375       Desktop.addInternalFrame(cap, newTitle, 500, 100);
376     } catch (OutOfMemoryError oom)
377     {
378       new OOMWarning("generating newick tree file", oom);
379       cap.dispose();
380     }
381
382   }
383
384   /**
385    * DOCUMENT ME!
386    * 
387    * @param e
388    *          DOCUMENT ME!
389    */
390   @Override
391   public void saveAsNewick_actionPerformed(ActionEvent e)
392   {
393     JalviewFileChooser chooser = new JalviewFileChooser(
394             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
395     chooser.setFileView(new JalviewFileView());
396     chooser.setDialogTitle(
397             MessageManager.getString("label.save_tree_as_newick"));
398     chooser.setToolTipText(MessageManager.getString("action.save"));
399
400     int value = chooser.showSaveDialog(null);
401
402     if (value == JalviewFileChooser.APPROVE_OPTION)
403     {
404       String choice = chooser.getSelectedFile().getPath();
405       jalview.bin.Cache.setProperty("LAST_DIRECTORY",
406               chooser.getSelectedFile().getParent());
407
408       try
409       {
410         jalview.io.NewickFile fout = new jalview.io.NewickFile(
411                 tree.getTopNode());
412         String output = fout.print(tree.hasBootstrap(), tree.hasDistances(),
413                 tree.hasRootDistance());
414         java.io.PrintWriter out = new java.io.PrintWriter(
415                 new java.io.FileWriter(choice));
416         out.println(output);
417         out.close();
418       } catch (Exception ex)
419       {
420         ex.printStackTrace();
421       }
422     }
423   }
424
425   /**
426    * DOCUMENT ME!
427    * 
428    * @param e
429    *          DOCUMENT ME!
430    */
431   @Override
432   public void printMenu_actionPerformed(ActionEvent e)
433   {
434     // Putting in a thread avoids Swing painting problems
435     treeCanvas.startPrinting();
436   }
437
438   @Override
439   public void originalSeqData_actionPerformed(ActionEvent e)
440   {
441     AlignmentView originalData = tree.getOriginalData();
442     if (originalData == null)
443     {
444       jalview.bin.Cache.log.info(
445               "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
446       return;
447     }
448     // decide if av alignment is sufficiently different to original data to
449     // warrant a new window to be created
450     // create new alignmnt window with hidden regions (unhiding hidden regions
451     // yields unaligned seqs)
452     // or create a selection box around columns in alignment view
453     // test Alignment(SeqCigar[])
454     char gc = '-';
455     try
456     {
457       // we try to get the associated view's gap character
458       // but this may fail if the view was closed...
459       gc = av.getGapCharacter();
460
461     } catch (Exception ex)
462     {
463     }
464
465     Object[] alAndColsel = originalData.getAlignmentAndHiddenColumns(gc);
466
467     if (alAndColsel != null && alAndColsel[0] != null)
468     {
469       // AlignmentOrder origorder = new AlignmentOrder(alAndColsel[0]);
470
471       AlignmentI al = new Alignment((SequenceI[]) alAndColsel[0]);
472       AlignmentI dataset = (av != null && av.getAlignment() != null)
473               ? av.getAlignment().getDataset()
474               : null;
475       if (dataset != null)
476       {
477         al.setDataset(dataset);
478       }
479
480       if (true)
481       {
482         // make a new frame!
483         AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
484                 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
485
486         // >>>This is a fix for the moment, until a better solution is
487         // found!!<<<
488         // af.getFeatureRenderer().transferSettings(alignFrame.getFeatureRenderer());
489
490         // af.addSortByOrderMenuItem(ServiceName + " Ordering",
491         // msaorder);
492
493         Desktop.addInternalFrame(af, MessageManager.formatMessage(
494                 "label.original_data_for_params", new Object[]
495                 { this.title }), AlignFrame.DEFAULT_WIDTH,
496                 AlignFrame.DEFAULT_HEIGHT);
497       }
498     }
499   }
500
501   /**
502    * DOCUMENT ME!
503    * 
504    * @param e
505    *          DOCUMENT ME!
506    */
507   @Override
508   public void fitToWindow_actionPerformed(ActionEvent e)
509   {
510     treeCanvas.fitToWindow = fitToWindow.isSelected();
511     repaint();
512   }
513
514   /**
515    * sort the associated alignment view by the current tree.
516    * 
517    * @param e
518    */
519   @Override
520   public void sortByTree_actionPerformed()
521   {
522
523     if (treeCanvas.applyToAllViews)
524     {
525       final ArrayList<CommandI> commands = new ArrayList<>();
526       for (AlignmentPanel ap : PaintRefresher
527               .getAssociatedPanels(av.getSequenceSetId()))
528       {
529         commands.add(sortAlignmentIn(ap.av.getAlignPanel()));
530       }
531       av.getAlignPanel().alignFrame.addHistoryItem(new CommandI()
532       {
533
534         @Override
535         public void undoCommand(AlignmentI[] views)
536         {
537           for (CommandI tsort : commands)
538           {
539             tsort.undoCommand(views);
540           }
541         }
542
543         @Override
544         public int getSize()
545         {
546           return commands.size();
547         }
548
549         @Override
550         public String getDescription()
551         {
552           return "Tree Sort (many views)";
553         }
554
555         @Override
556         public void doCommand(AlignmentI[] views)
557         {
558
559           for (CommandI tsort : commands)
560           {
561             tsort.doCommand(views);
562           }
563         }
564       });
565       for (AlignmentPanel ap : PaintRefresher
566               .getAssociatedPanels(av.getSequenceSetId()))
567       {
568         // ensure all the alignFrames refresh their GI after adding an undo item
569         ap.alignFrame.updateEditMenuBar();
570       }
571     }
572     else
573     {
574       treeCanvas.ap.alignFrame
575               .addHistoryItem(sortAlignmentIn(treeCanvas.ap));
576     }
577
578   }
579
580   public CommandI sortAlignmentIn(AlignmentPanel ap)
581   {
582     // TODO: move to alignment view controller
583     AlignmentViewport viewport = ap.av;
584     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
585     AlignmentSorter.sortByTree(viewport.getAlignment(), tree);
586     CommandI undo;
587     undo = new OrderCommand("Tree Sort", oldOrder, viewport.getAlignment());
588
589     ap.paintAlignment(true, false);
590     return undo;
591   }
592
593   /**
594    * DOCUMENT ME!
595    * 
596    * @param e
597    *          DOCUMENT ME!
598    */
599   @Override
600   public void font_actionPerformed(ActionEvent e)
601   {
602     if (treeCanvas == null)
603     {
604       return;
605     }
606
607     new FontChooser(this);
608   }
609
610   public Font getTreeFont()
611   {
612     return treeCanvas.font;
613   }
614
615   public void setTreeFont(Font f)
616   {
617     if (treeCanvas != null)
618     {
619       treeCanvas.setFont(f);
620     }
621   }
622
623   /**
624    * DOCUMENT ME!
625    * 
626    * @param e
627    *          DOCUMENT ME!
628    */
629   @Override
630   public void distanceMenu_actionPerformed(ActionEvent e)
631   {
632     treeCanvas.setShowDistances(distanceMenu.isSelected());
633   }
634
635   /**
636    * DOCUMENT ME!
637    * 
638    * @param e
639    *          DOCUMENT ME!
640    */
641   @Override
642   public void bootstrapMenu_actionPerformed(ActionEvent e)
643   {
644     treeCanvas.setShowBootstrap(bootstrapMenu.isSelected());
645   }
646
647   /**
648    * DOCUMENT ME!
649    * 
650    * @param e
651    *          DOCUMENT ME!
652    */
653   @Override
654   public void placeholdersMenu_actionPerformed(ActionEvent e)
655   {
656     treeCanvas.setMarkPlaceholders(placeholdersMenu.isSelected());
657   }
658
659   /**
660    * DOCUMENT ME!
661    * 
662    * @param e
663    *          DOCUMENT ME!
664    */
665   @Override
666   public void epsTree_actionPerformed(ActionEvent e)
667   {
668     boolean accurateText = true;
669
670     String renderStyle = jalview.bin.Cache.getDefault("EPS_RENDERING",
671             "Prompt each time");
672
673     // If we need to prompt, and if the GUI is visible then
674     // Prompt for EPS rendering style
675     if (renderStyle.equalsIgnoreCase("Prompt each time")
676             && !(System.getProperty("java.awt.headless") != null && System
677                     .getProperty("java.awt.headless").equals("true")))
678     {
679       EPSOptions eps = new EPSOptions();
680       renderStyle = eps.getValue();
681
682       if (renderStyle == null || eps.cancelled)
683       {
684         return;
685       }
686
687     }
688
689     if (renderStyle.equalsIgnoreCase("text"))
690     {
691       accurateText = false;
692     }
693
694     int width = treeCanvas.getWidth();
695     int height = treeCanvas.getHeight();
696
697     try
698     {
699       JalviewFileChooser chooser = new JalviewFileChooser(
700               ImageMaker.EPS_EXTENSION, ImageMaker.EPS_EXTENSION);
701       chooser.setFileView(new JalviewFileView());
702       chooser.setDialogTitle(
703               MessageManager.getString("label.create_eps_from_tree"));
704       chooser.setToolTipText(MessageManager.getString("action.save"));
705
706       int value = chooser.showSaveDialog(this);
707
708       if (value != JalviewFileChooser.APPROVE_OPTION)
709       {
710         return;
711       }
712
713       Cache.setProperty("LAST_DIRECTORY",
714               chooser.getSelectedFile().getParent());
715
716       FileOutputStream out = new FileOutputStream(
717               chooser.getSelectedFile());
718       EpsGraphics2D pg = new EpsGraphics2D("Tree", out, 0, 0, width,
719               height);
720
721       pg.setAccurateTextMode(accurateText);
722
723       treeCanvas.draw(pg, width, height);
724
725       pg.flush();
726       pg.close();
727     } catch (Exception ex)
728     {
729       ex.printStackTrace();
730     }
731   }
732
733   /**
734    * DOCUMENT ME!
735    * 
736    * @param e
737    *          DOCUMENT ME!
738    */
739   @Override
740   public void pngTree_actionPerformed(ActionEvent e)
741   {
742     int width = treeCanvas.getWidth();
743     int height = treeCanvas.getHeight();
744
745     try
746     {
747       JalviewFileChooser chooser = new JalviewFileChooser(
748               ImageMaker.PNG_EXTENSION, ImageMaker.PNG_DESCRIPTION);
749
750       chooser.setFileView(new jalview.io.JalviewFileView());
751       chooser.setDialogTitle(
752               MessageManager.getString("label.create_png_from_tree"));
753       chooser.setToolTipText(MessageManager.getString("action.save"));
754
755       int value = chooser.showSaveDialog(this);
756
757       if (value != jalview.io.JalviewFileChooser.APPROVE_OPTION)
758       {
759         return;
760       }
761
762       jalview.bin.Cache.setProperty("LAST_DIRECTORY",
763               chooser.getSelectedFile().getParent());
764
765       FileOutputStream out = new FileOutputStream(
766               chooser.getSelectedFile());
767
768       BufferedImage bi = new BufferedImage(width, height,
769               BufferedImage.TYPE_INT_RGB);
770       Graphics png = bi.getGraphics();
771
772       treeCanvas.draw(png, width, height);
773
774       ImageIO.write(bi, "png", out);
775       out.close();
776     } catch (Exception ex)
777     {
778       ex.printStackTrace();
779     }
780   }
781
782   /**
783    * change node labels to the annotation referred to by labelClass TODO:
784    * promote to a datamodel modification that can be undone TODO: make argument
785    * one case of a generic transformation function ie { undoStep = apply(Tree,
786    * TransformFunction)};
787    * 
788    * @param labelClass
789    */
790   public void changeNames(final String labelClass)
791   {
792     tree.applyToNodes(new NodeTransformI()
793     {
794
795       @Override
796       public void transform(BinaryNode node)
797       {
798         if (node instanceof SequenceNode
799                 && !((SequenceNode) node).isPlaceholder()
800                 && !((SequenceNode) node).isDummy())
801         {
802           String newname = null;
803           SequenceI sq = (SequenceI) ((SequenceNode) node).element();
804           if (sq != null)
805           {
806             // search dbrefs, features and annotation
807             DBRefEntry[] refs = jalview.util.DBRefUtils
808                     .selectRefs(sq.getDBRefs(), new String[]
809                     { labelClass.toUpperCase() });
810             if (refs != null)
811             {
812               for (int i = 0; i < refs.length; i++)
813               {
814                 if (newname == null)
815                 {
816                   newname = new String(refs[i].getAccessionId());
817                 }
818                 else
819                 {
820                   newname = newname + "; " + refs[i].getAccessionId();
821                 }
822               }
823             }
824             if (newname == null)
825             {
826               List<SequenceFeature> features = sq.getFeatures()
827                       .getPositionalFeatures(labelClass);
828               for (SequenceFeature feature : features)
829               {
830                 if (newname == null)
831                 {
832                   newname = feature.getDescription();
833                 }
834                 else
835                 {
836                   newname = newname + "; " + feature.getDescription();
837                 }
838               }
839             }
840           }
841           if (newname != null)
842           {
843             // String oldname = ((SequenceNode) node).getName();
844             // TODO : save oldname in the undo object for this modification.
845             ((SequenceNode) node).setName(newname);
846           }
847         }
848       }
849     });
850   }
851
852   /**
853    * Formats a localised title for the tree panel, like
854    * <p>
855    * Neighbour Joining Using BLOSUM62
856    * <p>
857    * For a tree loaded from file, just uses the file name
858    * 
859    * @return
860    */
861   public String getPanelTitle()
862   {
863     if (treeTitle != null)
864     {
865       return treeTitle;
866     }
867
868     /*
869      * i18n description of Neighbour Joining or Average Distance method
870      */
871     String treecalcnm = MessageManager
872             .getString("label.tree_calc_" + treeType.toLowerCase());
873
874     /*
875      * short score model name (long description can be too long)
876      */
877     String smn = scoreModelName;
878
879     /*
880      * put them together as <method> Using <model>
881      */
882     final String ttl = MessageManager.formatMessage("label.treecalc_title",
883             treecalcnm, smn);
884     return ttl;
885   }
886 }