JAL-1460 new data flavor for URLs added belt-and-braces URL drop code found in Marina...
[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())
283               ? (PLACEHOLDER + node.getName())
284               : node.getName();
285
286       int charWidth = fm.stringWidth(name) + 3;
287       int charHeight = font.getSize();
288
289       Rectangle rect = new Rectangle(xend + 10, ypos - charHeight / 2,
290               charWidth, charHeight);
291
292       nameHash.put(node.element(), rect);
293
294       // Colour selected leaves differently
295       SequenceGroup selected = av.getSelectionGroup();
296
297       if ((selected != null)
298               && selected.getSequences(null).contains(node.element()))
299       {
300         g.setColor(Color.gray);
301
302         g.fillRect(xend + 10, ypos - charHeight / 2, charWidth, charHeight);
303         g.setColor(Color.white);
304       }
305
306       g.drawString(name, xend + 10, ypos + fm.getDescent());
307       g.setColor(Color.black);
308     }
309     else
310     {
311       drawNode(g, (SequenceNode) node.left(), chunk, wscale, width, offx,
312               offy);
313       drawNode(g, (SequenceNode) node.right(), chunk, wscale, width, offx,
314               offy);
315
316       double height = node.height;
317       double dist = node.dist;
318
319       int xstart = (int) ((height - dist) * wscale) + offx;
320       int xend = (int) (height * wscale) + offx;
321       int ypos = (int) (node.ycount * chunk) + offy;
322
323       g.setColor(node.color.darker());
324
325       // Draw horizontal line
326       g.drawLine(xstart, ypos, xend, ypos);
327       if (node == highlightNode)
328       {
329         g.fillRect(xend - 3, ypos - 3, 6, 6);
330       }
331       else
332       {
333         g.fillRect(xend - 2, ypos - 2, 4, 4);
334       }
335
336       int ystart = (node.left() == null ? 0
337               : (int) (((SequenceNode) node.left()).ycount * chunk)) + offy;
338       int yend = (node.right() == null ? 0
339               : (int) (((SequenceNode) node.right()).ycount * chunk))
340               + offy;
341
342       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
343       nodeHash.put(node, pos);
344
345       g.drawLine((int) (height * wscale) + offx, ystart,
346               (int) (height * wscale) + offx, yend);
347
348       String nodeLabel = "";
349
350       if (showDistances && (node.dist > 0))
351       {
352         nodeLabel = new Format("%-.2f").form(node.dist);
353       }
354
355       if (showBootstrap && node.bootstrap > -1)
356       {
357         if (showDistances)
358         {
359           nodeLabel = nodeLabel + " : ";
360         }
361
362         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
363       }
364
365       if (!nodeLabel.equals(""))
366       {
367         g.drawString(nodeLabel, xstart + 2, ypos - 2);
368       }
369     }
370   }
371
372   /**
373    * DOCUMENT ME!
374    * 
375    * @param x
376    *          DOCUMENT ME!
377    * @param y
378    *          DOCUMENT ME!
379    * 
380    * @return DOCUMENT ME!
381    */
382   public Object findElement(int x, int y)
383   {
384     Enumeration keys = nameHash.keys();
385
386     while (keys.hasMoreElements())
387     {
388       Object ob = keys.nextElement();
389       Rectangle rect = (Rectangle) nameHash.get(ob);
390
391       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
392               && (y <= (rect.y + rect.height)))
393       {
394         return ob;
395       }
396     }
397
398     keys = nodeHash.keys();
399
400     while (keys.hasMoreElements())
401     {
402       Object ob = keys.nextElement();
403       Rectangle rect = (Rectangle) nodeHash.get(ob);
404
405       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
406               && (y <= (rect.y + rect.height)))
407       {
408         return ob;
409       }
410     }
411
412     return null;
413   }
414
415   /**
416    * DOCUMENT ME!
417    * 
418    * @param pickBox
419    *          DOCUMENT ME!
420    */
421   public void pickNodes(Rectangle pickBox)
422   {
423     int width = getWidth();
424     int height = getHeight();
425
426     SequenceNode top = tree.getTopNode();
427
428     double wscale = ((width * .8) - (offx * 2)) / tree.getMaxHeight();
429
430     if (top.count == 0)
431     {
432       top.count = ((SequenceNode) top.left()).count
433               + ((SequenceNode) top.right()).count;
434     }
435
436     float chunk = (float) (height - (offy)) / top.count;
437
438     pickNode(pickBox, top, chunk, wscale, width, offx, offy);
439   }
440
441   /**
442    * DOCUMENT ME!
443    * 
444    * @param pickBox
445    *          DOCUMENT ME!
446    * @param node
447    *          DOCUMENT ME!
448    * @param chunk
449    *          DOCUMENT ME!
450    * @param wscale
451    *          DOCUMENT ME!
452    * @param width
453    *          DOCUMENT ME!
454    * @param offx
455    *          DOCUMENT ME!
456    * @param offy
457    *          DOCUMENT ME!
458    */
459   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
460           double wscale, int width, int offx, int offy)
461   {
462     if (node == null)
463     {
464       return;
465     }
466
467     if ((node.left() == null) && (node.right() == null))
468     {
469       double height = node.height;
470       double dist = node.dist;
471
472       int xstart = (int) ((height - dist) * wscale) + offx;
473       int xend = (int) (height * wscale) + offx;
474
475       int ypos = (int) (node.ycount * chunk) + offy;
476
477       if (pickBox.contains(new Point(xend, ypos)))
478       {
479         if (node.element() instanceof SequenceI)
480         {
481           SequenceI seq = (SequenceI) node.element();
482           SequenceGroup sg = av.getSelectionGroup();
483
484           if (sg != null)
485           {
486             sg.addOrRemove(seq, true);
487           }
488         }
489       }
490     }
491     else
492     {
493       pickNode(pickBox, (SequenceNode) node.left(), chunk, wscale, width,
494               offx, offy);
495       pickNode(pickBox, (SequenceNode) node.right(), chunk, wscale, width,
496               offx, offy);
497     }
498   }
499
500   /**
501    * DOCUMENT ME!
502    * 
503    * @param node
504    *          DOCUMENT ME!
505    * @param c
506    *          DOCUMENT ME!
507    */
508   public void setColor(SequenceNode node, Color c)
509   {
510     if (node == null)
511     {
512       return;
513     }
514
515     if ((node.left() == null) && (node.right() == null)) // TODO: internal node
516     {
517       node.color = c;
518
519       if (node.element() instanceof SequenceI)
520       {
521         AlignmentPanel[] aps = getAssociatedPanels();
522         if (aps != null)
523         {
524           for (int a = 0; a < aps.length; a++)
525           {
526             final SequenceI seq = (SequenceI) node.element();
527             aps[a].av.setSequenceColour(seq, c);
528           }
529         }
530       }
531     }
532     else
533     {
534       node.color = c;
535       setColor((SequenceNode) node.left(), c);
536       setColor((SequenceNode) node.right(), c);
537     }
538   }
539
540   /**
541    * DOCUMENT ME!
542    */
543   void startPrinting()
544   {
545     Thread thread = new Thread(this);
546     thread.start();
547   }
548
549   // put printing in a thread to avoid painting problems
550   @Override
551   public void run()
552   {
553     PrinterJob printJob = PrinterJob.getPrinterJob();
554     PageFormat defaultPage = printJob.defaultPage();
555     PageFormat pf = printJob.pageDialog(defaultPage);
556
557     if (defaultPage == pf)
558     {
559       /*
560        * user cancelled
561        */
562       return;
563     }
564
565     printJob.setPrintable(this, pf);
566
567     if (printJob.printDialog())
568     {
569       try
570       {
571         printJob.print();
572       } catch (Exception PrintException)
573       {
574         PrintException.printStackTrace();
575       }
576     }
577   }
578
579   /**
580    * DOCUMENT ME!
581    * 
582    * @param pg
583    *          DOCUMENT ME!
584    * @param pf
585    *          DOCUMENT ME!
586    * @param pi
587    *          DOCUMENT ME!
588    * 
589    * @return DOCUMENT ME!
590    * 
591    * @throws PrinterException
592    *           DOCUMENT ME!
593    */
594   @Override
595   public int print(Graphics pg, PageFormat pf, int pi)
596           throws PrinterException
597   {
598     pg.setFont(font);
599     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
600
601     int pwidth = (int) pf.getImageableWidth();
602     int pheight = (int) pf.getImageableHeight();
603
604     int noPages = getHeight() / pheight;
605
606     if (pi > noPages)
607     {
608       return Printable.NO_SUCH_PAGE;
609     }
610
611     if (pwidth > getWidth())
612     {
613       pwidth = getWidth();
614     }
615
616     if (fitToWindow)
617     {
618       if (pheight > getHeight())
619       {
620         pheight = getHeight();
621       }
622
623       noPages = 0;
624     }
625     else
626     {
627       FontMetrics fm = pg.getFontMetrics(font);
628       int height = fm.getHeight() * nameHash.size();
629       pg.translate(0, -pi * pheight);
630       pg.setClip(0, pi * pheight, pwidth, (pi * pheight) + pheight);
631
632       // translate number of pages,
633       // height is screen size as this is the
634       // non overlapping text size
635       pheight = height;
636     }
637
638     draw(pg, pwidth, pheight);
639
640     return Printable.PAGE_EXISTS;
641   }
642
643   /**
644    * DOCUMENT ME!
645    * 
646    * @param g
647    *          DOCUMENT ME!
648    */
649   @Override
650   public void paintComponent(Graphics g)
651   {
652     super.paintComponent(g);
653     g.setFont(font);
654
655     if (tree == null)
656     {
657       g.drawString(
658               MessageManager.getString("label.calculating_tree") + "....",
659               20, getHeight() / 2);
660     }
661     else
662     {
663       fm = g.getFontMetrics(font);
664
665       if (nameHash.size() == 0)
666       {
667         repaint();
668       }
669
670       if (fitToWindow || (!fitToWindow && (scrollPane
671               .getHeight() > ((fm.getHeight() * nameHash.size()) + offy))))
672       {
673         draw(g, scrollPane.getWidth(), scrollPane.getHeight());
674         setPreferredSize(null);
675       }
676       else
677       {
678         setPreferredSize(new Dimension(scrollPane.getWidth(),
679                 fm.getHeight() * nameHash.size()));
680         draw(g, scrollPane.getWidth(), fm.getHeight() * nameHash.size());
681       }
682
683       scrollPane.revalidate();
684     }
685   }
686
687   /**
688    * DOCUMENT ME!
689    * 
690    * @param fontSize
691    *          DOCUMENT ME!
692    */
693   @Override
694   public void setFont(Font font)
695   {
696     this.font = font;
697     repaint();
698   }
699
700   /**
701    * DOCUMENT ME!
702    * 
703    * @param g1
704    *          DOCUMENT ME!
705    * @param width
706    *          DOCUMENT ME!
707    * @param height
708    *          DOCUMENT ME!
709    */
710   public void draw(Graphics g1, int width, int height)
711   {
712     Graphics2D g2 = (Graphics2D) g1;
713     g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
714             RenderingHints.VALUE_ANTIALIAS_ON);
715     g2.setColor(Color.white);
716     g2.fillRect(0, 0, width, height);
717     g2.setFont(font);
718
719     if (longestName == null || tree == null)
720     {
721       g2.drawString("Calculating tree.", 20, 20);
722     }
723     offy = font.getSize() + 10;
724
725     fm = g2.getFontMetrics(font);
726
727     labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
728
729     double wscale = (width - labelLength - (offx * 2))
730             / tree.getMaxHeight();
731
732     SequenceNode top = tree.getTopNode();
733
734     if (top.count == 0)
735     {
736       top.count = ((SequenceNode) top.left()).count
737               + ((SequenceNode) top.right()).count;
738     }
739
740     float chunk = (float) (height - (offy)) / top.count;
741
742     drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
743
744     if (threshold != 0)
745     {
746       if (av.getCurrentTree() == tree)
747       {
748         g2.setColor(Color.red);
749       }
750       else
751       {
752         g2.setColor(Color.gray);
753       }
754
755       int x = (int) ((threshold * (getWidth() - labelLength - (2 * offx)))
756               + offx);
757
758       g2.drawLine(x, 0, x, getHeight());
759     }
760   }
761
762   /**
763    * Empty method to satisfy the MouseListener interface
764    * 
765    * @param e
766    */
767   @Override
768   public void mouseReleased(MouseEvent e)
769   {
770     /*
771      * isPopupTrigger is set on mouseReleased on Windows
772      */
773     if (e.isPopupTrigger())
774     {
775       chooseSubtreeColour();
776       e.consume(); // prevent mouseClicked happening
777     }
778   }
779
780   /**
781    * Empty method to satisfy the MouseListener interface
782    * 
783    * @param e
784    */
785   @Override
786   public void mouseEntered(MouseEvent e)
787   {
788   }
789
790   /**
791    * Empty method to satisfy the MouseListener interface
792    * 
793    * @param e
794    */
795   @Override
796   public void mouseExited(MouseEvent e)
797   {
798   }
799
800   /**
801    * Handles a mouse click on a tree node (clicks elsewhere are handled in
802    * mousePressed). Click selects the sub-tree, double-click swaps leaf nodes
803    * order, right-click opens a dialogue to choose colour for the sub-tree.
804    * 
805    * @param e
806    */
807   @Override
808   public void mouseClicked(MouseEvent evt)
809   {
810     if (highlightNode == null)
811     {
812       return;
813     }
814
815     if (evt.getClickCount() > 1)
816     {
817       tree.swapNodes(highlightNode);
818       tree.reCount(tree.getTopNode());
819       tree.findHeight(tree.getTopNode());
820     }
821     else
822     {
823       Vector<SequenceNode> leaves = tree.findLeaves(highlightNode);
824
825       for (int i = 0; i < leaves.size(); i++)
826       {
827         SequenceI seq = (SequenceI) leaves.elementAt(i).element();
828         treeSelectionChanged(seq);
829       }
830       av.sendSelection();
831     }
832
833     PaintRefresher.Refresh(tp, av.getSequenceSetId());
834     repaint();
835   }
836
837   /**
838    * Offer the user the option to choose a colour for the highlighted node and
839    * its children; this colour is also applied to the corresponding sequence ids
840    * in the alignment
841    */
842   void chooseSubtreeColour()
843   {
844     Color col = JColorChooser.showDialog(this,
845             MessageManager.getString("label.select_subtree_colour"),
846             highlightNode.color);
847     if (col != null)
848     {
849       setColor(highlightNode, col);
850       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
851       repaint();
852     }
853   }
854
855   @Override
856   public void mouseMoved(MouseEvent evt)
857   {
858     av.setCurrentTree(tree);
859
860     Object ob = findElement(evt.getX(), evt.getY());
861
862     if (ob instanceof SequenceNode)
863     {
864       highlightNode = (SequenceNode) ob;
865       this.setToolTipText(
866               "<html>" + MessageManager.getString("label.highlightnode"));
867       repaint();
868
869     }
870     else
871     {
872       if (highlightNode != null)
873       {
874         highlightNode = null;
875         setToolTipText(null);
876         repaint();
877       }
878     }
879   }
880
881   @Override
882   public void mouseDragged(MouseEvent ect)
883   {
884   }
885
886   /**
887    * Handles a mouse press on a sequence name or the tree background canvas
888    * (click on a node is handled in mouseClicked). The action is to create
889    * groups by partitioning the tree at the mouse position. Colours for the
890    * groups (and sequence names) are generated randomly.
891    * 
892    * @param e
893    */
894   @Override
895   public void mousePressed(MouseEvent e)
896   {
897     av.setCurrentTree(tree);
898
899     /*
900      * isPopupTrigger is set for mousePressed (Mac)
901      * or mouseReleased (Windows)
902      */
903     if (e.isPopupTrigger())
904     {
905       if (highlightNode != null)
906       {
907         chooseSubtreeColour();
908       }
909       return;
910     }
911
912     /*
913      * defer right-click handling on Windows to
914      * mouseClicked; note isRightMouseButton
915      * also matches Cmd-click on Mac which should do
916      * nothing here
917      */
918     if (SwingUtilities.isRightMouseButton(e))
919     {
920       return;
921     }
922
923     int x = e.getX();
924     int y = e.getY();
925
926     Object ob = findElement(x, y);
927
928     if (ob instanceof SequenceI)
929     {
930       treeSelectionChanged((Sequence) ob);
931       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
932       repaint();
933       av.sendSelection();
934       return;
935     }
936     else if (!(ob instanceof SequenceNode))
937     {
938       // Find threshold
939       if (tree.getMaxHeight() != 0)
940       {
941         threshold = (float) (x - offx)
942                 / (float) (getWidth() - labelLength - (2 * offx));
943
944         List<SequenceNode> groups = tree.groupNodes(threshold);
945         setColor(tree.getTopNode(), Color.black);
946
947         AlignmentPanel[] aps = getAssociatedPanels();
948
949         // TODO push calls below into a single AlignViewportI method?
950         // see also AlignViewController.deleteGroups
951         for (int a = 0; a < aps.length; a++)
952         {
953           aps[a].av.setSelectionGroup(null);
954           aps[a].av.getAlignment().deleteAllGroups();
955           aps[a].av.clearSequenceColours();
956           if (aps[a].av.getCodingComplement() != null)
957           {
958             aps[a].av.getCodingComplement().setSelectionGroup(null);
959             aps[a].av.getCodingComplement().getAlignment()
960                     .deleteAllGroups();
961             aps[a].av.getCodingComplement().clearSequenceColours();
962           }
963         }
964         colourGroups(groups);
965       }
966
967       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
968       repaint();
969     }
970
971   }
972
973   void colourGroups(List<SequenceNode> groups)
974   {
975     AlignmentPanel[] aps = getAssociatedPanels();
976     for (int i = 0; i < groups.size(); i++)
977     {
978       Color col = new Color((int) (Math.random() * 255),
979               (int) (Math.random() * 255), (int) (Math.random() * 255));
980       setColor(groups.get(i), col.brighter());
981
982       Vector<SequenceNode> l = tree.findLeaves(groups.get(i));
983
984       Vector<SequenceI> sequences = new Vector<SequenceI>();
985
986       for (int j = 0; j < l.size(); j++)
987       {
988         SequenceI s1 = (SequenceI) l.elementAt(j).element();
989
990         if (!sequences.contains(s1))
991         {
992           sequences.addElement(s1);
993         }
994       }
995
996       ColourSchemeI cs = null;
997       SequenceGroup sg = new SequenceGroup(sequences, null, cs, true, true,
998               false, 0, av.getAlignment().getWidth() - 1);
999
1000       if (av.getGlobalColourScheme() != null)
1001       {
1002         if (av.getGlobalColourScheme() instanceof UserColourScheme)
1003         {
1004           cs = new UserColourScheme(
1005                   ((UserColourScheme) av.getGlobalColourScheme())
1006                           .getColours());
1007
1008         }
1009         else
1010         {
1011           cs = ColourSchemeProperty.getColourScheme(sg, ColourSchemeProperty
1012                   .getColourName(av.getGlobalColourScheme()));
1013         }
1014         // cs is null if shading is an annotationColourGradient
1015         // if (cs != null)
1016         // {
1017         // cs.setThreshold(av.getViewportColourScheme().getThreshold(),
1018         // av.isIgnoreGapsConsensus());
1019         // }
1020       }
1021       sg.setColourScheme(cs);
1022       sg.getGroupColourScheme().setThreshold(
1023               av.getResidueShading().getThreshold(),
1024               av.isIgnoreGapsConsensus());
1025       // sg.recalcConservation();
1026       sg.setName("JTreeGroup:" + sg.hashCode());
1027       sg.setIdColour(col);
1028
1029       for (int a = 0; a < aps.length; a++)
1030       {
1031         if (aps[a].av.getGlobalColourScheme() != null
1032                 && aps[a].av.getResidueShading().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 }