6d32c043aabdaa95d4008530258ce98732840185
[jalview.git] / src / jalview / gui / AnnotationLabels.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.gui;
19
20 import java.util.*;
21 import java.util.regex.Pattern;
22
23 import java.awt.*;
24 import java.awt.datatransfer.*;
25 import java.awt.event.*;
26 import java.awt.image.*;
27 import javax.swing.*;
28
29 import jalview.datamodel.*;
30 import jalview.io.*;
31
32 /**
33  * DOCUMENT ME!
34  * 
35  * @author $author$
36  * @version $Revision$
37  */
38 public class AnnotationLabels extends JPanel implements MouseListener,
39         MouseMotionListener, ActionListener
40 {
41   static String TOGGLE_LABELSCALE = "Scale Label to Column";
42
43   static String ADDNEW = "Add New Row";
44
45   static String EDITNAME = "Edit Label/Description";
46
47   static String HIDE = "Hide This Row";
48
49   static String DELETE = "Delete This Row";
50
51   static String SHOWALL = "Show All Hidden Rows";
52
53   static String OUTPUT_TEXT = "Export Annotation";
54
55   static String COPYCONS_SEQ = "Copy Consensus Sequence";
56
57   boolean resizePanel = false;
58
59   Image image;
60
61   AlignmentPanel ap;
62
63   AlignViewport av;
64
65   boolean resizing = false;
66
67   MouseEvent dragEvent;
68
69   int oldY;
70
71   int selectedRow;
72
73   int scrollOffset = 0;
74
75   Font font = new Font("Arial", Font.PLAIN, 11);
76
77   private boolean hasHiddenRows;
78
79   /**
80    * Creates a new AnnotationLabels object.
81    * 
82    * @param ap
83    *          DOCUMENT ME!
84    */
85   public AnnotationLabels(AlignmentPanel ap)
86   {
87     this.ap = ap;
88     av = ap.av;
89     ToolTipManager.sharedInstance().registerComponent(this);
90
91     java.net.URL url = getClass().getResource("/images/idwidth.gif");
92     Image temp = null;
93
94     if (url != null)
95     {
96       temp = java.awt.Toolkit.getDefaultToolkit().createImage(url);
97     }
98
99     try
100     {
101       MediaTracker mt = new MediaTracker(this);
102       mt.addImage(temp, 0);
103       mt.waitForID(0);
104     } catch (Exception ex)
105     {
106     }
107
108     BufferedImage bi = new BufferedImage(temp.getHeight(this),
109             temp.getWidth(this), BufferedImage.TYPE_INT_RGB);
110     Graphics2D g = (Graphics2D) bi.getGraphics();
111     g.rotate(Math.toRadians(90));
112     g.drawImage(temp, 0, -bi.getWidth(this), this);
113     image = (Image) bi;
114
115     addMouseListener(this);
116     addMouseMotionListener(this);
117   }
118
119   public AnnotationLabels(AlignViewport av)
120   {
121     this.av = av;
122   }
123
124   /**
125    * DOCUMENT ME!
126    * 
127    * @param y
128    *          DOCUMENT ME!
129    */
130   public void setScrollOffset(int y)
131   {
132     scrollOffset = y;
133     repaint();
134   }
135
136   /**
137    * sets selectedRow to -2 if no annotation preset, -1 if no visible row is at
138    * y
139    * 
140    * @param y
141    *          coordinate position to search for a row
142    */
143   void getSelectedRow(int y)
144   {
145     int height = 0;
146     AlignmentAnnotation[] aa = ap.av.getAlignment().getAlignmentAnnotation();
147     selectedRow = -2;
148     if (aa != null)
149     {
150       for (int i = 0; i < aa.length; i++)
151       {
152         selectedRow = -1;
153         if (!aa[i].visible)
154         {
155           continue;
156         }
157
158         height += aa[i].height;
159
160         if (y < height)
161         {
162           selectedRow = i;
163
164           break;
165         }
166       }
167     }
168   }
169
170   /**
171    * DOCUMENT ME!
172    * 
173    * @param evt
174    *          DOCUMENT ME!
175    */
176   public void actionPerformed(ActionEvent evt)
177   {
178     AlignmentAnnotation[] aa = ap.av.getAlignment().getAlignmentAnnotation();
179
180     if (evt.getActionCommand().equals(ADDNEW))
181     {
182       AlignmentAnnotation newAnnotation = new AlignmentAnnotation(null,
183               null, new Annotation[ap.av.getAlignment().getWidth()]);
184
185       if (!editLabelDescription(newAnnotation))
186       {
187         return;
188       }
189
190       ap.av.getAlignment().addAnnotation(newAnnotation);
191       ap.av.getAlignment().setAnnotationIndex(newAnnotation, 0);
192     }
193     else if (evt.getActionCommand().equals(EDITNAME))
194     {
195       editLabelDescription(aa[selectedRow]);
196       repaint();
197     }
198     else if (evt.getActionCommand().equals(HIDE))
199     {
200       aa[selectedRow].visible = false;
201     }
202     else if (evt.getActionCommand().equals(DELETE))
203     {
204       ap.av.getAlignment().deleteAnnotation(aa[selectedRow]);
205     }
206     else if (evt.getActionCommand().equals(SHOWALL))
207     {
208       for (int i = 0; i < aa.length; i++)
209       {
210         if (!aa[i].visible && aa[i].annotations != null)
211         {
212           aa[i].visible = true;
213         }
214       }
215     }
216     else if (evt.getActionCommand().equals(OUTPUT_TEXT))
217     {
218       new AnnotationExporter().exportAnnotations(ap,
219               new AlignmentAnnotation[]
220               { aa[selectedRow] }, null, null);
221     }
222     else if (evt.getActionCommand().equals(COPYCONS_SEQ))
223     {
224       SequenceI cons = null;
225       if (aa[selectedRow].groupRef != null)
226       {
227         cons = aa[selectedRow].groupRef.getConsensusSeq();
228       }
229       else
230       {
231         cons = av.getConsensusSeq();
232       }
233       if (cons != null)
234       {
235         copy_annotseqtoclipboard(cons);
236       }
237
238     }
239     else if (evt.getActionCommand().equals(TOGGLE_LABELSCALE))
240     {
241       aa[selectedRow].scaleColLabel = !aa[selectedRow].scaleColLabel;
242     }
243
244     ap.validateAnnotationDimensions(false);
245     ap.addNotify();
246     ap.repaint();
247     //validate();
248     //ap.paintAlignment(true);
249   }
250
251   /**
252    * DOCUMENT ME!
253    * 
254    * @param e
255    *          DOCUMENT ME!
256    */
257   boolean editLabelDescription(AlignmentAnnotation annotation)
258   {
259     EditNameDialog dialog = new EditNameDialog(annotation.label,
260             annotation.description, "       Annotation Name ",
261             "Annotation Description ", "Edit Annotation Name/Description",
262             ap.alignFrame);
263
264     if (!dialog.accept)
265     {
266       return false;
267     }
268
269     annotation.label = dialog.getName();
270
271     String text = dialog.getDescription();
272     if (text != null && text.length() == 0)
273     {
274       text = null;
275     }
276     annotation.description = text;
277
278     return true;
279   }
280
281   /**
282    * DOCUMENT ME!
283    * 
284    * @param evt
285    *          DOCUMENT ME!
286    */
287   public void mousePressed(MouseEvent evt)
288   {
289     getSelectedRow(evt.getY() - scrollOffset);
290     oldY = evt.getY();
291   }
292
293   /**
294    * DOCUMENT ME!
295    * 
296    * @param evt
297    *          DOCUMENT ME!
298    */
299   public void mouseReleased(MouseEvent evt)
300   {
301     int start = selectedRow;
302     getSelectedRow(evt.getY() - scrollOffset);
303     int end = selectedRow;
304
305     if (start != end)
306     {
307       // Swap these annotations
308       AlignmentAnnotation startAA = ap.av.getAlignment()
309               .getAlignmentAnnotation()[start];
310       if (end == -1)
311       {
312         end = ap.av.getAlignment().getAlignmentAnnotation().length - 1;
313       }
314       AlignmentAnnotation endAA = ap.av.getAlignment().getAlignmentAnnotation()[end];
315
316       ap.av.getAlignment().getAlignmentAnnotation()[end] = startAA;
317       ap.av.getAlignment().getAlignmentAnnotation()[start] = endAA;
318     }
319
320     resizePanel = false;
321     dragEvent = null;
322     repaint();
323     ap.annotationPanel.repaint();
324   }
325
326   /**
327    * DOCUMENT ME!
328    * 
329    * @param evt
330    *          DOCUMENT ME!
331    */
332   public void mouseEntered(MouseEvent evt)
333   {
334     if (evt.getY() < 10)
335     {
336       resizePanel = true;
337       repaint();
338     }
339   }
340
341   /**
342    * DOCUMENT ME!
343    * 
344    * @param evt
345    *          DOCUMENT ME!
346    */
347   public void mouseExited(MouseEvent evt)
348   {
349     if (dragEvent == null)
350     {
351       resizePanel = false;
352       repaint();
353     }
354   }
355
356   /**
357    * DOCUMENT ME!
358    * 
359    * @param evt
360    *          DOCUMENT ME!
361    */
362   public void mouseDragged(MouseEvent evt)
363   {
364     dragEvent = evt;
365
366     if (resizePanel)
367     {
368       Dimension d = ap.annotationScroller.getPreferredSize();
369       int dif = evt.getY() - oldY;
370
371       dif /= ap.av.charHeight;
372       dif *= ap.av.charHeight;
373
374       if ((d.height - dif) > 20)
375       {
376         ap.annotationScroller.setPreferredSize(new Dimension(d.width,
377                 d.height - dif));
378         d = ap.annotationSpaceFillerHolder.getPreferredSize();
379         ap.annotationSpaceFillerHolder.setPreferredSize(new Dimension(
380                 d.width, d.height - dif));
381         ap.paintAlignment(true);
382       }
383
384       ap.addNotify();
385     }
386     else
387     {
388       repaint();
389     }
390   }
391
392   /**
393    * DOCUMENT ME!
394    * 
395    * @param evt
396    *          DOCUMENT ME!
397    */
398   public void mouseMoved(MouseEvent evt)
399   {
400     resizePanel = evt.getY() < 10;
401
402     getSelectedRow(evt.getY() - scrollOffset);
403
404     if (selectedRow > -1
405             && ap.av.getAlignment().getAlignmentAnnotation().length > selectedRow)
406     {
407       AlignmentAnnotation aa = ap.av.getAlignment().getAlignmentAnnotation()[selectedRow];
408       
409       StringBuffer desc = new StringBuffer();
410       if (aa.description != null
411               && !aa.description.equals("New description"))
412       {
413         // TODO: we could refactor and merge this code with the code in jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature tooltips
414         desc.append(aa.getDescription(true).trim());
415         // check to see if the description is an html fragment.
416         if (desc.length()<6 || (desc.substring(0,6).toLowerCase().indexOf("<html>")<0))
417         {
418           // clean the description ready for embedding in html
419           desc = new StringBuffer(Pattern.compile("<").matcher(desc).replaceAll("&lt;"));        
420           desc.insert(0, "<html>");
421         } else {
422                 // remove terminating html if any
423                 int i=desc.substring(desc.length()-7).toLowerCase().lastIndexOf("</html>");
424                 if (i>-1) {
425                   desc.setLength(desc.length()-7+i);
426                 }
427         }
428         if (aa.hasScore())
429         {
430           desc.append("<br/>");
431         }
432         
433         
434       } else {
435         // begin the tooltip's html fragment
436         desc.append("<html>");
437       }
438       if (aa.hasScore())
439       {
440         // TODO: limit precision of score to avoid noise from imprecise doubles (64.7 becomes 64.7+/some tiny value).
441         desc.append(" Score: " + aa.score);
442       }
443
444       if (desc.length() > 6)
445       {
446         desc.append("</html>");
447         this.setToolTipText(desc.toString());
448       }
449       else
450         this.setToolTipText(null);
451     }
452
453   }
454
455   /**
456    * DOCUMENT ME!
457    * 
458    * @param evt
459    *          DOCUMENT ME!
460    */
461   public void mouseClicked(MouseEvent evt)
462   {
463     AlignmentAnnotation[] aa = ap.av.getAlignment().getAlignmentAnnotation();
464     if (SwingUtilities.isLeftMouseButton(evt))
465     {
466       if (selectedRow > -1 && selectedRow < aa.length)
467       {
468         if (aa[selectedRow].groupRef != null)
469         {
470           if (evt.getClickCount() >= 2)
471           {
472             // todo: make the ap scroll to the selection - not necessary, first click highlights/scrolls, second selects
473             ap.seqPanel.ap.idPanel.highlightSearchResults(null);
474             ap.av.setSelectionGroup(// new SequenceGroup(
475             aa[selectedRow].groupRef); // );
476             ap.paintAlignment(false);
477             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
478             ap.av.sendSelection();
479           }
480           else
481           {
482             ap.seqPanel.ap.idPanel
483                     .highlightSearchResults(aa[selectedRow].groupRef
484                             .getSequences(null));
485           }
486           return;
487         }
488         else if (aa[selectedRow].sequenceRef != null)
489         {
490           Vector sr = new Vector();
491           sr.addElement(aa[selectedRow].sequenceRef);
492           if (evt.getClickCount() == 1)
493           {
494             ap.seqPanel.ap.idPanel.highlightSearchResults(sr);
495           }
496           else if (evt.getClickCount() >= 2)
497           {
498             ap.seqPanel.ap.idPanel.highlightSearchResults(null);
499             SequenceGroup sg = new SequenceGroup();
500             sg.addSequence(aa[selectedRow].sequenceRef, false);
501             ap.av.setSelectionGroup(sg);
502             ap.av.sendSelection();
503             ap.paintAlignment(false);
504             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
505           }
506
507         }
508       }
509     }
510     if (!SwingUtilities.isRightMouseButton(evt))
511     {
512       return;
513     }
514
515     JPopupMenu pop = new JPopupMenu("Annotations");
516     JMenuItem item = new JMenuItem(ADDNEW);
517     item.addActionListener(this);
518     pop.add(item);
519     if (selectedRow < 0)
520     {
521       if (hasHiddenRows)
522       { // let the user make everything visible again
523         item = new JMenuItem(SHOWALL);
524         item.addActionListener(this);
525         pop.add(item);
526       }
527       pop.show(this, evt.getX(), evt.getY());
528       return;
529     }
530     item = new JMenuItem(EDITNAME);
531     item.addActionListener(this);
532     pop.add(item);
533     item = new JMenuItem(HIDE);
534     item.addActionListener(this);
535     pop.add(item);
536     item = new JMenuItem(DELETE);
537     item.addActionListener(this);
538     pop.add(item);
539     if (hasHiddenRows)
540     {
541       item = new JMenuItem(SHOWALL);
542       item.addActionListener(this);
543       pop.add(item);
544     }
545     item = new JMenuItem(OUTPUT_TEXT);
546     item.addActionListener(this);
547     pop.add(item);
548     // TODO: annotation object should be typed for autocalculated/derived
549     // property methods
550     if (selectedRow < aa.length)
551     {
552       if (!aa[selectedRow].autoCalculated)
553       {
554         if (aa[selectedRow].graph == AlignmentAnnotation.NO_GRAPH)
555         {
556           // display formatting settings for this row.
557           pop.addSeparator();
558           // av and sequencegroup need to implement same interface for
559           item = new JCheckBoxMenuItem(TOGGLE_LABELSCALE,
560                   aa[selectedRow].scaleColLabel);
561           item.addActionListener(this);
562           pop.add(item);
563         }
564       }
565       else if (aa[selectedRow].label.indexOf("Consensus") > -1)
566       {
567         pop.addSeparator();
568         // av and sequencegroup need to implement same interface for
569         final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
570                 "Ignore Gaps In Consensus",
571                 (aa[selectedRow].groupRef != null) ? aa[selectedRow].groupRef
572                         .getIgnoreGapsConsensus() : ap.av
573                         .getIgnoreGapsConsensus());
574         final AlignmentAnnotation aaa = aa[selectedRow];
575         cbmi.addActionListener(new ActionListener()
576         {
577           public void actionPerformed(ActionEvent e)
578           {
579             if (aaa.groupRef != null)
580             {
581               // TODO: pass on reference to ap so the view can be updated.
582               aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
583               ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
584             }
585             else
586             {
587               ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
588             }
589           }
590         });
591         pop.add(cbmi);
592         // av and sequencegroup need to implement same interface for
593         if (aaa.groupRef != null)
594         {
595           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
596                   "Show Group Histogram",
597                   aa[selectedRow].groupRef.isShowConsensusHistogram());
598           chist.addActionListener(new ActionListener()
599           {
600             public void actionPerformed(ActionEvent e)
601             {
602               // TODO: pass on reference
603               // to ap
604               // so the
605               // view
606               // can be
607               // updated.
608               aaa.groupRef.setShowConsensusHistogram(chist.getState());
609               ap.repaint();
610               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
611             }
612           });
613           pop.add(chist);
614           final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
615                   "Show Group Logo",
616                   aa[selectedRow].groupRef.isShowSequenceLogo());
617           cprofl.addActionListener(new ActionListener()
618           {
619             public void actionPerformed(ActionEvent e)
620             {
621               // TODO: pass on reference
622               // to ap
623               // so the
624               // view
625               // can be
626               // updated.
627               aaa.groupRef.setshowSequenceLogo(cprofl.getState());
628               ap.repaint();
629               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
630             }
631           });
632           pop.add(cprofl);
633           final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
634                   "Normalise Group Logo",
635                   aa[selectedRow].groupRef.isNormaliseSequenceLogo());
636           cproflnorm.addActionListener(new ActionListener()
637           {
638             public void actionPerformed(ActionEvent e)
639             {
640               
641               // TODO: pass on reference
642               // to ap
643               // so the
644               // view
645               // can be
646               // updated.
647               aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
648               // automatically enable logo display if we're clicked
649               aaa.groupRef.setshowSequenceLogo(true);
650               ap.repaint();
651               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
652             }
653           });
654           pop.add(cproflnorm);
655         }
656         else
657         {
658           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
659                   "Show Histogram", av.isShowConsensusHistogram());
660           chist.addActionListener(new ActionListener()
661           {
662             public void actionPerformed(ActionEvent e)
663             {
664               // TODO: pass on reference
665               // to ap
666               // so the
667               // view
668               // can be
669               // updated.
670               av.setShowConsensusHistogram(chist.getState());
671               ap.alignFrame.setMenusForViewport();
672               ap.repaint();
673               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
674             }
675           });
676           pop.add(chist);
677           final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
678                   "Show Logo", av.isShowSequenceLogo());
679           cprof.addActionListener(new ActionListener()
680           {
681             public void actionPerformed(ActionEvent e)
682             {
683               // TODO: pass on reference
684               // to ap
685               // so the
686               // view
687               // can be
688               // updated.
689               av.setShowSequenceLogo(cprof.getState());
690               ap.alignFrame.setMenusForViewport();
691               ap.repaint();
692               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
693             }
694           });
695           pop.add(cprof);
696           final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
697                   "Normalise Logo", av.isNormaliseSequenceLogo());
698           cprofnorm.addActionListener(new ActionListener()
699           {
700             public void actionPerformed(ActionEvent e)
701             {
702               // TODO: pass on reference
703               // to ap
704               // so the
705               // view
706               // can be
707               // updated.
708               av.setShowSequenceLogo(true);
709               av.setNormaliseSequenceLogo(cprofnorm.getState());
710               ap.alignFrame.setMenusForViewport();
711               ap.repaint();
712               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
713             }
714           });
715           pop.add(cprofnorm);
716         }
717         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
718         consclipbrd.addActionListener(this);
719         pop.add(consclipbrd);
720       }
721     }
722     pop.show(this, evt.getX(), evt.getY());
723   }
724
725   /**
726    * do a single sequence copy to jalview and the system clipboard
727    * 
728    * @param sq
729    *          sequence to be copied to clipboard
730    */
731   protected void copy_annotseqtoclipboard(SequenceI sq)
732   {
733     SequenceI[] seqs = new SequenceI[]
734     { sq };
735     String[] omitHidden = null;
736     SequenceI[] dseqs = new SequenceI[]
737     { sq.getDatasetSequence() };
738     if (dseqs[0] == null)
739     {
740       dseqs[0] = new Sequence(sq);
741       dseqs[0].setSequence(jalview.analysis.AlignSeq.extractGaps(
742               jalview.util.Comparison.GapChars, sq.getSequenceAsString()));
743
744       sq.setDatasetSequence(dseqs[0]);
745     }
746     Alignment ds = new Alignment(dseqs);
747     if (av.hasHiddenColumns())
748     {
749       omitHidden = av.getColumnSelection().getVisibleSequenceStrings(0,
750               sq.getLength(), seqs);
751     }
752
753     String output = new FormatAdapter().formatSequences("Fasta", seqs,
754             omitHidden);
755
756     Toolkit.getDefaultToolkit().getSystemClipboard()
757             .setContents(new StringSelection(output), Desktop.instance);
758
759     Vector hiddenColumns = null;
760     if (av.hasHiddenColumns())
761     {
762       hiddenColumns = new Vector();
763       for (int i = 0; i < av.getColumnSelection().getHiddenColumns().size(); i++)
764       {
765         int[] region = (int[]) av.getColumnSelection().getHiddenColumns()
766                 .elementAt(i);
767
768         hiddenColumns.addElement(new int[]
769         { region[0], region[1] });
770       }
771     }
772
773     Desktop.jalviewClipboard = new Object[]
774     { seqs, ds, // what is the dataset of a consensus sequence ? need to flag
775         // sequence as special.
776         hiddenColumns };
777   }
778
779   /**
780    * DOCUMENT ME!
781    * 
782    * @param g1
783    *          DOCUMENT ME!
784    */
785   public void paintComponent(Graphics g)
786   {
787
788     int width = getWidth();
789     if (width == 0)
790     {
791       width = ap.calculateIdWidth().width + 4;
792     }
793
794     Graphics2D g2 = (Graphics2D) g;
795     if (av.antiAlias)
796     {
797       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
798               RenderingHints.VALUE_ANTIALIAS_ON);
799     }
800
801     drawComponent(g2, width);
802
803   }
804
805   /**
806    * DOCUMENT ME!
807    * 
808    * @param g
809    *          DOCUMENT ME!
810    */
811   public void drawComponent(Graphics g, int width)
812   {
813     if (av.getFont().getSize() < 10)
814     {
815       g.setFont(font);
816     }
817     else
818     {
819       g.setFont(av.getFont());
820     }
821
822     FontMetrics fm = g.getFontMetrics(g.getFont());
823     g.setColor(Color.white);
824     g.fillRect(0, 0, getWidth(), getHeight());
825
826     g.translate(0, scrollOffset);
827     g.setColor(Color.black);
828
829     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
830     int fontHeight = g.getFont().getSize();
831     int y = 0;
832     int x = 0;
833     int graphExtras = 0;
834     int offset = 0;
835
836     if (aa != null)
837     {
838       hasHiddenRows = false;
839       for (int i = 0; i < aa.length; i++)
840       {
841         g.setColor(Color.black);
842
843         if (!aa[i].visible)
844         {
845           hasHiddenRows = true;
846           continue;
847         }
848
849         y += aa[i].height;
850
851         offset = -aa[i].height / 2;
852
853         if (aa[i].hasText)
854         {
855           offset += fm.getHeight() / 2;
856           offset -= fm.getDescent();
857         }
858         else
859           offset += fm.getDescent();
860
861         x = width - fm.stringWidth(aa[i].label) - 3;
862
863         if (aa[i].graphGroup > -1)
864         {
865           int groupSize = 0;
866           for (int gg = 0; gg < aa.length; gg++)
867           {
868             if (aa[gg].graphGroup == aa[i].graphGroup)
869             {
870               groupSize++;
871             }
872           }
873
874           if (groupSize * (fontHeight + 8) < aa[i].height)
875           {
876             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8))) / 2;
877           }
878
879           for (int gg = 0; gg < aa.length; gg++)
880           {
881             if (aa[gg].graphGroup == aa[i].graphGroup)
882             {
883               x = width - fm.stringWidth(aa[gg].label) - 3;
884               g.drawString(aa[gg].label, x, y - graphExtras);
885               if (aa[gg].annotations[0] != null)
886               {
887                 g.setColor(aa[gg].annotations[0].colour);
888               }
889
890               g.drawLine(x, y - graphExtras - 3,
891                       x + fm.stringWidth(aa[gg].label), y - graphExtras - 3);
892
893               g.setColor(Color.black);
894               graphExtras += fontHeight + 8;
895             }
896           }
897         }
898         else
899         {
900           g.drawString(aa[i].label, x, y + offset);
901         }
902       }
903     }
904
905     if (resizePanel)
906     {
907       g.drawImage(image, 2, 0 - scrollOffset, this);
908     }
909     else if (dragEvent != null && aa != null)
910     {
911       g.setColor(Color.lightGray);
912       g.drawString(aa[selectedRow].label, dragEvent.getX(),
913               dragEvent.getY() - scrollOffset);
914     }
915
916     if ((aa == null) || (aa.length < 1))
917     {
918       g.drawString("Right click", 2, 8);
919       g.drawString("to add annotation", 2, 18);
920     }
921   }
922 }