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