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