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