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