72789016f914bd4e720a4f42ad1cb93fc59aaf47
[jalview.git] / src / jalview / gui / AnnotationLabels.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.6)
3  * Copyright (C) 2010 J Procter, AM Waterhouse, 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         System.err.println(desc.toString());
448         this.setToolTipText(desc.toString());
449       }
450       else
451         this.setToolTipText(null);
452     }
453
454   }
455
456   /**
457    * DOCUMENT ME!
458    * 
459    * @param evt
460    *          DOCUMENT ME!
461    */
462   public void mouseClicked(MouseEvent evt)
463   {
464     AlignmentAnnotation[] aa = ap.av.alignment.getAlignmentAnnotation();
465     if (SwingUtilities.isLeftMouseButton(evt))
466     {
467       if (selectedRow > -1 && selectedRow < aa.length)
468       {
469         if (aa[selectedRow].groupRef != null)
470         {
471           if (evt.getClickCount() >= 2)
472           {
473             // todo: make the ap scroll to the selection - not necessary, first click highlights/scrolls, second selects
474             ap.seqPanel.ap.idPanel.highlightSearchResults(null);
475             ap.av.setSelectionGroup(// new SequenceGroup(
476             aa[selectedRow].groupRef); // );
477             ap.paintAlignment(false);
478             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
479             ap.av.sendSelection();
480           }
481           else
482           {
483             ap.seqPanel.ap.idPanel
484                     .highlightSearchResults(aa[selectedRow].groupRef
485                             .getSequences(null));
486           }
487           return;
488         }
489         else if (aa[selectedRow].sequenceRef != null)
490         {
491           Vector sr = new Vector();
492           sr.addElement(aa[selectedRow].sequenceRef);
493           if (evt.getClickCount() == 1)
494           {
495             ap.seqPanel.ap.idPanel.highlightSearchResults(sr);
496           }
497           else if (evt.getClickCount() >= 2)
498           {
499             ap.seqPanel.ap.idPanel.highlightSearchResults(null);
500             SequenceGroup sg = new SequenceGroup();
501             sg.addSequence(aa[selectedRow].sequenceRef, false);
502             ap.av.setSelectionGroup(sg);
503             ap.av.sendSelection();
504             ap.paintAlignment(false);
505             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
506           }
507
508         }
509       }
510     }
511     if (!SwingUtilities.isRightMouseButton(evt))
512     {
513       return;
514     }
515
516     JPopupMenu pop = new JPopupMenu("Annotations");
517     JMenuItem item = new JMenuItem(ADDNEW);
518     item.addActionListener(this);
519     pop.add(item);
520     if (selectedRow < 0)
521     {
522       if (hasHiddenRows)
523       { // let the user make everything visible again
524         item = new JMenuItem(SHOWALL);
525         item.addActionListener(this);
526         pop.add(item);
527       }
528       pop.show(this, evt.getX(), evt.getY());
529       return;
530     }
531     item = new JMenuItem(EDITNAME);
532     item.addActionListener(this);
533     pop.add(item);
534     item = new JMenuItem(HIDE);
535     item.addActionListener(this);
536     pop.add(item);
537     item = new JMenuItem(DELETE);
538     item.addActionListener(this);
539     pop.add(item);
540     if (hasHiddenRows)
541     {
542       item = new JMenuItem(SHOWALL);
543       item.addActionListener(this);
544       pop.add(item);
545     }
546     item = new JMenuItem(OUTPUT_TEXT);
547     item.addActionListener(this);
548     pop.add(item);
549     // TODO: annotation object should be typed for autocalculated/derived
550     // property methods
551     if (selectedRow < aa.length)
552     {
553       if (!aa[selectedRow].autoCalculated)
554       {
555         if (aa[selectedRow].graph == AlignmentAnnotation.NO_GRAPH)
556         {
557           // display formatting settings for this row.
558           pop.addSeparator();
559           // av and sequencegroup need to implement same interface for
560           item = new JCheckBoxMenuItem(TOGGLE_LABELSCALE,
561                   aa[selectedRow].scaleColLabel);
562           item.addActionListener(this);
563           pop.add(item);
564         }
565       }
566       else if (aa[selectedRow].label.indexOf("Consensus") > -1)
567       {
568         pop.addSeparator();
569         // av and sequencegroup need to implement same interface for
570         final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
571                 "Ignore Gaps In Consensus",
572                 (aa[selectedRow].groupRef != null) ? aa[selectedRow].groupRef
573                         .getIgnoreGapsConsensus() : ap.av
574                         .getIgnoreGapsConsensus());
575         final AlignmentAnnotation aaa = aa[selectedRow];
576         cbmi.addActionListener(new ActionListener()
577         {
578           public void actionPerformed(ActionEvent e)
579           {
580             if (aaa.groupRef != null)
581             {
582               // TODO: pass on reference to ap so the view can be updated.
583               aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
584               ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
585             }
586             else
587             {
588               ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
589             }
590           }
591         });
592         pop.add(cbmi);
593         // av and sequencegroup need to implement same interface for
594         if (aaa.groupRef != null)
595         {
596           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
597                   "Show Group Histogram",
598                   aa[selectedRow].groupRef.isShowConsensusHistogram());
599           chist.addActionListener(new ActionListener()
600           {
601             public void actionPerformed(ActionEvent e)
602             {
603               // TODO: pass on reference
604               // to ap
605               // so the
606               // view
607               // can be
608               // updated.
609               aaa.groupRef.setShowConsensusHistogram(chist.getState());
610               ap.repaint();
611               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
612             }
613           });
614           pop.add(chist);
615           final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
616                   "Show Group Logo",
617                   aa[selectedRow].groupRef.isShowSequenceLogo());
618           cprofl.addActionListener(new ActionListener()
619           {
620             public void actionPerformed(ActionEvent e)
621             {
622               // TODO: pass on reference
623               // to ap
624               // so the
625               // view
626               // can be
627               // updated.
628               aaa.groupRef.setshowSequenceLogo(cprofl.getState());
629               ap.repaint();
630               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
631             }
632           });
633           pop.add(cprofl);
634         }
635         else
636         {
637           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
638                   "Show Histogram", av.isShowConsensusHistogram());
639           chist.addActionListener(new ActionListener()
640           {
641             public void actionPerformed(ActionEvent e)
642             {
643               // TODO: pass on reference
644               // to ap
645               // so the
646               // view
647               // can be
648               // updated.
649               av.setShowConsensusHistogram(chist.getState());
650               ap.repaint();
651               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
652             }
653           });
654           pop.add(chist);
655           final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
656                   "Show Logo", av.isShowSequenceLogo());
657           cprof.addActionListener(new ActionListener()
658           {
659             public void actionPerformed(ActionEvent e)
660             {
661               // TODO: pass on reference
662               // to ap
663               // so the
664               // view
665               // can be
666               // updated.
667               av.setShowSequenceLogo(cprof.getState());
668               ap.repaint();
669               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
670             }
671           });
672           pop.add(cprof);
673         }
674         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
675         consclipbrd.addActionListener(this);
676         pop.add(consclipbrd);
677       }
678     }
679     pop.show(this, evt.getX(), evt.getY());
680   }
681
682   /**
683    * do a single sequence copy to jalview and the system clipboard
684    * 
685    * @param sq
686    *          sequence to be copied to clipboard
687    */
688   protected void copy_annotseqtoclipboard(SequenceI sq)
689   {
690     SequenceI[] seqs = new SequenceI[]
691     { sq };
692     String[] omitHidden = null;
693     SequenceI[] dseqs = new SequenceI[]
694     { sq.getDatasetSequence() };
695     if (dseqs[0] == null)
696     {
697       dseqs[0] = new Sequence(sq);
698       dseqs[0].setSequence(jalview.analysis.AlignSeq.extractGaps(
699               jalview.util.Comparison.GapChars, sq.getSequenceAsString()));
700
701       sq.setDatasetSequence(dseqs[0]);
702     }
703     Alignment ds = new Alignment(dseqs);
704     if (av.hasHiddenColumns)
705     {
706       omitHidden = av.getColumnSelection().getVisibleSequenceStrings(0,
707               sq.getLength(), seqs);
708     }
709
710     String output = new FormatAdapter().formatSequences("Fasta", seqs,
711             omitHidden);
712
713     Toolkit.getDefaultToolkit().getSystemClipboard()
714             .setContents(new StringSelection(output), Desktop.instance);
715
716     Vector hiddenColumns = null;
717     if (av.hasHiddenColumns)
718     {
719       hiddenColumns = new Vector();
720       for (int i = 0; i < av.getColumnSelection().getHiddenColumns().size(); i++)
721       {
722         int[] region = (int[]) av.getColumnSelection().getHiddenColumns()
723                 .elementAt(i);
724
725         hiddenColumns.addElement(new int[]
726         { region[0], region[1] });
727       }
728     }
729
730     Desktop.jalviewClipboard = new Object[]
731     { seqs, ds, // what is the dataset of a consensus sequence ? need to flag
732         // sequence as special.
733         hiddenColumns };
734   }
735
736   /**
737    * DOCUMENT ME!
738    * 
739    * @param g1
740    *          DOCUMENT ME!
741    */
742   public void paintComponent(Graphics g)
743   {
744
745     int width = getWidth();
746     if (width == 0)
747     {
748       width = ap.calculateIdWidth().width + 4;
749     }
750
751     Graphics2D g2 = (Graphics2D) g;
752     if (av.antiAlias)
753     {
754       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
755               RenderingHints.VALUE_ANTIALIAS_ON);
756     }
757
758     drawComponent(g2, width);
759
760   }
761
762   /**
763    * DOCUMENT ME!
764    * 
765    * @param g
766    *          DOCUMENT ME!
767    */
768   public void drawComponent(Graphics g, int width)
769   {
770     if (av.getFont().getSize() < 10)
771     {
772       g.setFont(font);
773     }
774     else
775     {
776       g.setFont(av.getFont());
777     }
778
779     FontMetrics fm = g.getFontMetrics(g.getFont());
780     g.setColor(Color.white);
781     g.fillRect(0, 0, getWidth(), getHeight());
782
783     g.translate(0, scrollOffset);
784     g.setColor(Color.black);
785
786     AlignmentAnnotation[] aa = av.alignment.getAlignmentAnnotation();
787     int fontHeight = g.getFont().getSize();
788     int y = 0;
789     int x = 0;
790     int graphExtras = 0;
791     int offset = 0;
792
793     if (aa != null)
794     {
795       hasHiddenRows = false;
796       for (int i = 0; i < aa.length; i++)
797       {
798         g.setColor(Color.black);
799
800         if (!aa[i].visible)
801         {
802           hasHiddenRows = true;
803           continue;
804         }
805
806         y += aa[i].height;
807
808         offset = -aa[i].height / 2;
809
810         if (aa[i].hasText)
811         {
812           offset += fm.getHeight() / 2;
813           offset -= fm.getDescent();
814         }
815         else
816           offset += fm.getDescent();
817
818         x = width - fm.stringWidth(aa[i].label) - 3;
819
820         if (aa[i].graphGroup > -1)
821         {
822           int groupSize = 0;
823           for (int gg = 0; gg < aa.length; gg++)
824           {
825             if (aa[gg].graphGroup == aa[i].graphGroup)
826             {
827               groupSize++;
828             }
829           }
830
831           if (groupSize * (fontHeight + 8) < aa[i].height)
832           {
833             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8))) / 2;
834           }
835
836           for (int gg = 0; gg < aa.length; gg++)
837           {
838             if (aa[gg].graphGroup == aa[i].graphGroup)
839             {
840               x = width - fm.stringWidth(aa[gg].label) - 3;
841               g.drawString(aa[gg].label, x, y - graphExtras);
842               if (aa[gg].annotations[0] != null)
843               {
844                 g.setColor(aa[gg].annotations[0].colour);
845               }
846
847               g.drawLine(x, y - graphExtras - 3,
848                       x + fm.stringWidth(aa[gg].label), y - graphExtras - 3);
849
850               g.setColor(Color.black);
851               graphExtras += fontHeight + 8;
852             }
853           }
854         }
855         else
856         {
857           g.drawString(aa[i].label, x, y + offset);
858         }
859       }
860     }
861
862     if (resizePanel)
863     {
864       g.drawImage(image, 2, 0 - scrollOffset, this);
865     }
866     else if (dragEvent != null && aa != null)
867     {
868       g.setColor(Color.lightGray);
869       g.drawString(aa[selectedRow].label, dragEvent.getX(),
870               dragEvent.getY() - scrollOffset);
871     }
872
873     if ((aa == null) || (aa.length < 1))
874     {
875       g.drawString("Right click", 2, 8);
876       g.drawString("to add annotation", 2, 18);
877     }
878   }
879 }