update author list in license for (JAL-826)
[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.alignment.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.alignment.getAlignmentAnnotation();
179
180     if (evt.getActionCommand().equals(ADDNEW))
181     {
182       AlignmentAnnotation newAnnotation = new AlignmentAnnotation(null,
183               null, new Annotation[ap.av.alignment.getWidth()]);
184
185       if (!editLabelDescription(newAnnotation))
186       {
187         return;
188       }
189
190       ap.av.alignment.addAnnotation(newAnnotation);
191       ap.av.alignment.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.alignment.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.alignment
309               .getAlignmentAnnotation()[start];
310       if (end == -1)
311       {
312         end = ap.av.alignment.getAlignmentAnnotation().length - 1;
313       }
314       AlignmentAnnotation endAA = ap.av.alignment.getAlignmentAnnotation()[end];
315
316       ap.av.alignment.getAlignmentAnnotation()[end] = startAA;
317       ap.av.alignment.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.alignment.getAlignmentAnnotation().length > selectedRow)
406     {
407       AlignmentAnnotation aa = ap.av.alignment.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.alignment.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         }
634         else
635         {
636           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
637                   "Show Histogram", av.isShowConsensusHistogram());
638           chist.addActionListener(new ActionListener()
639           {
640             public void actionPerformed(ActionEvent e)
641             {
642               // TODO: pass on reference
643               // to ap
644               // so the
645               // view
646               // can be
647               // updated.
648               av.setShowConsensusHistogram(chist.getState());
649               ap.repaint();
650               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
651             }
652           });
653           pop.add(chist);
654           final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
655                   "Show Logo", av.isShowSequenceLogo());
656           cprof.addActionListener(new ActionListener()
657           {
658             public void actionPerformed(ActionEvent e)
659             {
660               // TODO: pass on reference
661               // to ap
662               // so the
663               // view
664               // can be
665               // updated.
666               av.setShowSequenceLogo(cprof.getState());
667               ap.repaint();
668               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
669             }
670           });
671           pop.add(cprof);
672         }
673         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
674         consclipbrd.addActionListener(this);
675         pop.add(consclipbrd);
676       }
677     }
678     pop.show(this, evt.getX(), evt.getY());
679   }
680
681   /**
682    * do a single sequence copy to jalview and the system clipboard
683    * 
684    * @param sq
685    *          sequence to be copied to clipboard
686    */
687   protected void copy_annotseqtoclipboard(SequenceI sq)
688   {
689     SequenceI[] seqs = new SequenceI[]
690     { sq };
691     String[] omitHidden = null;
692     SequenceI[] dseqs = new SequenceI[]
693     { sq.getDatasetSequence() };
694     if (dseqs[0] == null)
695     {
696       dseqs[0] = new Sequence(sq);
697       dseqs[0].setSequence(jalview.analysis.AlignSeq.extractGaps(
698               jalview.util.Comparison.GapChars, sq.getSequenceAsString()));
699
700       sq.setDatasetSequence(dseqs[0]);
701     }
702     Alignment ds = new Alignment(dseqs);
703     if (av.hasHiddenColumns)
704     {
705       omitHidden = av.getColumnSelection().getVisibleSequenceStrings(0,
706               sq.getLength(), seqs);
707     }
708
709     String output = new FormatAdapter().formatSequences("Fasta", seqs,
710             omitHidden);
711
712     Toolkit.getDefaultToolkit().getSystemClipboard()
713             .setContents(new StringSelection(output), Desktop.instance);
714
715     Vector hiddenColumns = null;
716     if (av.hasHiddenColumns)
717     {
718       hiddenColumns = new Vector();
719       for (int i = 0; i < av.getColumnSelection().getHiddenColumns().size(); i++)
720       {
721         int[] region = (int[]) av.getColumnSelection().getHiddenColumns()
722                 .elementAt(i);
723
724         hiddenColumns.addElement(new int[]
725         { region[0], region[1] });
726       }
727     }
728
729     Desktop.jalviewClipboard = new Object[]
730     { seqs, ds, // what is the dataset of a consensus sequence ? need to flag
731         // sequence as special.
732         hiddenColumns };
733   }
734
735   /**
736    * DOCUMENT ME!
737    * 
738    * @param g1
739    *          DOCUMENT ME!
740    */
741   public void paintComponent(Graphics g)
742   {
743
744     int width = getWidth();
745     if (width == 0)
746     {
747       width = ap.calculateIdWidth().width + 4;
748     }
749
750     Graphics2D g2 = (Graphics2D) g;
751     if (av.antiAlias)
752     {
753       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
754               RenderingHints.VALUE_ANTIALIAS_ON);
755     }
756
757     drawComponent(g2, width);
758
759   }
760
761   /**
762    * DOCUMENT ME!
763    * 
764    * @param g
765    *          DOCUMENT ME!
766    */
767   public void drawComponent(Graphics g, int width)
768   {
769     if (av.getFont().getSize() < 10)
770     {
771       g.setFont(font);
772     }
773     else
774     {
775       g.setFont(av.getFont());
776     }
777
778     FontMetrics fm = g.getFontMetrics(g.getFont());
779     g.setColor(Color.white);
780     g.fillRect(0, 0, getWidth(), getHeight());
781
782     g.translate(0, scrollOffset);
783     g.setColor(Color.black);
784
785     AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();
786     int fontHeight = g.getFont().getSize();
787     int y = 0;
788     int x = 0;
789     int graphExtras = 0;
790     int offset = 0;
791
792     if (aa != null)
793     {
794       hasHiddenRows = false;
795       for (int i = 0; i < aa.length; i++)
796       {
797         g.setColor(Color.black);
798
799         if (!aa[i].visible)
800         {
801           hasHiddenRows = true;
802           continue;
803         }
804
805         y += aa[i].height;
806
807         offset = -aa[i].height / 2;
808
809         if (aa[i].hasText)
810         {
811           offset += fm.getHeight() / 2;
812           offset -= fm.getDescent();
813         }
814         else
815           offset += fm.getDescent();
816
817         x = width - fm.stringWidth(aa[i].label) - 3;
818
819         if (aa[i].graphGroup > -1)
820         {
821           int groupSize = 0;
822           for (int gg = 0; gg < aa.length; gg++)
823           {
824             if (aa[gg].graphGroup == aa[i].graphGroup)
825             {
826               groupSize++;
827             }
828           }
829
830           if (groupSize * (fontHeight + 8) < aa[i].height)
831           {
832             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8))) / 2;
833           }
834
835           for (int gg = 0; gg < aa.length; gg++)
836           {
837             if (aa[gg].graphGroup == aa[i].graphGroup)
838             {
839               x = width - fm.stringWidth(aa[gg].label) - 3;
840               g.drawString(aa[gg].label, x, y - graphExtras);
841               if (aa[gg].annotations[0] != null)
842               {
843                 g.setColor(aa[gg].annotations[0].colour);
844               }
845
846               g.drawLine(x, y - graphExtras - 3,
847                       x + fm.stringWidth(aa[gg].label), y - graphExtras - 3);
848
849               g.setColor(Color.black);
850               graphExtras += fontHeight + 8;
851             }
852           }
853         }
854         else
855         {
856           g.drawString(aa[i].label, x, y + offset);
857         }
858       }
859     }
860
861     if (resizePanel)
862     {
863       g.drawImage(image, 2, 0 - scrollOffset, this);
864     }
865     else if (dragEvent != null && aa != null)
866     {
867       g.setColor(Color.lightGray);
868       g.drawString(aa[selectedRow].label, dragEvent.getX(),
869               dragEvent.getY() - scrollOffset);
870     }
871
872     if ((aa == null) || (aa.length < 1))
873     {
874       g.drawString("Right click", 2, 8);
875       g.drawString("to add annotation", 2, 18);
876     }
877   }
878 }