JAL-2464 fix cancel tree print behaviour
[jalview.git] / src / jalview / gui / TreeCanvas.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.Conservation;
24 import jalview.analysis.NJTree;
25 import jalview.api.AlignViewportI;
26 import jalview.datamodel.Sequence;
27 import jalview.datamodel.SequenceGroup;
28 import jalview.datamodel.SequenceI;
29 import jalview.datamodel.SequenceNode;
30 import jalview.schemes.ColourSchemeI;
31 import jalview.schemes.ColourSchemeProperty;
32 import jalview.schemes.UserColourScheme;
33 import jalview.structure.SelectionSource;
34 import jalview.util.Format;
35 import jalview.util.MappingUtils;
36 import jalview.util.MessageManager;
37
38 import java.awt.Color;
39 import java.awt.Dimension;
40 import java.awt.Font;
41 import java.awt.FontMetrics;
42 import java.awt.Graphics;
43 import java.awt.Graphics2D;
44 import java.awt.Point;
45 import java.awt.Rectangle;
46 import java.awt.RenderingHints;
47 import java.awt.event.MouseEvent;
48 import java.awt.event.MouseListener;
49 import java.awt.event.MouseMotionListener;
50 import java.awt.print.PageFormat;
51 import java.awt.print.Printable;
52 import java.awt.print.PrinterException;
53 import java.awt.print.PrinterJob;
54 import java.util.Enumeration;
55 import java.util.Hashtable;
56 import java.util.Vector;
57
58 import javax.swing.JColorChooser;
59 import javax.swing.JPanel;
60 import javax.swing.JScrollPane;
61 import javax.swing.SwingUtilities;
62 import javax.swing.ToolTipManager;
63
64 /**
65  * DOCUMENT ME!
66  * 
67  * @author $author$
68  * @version $Revision$
69  */
70 public class TreeCanvas extends JPanel implements MouseListener, Runnable,
71         Printable, MouseMotionListener, SelectionSource
72 {
73   /** DOCUMENT ME!! */
74   public static final String PLACEHOLDER = " * ";
75
76   NJTree tree;
77
78   JScrollPane scrollPane;
79
80   TreePanel tp;
81
82   AlignViewport av;
83
84   AlignmentPanel ap;
85
86   Font font;
87
88   FontMetrics fm;
89
90   boolean fitToWindow = true;
91
92   boolean showDistances = false;
93
94   boolean showBootstrap = false;
95
96   boolean markPlaceholders = false;
97
98   int offx = 20;
99
100   int offy;
101
102   float threshold;
103
104   String longestName;
105
106   int labelLength = -1;
107
108   Hashtable nameHash = new Hashtable();
109
110   Hashtable nodeHash = new Hashtable();
111
112   SequenceNode highlightNode;
113
114   boolean applyToAllViews = false;
115
116   /**
117    * Creates a new TreeCanvas object.
118    * 
119    * @param av
120    *          DOCUMENT ME!
121    * @param tree
122    *          DOCUMENT ME!
123    * @param scroller
124    *          DOCUMENT ME!
125    * @param label
126    *          DOCUMENT ME!
127    */
128   public TreeCanvas(TreePanel tp, AlignmentPanel ap, JScrollPane scroller)
129   {
130     this.tp = tp;
131     this.av = ap.av;
132     this.ap = ap;
133     font = av.getFont();
134     scrollPane = scroller;
135     addMouseListener(this);
136     addMouseMotionListener(this);
137     ToolTipManager.sharedInstance().registerComponent(this);
138   }
139
140   /**
141    * DOCUMENT ME!
142    * 
143    * @param sequence
144    *          DOCUMENT ME!
145    */
146   public void treeSelectionChanged(SequenceI sequence)
147   {
148     AlignmentPanel[] aps = getAssociatedPanels();
149
150     for (int a = 0; a < aps.length; a++)
151     {
152       SequenceGroup selected = aps[a].av.getSelectionGroup();
153
154       if (selected == null)
155       {
156         selected = new SequenceGroup();
157         aps[a].av.setSelectionGroup(selected);
158       }
159
160       selected.setEndRes(aps[a].av.getAlignment().getWidth() - 1);
161       selected.addOrRemove(sequence, true);
162     }
163   }
164
165   /**
166    * DOCUMENT ME!
167    * 
168    * @param tree
169    *          DOCUMENT ME!
170    */
171   public void setTree(NJTree tree)
172   {
173     this.tree = tree;
174     tree.findHeight(tree.getTopNode());
175
176     // Now have to calculate longest name based on the leaves
177     Vector<SequenceNode> leaves = tree.findLeaves(tree.getTopNode());
178     boolean has_placeholders = false;
179     longestName = "";
180
181     for (int i = 0; i < leaves.size(); i++)
182     {
183       SequenceNode lf = leaves.elementAt(i);
184
185       if (lf.isPlaceholder())
186       {
187         has_placeholders = true;
188       }
189
190       if (longestName.length() < ((Sequence) lf.element()).getName()
191               .length())
192       {
193         longestName = TreeCanvas.PLACEHOLDER
194                 + ((Sequence) lf.element()).getName();
195       }
196     }
197
198     setMarkPlaceholders(has_placeholders);
199   }
200
201   /**
202    * DOCUMENT ME!
203    * 
204    * @param g
205    *          DOCUMENT ME!
206    * @param node
207    *          DOCUMENT ME!
208    * @param chunk
209    *          DOCUMENT ME!
210    * @param scale
211    *          DOCUMENT ME!
212    * @param width
213    *          DOCUMENT ME!
214    * @param offx
215    *          DOCUMENT ME!
216    * @param offy
217    *          DOCUMENT ME!
218    */
219   public void drawNode(Graphics g, SequenceNode node, float chunk,
220           float scale, int width, int offx, int offy)
221   {
222     if (node == null)
223     {
224       return;
225     }
226
227     if ((node.left() == null) && (node.right() == null))
228     {
229       // Drawing leaf node
230       float height = node.height;
231       float dist = node.dist;
232
233       int xstart = (int) ((height - dist) * scale) + offx;
234       int xend = (int) (height * scale) + offx;
235
236       int ypos = (int) (node.ycount * chunk) + offy;
237
238       if (node.element() instanceof SequenceI)
239       {
240         SequenceI seq = (SequenceI) node.element();
241
242         if (av.getSequenceColour(seq) == Color.white)
243         {
244           g.setColor(Color.black);
245         }
246         else
247         {
248           g.setColor(av.getSequenceColour(seq).darker());
249         }
250       }
251       else
252       {
253         g.setColor(Color.black);
254       }
255
256       // Draw horizontal line
257       g.drawLine(xstart, ypos, xend, ypos);
258
259       String nodeLabel = "";
260
261       if (showDistances && (node.dist > 0))
262       {
263         nodeLabel = new Format("%-.2f").form(node.dist);
264       }
265
266       if (showBootstrap && node.bootstrap > -1)
267       {
268         if (showDistances)
269         {
270           nodeLabel = nodeLabel + " : ";
271         }
272
273         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
274       }
275
276       if (!nodeLabel.equals(""))
277       {
278         g.drawString(nodeLabel, xstart + 2, ypos - 2);
279       }
280
281       String name = (markPlaceholders && node.isPlaceholder()) ? (PLACEHOLDER + node
282               .getName()) : node.getName();
283
284       int charWidth = fm.stringWidth(name) + 3;
285       int charHeight = font.getSize();
286
287       Rectangle rect = new Rectangle(xend + 10, ypos - charHeight / 2,
288               charWidth, charHeight);
289
290       nameHash.put(node.element(), rect);
291
292       // Colour selected leaves differently
293       SequenceGroup selected = av.getSelectionGroup();
294
295       if ((selected != null)
296               && selected.getSequences(null).contains(node.element()))
297       {
298         g.setColor(Color.gray);
299
300         g.fillRect(xend + 10, ypos - charHeight / 2, charWidth, charHeight);
301         g.setColor(Color.white);
302       }
303
304       g.drawString(name, xend + 10, ypos + fm.getDescent());
305       g.setColor(Color.black);
306     }
307     else
308     {
309       drawNode(g, (SequenceNode) node.left(), chunk, scale, width, offx,
310               offy);
311       drawNode(g, (SequenceNode) node.right(), chunk, scale, width, offx,
312               offy);
313
314       float height = node.height;
315       float dist = node.dist;
316
317       int xstart = (int) ((height - dist) * scale) + offx;
318       int xend = (int) (height * scale) + offx;
319       int ypos = (int) (node.ycount * chunk) + offy;
320
321       g.setColor(node.color.darker());
322
323       // Draw horizontal line
324       g.drawLine(xstart, ypos, xend, ypos);
325       if (node == highlightNode)
326       {
327         g.fillRect(xend - 3, ypos - 3, 6, 6);
328       }
329       else
330       {
331         g.fillRect(xend - 2, ypos - 2, 4, 4);
332       }
333
334       int ystart = (int) (((SequenceNode) node.left()).ycount * chunk)
335               + offy;
336       int yend = (int) (((SequenceNode) node.right()).ycount * chunk)
337               + offy;
338
339       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
340       nodeHash.put(node, pos);
341
342       g.drawLine((int) (height * scale) + offx, ystart,
343               (int) (height * scale) + offx, yend);
344
345       String nodeLabel = "";
346
347       if (showDistances && (node.dist > 0))
348       {
349         nodeLabel = new Format("%-.2f").form(node.dist);
350       }
351
352       if (showBootstrap && node.bootstrap > -1)
353       {
354         if (showDistances)
355         {
356           nodeLabel = nodeLabel + " : ";
357         }
358
359         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
360       }
361
362       if (!nodeLabel.equals(""))
363       {
364         g.drawString(nodeLabel, xstart + 2, ypos - 2);
365       }
366     }
367   }
368
369   /**
370    * DOCUMENT ME!
371    * 
372    * @param x
373    *          DOCUMENT ME!
374    * @param y
375    *          DOCUMENT ME!
376    * 
377    * @return DOCUMENT ME!
378    */
379   public Object findElement(int x, int y)
380   {
381     Enumeration keys = nameHash.keys();
382
383     while (keys.hasMoreElements())
384     {
385       Object ob = keys.nextElement();
386       Rectangle rect = (Rectangle) nameHash.get(ob);
387
388       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
389               && (y <= (rect.y + rect.height)))
390       {
391         return ob;
392       }
393     }
394
395     keys = nodeHash.keys();
396
397     while (keys.hasMoreElements())
398     {
399       Object ob = keys.nextElement();
400       Rectangle rect = (Rectangle) nodeHash.get(ob);
401
402       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
403               && (y <= (rect.y + rect.height)))
404       {
405         return ob;
406       }
407     }
408
409     return null;
410   }
411
412   /**
413    * DOCUMENT ME!
414    * 
415    * @param pickBox
416    *          DOCUMENT ME!
417    */
418   public void pickNodes(Rectangle pickBox)
419   {
420     int width = getWidth();
421     int height = getHeight();
422
423     SequenceNode top = tree.getTopNode();
424
425     float wscale = (float) ((width * .8) - (offx * 2))
426             / tree.getMaxHeight();
427
428     if (top.count == 0)
429     {
430       top.count = ((SequenceNode) top.left()).count
431               + ((SequenceNode) top.right()).count;
432     }
433
434     float chunk = (float) (height - (offy)) / top.count;
435
436     pickNode(pickBox, top, chunk, wscale, width, offx, offy);
437   }
438
439   /**
440    * DOCUMENT ME!
441    * 
442    * @param pickBox
443    *          DOCUMENT ME!
444    * @param node
445    *          DOCUMENT ME!
446    * @param chunk
447    *          DOCUMENT ME!
448    * @param scale
449    *          DOCUMENT ME!
450    * @param width
451    *          DOCUMENT ME!
452    * @param offx
453    *          DOCUMENT ME!
454    * @param offy
455    *          DOCUMENT ME!
456    */
457   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
458           float scale, int width, int offx, int offy)
459   {
460     if (node == null)
461     {
462       return;
463     }
464
465     if ((node.left() == null) && (node.right() == null))
466     {
467       float height = node.height;
468       float dist = node.dist;
469
470       int xstart = (int) ((height - dist) * scale) + offx;
471       int xend = (int) (height * scale) + offx;
472
473       int ypos = (int) (node.ycount * chunk) + offy;
474
475       if (pickBox.contains(new Point(xend, ypos)))
476       {
477         if (node.element() instanceof SequenceI)
478         {
479           SequenceI seq = (SequenceI) node.element();
480           SequenceGroup sg = av.getSelectionGroup();
481
482           if (sg != null)
483           {
484             sg.addOrRemove(seq, true);
485           }
486         }
487       }
488     }
489     else
490     {
491       pickNode(pickBox, (SequenceNode) node.left(), chunk, scale, width,
492               offx, offy);
493       pickNode(pickBox, (SequenceNode) node.right(), chunk, scale, width,
494               offx, offy);
495     }
496   }
497
498   /**
499    * DOCUMENT ME!
500    * 
501    * @param node
502    *          DOCUMENT ME!
503    * @param c
504    *          DOCUMENT ME!
505    */
506   public void setColor(SequenceNode node, Color c)
507   {
508     if (node == null)
509     {
510       return;
511     }
512
513     if ((node.left() == null) && (node.right() == null)) // TODO: internal node
514     {
515       node.color = c;
516
517       if (node.element() instanceof SequenceI)
518       {
519         AlignmentPanel[] aps = getAssociatedPanels();
520         if (aps != null)
521         {
522           for (int a = 0; a < aps.length; a++)
523           {
524             final SequenceI seq = (SequenceI) node.element();
525             aps[a].av.setSequenceColour(seq, c);
526           }
527         }
528       }
529     }
530     else
531     {
532       node.color = c;
533       setColor((SequenceNode) node.left(), c);
534       setColor((SequenceNode) node.right(), c);
535     }
536   }
537
538   /**
539    * DOCUMENT ME!
540    */
541   void startPrinting()
542   {
543     Thread thread = new Thread(this);
544     thread.start();
545   }
546
547   // put printing in a thread to avoid painting problems
548   @Override
549   public void run()
550   {
551     PrinterJob printJob = PrinterJob.getPrinterJob();
552     PageFormat defaultPage = printJob.defaultPage();
553     PageFormat pf = printJob.pageDialog(defaultPage);
554
555     if (defaultPage == pf)
556     {
557       /*
558        * user cancelled
559        */
560       return;
561     }
562
563     printJob.setPrintable(this, pf);
564
565     if (printJob.printDialog())
566     {
567       try
568       {
569         printJob.print();
570       } catch (Exception PrintException)
571       {
572         PrintException.printStackTrace();
573       }
574     }
575   }
576
577   /**
578    * DOCUMENT ME!
579    * 
580    * @param pg
581    *          DOCUMENT ME!
582    * @param pf
583    *          DOCUMENT ME!
584    * @param pi
585    *          DOCUMENT ME!
586    * 
587    * @return DOCUMENT ME!
588    * 
589    * @throws PrinterException
590    *           DOCUMENT ME!
591    */
592   @Override
593   public int print(Graphics pg, PageFormat pf, int pi)
594           throws PrinterException
595   {
596     pg.setFont(font);
597     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
598
599     int pwidth = (int) pf.getImageableWidth();
600     int pheight = (int) pf.getImageableHeight();
601
602     int noPages = getHeight() / pheight;
603
604     if (pi > noPages)
605     {
606       return Printable.NO_SUCH_PAGE;
607     }
608
609     if (pwidth > getWidth())
610     {
611       pwidth = getWidth();
612     }
613
614     if (fitToWindow)
615     {
616       if (pheight > getHeight())
617       {
618         pheight = getHeight();
619       }
620
621       noPages = 0;
622     }
623     else
624     {
625       FontMetrics fm = pg.getFontMetrics(font);
626       int height = fm.getHeight() * nameHash.size();
627       pg.translate(0, -pi * pheight);
628       pg.setClip(0, pi * pheight, pwidth, (pi * pheight) + pheight);
629
630       // translate number of pages,
631       // height is screen size as this is the
632       // non overlapping text size
633       pheight = height;
634     }
635
636     draw(pg, pwidth, pheight);
637
638     return Printable.PAGE_EXISTS;
639   }
640
641   /**
642    * DOCUMENT ME!
643    * 
644    * @param g
645    *          DOCUMENT ME!
646    */
647   @Override
648   public void paintComponent(Graphics g)
649   {
650     super.paintComponent(g);
651     g.setFont(font);
652
653     if (tree == null)
654     {
655       g.drawString(MessageManager.getString("label.calculating_tree")
656               + "....", 20, getHeight() / 2);
657     }
658     else
659     {
660       fm = g.getFontMetrics(font);
661
662       if (nameHash.size() == 0)
663       {
664         repaint();
665       }
666
667       if (fitToWindow
668               || (!fitToWindow && (scrollPane.getHeight() > ((fm
669                       .getHeight() * nameHash.size()) + offy))))
670       {
671         draw(g, scrollPane.getWidth(), scrollPane.getHeight());
672         setPreferredSize(null);
673       }
674       else
675       {
676         setPreferredSize(new Dimension(scrollPane.getWidth(),
677                 fm.getHeight() * nameHash.size()));
678         draw(g, scrollPane.getWidth(), fm.getHeight() * nameHash.size());
679       }
680
681       scrollPane.revalidate();
682     }
683   }
684
685   /**
686    * DOCUMENT ME!
687    * 
688    * @param fontSize
689    *          DOCUMENT ME!
690    */
691   @Override
692   public void setFont(Font font)
693   {
694     this.font = font;
695     repaint();
696   }
697
698   /**
699    * DOCUMENT ME!
700    * 
701    * @param g1
702    *          DOCUMENT ME!
703    * @param width
704    *          DOCUMENT ME!
705    * @param height
706    *          DOCUMENT ME!
707    */
708   public void draw(Graphics g1, int width, int height)
709   {
710     Graphics2D g2 = (Graphics2D) g1;
711     g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
712             RenderingHints.VALUE_ANTIALIAS_ON);
713     g2.setColor(Color.white);
714     g2.fillRect(0, 0, width, height);
715     g2.setFont(font);
716
717     if (longestName == null || tree == null)
718     {
719       g2.drawString("Calculating tree.", 20, 20);
720     }
721     offy = font.getSize() + 10;
722
723     fm = g2.getFontMetrics(font);
724
725     labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
726
727     float wscale = (width - labelLength - (offx * 2)) / tree.getMaxHeight();
728
729     SequenceNode top = tree.getTopNode();
730
731     if (top.count == 0)
732     {
733       top.count = ((SequenceNode) top.left()).count
734               + ((SequenceNode) top.right()).count;
735     }
736
737     float chunk = (float) (height - (offy)) / top.count;
738
739     drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
740
741     if (threshold != 0)
742     {
743       if (av.getCurrentTree() == tree)
744       {
745         g2.setColor(Color.red);
746       }
747       else
748       {
749         g2.setColor(Color.gray);
750       }
751
752       int x = (int) ((threshold * (getWidth() - labelLength - (2 * offx))) + offx);
753
754       g2.drawLine(x, 0, x, getHeight());
755     }
756   }
757
758   /**
759    * Empty method to satisfy the MouseListener interface
760    * 
761    * @param e
762    */
763   @Override
764   public void mouseReleased(MouseEvent e)
765   {
766     /*
767      * isPopupTrigger is set on mouseReleased on Windows
768      */
769     if (e.isPopupTrigger())
770     {
771       chooseSubtreeColour();
772       e.consume(); // prevent mouseClicked happening
773     }
774   }
775
776   /**
777    * Empty method to satisfy the MouseListener interface
778    * 
779    * @param e
780    */
781   @Override
782   public void mouseEntered(MouseEvent e)
783   {
784   }
785
786   /**
787    * Empty method to satisfy the MouseListener interface
788    * 
789    * @param e
790    */
791   @Override
792   public void mouseExited(MouseEvent e)
793   {
794   }
795
796   /**
797    * Handles a mouse click on a tree node (clicks elsewhere are handled in
798    * mousePressed). Click selects the sub-tree, double-click swaps leaf nodes
799    * order, right-click opens a dialogue to choose colour for the sub-tree.
800    * 
801    * @param e
802    */
803   @Override
804   public void mouseClicked(MouseEvent evt)
805   {
806     if (highlightNode == null)
807     {
808       return;
809     }
810
811     if (evt.getClickCount() > 1)
812     {
813       tree.swapNodes(highlightNode);
814       tree.reCount(tree.getTopNode());
815       tree.findHeight(tree.getTopNode());
816     }
817     else
818     {
819       Vector<SequenceNode> leaves = tree.findLeaves(highlightNode);
820
821       for (int i = 0; i < leaves.size(); i++)
822       {
823         SequenceI seq = (SequenceI) leaves.elementAt(i).element();
824         treeSelectionChanged(seq);
825       }
826       av.sendSelection();
827     }
828
829     PaintRefresher.Refresh(tp, av.getSequenceSetId());
830     repaint();
831   }
832
833   /**
834    * Offer the user the option to choose a colour for the highlighted node and
835    * its children; this colour is also applied to the corresponding sequence ids
836    * in the alignment
837    */
838   void chooseSubtreeColour()
839   {
840     Color col = JColorChooser.showDialog(this,
841             MessageManager.getString("label.select_subtree_colour"),
842             highlightNode.color);
843     if (col != null)
844     {
845       setColor(highlightNode, col);
846       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
847       repaint();
848     }
849   }
850
851   @Override
852   public void mouseMoved(MouseEvent evt)
853   {
854     av.setCurrentTree(tree);
855
856     Object ob = findElement(evt.getX(), evt.getY());
857
858     if (ob instanceof SequenceNode)
859     {
860       highlightNode = (SequenceNode) ob;
861       this.setToolTipText("<html>"
862               + MessageManager.getString("label.highlightnode"));
863       repaint();
864
865     }
866     else
867     {
868       if (highlightNode != null)
869       {
870         highlightNode = null;
871         setToolTipText(null);
872         repaint();
873       }
874     }
875   }
876
877   @Override
878   public void mouseDragged(MouseEvent ect)
879   {
880   }
881
882   /**
883    * Handles a mouse press on a sequence name or the tree background canvas
884    * (click on a node is handled in mouseClicked). The action is to create
885    * groups by partitioning the tree at the mouse position. Colours for the
886    * groups (and sequence names) are generated randomly.
887    * 
888    * @param e
889    */
890   @Override
891   public void mousePressed(MouseEvent e)
892   {
893     av.setCurrentTree(tree);
894
895     /*
896      * isPopupTrigger is set for mousePressed (Mac)
897      * or mouseReleased (Windows)
898      */
899     if (e.isPopupTrigger())
900     {
901       if (highlightNode != null)
902       {
903         chooseSubtreeColour();
904       }
905       return;
906     }
907
908     /*
909      * defer right-click handling on Windows to
910      * mouseClicked; note isRightMouseButton
911      * also matches Cmd-click on Mac which should do
912      * nothing here
913      */
914     if (SwingUtilities.isRightMouseButton(e))
915     {
916       return;
917     }
918
919     int x = e.getX();
920     int y = e.getY();
921
922     Object ob = findElement(x, y);
923
924     if (ob instanceof SequenceI)
925     {
926       treeSelectionChanged((Sequence) ob);
927       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
928       repaint();
929       av.sendSelection();
930       return;
931     }
932     else if (!(ob instanceof SequenceNode))
933     {
934       // Find threshold
935       if (tree.getMaxHeight() != 0)
936       {
937         threshold = (float) (x - offx)
938                 / (float) (getWidth() - labelLength - (2 * offx));
939
940         tree.getGroups().removeAllElements();
941         tree.groupNodes(tree.getTopNode(), threshold);
942         setColor(tree.getTopNode(), Color.black);
943
944         AlignmentPanel[] aps = getAssociatedPanels();
945
946         // TODO push calls below into a single AlignViewportI method?
947         // see also AlignViewController.deleteGroups
948         for (int a = 0; a < aps.length; a++)
949         {
950           aps[a].av.setSelectionGroup(null);
951           aps[a].av.getAlignment().deleteAllGroups();
952           aps[a].av.clearSequenceColours();
953           if (aps[a].av.getCodingComplement() != null)
954           {
955             aps[a].av.getCodingComplement().setSelectionGroup(null);
956             aps[a].av.getCodingComplement().getAlignment()
957                     .deleteAllGroups();
958             aps[a].av.getCodingComplement().clearSequenceColours();
959           }
960         }
961         colourGroups();
962       }
963
964       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
965       repaint();
966     }
967
968   }
969
970   void colourGroups()
971   {
972     AlignmentPanel[] aps = getAssociatedPanels();
973     for (int i = 0; i < tree.getGroups().size(); i++)
974     {
975       Color col = new Color((int) (Math.random() * 255),
976               (int) (Math.random() * 255), (int) (Math.random() * 255));
977       setColor(tree.getGroups().elementAt(i), col.brighter());
978
979       Vector<SequenceNode> l = tree.findLeaves(tree.getGroups()
980               .elementAt(i));
981
982       Vector<SequenceI> sequences = new Vector<SequenceI>();
983
984       for (int j = 0; j < l.size(); j++)
985       {
986         SequenceI s1 = (SequenceI) l.elementAt(j).element();
987
988         if (!sequences.contains(s1))
989         {
990           sequences.addElement(s1);
991         }
992       }
993
994       ColourSchemeI cs = null;
995       SequenceGroup sg = new SequenceGroup(sequences, null, cs, true, true,
996               false, 0, av.getAlignment().getWidth() - 1);
997
998       if (av.getGlobalColourScheme() != null)
999       {
1000         if (av.getGlobalColourScheme() instanceof UserColourScheme)
1001         {
1002           cs = new UserColourScheme(
1003                   ((UserColourScheme) av.getGlobalColourScheme())
1004                           .getColours());
1005
1006         }
1007         else
1008         {
1009           cs = ColourSchemeProperty.getColourScheme(sg,
1010                   ColourSchemeProperty.getColourName(av
1011                           .getGlobalColourScheme()));
1012         }
1013         // cs is null if shading is an annotationColourGradient
1014         // if (cs != null)
1015         // {
1016         // cs.setThreshold(av.getViewportColourScheme().getThreshold(),
1017         // av.isIgnoreGapsConsensus());
1018         // }
1019       }
1020       sg.setColourScheme(cs);
1021       sg.getGroupColourScheme().setThreshold(
1022               av.getResidueShading().getThreshold(),
1023               av.isIgnoreGapsConsensus());
1024       // sg.recalcConservation();
1025       sg.setName("JTreeGroup:" + sg.hashCode());
1026       sg.setIdColour(col);
1027
1028       for (int a = 0; a < aps.length; a++)
1029       {
1030         if (aps[a].av.getGlobalColourScheme() != null
1031                 && aps[a].av.getResidueShading()
1032                         .conservationApplied())
1033         {
1034           Conservation c = new Conservation("Group", sg.getSequences(null),
1035                   sg.getStartRes(), sg.getEndRes());
1036           c.calculate();
1037           c.verdict(false, aps[a].av.getConsPercGaps());
1038           sg.cs.setConservation(c);
1039         }
1040
1041         aps[a].av.getAlignment().addGroup(new SequenceGroup(sg));
1042         // TODO can we push all of the below into AlignViewportI?
1043         final AlignViewportI codingComplement = aps[a].av
1044                 .getCodingComplement();
1045         if (codingComplement != null)
1046         {
1047           SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg, av,
1048                   codingComplement);
1049           if (mappedGroup.getSequences().size() > 0)
1050           {
1051             codingComplement.getAlignment().addGroup(mappedGroup);
1052             for (SequenceI seq : mappedGroup.getSequences())
1053             {
1054               codingComplement.setSequenceColour(seq, col.brighter());
1055             }
1056           }
1057         }
1058       }
1059     }
1060
1061     // notify the panel(s) to redo any group specific stuff.
1062     for (int a = 0; a < aps.length; a++)
1063     {
1064       aps[a].updateAnnotation();
1065       // TODO: JAL-868 - need to ensure view colour change message is broadcast
1066       // to any Jmols listening in
1067       final AlignViewportI codingComplement = aps[a].av
1068               .getCodingComplement();
1069       if (codingComplement != null)
1070       {
1071         ((AlignViewport) codingComplement).getAlignPanel()
1072                 .updateAnnotation();
1073       }
1074     }
1075   }
1076
1077   /**
1078    * DOCUMENT ME!
1079    * 
1080    * @param state
1081    *          DOCUMENT ME!
1082    */
1083   public void setShowDistances(boolean state)
1084   {
1085     this.showDistances = state;
1086     repaint();
1087   }
1088
1089   /**
1090    * DOCUMENT ME!
1091    * 
1092    * @param state
1093    *          DOCUMENT ME!
1094    */
1095   public void setShowBootstrap(boolean state)
1096   {
1097     this.showBootstrap = state;
1098     repaint();
1099   }
1100
1101   /**
1102    * DOCUMENT ME!
1103    * 
1104    * @param state
1105    *          DOCUMENT ME!
1106    */
1107   public void setMarkPlaceholders(boolean state)
1108   {
1109     this.markPlaceholders = state;
1110     repaint();
1111   }
1112
1113   AlignmentPanel[] getAssociatedPanels()
1114   {
1115     if (applyToAllViews)
1116     {
1117       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
1118     }
1119     else
1120     {
1121       return new AlignmentPanel[] { ap };
1122     }
1123   }
1124 }