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