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