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