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