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