354feac25a5fde28c0d08a4327b0c426c111812e
[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       boolean isSelected = false;
298       if (tp.getColumnWise())
299       {
300         isSelected = isColumnForNodeSelected(node);
301       }
302       else
303       {
304         SequenceGroup selected = av.getSelectionGroup();
305
306         if ((selected != null)
307                 && selected.getSequences(null).contains(node.element()))
308         {
309           isSelected = true;
310         }
311       }
312       if (isSelected)
313       {
314         g.setColor(Color.gray);
315
316         g.fillRect(xend + 10, ypos - charHeight / 2, charWidth, charHeight);
317         g.setColor(Color.white);
318       }
319
320       g.drawString(name, xend + 10, ypos + fm.getDescent());
321       g.setColor(Color.black);
322     }
323     else
324     {
325       drawNode(g, (BinaryNode) node.left(), chunk, wscale, width, offx,
326               offy);
327       drawNode(g, (BinaryNode) node.right(), chunk, wscale, width, offx,
328               offy);
329
330       double height = node.height;
331       double dist = node.dist;
332
333       int xstart = (int) ((height - dist) * wscale) + offx;
334       int xend = (int) (height * wscale) + offx;
335       int ypos = (int) (node.ycount * chunk) + offy;
336
337       g.setColor(node.color.darker());
338
339       // Draw horizontal line
340       g.drawLine(xstart, ypos, xend, ypos);
341       if (node == highlightNode)
342       {
343         g.fillRect(xend - 3, ypos - 3, 6, 6);
344       }
345       else
346       {
347         g.fillRect(xend - 2, ypos - 2, 4, 4);
348       }
349
350       int ystart = (node.left() == null ? 0
351               : (int) (((BinaryNode) node.left()).ycount * chunk)) + offy;
352       int yend = (node.right() == null ? 0
353               : (int) (((BinaryNode) node.right()).ycount * chunk))
354               + offy;
355
356       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
357       nodeHash.put(node, pos);
358
359       g.drawLine((int) (height * wscale) + offx, ystart,
360               (int) (height * wscale) + offx, yend);
361
362       String nodeLabel = "";
363
364       if (showDistances && (node.dist > 0))
365       {
366         nodeLabel = new Format("%g").form(node.dist);
367       }
368
369       if (showBootstrap && node.bootstrap > -1)
370       {
371         if (showDistances)
372         {
373           nodeLabel = nodeLabel + " : ";
374         }
375
376         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
377       }
378
379       if (!nodeLabel.equals(""))
380       {
381         g.drawString(nodeLabel, xstart + 2, ypos - 2);
382       }
383     }
384   }
385
386   /**
387    * DOCUMENT ME!
388    * 
389    * @param x
390    *          DOCUMENT ME!
391    * @param y
392    *          DOCUMENT ME!
393    * 
394    * @return DOCUMENT ME!
395    */
396   public Object findElement(int x, int y)
397   {
398     for (Entry<Object, Rectangle> entry : nameHash.entrySet())
399     {
400       Rectangle rect = entry.getValue();
401
402       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
403               && (y <= (rect.y + rect.height)))
404       {
405         return entry.getKey();
406       }
407     }
408
409     for (Entry<BinaryNode, Rectangle> entry : nodeHash.entrySet())
410     {
411       Rectangle rect = entry.getValue();
412
413       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
414               && (y <= (rect.y + rect.height)))
415       {
416         return entry.getKey();
417       }
418     }
419
420     return null;
421   }
422
423   /**
424    * DOCUMENT ME!
425    * 
426    * @param pickBox
427    *          DOCUMENT ME!
428    */
429   public void pickNodes(Rectangle pickBox)
430   {
431     int width = getWidth();
432     int height = getHeight();
433
434     BinaryNode top = tree.getTopNode();
435
436     double wscale = ((width * .8) - (offx * 2)) / tree.getMaxHeight();
437
438     if (top.count == 0)
439     {
440       top.count = ((BinaryNode) top.left()).count
441               + ((BinaryNode) top.right()).count;
442     }
443
444     float chunk = (float) (height - (offy)) / top.count;
445
446     pickNode(pickBox, top, chunk, wscale, width, offx, offy);
447   }
448
449   /**
450    * DOCUMENT ME!
451    * 
452    * @param pickBox
453    *          DOCUMENT ME!
454    * @param node
455    *          DOCUMENT ME!
456    * @param chunk
457    *          DOCUMENT ME!
458    * @param wscale
459    *          DOCUMENT ME!
460    * @param width
461    *          DOCUMENT ME!
462    * @param offx
463    *          DOCUMENT ME!
464    * @param offy
465    *          DOCUMENT ME!
466    */
467   public void pickNode(Rectangle pickBox, BinaryNode node, float chunk,
468           double wscale, int width, int offx, int offy)
469   {
470     if (node == null)
471     {
472       return;
473     }
474
475     if ((node.left() == null) && (node.right() == null))
476     {
477       double height = node.height;
478       // double dist = node.dist;
479       // int xstart = (int) ((height - dist) * wscale) + offx;
480       int xend = (int) (height * wscale) + offx;
481
482       int ypos = (int) (node.ycount * chunk) + offy;
483
484       if (pickBox.contains(new Point(xend, ypos)))
485       {
486         if (node.element() instanceof SequenceI)
487         {
488           SequenceI seq = (SequenceI) node.element();
489           SequenceGroup sg = av.getSelectionGroup();
490
491           if (sg != null)
492           {
493             sg.addOrRemove(seq, true);
494           }
495         }
496       }
497     }
498     else
499     {
500       pickNode(pickBox, (BinaryNode) node.left(), chunk, wscale, width,
501               offx, offy);
502       pickNode(pickBox, (BinaryNode) node.right(), chunk, wscale, width,
503               offx, offy);
504     }
505   }
506
507   /**
508    * DOCUMENT ME!
509    * 
510    * @param node
511    *          DOCUMENT ME!
512    * @param c
513    *          DOCUMENT ME!
514    */
515   public void setColor(BinaryNode node, Color c)
516   {
517     if (node == null)
518     {
519       return;
520     }
521
522     node.color = c;
523     if (node.element() instanceof SequenceI)
524     {
525       final SequenceI seq = (SequenceI) node.element();
526       AlignmentPanel[] aps = getAssociatedPanels();
527       if (aps != null)
528       {
529         for (int a = 0; a < aps.length; a++)
530         {
531           aps[a].av.setSequenceColour(seq, c);
532         }
533       }
534     }
535     setColor((BinaryNode) node.left(), c);
536     setColor((BinaryNode) node.right(), c);
537   }
538
539   /**
540    * DOCUMENT ME!
541    */
542   void startPrinting()
543   {
544     Thread thread = new Thread(this);
545     thread.start();
546   }
547
548   // put printing in a thread to avoid painting problems
549   @Override
550   public void run()
551   {
552     PrinterJob printJob = PrinterJob.getPrinterJob();
553     PageFormat defaultPage = printJob.defaultPage();
554     PageFormat pf = printJob.pageDialog(defaultPage);
555
556     if (defaultPage == pf)
557     {
558       /*
559        * user cancelled
560        */
561       return;
562     }
563
564     printJob.setPrintable(this, pf);
565
566     if (printJob.printDialog())
567     {
568       try
569       {
570         printJob.print();
571       } catch (Exception PrintException)
572       {
573         PrintException.printStackTrace();
574       }
575     }
576   }
577
578   /**
579    * DOCUMENT ME!
580    * 
581    * @param pg
582    *          DOCUMENT ME!
583    * @param pf
584    *          DOCUMENT ME!
585    * @param pi
586    *          DOCUMENT ME!
587    * 
588    * @return DOCUMENT ME!
589    * 
590    * @throws PrinterException
591    *           DOCUMENT ME!
592    */
593   @Override
594   public int print(Graphics pg, PageFormat pf, int pi)
595           throws PrinterException
596   {
597     pg.setFont(font);
598     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
599
600     int pwidth = (int) pf.getImageableWidth();
601     int pheight = (int) pf.getImageableHeight();
602
603     int noPages = getHeight() / pheight;
604
605     if (pi > noPages)
606     {
607       return Printable.NO_SUCH_PAGE;
608     }
609
610     if (pwidth > getWidth())
611     {
612       pwidth = getWidth();
613     }
614
615     if (fitToWindow)
616     {
617       if (pheight > getHeight())
618       {
619         pheight = getHeight();
620       }
621
622       noPages = 0;
623     }
624     else
625     {
626       FontMetrics fm = pg.getFontMetrics(font);
627       int height = fm.getHeight() * nameHash.size();
628       pg.translate(0, -pi * pheight);
629       pg.setClip(0, pi * pheight, pwidth, (pi * pheight) + pheight);
630
631       // translate number of pages,
632       // height is screen size as this is the
633       // non overlapping text size
634       pheight = height;
635     }
636
637     draw(pg, pwidth, pheight);
638
639     return Printable.PAGE_EXISTS;
640   }
641
642   /**
643    * DOCUMENT ME!
644    * 
645    * @param g
646    *          DOCUMENT ME!
647    */
648   @Override
649   public void paintComponent(Graphics g)
650   {
651     super.paintComponent(g);
652     g.setFont(font);
653
654     if (tree == null)
655     {
656       g.drawString(
657               MessageManager.getString("label.calculating_tree") + "....",
658               20, getHeight() / 2);
659     }
660     else
661     {
662       fm = g.getFontMetrics(font);
663
664       int nameCount = nameHash.size();
665       if (nameCount == 0)
666       {
667         repaint();
668       }
669
670       if (fitToWindow || (!fitToWindow && (scrollPane
671               .getHeight() > ((fm.getHeight() * nameCount) + 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() * nameCount));
680         draw(g, scrollPane.getWidth(), fm.getHeight() * nameCount);
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       return;
723     }
724     offy = font.getSize() + 10;
725
726     fm = g2.getFontMetrics(font);
727
728     labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
729
730     double wscale = (width - labelLength - (offx * 2))
731             / tree.getMaxHeight();
732
733     BinaryNode top = tree.getTopNode();
734
735     if (top.count == 0)
736     {
737       top.count = ((BinaryNode) top.left()).count
738               + ((BinaryNode) top.right()).count;
739     }
740
741     float chunk = (float) (height - (offy)) / top.count;
742
743     drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
744
745     if (threshold != 0)
746     {
747       if (av.getCurrentTree() == tree)
748       {
749         g2.setColor(Color.red);
750       }
751       else
752       {
753         g2.setColor(Color.gray);
754       }
755
756       int x = (int) ((threshold * (getWidth() - labelLength - (2 * offx)))
757               + offx);
758
759       g2.drawLine(x, 0, x, getHeight());
760     }
761   }
762
763   /**
764    * Empty method to satisfy the MouseListener interface
765    * 
766    * @param e
767    */
768   @Override
769   public void mouseReleased(MouseEvent e)
770   {
771     /*
772      * isPopupTrigger is set on mouseReleased on Windows
773      */
774     if (e.isPopupTrigger())
775     {
776       chooseSubtreeColour();
777       e.consume(); // prevent mouseClicked happening
778     }
779   }
780
781   /**
782    * Empty method to satisfy the MouseListener interface
783    * 
784    * @param e
785    */
786   @Override
787   public void mouseEntered(MouseEvent e)
788   {
789   }
790
791   /**
792    * Empty method to satisfy the MouseListener interface
793    * 
794    * @param e
795    */
796   @Override
797   public void mouseExited(MouseEvent e)
798   {
799   }
800
801   /**
802    * Handles a mouse click on a tree node (clicks elsewhere are handled in
803    * mousePressed). Click selects the sub-tree, double-click swaps leaf nodes
804    * order, right-click opens a dialogue to choose colour for the sub-tree.
805    * 
806    * @param e
807    */
808   @Override
809   public void mouseClicked(MouseEvent evt)
810   {
811     if (highlightNode == null)
812     {
813       return;
814     }
815
816     if (evt.getClickCount() > 1)
817     {
818       tree.swapNodes(highlightNode);
819       tree.reCount(tree.getTopNode());
820       tree.findHeight(tree.getTopNode());
821     }
822     else
823     {
824       Vector<BinaryNode> leaves = tree.findLeaves(highlightNode);
825       if (tp.getColumnWise()) {
826         markColumnsFor(getAssociatedPanels(), leaves, Color.red);
827       } else {
828       for (int i = 0; i < leaves.size(); i++)
829       {
830         SequenceI seq = (SequenceI) leaves.elementAt(i).element();
831         treeSelectionChanged(seq);
832       }
833       }
834       av.sendSelection();
835     }
836
837     PaintRefresher.Refresh(tp, av.getSequenceSetId());
838     repaint();
839   }
840
841   /**
842    * Offer the user the option to choose a colour for the highlighted node and
843    * its children; this colour is also applied to the corresponding sequence ids
844    * in the alignment
845    */
846   void chooseSubtreeColour()
847   {
848     String ttl = MessageManager.getString("label.select_subtree_colour");
849     ColourChooserListener listener = new ColourChooserListener()
850     {
851       @Override
852       public void colourSelected(Color c)
853       {
854         setColor(highlightNode, c);
855         PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
856         repaint();
857       }
858     };
859     JalviewColourChooser.showColourChooser(this, ttl, highlightNode.color,
860             listener);
861   }
862
863   @Override
864   public void mouseMoved(MouseEvent evt)
865   {
866     av.setCurrentTree(tree);
867
868     Object ob = findElement(evt.getX(), evt.getY());
869
870     if (ob instanceof BinaryNode)
871     {
872       highlightNode = (BinaryNode) ob;
873       this.setToolTipText(
874               "<html>" + MessageManager.getString("label.highlightnode"));
875       repaint();
876
877     }
878     else
879     {
880       if (highlightNode != null)
881       {
882         highlightNode = null;
883         setToolTipText(null);
884         repaint();
885       }
886     }
887   }
888
889   @Override
890   public void mouseDragged(MouseEvent ect)
891   {
892   }
893
894   /**
895    * Handles a mouse press on a sequence name or the tree background canvas
896    * (click on a node is handled in mouseClicked). The action is to create
897    * groups by partitioning the tree at the mouse position. Colours for the
898    * groups (and sequence names) are generated randomly.
899    * 
900    * @param e
901    */
902   @Override
903   public void mousePressed(MouseEvent e)
904   {
905     av.setCurrentTree(tree);
906
907     /*
908      * isPopupTrigger is set for mousePressed (Mac)
909      * or mouseReleased (Windows)
910      */
911     if (e.isPopupTrigger())
912     {
913       if (highlightNode != null)
914       {
915         chooseSubtreeColour();
916       }
917       return;
918     }
919
920     /*
921      * defer right-click handling on Windows to
922      * mouseClicked; note isRightMouseButton
923      * also matches Cmd-click on Mac which should do
924      * nothing here
925      */
926     if (SwingUtilities.isRightMouseButton(e))
927     {
928       return;
929     }
930
931     int x = e.getX();
932     int y = e.getY();
933
934     Object ob = findElement(x, y);
935
936     if (ob instanceof SequenceI)
937     {
938       treeSelectionChanged((Sequence) ob);
939       PaintRefresher.Refresh(tp,
940               getAssociatedPanel().av.getSequenceSetId());
941       repaint();
942       av.sendSelection();
943       return;
944     }
945     else if (!(ob instanceof BinaryNode))
946     {
947       // Find threshold
948       if (tree.getMaxHeight() != 0)
949       {
950         threshold = (float) (x - offx)
951                 / (float) (getWidth() - labelLength - (2 * offx));
952
953         List<BinaryNode> groups = tree.groupNodes(threshold);
954         setColor(tree.getTopNode(), Color.black);
955
956         AlignmentPanel[] aps = getAssociatedPanels();
957
958         // TODO push calls below into a single AlignViewportI method?
959         // see also AlignViewController.deleteGroups
960         for (int a = 0; a < aps.length; a++)
961         {
962           aps[a].av.setSelectionGroup(null);
963           aps[a].av.getAlignment().deleteAllGroups();
964           aps[a].av.clearSequenceColours();
965           if (aps[a].av.getCodingComplement() != null)
966           {
967             aps[a].av.getCodingComplement().setSelectionGroup(null);
968             aps[a].av.getCodingComplement().getAlignment()
969                     .deleteAllGroups();
970             aps[a].av.getCodingComplement().clearSequenceColours();
971           }
972           aps[a].av.setUpdateStructures(true);
973         }
974         colourGroups(groups);
975
976         /*
977          * clear partition (don't show vertical line) if
978          * it is to the right of all nodes
979          */
980         if (groups.isEmpty())
981         {
982           threshold = 0f;
983         }
984       }
985
986       PaintRefresher.Refresh(tp,
987               getAssociatedPanel().av.getSequenceSetId());
988       repaint();
989     }
990
991   }
992
993   void colourGroups(List<BinaryNode> groups)
994   {
995     AlignmentPanel[] aps = getAssociatedPanels();
996     for (int i = 0; i < groups.size(); i++)
997     {
998       Color col = new Color((int) (Math.random() * 255),
999               (int) (Math.random() * 255), (int) (Math.random() * 255));
1000       setColor(groups.get(i), col.brighter());
1001
1002       Vector<BinaryNode> l = tree.findLeaves(groups.get(i));
1003       if (!tp.getColumnWise()) {
1004         createSeqGroupFor(aps, l, col);
1005       } else {
1006         markColumnsFor(aps,l,col);
1007       }
1008     }
1009
1010     // notify the panel(s) to redo any group specific stuff
1011     // also updates structure views if necessary
1012     for (int a = 0; a < aps.length; a++)
1013     {
1014       aps[a].updateAnnotation();
1015       final AlignViewportI codingComplement = aps[a].av
1016               .getCodingComplement();
1017       if (codingComplement != null)
1018       {
1019         ((AlignViewport) codingComplement).getAlignPanel()
1020                 .updateAnnotation();
1021       }
1022     }
1023   }
1024
1025   private boolean isColumnForNodeSelected(BinaryNode bn)
1026   {
1027     SequenceI rseq = tp.assocAnnotation.sequenceRef;
1028     int colm = -1;
1029     try
1030     {
1031       colm = Integer.parseInt(
1032               bn.getName().substring(bn.getName().indexOf("c") + 1));
1033     } catch (Exception e)
1034     {
1035       return false;
1036     }
1037     ColumnSelection cs = av.getColumnSelection();
1038     HiddenColumns hc = av.getAlignment().getHiddenColumns();
1039     int offp = (rseq != null) ? rseq.findIndex(rseq.getStart() - 1 + colm)
1040             : colm;
1041
1042     if (!av.hasHiddenColumns() || hc.isVisible(offp))
1043     {
1044       return cs.contains(offp);
1045     }
1046     return false;
1047   }
1048
1049   private void markColumnsFor(AlignmentPanel[] aps, Vector<BinaryNode> l,
1050           Color col)
1051   {
1052     SequenceI rseq = tp.assocAnnotation.sequenceRef;
1053     for (BinaryNode bn:l)
1054     {
1055       int colm=-1;
1056       try {
1057         colm = Integer.parseInt(bn.getName().substring(bn.getName().indexOf("c")+1));
1058       } catch (Exception e)
1059       {
1060         continue;
1061       }
1062       ColumnSelection cs = av.getColumnSelection();
1063       HiddenColumns hc = av.getAlignment().getHiddenColumns();
1064       {
1065         int offp = (rseq!=null) ? rseq.findIndex(rseq.getStart()-1+colm) : colm;
1066         
1067         if (!av.hasHiddenColumns() || hc.isVisible(offp))
1068         { 
1069           if (cs.contains(offp))
1070           {
1071             cs.removeElement(offp);
1072           } else {
1073             cs.addElement(offp);
1074           }
1075         }
1076       }
1077     } 
1078   }
1079
1080   public void createSeqGroupFor(AlignmentPanel[] aps, Vector<BinaryNode> l,
1081           Color col)
1082   {
1083
1084     Vector<SequenceI> sequences = new Vector<>();
1085
1086     for (int j = 0; j < l.size(); j++)
1087     {
1088       SequenceI s1 = (SequenceI) l.elementAt(j).element();
1089
1090       if (!sequences.contains(s1))
1091       {
1092         sequences.addElement(s1);
1093       }
1094     }
1095
1096     ColourSchemeI cs = null;
1097     SequenceGroup _sg = new SequenceGroup(sequences, null, cs, true, true,
1098             false, 0, av.getAlignment().getWidth() - 1);
1099
1100     _sg.setName("JTreeGroup:" + _sg.hashCode());
1101     _sg.setIdColour(col);
1102
1103     for (int a = 0; a < aps.length; a++)
1104     {
1105       SequenceGroup sg = new SequenceGroup(_sg);
1106       AlignViewport viewport = aps[a].av;
1107
1108       // Propagate group colours in each view
1109       if (viewport.getGlobalColourScheme() != null)
1110       {
1111         cs = viewport.getGlobalColourScheme().getInstance(viewport, sg);
1112         sg.setColourScheme(cs);
1113         sg.getGroupColourScheme().setThreshold(
1114                 viewport.getResidueShading().getThreshold(),
1115                 viewport.isIgnoreGapsConsensus());
1116
1117         if (viewport.getResidueShading().conservationApplied())
1118         {
1119           Conservation c = new Conservation("Group", sg.getSequences(null),
1120                   sg.getStartRes(), sg.getEndRes());
1121           c.calculate();
1122           c.verdict(false, viewport.getConsPercGaps());
1123           sg.cs.setConservation(c);
1124         }
1125       }
1126       // indicate that associated structure views will need an update
1127       viewport.setUpdateStructures(true);
1128       // propagate structure view update and sequence group to complement view
1129       viewport.addSequenceGroup(sg);
1130     }
1131   }
1132
1133   /**
1134    * DOCUMENT ME!
1135    * 
1136    * @param state
1137    *          DOCUMENT ME!
1138    */
1139   public void setShowDistances(boolean state)
1140   {
1141     this.showDistances = state;
1142     repaint();
1143   }
1144
1145   /**
1146    * DOCUMENT ME!
1147    * 
1148    * @param state
1149    *          DOCUMENT ME!
1150    */
1151   public void setShowBootstrap(boolean state)
1152   {
1153     this.showBootstrap = state;
1154     repaint();
1155   }
1156
1157   /**
1158    * DOCUMENT ME!
1159    * 
1160    * @param state
1161    *          DOCUMENT ME!
1162    */
1163   public void setMarkPlaceholders(boolean state)
1164   {
1165     this.markPlaceholders = state;
1166     repaint();
1167   }
1168
1169   AlignmentPanel[] getAssociatedPanels()
1170   {
1171     if (applyToAllViews)
1172     {
1173       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
1174     }
1175     else
1176     {
1177       return new AlignmentPanel[] { getAssociatedPanel() };
1178     }
1179   }
1180
1181   public AlignmentPanel getAssociatedPanel()
1182   {
1183     return ap;
1184   }
1185
1186   public void setAssociatedPanel(AlignmentPanel ap)
1187   {
1188     this.ap = ap;
1189   }
1190
1191   public AlignViewport getViewport()
1192   {
1193     return av;
1194   }
1195
1196   public void setViewport(AlignViewport av)
1197   {
1198     this.av = av;
1199   }
1200
1201   public float getThreshold()
1202   {
1203     return threshold;
1204   }
1205
1206   public void setThreshold(float threshold)
1207   {
1208     this.threshold = threshold;
1209   }
1210
1211   public boolean isApplyToAllViews()
1212   {
1213     return this.applyToAllViews;
1214   }
1215
1216   public void setApplyToAllViews(boolean applyToAllViews)
1217   {
1218     this.applyToAllViews = applyToAllViews;
1219   }
1220 }