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