b58269d10aa1823f76092eddd5b1aaf074291976
[jalview.git] / src / jalview / gui / AnnotationLabels.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.analysis.AlignmentUtils;
25 import jalview.datamodel.Alignment;
26 import jalview.datamodel.AlignmentAnnotation;
27 import jalview.datamodel.Annotation;
28 import jalview.datamodel.HiddenColumns;
29 import jalview.datamodel.Sequence;
30 import jalview.datamodel.SequenceGroup;
31 import jalview.datamodel.SequenceI;
32 import jalview.io.FileFormat;
33 import jalview.io.FormatAdapter;
34 import jalview.util.Comparison;
35 import jalview.util.MessageManager;
36 import jalview.util.Platform;
37
38 import java.awt.Color;
39 import java.awt.Cursor;
40 import java.awt.Dimension;
41 import java.awt.Font;
42 import java.awt.FontMetrics;
43 import java.awt.Graphics;
44 import java.awt.Graphics2D;
45 import java.awt.RenderingHints;
46 import java.awt.Toolkit;
47 import java.awt.datatransfer.StringSelection;
48 import java.awt.event.ActionEvent;
49 import java.awt.event.ActionListener;
50 import java.awt.event.MouseEvent;
51 import java.awt.event.MouseListener;
52 import java.awt.event.MouseMotionListener;
53 import java.awt.geom.AffineTransform;
54 import java.awt.image.BufferedImage;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.Collections;
58 import java.util.Iterator;
59 import java.util.regex.Pattern;
60
61 import javax.swing.JCheckBoxMenuItem;
62 import javax.swing.JMenuItem;
63 import javax.swing.JPanel;
64 import javax.swing.JPopupMenu;
65 import javax.swing.SwingUtilities;
66 import javax.swing.ToolTipManager;
67
68 /**
69  * The panel that holds the labels for alignment annotations, providing
70  * tooltips, context menus, drag to reorder rows, and drag to adjust panel
71  * height
72  */
73 public class AnnotationLabels extends JPanel
74         implements MouseListener, MouseMotionListener, ActionListener
75 {
76   /**
77    * width in pixels within which height adjuster arrows are shown and active
78    */
79   private static final int HEIGHT_ADJUSTER_WIDTH = 50;
80
81   /**
82    * height in pixels for allowing height adjuster to be active
83    */
84   private static int HEIGHT_ADJUSTER_HEIGHT = 10;
85
86   private static final Pattern LEFT_ANGLE_BRACKET_PATTERN = Pattern
87           .compile("<");
88
89   private static final Font font = new Font("Arial", Font.PLAIN, 11);
90
91   private static final String TOGGLE_LABELSCALE = MessageManager
92           .getString("label.scale_label_to_column");
93
94   private static final String ADDNEW = MessageManager
95           .getString("label.add_new_row");
96
97   private static final String EDITNAME = MessageManager
98           .getString("label.edit_label_description");
99
100   private static final String HIDE = MessageManager
101           .getString("label.hide_row");
102
103   private static final String DELETE = MessageManager
104           .getString("label.delete_row");
105
106   private static final String SHOWALL = MessageManager
107           .getString("label.show_all_hidden_rows");
108
109   private static final String OUTPUT_TEXT = MessageManager
110           .getString("label.export_annotation");
111
112   private static final String COPYCONS_SEQ = MessageManager
113           .getString("label.copy_consensus_sequence");
114
115   private final boolean debugRedraw = false;
116
117   private AlignmentPanel ap;
118
119   AlignViewport av;
120
121   private MouseEvent dragEvent;
122
123   private int oldY;
124
125   private int selectedRow;
126
127   private int scrollOffset = 0;
128
129   private boolean hasHiddenRows;
130
131   private boolean resizePanel = false;
132
133   /**
134    * Creates a new AnnotationLabels object
135    * 
136    * @param ap
137    */
138   public AnnotationLabels(AlignmentPanel ap)
139   {
140     this.ap = ap;
141     av = ap.av;
142     ToolTipManager.sharedInstance().registerComponent(this);
143
144     addMouseListener(this);
145     addMouseMotionListener(this);
146     addMouseWheelListener(ap.getAnnotationPanel());
147   }
148
149   public AnnotationLabels(AlignViewport av)
150   {
151     this.av = av;
152   }
153
154   /**
155    * DOCUMENT ME!
156    * 
157    * @param y
158    *          DOCUMENT ME!
159    */
160   public void setScrollOffset(int y)
161   {
162     scrollOffset = y;
163     repaint();
164   }
165
166   /**
167    * sets selectedRow to -2 if no annotation preset, -1 if no visible row is at
168    * y
169    * 
170    * @param y
171    *          coordinate position to search for a row
172    */
173   void getSelectedRow(int y)
174   {
175     int height = 0;
176     AlignmentAnnotation[] aa = ap.av.getAlignment()
177             .getAlignmentAnnotation();
178     selectedRow = -2;
179     if (aa != null)
180     {
181       for (int i = 0; i < aa.length; i++)
182       {
183         selectedRow = -1;
184         if (!aa[i].visible)
185         {
186           continue;
187         }
188
189         height += aa[i].height;
190
191         if (y < height)
192         {
193           selectedRow = i;
194
195           break;
196         }
197       }
198     }
199   }
200
201   /**
202    * DOCUMENT ME!
203    * 
204    * @param evt
205    *          DOCUMENT ME!
206    */
207   @Override
208   public void actionPerformed(ActionEvent evt)
209   {
210     AlignmentAnnotation[] aa = ap.av.getAlignment()
211             .getAlignmentAnnotation();
212
213     boolean fullRepaint = false;
214     if (evt.getActionCommand().equals(ADDNEW))
215     {
216       AlignmentAnnotation newAnnotation = new AlignmentAnnotation(null,
217               null, new Annotation[ap.av.getAlignment().getWidth()]);
218
219       if (!editLabelDescription(newAnnotation))
220       {
221         return;
222       }
223
224       ap.av.getAlignment().addAnnotation(newAnnotation);
225       ap.av.getAlignment().setAnnotationIndex(newAnnotation, 0);
226       fullRepaint = true;
227     }
228     else if (evt.getActionCommand().equals(EDITNAME))
229     {
230       String name = aa[selectedRow].label;
231       editLabelDescription(aa[selectedRow]);
232       if (!name.equalsIgnoreCase(aa[selectedRow].label))
233       {
234         fullRepaint = true;
235       }
236     }
237     else if (evt.getActionCommand().equals(HIDE))
238     {
239       aa[selectedRow].visible = false;
240     }
241     else if (evt.getActionCommand().equals(DELETE))
242     {
243       ap.av.getAlignment().deleteAnnotation(aa[selectedRow]);
244       ap.av.getCalcManager().removeWorkerForAnnotation(aa[selectedRow]);
245       fullRepaint = true;
246     }
247     else if (evt.getActionCommand().equals(SHOWALL))
248     {
249       for (int i = 0; i < aa.length; i++)
250       {
251         if (!aa[i].visible && aa[i].annotations != null)
252         {
253           aa[i].visible = true;
254         }
255       }
256       fullRepaint = true;
257     }
258     else if (evt.getActionCommand().equals(OUTPUT_TEXT))
259     {
260       new AnnotationExporter().exportAnnotations(ap,
261               new AlignmentAnnotation[]
262               { aa[selectedRow] });
263     }
264     else if (evt.getActionCommand().equals(COPYCONS_SEQ))
265     {
266       SequenceI cons = null;
267       if (aa[selectedRow].groupRef != null)
268       {
269         cons = aa[selectedRow].groupRef.getConsensusSeq();
270       }
271       else
272       {
273         cons = av.getConsensusSeq();
274       }
275       if (cons != null)
276       {
277         copy_annotseqtoclipboard(cons);
278       }
279
280     }
281     else if (evt.getActionCommand().equals(TOGGLE_LABELSCALE))
282     {
283       aa[selectedRow].scaleColLabel = !aa[selectedRow].scaleColLabel;
284     }
285
286     ap.refresh(fullRepaint);
287
288   }
289
290   /**
291    * DOCUMENT ME!
292    * 
293    * @param e
294    *          DOCUMENT ME!
295    */
296   boolean editLabelDescription(AlignmentAnnotation annotation)
297   {
298     // TODO i18n
299     EditNameDialog dialog = new EditNameDialog(annotation.label,
300             annotation.description, "       Annotation Name ",
301             "Annotation Description ", "Edit Annotation Name/Description",
302             ap.alignFrame);
303
304     if (!dialog.accept)
305     {
306       return false;
307     }
308
309     annotation.label = dialog.getName();
310
311     String text = dialog.getDescription();
312     if (text != null && text.length() == 0)
313     {
314       text = null;
315     }
316     annotation.description = text;
317
318     return true;
319   }
320
321   @Override
322   public void mousePressed(MouseEvent evt)
323   {
324     getSelectedRow(evt.getY() - getScrollOffset());
325     oldY = evt.getY();
326     if (evt.isPopupTrigger())
327     {
328       showPopupMenu(evt);
329     }
330   }
331
332   /**
333    * Build and show the Pop-up menu at the right-click mouse position
334    * 
335    * @param evt
336    */
337   void showPopupMenu(MouseEvent evt)
338   {
339     evt.consume();
340     final AlignmentAnnotation[] aa = ap.av.getAlignment()
341             .getAlignmentAnnotation();
342
343     JPopupMenu pop = new JPopupMenu(
344             MessageManager.getString("label.annotations"));
345     JMenuItem item = new JMenuItem(ADDNEW);
346     item.addActionListener(this);
347     pop.add(item);
348     if (selectedRow < 0)
349     {
350       if (hasHiddenRows)
351       { // let the user make everything visible again
352         item = new JMenuItem(SHOWALL);
353         item.addActionListener(this);
354         pop.add(item);
355       }
356       pop.show(this, evt.getX(), evt.getY());
357       return;
358     }
359     item = new JMenuItem(EDITNAME);
360     item.addActionListener(this);
361     pop.add(item);
362     item = new JMenuItem(HIDE);
363     item.addActionListener(this);
364     pop.add(item);
365     // JAL-1264 hide all sequence-specific annotations of this type
366     if (selectedRow < aa.length)
367     {
368       if (aa[selectedRow].sequenceRef != null)
369       {
370         final String label = aa[selectedRow].label;
371         JMenuItem hideType = new JMenuItem();
372         String text = MessageManager.getString("label.hide_all") + " "
373                 + label;
374         hideType.setText(text);
375         hideType.addActionListener(new ActionListener()
376         {
377           @Override
378           public void actionPerformed(ActionEvent e)
379           {
380             AlignmentUtils.showOrHideSequenceAnnotations(
381                     ap.av.getAlignment(), Collections.singleton(label),
382                     null, false, false);
383             // for (AlignmentAnnotation ann : ap.av.getAlignment()
384             // .getAlignmentAnnotation())
385             // {
386             // if (ann.sequenceRef != null && ann.label != null
387             // && ann.label.equals(label))
388             // {
389             // ann.visible = false;
390             // }
391             // }
392             ap.refresh(true);
393           }
394         });
395         pop.add(hideType);
396       }
397     }
398     item = new JMenuItem(DELETE);
399     item.addActionListener(this);
400     pop.add(item);
401     if (hasHiddenRows)
402     {
403       item = new JMenuItem(SHOWALL);
404       item.addActionListener(this);
405       pop.add(item);
406     }
407     item = new JMenuItem(OUTPUT_TEXT);
408     item.addActionListener(this);
409     pop.add(item);
410     // TODO: annotation object should be typed for autocalculated/derived
411     // property methods
412     if (selectedRow < aa.length)
413     {
414       final String label = aa[selectedRow].label;
415       if (!aa[selectedRow].autoCalculated)
416       {
417         if (aa[selectedRow].graph == AlignmentAnnotation.NO_GRAPH)
418         {
419           // display formatting settings for this row.
420           pop.addSeparator();
421           // av and sequencegroup need to implement same interface for
422           item = new JCheckBoxMenuItem(TOGGLE_LABELSCALE,
423                   aa[selectedRow].scaleColLabel);
424           item.addActionListener(this);
425           pop.add(item);
426         }
427       }
428       else if (label.indexOf("Consensus") > -1)
429       {
430         pop.addSeparator();
431         // av and sequencegroup need to implement same interface for
432         final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
433                 MessageManager.getString("label.ignore_gaps_consensus"),
434                 (aa[selectedRow].groupRef != null)
435                         ? aa[selectedRow].groupRef.getIgnoreGapsConsensus()
436                         : ap.av.isIgnoreGapsConsensus());
437         final AlignmentAnnotation aaa = aa[selectedRow];
438         cbmi.addActionListener(new ActionListener()
439         {
440           @Override
441           public void actionPerformed(ActionEvent e)
442           {
443             if (aaa.groupRef != null)
444             {
445               // TODO: pass on reference to ap so the view can be updated.
446               aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
447               ap.getAnnotationPanel()
448                       .paint(ap.getAnnotationPanel().getGraphics());
449             }
450             else
451             {
452               ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
453             }
454             ap.alignmentChanged();
455           }
456         });
457         pop.add(cbmi);
458         // av and sequencegroup need to implement same interface for
459         if (aaa.groupRef != null)
460         {
461           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
462                   MessageManager.getString("label.show_group_histogram"),
463                   aa[selectedRow].groupRef.isShowConsensusHistogram());
464           chist.addActionListener(new ActionListener()
465           {
466             @Override
467             public void actionPerformed(ActionEvent e)
468             {
469               // TODO: pass on reference
470               // to ap
471               // so the
472               // view
473               // can be
474               // updated.
475               aaa.groupRef.setShowConsensusHistogram(chist.getState());
476               ap.repaint();
477               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
478             }
479           });
480           pop.add(chist);
481           final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
482                   MessageManager.getString("label.show_group_logo"),
483                   aa[selectedRow].groupRef.isShowSequenceLogo());
484           cprofl.addActionListener(new ActionListener()
485           {
486             @Override
487             public void actionPerformed(ActionEvent e)
488             {
489               // TODO: pass on reference
490               // to ap
491               // so the
492               // view
493               // can be
494               // updated.
495               aaa.groupRef.setshowSequenceLogo(cprofl.getState());
496               ap.repaint();
497               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
498             }
499           });
500           pop.add(cprofl);
501           final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
502                   MessageManager.getString("label.normalise_group_logo"),
503                   aa[selectedRow].groupRef.isNormaliseSequenceLogo());
504           cproflnorm.addActionListener(new ActionListener()
505           {
506             @Override
507             public void actionPerformed(ActionEvent e)
508             {
509
510               // TODO: pass on reference
511               // to ap
512               // so the
513               // view
514               // can be
515               // updated.
516               aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
517               // automatically enable logo display if we're clicked
518               aaa.groupRef.setshowSequenceLogo(true);
519               ap.repaint();
520               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
521             }
522           });
523           pop.add(cproflnorm);
524         }
525         else
526         {
527           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
528                   MessageManager.getString("label.show_histogram"),
529                   av.isShowConsensusHistogram());
530           chist.addActionListener(new ActionListener()
531           {
532             @Override
533             public void actionPerformed(ActionEvent e)
534             {
535               // TODO: pass on reference
536               // to ap
537               // so the
538               // view
539               // can be
540               // updated.
541               av.setShowConsensusHistogram(chist.getState());
542               ap.alignFrame.setMenusForViewport();
543               ap.repaint();
544               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
545             }
546           });
547           pop.add(chist);
548           final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
549                   MessageManager.getString("label.show_logo"),
550                   av.isShowSequenceLogo());
551           cprof.addActionListener(new ActionListener()
552           {
553             @Override
554             public void actionPerformed(ActionEvent e)
555             {
556               // TODO: pass on reference
557               // to ap
558               // so the
559               // view
560               // can be
561               // updated.
562               av.setShowSequenceLogo(cprof.getState());
563               ap.alignFrame.setMenusForViewport();
564               ap.repaint();
565               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
566             }
567           });
568           pop.add(cprof);
569           final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
570                   MessageManager.getString("label.normalise_logo"),
571                   av.isNormaliseSequenceLogo());
572           cprofnorm.addActionListener(new ActionListener()
573           {
574             @Override
575             public void actionPerformed(ActionEvent e)
576             {
577               // TODO: pass on reference
578               // to ap
579               // so the
580               // view
581               // can be
582               // updated.
583               av.setShowSequenceLogo(true);
584               av.setNormaliseSequenceLogo(cprofnorm.getState());
585               ap.alignFrame.setMenusForViewport();
586               ap.repaint();
587               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
588             }
589           });
590           pop.add(cprofnorm);
591         }
592         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
593         consclipbrd.addActionListener(this);
594         pop.add(consclipbrd);
595       }
596     }
597     pop.show(this, evt.getX(), evt.getY());
598   }
599
600   /**
601    * Reorders annotation rows after a drag of a label
602    * 
603    * @param evt
604    */
605   @Override
606   public void mouseReleased(MouseEvent evt)
607   {
608     if (evt.isPopupTrigger())
609     {
610       showPopupMenu(evt);
611       return;
612     }
613
614     int start = selectedRow;
615     getSelectedRow(evt.getY() - getScrollOffset());
616     int end = selectedRow;
617
618     /*
619      * if dragging to resize instead, start == end
620      */
621     if (start != end)
622     {
623       // Swap these annotations
624       AlignmentAnnotation startAA = ap.av.getAlignment()
625               .getAlignmentAnnotation()[start];
626       if (end == -1)
627       {
628         end = ap.av.getAlignment().getAlignmentAnnotation().length - 1;
629       }
630       AlignmentAnnotation endAA = ap.av.getAlignment()
631               .getAlignmentAnnotation()[end];
632
633       ap.av.getAlignment().getAlignmentAnnotation()[end] = startAA;
634       ap.av.getAlignment().getAlignmentAnnotation()[start] = endAA;
635     }
636
637     resizePanel = false;
638     dragEvent = null;
639     repaint();
640     ap.getAnnotationPanel().repaint();
641   }
642
643   /**
644    * Removes the height adjuster image on leaving the panel, unless currently
645    * dragging it
646    */
647   @Override
648   public void mouseExited(MouseEvent evt)
649   {
650     if (resizePanel && dragEvent == null)
651     {
652       resizePanel = false;
653       repaint();
654     }
655   }
656
657   /**
658    * A mouse drag may be either an adjustment of the panel height (if flag
659    * resizePanel is set on), or a reordering of the annotation rows. The former
660    * is dealt with by this method, the latter in mouseReleased.
661    * 
662    * @param evt
663    */
664   @Override
665   public void mouseDragged(MouseEvent evt)
666   {
667     dragEvent = evt;
668
669     if (resizePanel)
670     {
671       Dimension d = ap.annotationScroller.getPreferredSize();
672       int dif = evt.getY() - oldY;
673
674       dif /= ap.av.getCharHeight();
675       dif *= ap.av.getCharHeight();
676
677       if ((d.height - dif) > 20)
678       {
679         ap.annotationScroller
680                 .setPreferredSize(new Dimension(d.width, d.height - dif));
681         d = ap.annotationSpaceFillerHolder.getPreferredSize();
682         ap.annotationSpaceFillerHolder
683                 .setPreferredSize(new Dimension(d.width, d.height - dif));
684         ap.paintAlignment(true, false);
685       }
686
687       ap.addNotify();
688     }
689     else
690     {
691       repaint();
692     }
693   }
694
695   /**
696    * Updates the tooltip as the mouse moves over the labels
697    * 
698    * @param evt
699    */
700   @Override
701   public void mouseMoved(MouseEvent evt)
702   {
703     showOrHideAdjuster(evt);
704
705     getSelectedRow(evt.getY() - getScrollOffset());
706
707     if (selectedRow > -1 && ap.av.getAlignment()
708             .getAlignmentAnnotation().length > selectedRow)
709     {
710       AlignmentAnnotation aa = ap.av.getAlignment()
711               .getAlignmentAnnotation()[selectedRow];
712
713       StringBuffer desc = new StringBuffer();
714       if (aa.description != null
715               && !aa.description.equals("New description"))
716       {
717         // TODO: we could refactor and merge this code with the code in
718         // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
719         // tooltips
720         desc.append(aa.getDescription(true).trim());
721         // check to see if the description is an html fragment.
722         if (desc.length() < 6 || (desc.substring(0, 6).toLowerCase()
723                 .indexOf("<html>") < 0))
724         {
725           // clean the description ready for embedding in html
726           desc = new StringBuffer(LEFT_ANGLE_BRACKET_PATTERN.matcher(desc)
727                   .replaceAll("&lt;"));
728           desc.insert(0, "<html>");
729         }
730         else
731         {
732           // remove terminating html if any
733           int i = desc.substring(desc.length() - 7).toLowerCase()
734                   .lastIndexOf("</html>");
735           if (i > -1)
736           {
737             desc.setLength(desc.length() - 7 + i);
738           }
739         }
740         if (aa.hasScore())
741         {
742           desc.append("<br/>");
743         }
744         // if (aa.hasProperties())
745         // {
746         // desc.append("<table>");
747         // for (String prop : aa.getProperties())
748         // {
749         // desc.append("<tr><td>" + prop + "</td><td>"
750         // + aa.getProperty(prop) + "</td><tr>");
751         // }
752         // desc.append("</table>");
753         // }
754       }
755       else
756       {
757         // begin the tooltip's html fragment
758         desc.append("<html>");
759         if (aa.hasScore())
760         {
761           // TODO: limit precision of score to avoid noise from imprecise
762           // doubles
763           // (64.7 becomes 64.7+/some tiny value).
764           desc.append(" Score: " + aa.score);
765         }
766       }
767       if (desc.length() > 6)
768       {
769         desc.append("</html>");
770         this.setToolTipText(desc.toString());
771       }
772       else
773       {
774         this.setToolTipText(null);
775       }
776     }
777   }
778
779   /**
780    * Shows the height adjuster image if the mouse moves into the top left
781    * region, or hides it if the mouse leaves the regio
782    * 
783    * @param evt
784    */
785   protected void showOrHideAdjuster(MouseEvent evt)
786   {
787     boolean was = resizePanel;
788     resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
789
790     if (resizePanel != was)
791     {
792       setCursor(Cursor.getPredefinedCursor(
793               resizePanel ? Cursor.S_RESIZE_CURSOR
794                       : Cursor.DEFAULT_CURSOR));
795       repaint();
796     }
797   }
798
799   @Override
800   public void mouseClicked(MouseEvent evt)
801   {
802     final AlignmentAnnotation[] aa = ap.av.getAlignment()
803             .getAlignmentAnnotation();
804     if (!evt.isPopupTrigger() && SwingUtilities.isLeftMouseButton(evt))
805     {
806       if (selectedRow > -1 && selectedRow < aa.length)
807       {
808         if (aa[selectedRow].groupRef != null)
809         {
810           if (evt.getClickCount() >= 2)
811           {
812             // todo: make the ap scroll to the selection - not necessary, first
813             // click highlights/scrolls, second selects
814             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
815             // process modifiers
816             SequenceGroup sg = ap.av.getSelectionGroup();
817             if (sg == null || sg == aa[selectedRow].groupRef
818                     || !(Platform.isControlDown(evt) || evt.isShiftDown()))
819             {
820               if (Platform.isControlDown(evt) || evt.isShiftDown())
821               {
822                 // clone a new selection group from the associated group
823                 ap.av.setSelectionGroup(
824                         new SequenceGroup(aa[selectedRow].groupRef));
825               }
826               else
827               {
828                 // set selection to the associated group so it can be edited
829                 ap.av.setSelectionGroup(aa[selectedRow].groupRef);
830               }
831             }
832             else
833             {
834               // modify current selection with associated group
835               int remainToAdd = aa[selectedRow].groupRef.getSize();
836               for (SequenceI sgs : aa[selectedRow].groupRef.getSequences())
837               {
838                 if (jalview.util.Platform.isControlDown(evt))
839                 {
840                   sg.addOrRemove(sgs, --remainToAdd == 0);
841                 }
842                 else
843                 {
844                   // notionally, we should also add intermediate sequences from
845                   // last added sequence ?
846                   sg.addSequence(sgs, --remainToAdd == 0);
847                 }
848               }
849             }
850
851             ap.paintAlignment(false, false);
852             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
853             ap.av.sendSelection();
854           }
855           else
856           {
857             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(
858                     aa[selectedRow].groupRef.getSequences(null));
859           }
860           return;
861         }
862         else if (aa[selectedRow].sequenceRef != null)
863         {
864           if (evt.getClickCount() == 1)
865           {
866             ap.getSeqPanel().ap.getIdPanel()
867                     .highlightSearchResults(Arrays.asList(new SequenceI[]
868                     { aa[selectedRow].sequenceRef }));
869           }
870           else if (evt.getClickCount() >= 2)
871           {
872             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
873             SequenceGroup sg = ap.av.getSelectionGroup();
874             if (sg != null)
875             {
876               // we make a copy rather than edit the current selection if no
877               // modifiers pressed
878               // see Enhancement JAL-1557
879               if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
880               {
881                 sg = new SequenceGroup(sg);
882                 sg.clear();
883                 sg.addSequence(aa[selectedRow].sequenceRef, false);
884               }
885               else
886               {
887                 if (Platform.isControlDown(evt))
888                 {
889                   sg.addOrRemove(aa[selectedRow].sequenceRef, true);
890                 }
891                 else
892                 {
893                   // notionally, we should also add intermediate sequences from
894                   // last added sequence ?
895                   sg.addSequence(aa[selectedRow].sequenceRef, true);
896                 }
897               }
898             }
899             else
900             {
901               sg = new SequenceGroup();
902               sg.setStartRes(0);
903               sg.setEndRes(ap.av.getAlignment().getWidth() - 1);
904               sg.addSequence(aa[selectedRow].sequenceRef, false);
905             }
906             ap.av.setSelectionGroup(sg);
907             ap.paintAlignment(false, false);
908             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
909             ap.av.sendSelection();
910           }
911
912         }
913       }
914       return;
915     }
916   }
917
918   /**
919    * do a single sequence copy to jalview and the system clipboard
920    * 
921    * @param sq
922    *          sequence to be copied to clipboard
923    */
924   protected void copy_annotseqtoclipboard(SequenceI sq)
925   {
926     SequenceI[] seqs = new SequenceI[] { sq };
927     String[] omitHidden = null;
928     SequenceI[] dseqs = new SequenceI[] { sq.getDatasetSequence() };
929     if (dseqs[0] == null)
930     {
931       dseqs[0] = new Sequence(sq);
932       dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
933               sq.getSequenceAsString()));
934
935       sq.setDatasetSequence(dseqs[0]);
936     }
937     Alignment ds = new Alignment(dseqs);
938     if (av.hasHiddenColumns())
939     {
940       Iterator<int[]> it = av.getAlignment().getHiddenColumns()
941               .getVisContigsIterator(0, sq.getLength(), false);
942       omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
943     }
944
945     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
946     if (av.hasHiddenColumns())
947     {
948       alignmentStartEnd = av.getAlignment().getHiddenColumns()
949               .getVisibleStartAndEndIndex(av.getAlignment().getWidth());
950     }
951
952     String output = new FormatAdapter().formatSequences(FileFormat.Fasta,
953             seqs, omitHidden, alignmentStartEnd);
954
955     Toolkit.getDefaultToolkit().getSystemClipboard()
956             .setContents(new StringSelection(output), Desktop.instance);
957
958     HiddenColumns hiddenColumns = null;
959
960     if (av.hasHiddenColumns())
961     {
962       hiddenColumns = new HiddenColumns(
963               av.getAlignment().getHiddenColumns());
964     }
965
966     Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
967                                                         // of a consensus
968                                                         // sequence ? need to
969                                                         // flag
970         // sequence as special.
971         hiddenColumns };
972   }
973
974   /**
975    * DOCUMENT ME!
976    * 
977    * @param g1
978    *          DOCUMENT ME!
979    */
980   @Override
981   public void paintComponent(Graphics g)
982   {
983
984     int width = getWidth();
985     if (width == 0)
986     {
987       width = ap.calculateIdWidth().width + 4;
988     }
989
990     Graphics2D g2 = (Graphics2D) g;
991     if (av.antiAlias)
992     {
993       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
994               RenderingHints.VALUE_ANTIALIAS_ON);
995     }
996
997     drawComponent(g2, true, width);
998
999   }
1000
1001   /**
1002    * Draw the full set of annotation Labels for the alignment at the given
1003    * cursor
1004    * 
1005    * @param g
1006    *          Graphics2D instance (needed for font scaling)
1007    * @param width
1008    *          Width for scaling labels
1009    * 
1010    */
1011   public void drawComponent(Graphics g, int width)
1012   {
1013     drawComponent(g, false, width);
1014   }
1015
1016   /**
1017    * Draw the full set of annotation Labels for the alignment at the given
1018    * cursor
1019    * 
1020    * @param g
1021    *          Graphics2D instance (needed for font scaling)
1022    * @param clip
1023    *          - true indicates that only current visible area needs to be
1024    *          rendered
1025    * @param width
1026    *          Width for scaling labels
1027    */
1028   public void drawComponent(Graphics g, boolean clip, int width)
1029   {
1030     if (av.getFont().getSize() < 10)
1031     {
1032       g.setFont(font);
1033     }
1034     else
1035     {
1036       g.setFont(av.getFont());
1037     }
1038
1039     FontMetrics fm = g.getFontMetrics(g.getFont());
1040     g.setColor(Color.white);
1041     g.fillRect(0, 0, getWidth(), getHeight());
1042
1043     g.translate(0, getScrollOffset());
1044     g.setColor(Color.black);
1045
1046     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1047     int fontHeight = g.getFont().getSize();
1048     int y = 0;
1049     int x = 0;
1050     int graphExtras = 0;
1051     int offset = 0;
1052     Font baseFont = g.getFont();
1053     FontMetrics baseMetrics = fm;
1054     int ofontH = fontHeight;
1055     int sOffset = 0;
1056     int visHeight = 0;
1057     int[] visr = (ap != null && ap.getAnnotationPanel() != null)
1058             ? ap.getAnnotationPanel().getVisibleVRange()
1059             : null;
1060     if (clip && visr != null)
1061     {
1062       sOffset = visr[0];
1063       visHeight = visr[1];
1064     }
1065     boolean visible = true, before = false, after = false;
1066     if (aa != null)
1067     {
1068       hasHiddenRows = false;
1069       int olY = 0;
1070       for (int i = 0; i < aa.length; i++)
1071       {
1072         visible = true;
1073         if (!aa[i].visible)
1074         {
1075           hasHiddenRows = true;
1076           continue;
1077         }
1078         olY = y;
1079         y += aa[i].height;
1080         if (clip)
1081         {
1082           if (y < sOffset)
1083           {
1084             if (!before)
1085             {
1086               if (debugRedraw)
1087               {
1088                 System.out.println("before vis: " + i);
1089               }
1090               before = true;
1091             }
1092             // don't draw what isn't visible
1093             continue;
1094           }
1095           if (olY > visHeight)
1096           {
1097
1098             if (!after)
1099             {
1100               if (debugRedraw)
1101               {
1102                 System.out.println(
1103                         "Scroll offset: " + sOffset + " after vis: " + i);
1104               }
1105               after = true;
1106             }
1107             // don't draw what isn't visible
1108             continue;
1109           }
1110         }
1111         g.setColor(Color.black);
1112
1113         offset = -aa[i].height / 2;
1114
1115         if (aa[i].hasText)
1116         {
1117           offset += fm.getHeight() / 2;
1118           offset -= fm.getDescent();
1119         }
1120         else
1121         {
1122           offset += fm.getDescent();
1123         }
1124
1125         x = width - fm.stringWidth(aa[i].label) - 3;
1126
1127         if (aa[i].graphGroup > -1)
1128         {
1129           int groupSize = 0;
1130           // TODO: JAL-1291 revise rendering model so the graphGroup map is
1131           // computed efficiently for all visible labels
1132           for (int gg = 0; gg < aa.length; gg++)
1133           {
1134             if (aa[gg].graphGroup == aa[i].graphGroup)
1135             {
1136               groupSize++;
1137             }
1138           }
1139           if (groupSize * (fontHeight + 8) < aa[i].height)
1140           {
1141             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1142                     / 2;
1143           }
1144           else
1145           {
1146             // scale font to fit
1147             float h = aa[i].height / (float) groupSize, s;
1148             if (h < 9)
1149             {
1150               visible = false;
1151             }
1152             else
1153             {
1154               fontHeight = -8 + (int) h;
1155               s = ((float) fontHeight) / (float) ofontH;
1156               Font f = baseFont
1157                       .deriveFont(AffineTransform.getScaleInstance(s, s));
1158               g.setFont(f);
1159               fm = g.getFontMetrics();
1160               graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1161                       / 2;
1162             }
1163           }
1164           if (visible)
1165           {
1166             for (int gg = 0; gg < aa.length; gg++)
1167             {
1168               if (aa[gg].graphGroup == aa[i].graphGroup)
1169               {
1170                 x = width - fm.stringWidth(aa[gg].label) - 3;
1171                 g.drawString(aa[gg].label, x, y - graphExtras);
1172
1173                 if (aa[gg]._linecolour != null)
1174                 {
1175
1176                   g.setColor(aa[gg]._linecolour);
1177                   g.drawLine(x, y - graphExtras + 3,
1178                           x + fm.stringWidth(aa[gg].label),
1179                           y - graphExtras + 3);
1180                 }
1181
1182                 g.setColor(Color.black);
1183                 graphExtras += fontHeight + 8;
1184               }
1185             }
1186           }
1187           g.setFont(baseFont);
1188           fm = baseMetrics;
1189           fontHeight = ofontH;
1190         }
1191         else
1192         {
1193           g.drawString(aa[i].label, x, y + offset);
1194         }
1195       }
1196     }
1197
1198     if (!resizePanel && dragEvent != null && aa != null)
1199     {
1200       g.setColor(Color.lightGray);
1201       g.drawString(aa[selectedRow].label, dragEvent.getX(),
1202               dragEvent.getY() - getScrollOffset());
1203     }
1204
1205     if (!av.getWrapAlignment() && ((aa == null) || (aa.length < 1)))
1206     {
1207       g.drawString(MessageManager.getString("label.right_click"), 2, 8);
1208       g.drawString(MessageManager.getString("label.to_add_annotation"), 2,
1209               18);
1210     }
1211   }
1212
1213   public int getScrollOffset()
1214   {
1215     return scrollOffset;
1216   }
1217
1218   @Override
1219   public void mouseEntered(MouseEvent e)
1220   {
1221   }
1222 }