JAL-244 noticed NPE exception was possible when visibility was adjusted in wrapped...
[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 java.awt.Canvas;
24 import java.awt.Color;
25 import java.awt.Cursor;
26 import java.awt.Dimension;
27 import java.awt.Font;
28 import java.awt.FontMetrics;
29 import java.awt.Graphics;
30 import java.awt.Graphics2D;
31 import java.awt.RenderingHints;
32 import java.awt.Toolkit;
33 import java.awt.datatransfer.StringSelection;
34 import java.awt.event.ActionEvent;
35 import java.awt.event.ActionListener;
36 import java.awt.event.MouseEvent;
37 import java.awt.event.MouseListener;
38 import java.awt.event.MouseMotionListener;
39 import java.awt.geom.AffineTransform;
40 import java.util.Arrays;
41 import java.util.Collections;
42 import java.util.Iterator;
43 import java.util.Locale;
44
45 import javax.swing.JCheckBoxMenuItem;
46 import javax.swing.JMenuItem;
47 import javax.swing.JPanel;
48 import javax.swing.JPopupMenu;
49 import javax.swing.SwingUtilities;
50 import javax.swing.ToolTipManager;
51
52 import jalview.analysis.AlignSeq;
53 import jalview.analysis.AlignmentUtils;
54 import jalview.bin.Cache;
55 import jalview.bin.Jalview;
56 import jalview.datamodel.Alignment;
57 import jalview.datamodel.AlignmentAnnotation;
58 import jalview.datamodel.Annotation;
59 import jalview.datamodel.ContactMatrixI;
60 import jalview.datamodel.GroupSet;
61 import jalview.datamodel.HiddenColumns;
62 import jalview.datamodel.Sequence;
63 import jalview.datamodel.SequenceGroup;
64 import jalview.datamodel.SequenceI;
65 import jalview.io.FileFormat;
66 import jalview.io.FormatAdapter;
67 import jalview.util.Comparison;
68 import jalview.util.MessageManager;
69 import jalview.util.Platform;
70
71 /**
72  * The panel that holds the labels for alignment annotations, providing
73  * tooltips, context menus, drag to reorder rows, and drag to adjust panel
74  * height
75  */
76 public class AnnotationLabels extends JPanel
77         implements MouseListener, MouseMotionListener, ActionListener
78 {
79   private static final String HTML_END_TAG = "</html>";
80
81   private static final String HTML_START_TAG = "<html>";
82
83   /**
84    * width in pixels within which height adjuster arrows are shown and active
85    */
86   private static final int HEIGHT_ADJUSTER_WIDTH = 50;
87
88   /**
89    * height in pixels for allowing height adjuster to be active
90    */
91   public static int HEIGHT_ADJUSTER_HEIGHT = 10;
92
93   private static final Font font = new Font("Arial", Font.PLAIN, 11);
94
95   private static final String TOGGLE_LABELSCALE = MessageManager
96           .getString("label.scale_label_to_column");
97
98   private static final String ADDNEW = MessageManager
99           .getString("label.add_new_row");
100
101   private static final String EDITNAME = MessageManager
102           .getString("label.edit_label_description");
103
104   private static final String HIDE = MessageManager
105           .getString("label.hide_row");
106
107   private static final String DELETE = MessageManager
108           .getString("label.delete_row");
109
110   private static final String SHOWALL = MessageManager
111           .getString("label.show_all_hidden_rows");
112
113   private static final String OUTPUT_TEXT = MessageManager
114           .getString("label.export_annotation");
115
116   private static final String COPYCONS_SEQ = MessageManager
117           .getString("label.copy_consensus_sequence");
118
119   private static final String ADJUST_ANNOTATION_LABELS_WIDTH_PREF = "ADJUST_ANNOTATION_LABELS_WIDTH";
120
121   private final boolean debugRedraw = false;
122
123   private AlignmentPanel ap;
124
125   AlignViewport av;
126
127   private MouseEvent dragEvent;
128
129   private int oldY;
130
131   private int selectedRow;
132
133   private int scrollOffset = 0;
134
135   private boolean hasHiddenRows;
136
137   private boolean resizePanel = false;
138
139   private int annotationIdWidth = -1;
140
141   public static final String RESIZE_MARGINS_MARK_PREF = "RESIZE_MARGINS_MARK";
142
143   /**
144    * Creates a new AnnotationLabels object
145    * 
146    * @param ap
147    */
148   public AnnotationLabels(AlignmentPanel ap)
149   {
150     this.ap = ap;
151     av = ap.av;
152     ToolTipManager.sharedInstance().registerComponent(this);
153
154     addMouseListener(this);
155     addMouseMotionListener(this);
156     addMouseWheelListener(ap.getAnnotationPanel());
157   }
158
159   public AnnotationLabels(AlignViewport av)
160   {
161     this.av = av;
162   }
163
164   /**
165    * DOCUMENT ME!
166    * 
167    * @param y
168    *          DOCUMENT ME!
169    */
170   public void setScrollOffset(int y)
171   {
172     scrollOffset = y;
173     repaint();
174   }
175
176   /**
177    * sets selectedRow to -2 if no annotation preset, -1 if no visible row is at
178    * y
179    * 
180    * @param y
181    *          coordinate position to search for a row
182    */
183   void getSelectedRow(int y)
184   {
185     int height = 0;
186     AlignmentAnnotation[] aa = ap.av.getAlignment()
187             .getAlignmentAnnotation();
188     selectedRow = -2;
189     if (aa != null)
190     {
191       for (int i = 0; i < aa.length; i++)
192       {
193         selectedRow = -1;
194         if (!aa[i].visible)
195         {
196           continue;
197         }
198
199         height += aa[i].height;
200
201         if (y < height)
202         {
203           selectedRow = i;
204
205           break;
206         }
207       }
208     }
209   }
210
211   /**
212    * DOCUMENT ME!
213    * 
214    * @param evt
215    *          DOCUMENT ME!
216    */
217   @Override
218   public void actionPerformed(ActionEvent evt)
219   {
220     AlignmentAnnotation[] aa = ap.av.getAlignment()
221             .getAlignmentAnnotation();
222
223     String action = evt.getActionCommand();
224     if (ADDNEW.equals(action))
225     {
226       /*
227        * non-returning dialog
228        */
229       AlignmentAnnotation newAnnotation = new AlignmentAnnotation(null,
230               null, new Annotation[ap.av.getAlignment().getWidth()]);
231       editLabelDescription(newAnnotation, true);
232     }
233     else if (EDITNAME.equals(action))
234     {
235       /*
236        * non-returning dialog
237        */
238       editLabelDescription(aa[selectedRow], false);
239     }
240     else if (HIDE.equals(action))
241     {
242       aa[selectedRow].visible = false;
243     }
244     else if (DELETE.equals(action))
245     {
246       ap.av.getAlignment().deleteAnnotation(aa[selectedRow]);
247       ap.av.getCalcManager().removeWorkerForAnnotation(aa[selectedRow]);
248     }
249     else if (SHOWALL.equals(action))
250     {
251       for (int i = 0; i < aa.length; i++)
252       {
253         if (!aa[i].visible && aa[i].annotations != null)
254         {
255           aa[i].visible = true;
256         }
257       }
258     }
259     else if (OUTPUT_TEXT.equals(action))
260     {
261       new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]);
262     }
263     else if (COPYCONS_SEQ.equals(action))
264     {
265       SequenceI cons = null;
266       if (aa[selectedRow].groupRef != null)
267       {
268         cons = aa[selectedRow].groupRef.getConsensusSeq();
269       }
270       else
271       {
272         cons = av.getConsensusSeq();
273       }
274       if (cons != null)
275       {
276         copy_annotseqtoclipboard(cons);
277       }
278     }
279     else if (TOGGLE_LABELSCALE.equals(action))
280     {
281       aa[selectedRow].scaleColLabel = !aa[selectedRow].scaleColLabel;
282     }
283
284     ap.refresh(true);
285   }
286
287   /**
288    * Shows a dialog where the annotation name and description may be edited. If
289    * parameter addNew is true, then on confirmation, a new AlignmentAnnotation
290    * is added, else an existing annotation is updated.
291    * 
292    * @param annotation
293    * @param addNew
294    */
295   void editLabelDescription(AlignmentAnnotation annotation, boolean addNew)
296   {
297     String name = MessageManager.getString("label.annotation_name");
298     String description = MessageManager
299             .getString("label.annotation_description");
300     String title = MessageManager
301             .getString("label.edit_annotation_name_description");
302     EditNameDialog dialog = new EditNameDialog(annotation.label,
303             annotation.description, name, description);
304
305     dialog.showDialog(ap.alignFrame, title, () -> {
306       annotation.label = dialog.getName();
307       String text = dialog.getDescription();
308       if (text != null && text.length() == 0)
309       {
310         text = null;
311       }
312       annotation.description = text;
313       if (addNew)
314       {
315         ap.av.getAlignment().addAnnotation(annotation);
316         ap.av.getAlignment().setAnnotationIndex(annotation, 0);
317       }
318       ap.refresh(true);
319     });
320   }
321
322   @Override
323   public void mousePressed(MouseEvent evt)
324   {
325     getSelectedRow(evt.getY() - getScrollOffset());
326     oldY = evt.getY();
327     if (evt.isPopupTrigger())
328     {
329       showPopupMenu(evt);
330     }
331   }
332
333   /**
334    * Build and show the Pop-up menu at the right-click mouse position
335    * 
336    * @param evt
337    */
338   void showPopupMenu(MouseEvent evt)
339   {
340     evt.consume();
341     final AlignmentAnnotation[] aa = ap.av.getAlignment()
342             .getAlignmentAnnotation();
343
344     JPopupMenu pop = new JPopupMenu(
345             MessageManager.getString("label.annotations"));
346     JMenuItem item = new JMenuItem(ADDNEW);
347     item.addActionListener(this);
348     pop.add(item);
349     if (selectedRow < 0)
350     {
351       if (hasHiddenRows)
352       { // let the user make everything visible again
353         item = new JMenuItem(SHOWALL);
354         item.addActionListener(this);
355         pop.add(item);
356       }
357       pop.show(this, evt.getX(), evt.getY());
358       return;
359     }
360     item = new JMenuItem(EDITNAME);
361     item.addActionListener(this);
362     pop.add(item);
363     item = new JMenuItem(HIDE);
364     item.addActionListener(this);
365     pop.add(item);
366     // JAL-1264 hide all sequence-specific annotations of this type
367     if (selectedRow < aa.length)
368     {
369       if (aa[selectedRow].sequenceRef != null)
370       {
371         final String label = aa[selectedRow].label;
372         JMenuItem hideType = new JMenuItem();
373         String text = MessageManager.getString("label.hide_all") + " "
374                 + label;
375         hideType.setText(text);
376         hideType.addActionListener(new ActionListener()
377         {
378           @Override
379           public void actionPerformed(ActionEvent e)
380           {
381             AlignmentUtils.showOrHideSequenceAnnotations(
382                     ap.av.getAlignment(), Collections.singleton(label),
383                     null, false, false);
384             ap.refresh(true);
385           }
386         });
387         pop.add(hideType);
388       }
389     }
390     item = new JMenuItem(DELETE);
391     item.addActionListener(this);
392     pop.add(item);
393     if (hasHiddenRows)
394     {
395       item = new JMenuItem(SHOWALL);
396       item.addActionListener(this);
397       pop.add(item);
398     }
399     item = new JMenuItem(OUTPUT_TEXT);
400     item.addActionListener(this);
401     pop.add(item);
402     // TODO: annotation object should be typed for autocalculated/derived
403     // property methods
404     if (selectedRow < aa.length)
405     {
406       final String label = aa[selectedRow].label;
407       if (!aa[selectedRow].autoCalculated)
408       {
409         if (aa[selectedRow].graph == AlignmentAnnotation.NO_GRAPH)
410         {
411           // display formatting settings for this row.
412           pop.addSeparator();
413           // av and sequencegroup need to implement same interface for
414           item = new JCheckBoxMenuItem(TOGGLE_LABELSCALE,
415                   aa[selectedRow].scaleColLabel);
416           item.addActionListener(this);
417           pop.add(item);
418         }
419       }
420       else if (label.indexOf("Consensus") > -1)
421       {
422         addConsensusMenuOptions(ap, aa[selectedRow], pop);
423
424         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
425         consclipbrd.addActionListener(this);
426         pop.add(consclipbrd);
427       }
428
429       addColourOrFilterByOptions(ap, aa[selectedRow], pop);
430
431       if (aa[selectedRow].graph == AlignmentAnnotation.CONTACT_MAP)
432       {
433         addContactMatrixOptions(ap, aa[selectedRow], pop);
434         // Set/adjust threshold for grouping ?
435         // colour alignment by this [type]
436         // select/hide columns by this row
437
438       }
439     }
440
441     pop.show(this, evt.getX(), evt.getY());
442   }
443
444   static void addColourOrFilterByOptions(final AlignmentPanel ap,
445           final AlignmentAnnotation alignmentAnnotation,
446           final JPopupMenu pop)
447   {
448     JMenuItem item;
449     item = new JMenuItem(
450             MessageManager.getString("label.colour_by_annotation"));
451     item.addActionListener(new ActionListener()
452     {
453
454       @Override
455       public void actionPerformed(ActionEvent e)
456       {
457         AnnotationColourChooser.displayFor(ap.av, ap, alignmentAnnotation,
458                 false);
459       };
460     });
461     pop.add(item);
462     if (alignmentAnnotation.sequenceRef != null)
463     {
464       item = new JMenuItem(
465               MessageManager.getString("label.colour_by_annotation") + " ("
466                       + MessageManager.getString("label.per_seq") + ")");
467       item.addActionListener(new ActionListener()
468       {
469         @Override
470         public void actionPerformed(ActionEvent e)
471         {
472           AnnotationColourChooser.displayFor(ap.av, ap, alignmentAnnotation,
473                   true);
474         };
475       });
476       pop.add(item);
477     }
478     item = new JMenuItem(
479             MessageManager.getString("action.select_by_annotation"));
480     item.addActionListener(new ActionListener()
481     {
482
483       @Override
484       public void actionPerformed(ActionEvent e)
485       {
486         AnnotationColumnChooser.displayFor(ap.av, ap, alignmentAnnotation);
487       };
488     });
489     pop.add(item);
490   }
491
492   static void addContactMatrixOptions(final AlignmentPanel ap,
493           final AlignmentAnnotation alignmentAnnotation,
494           final JPopupMenu pop)
495   {
496
497     final ContactMatrixI cm = ap.av.getContactMatrix(alignmentAnnotation);
498     JMenuItem item;
499     if (cm != null)
500     {
501       pop.addSeparator();
502
503       if (cm.hasGroups())
504       {
505         JCheckBoxMenuItem chitem = new JCheckBoxMenuItem(
506                 MessageManager.getString("action.show_groups_on_matrix"));
507         chitem.setToolTipText(MessageManager
508                 .getString("action.show_groups_on_matrix_tooltip"));
509         boolean showGroups = alignmentAnnotation
510                 .isShowGroupsForContactMatrix();
511         final AlignmentAnnotation sel_row = alignmentAnnotation;
512         chitem.setState(showGroups);
513         chitem.addActionListener(new ActionListener()
514         {
515
516           @Override
517           public void actionPerformed(ActionEvent e)
518           {
519             sel_row.setShowGroupsForContactMatrix(chitem.getState());
520             // so any annotation colour changes are propagated - though they
521             // probably won't be unless the annotation row colours are removed
522             // too!
523             ap.alignmentChanged();
524           }
525         });
526         pop.add(chitem);
527       }
528       if (cm.hasTree())
529       {
530         item = new JMenuItem(
531                 MessageManager.getString("action.show_tree_for_matrix"));
532         item.setToolTipText(MessageManager
533                 .getString("action.show_tree_for_matrix_tooltip"));
534         item.addActionListener(new ActionListener()
535         {
536
537           @Override
538           public void actionPerformed(ActionEvent e)
539           {
540
541             ap.alignFrame.showContactMapTree(alignmentAnnotation, cm);
542
543           }
544         });
545         pop.add(item);
546       }
547       else
548       {
549         item = new JMenuItem(
550                 MessageManager.getString("action.cluster_matrix"));
551         item.setToolTipText(
552                 MessageManager.getString("action.cluster_matrix_tooltip"));
553         item.addActionListener(new ActionListener()
554         {
555           @Override
556           public void actionPerformed(ActionEvent e)
557           {
558             new Thread(new Runnable()
559             {
560               @Override
561               public void run()
562               {
563                 final long progBar;
564                 ap.alignFrame.setProgressBar(
565                         MessageManager.formatMessage(
566                                 "action.clustering_matrix_for",
567                                 cm.getAnnotDescr(), 5f),
568                         progBar = System.currentTimeMillis());
569                 cm.setGroupSet(GroupSet.makeGroups(cm, true));
570                 cm.randomlyReColourGroups();
571                 cm.transferGroupColorsTo(alignmentAnnotation);
572                 ap.alignmentChanged();
573                 ap.alignFrame.showContactMapTree(alignmentAnnotation, cm);
574                 ap.alignFrame.setProgressBar(null, progBar);
575               }
576             }).start();
577           }
578         });
579         pop.add(item);
580       }
581     }
582   }
583
584   /**
585    * A helper method that adds menu options for calculation and visualisation of
586    * group and/or alignment consensus annotation to a popup menu. This is
587    * designed to be reusable for either unwrapped mode (popup menu is shown on
588    * component AnnotationLabels), or wrapped mode (popup menu is shown on
589    * IdPanel when the mouse is over an annotation label).
590    * 
591    * @param ap
592    * @param ann
593    * @param pop
594    */
595   static void addConsensusMenuOptions(AlignmentPanel ap,
596           AlignmentAnnotation ann, JPopupMenu pop)
597   {
598     pop.addSeparator();
599
600     final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
601             MessageManager.getString("label.ignore_gaps_consensus"),
602             (ann.groupRef != null) ? ann.groupRef.getIgnoreGapsConsensus()
603                     : ap.av.isIgnoreGapsConsensus());
604     final AlignmentAnnotation aaa = ann;
605     cbmi.addActionListener(new ActionListener()
606     {
607       @Override
608       public void actionPerformed(ActionEvent e)
609       {
610         if (aaa.groupRef != null)
611         {
612           aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
613           ap.getAnnotationPanel()
614                   .paint(ap.getAnnotationPanel().getGraphics());
615         }
616         else
617         {
618           ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
619         }
620         ap.alignmentChanged();
621       }
622     });
623     pop.add(cbmi);
624
625     if (aaa.groupRef != null)
626     {
627       /*
628        * group consensus options
629        */
630       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
631               MessageManager.getString("label.show_group_histogram"),
632               ann.groupRef.isShowConsensusHistogram());
633       chist.addActionListener(new ActionListener()
634       {
635         @Override
636         public void actionPerformed(ActionEvent e)
637         {
638           aaa.groupRef.setShowConsensusHistogram(chist.getState());
639           ap.repaint();
640         }
641       });
642       pop.add(chist);
643       final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
644               MessageManager.getString("label.show_group_logo"),
645               ann.groupRef.isShowSequenceLogo());
646       cprofl.addActionListener(new ActionListener()
647       {
648         @Override
649         public void actionPerformed(ActionEvent e)
650         {
651           aaa.groupRef.setshowSequenceLogo(cprofl.getState());
652           ap.repaint();
653         }
654       });
655       pop.add(cprofl);
656       final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
657               MessageManager.getString("label.normalise_group_logo"),
658               ann.groupRef.isNormaliseSequenceLogo());
659       cproflnorm.addActionListener(new ActionListener()
660       {
661         @Override
662         public void actionPerformed(ActionEvent e)
663         {
664           aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
665           // automatically enable logo display if we're clicked
666           aaa.groupRef.setshowSequenceLogo(true);
667           ap.repaint();
668         }
669       });
670       pop.add(cproflnorm);
671     }
672     else
673     {
674       /*
675        * alignment consensus options
676        */
677       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
678               MessageManager.getString("label.show_histogram"),
679               ap.av.isShowConsensusHistogram());
680       chist.addActionListener(new ActionListener()
681       {
682         @Override
683         public void actionPerformed(ActionEvent e)
684         {
685           ap.av.setShowConsensusHistogram(chist.getState());
686           ap.alignFrame.setMenusForViewport();
687           ap.repaint();
688         }
689       });
690       pop.add(chist);
691       final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
692               MessageManager.getString("label.show_logo"),
693               ap.av.isShowSequenceLogo());
694       cprof.addActionListener(new ActionListener()
695       {
696         @Override
697         public void actionPerformed(ActionEvent e)
698         {
699           ap.av.setShowSequenceLogo(cprof.getState());
700           ap.alignFrame.setMenusForViewport();
701           ap.repaint();
702         }
703       });
704       pop.add(cprof);
705       final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
706               MessageManager.getString("label.normalise_logo"),
707               ap.av.isNormaliseSequenceLogo());
708       cprofnorm.addActionListener(new ActionListener()
709       {
710         @Override
711         public void actionPerformed(ActionEvent e)
712         {
713           ap.av.setShowSequenceLogo(true);
714           ap.av.setNormaliseSequenceLogo(cprofnorm.getState());
715           ap.alignFrame.setMenusForViewport();
716           ap.repaint();
717         }
718       });
719       pop.add(cprofnorm);
720     }
721   }
722
723   /**
724    * Reorders annotation rows after a drag of a label
725    * 
726    * @param evt
727    */
728   @Override
729   public void mouseReleased(MouseEvent evt)
730   {
731     if (evt.isPopupTrigger())
732     {
733       showPopupMenu(evt);
734       return;
735     }
736
737     int start = selectedRow;
738     getSelectedRow(evt.getY() - getScrollOffset());
739     int end = selectedRow;
740
741     /*
742      * if dragging to resize instead, start == end
743      */
744     if (start != end)
745     {
746       // Swap these annotations
747       AlignmentAnnotation startAA = ap.av.getAlignment()
748               .getAlignmentAnnotation()[start];
749       if (end == -1)
750       {
751         end = ap.av.getAlignment().getAlignmentAnnotation().length - 1;
752       }
753       AlignmentAnnotation endAA = ap.av.getAlignment()
754               .getAlignmentAnnotation()[end];
755
756       ap.av.getAlignment().getAlignmentAnnotation()[end] = startAA;
757       ap.av.getAlignment().getAlignmentAnnotation()[start] = endAA;
758     }
759
760     resizePanel = false;
761     dragEvent = null;
762     repaint();
763     ap.getAnnotationPanel().repaint();
764   }
765
766   /**
767    * Removes the height adjuster image on leaving the panel, unless currently
768    * dragging it
769    */
770   @Override
771   public void mouseExited(MouseEvent evt)
772   {
773     if (resizePanel && dragEvent == null)
774     {
775       resizePanel = false;
776       repaint();
777     }
778   }
779
780   /**
781    * A mouse drag may be either an adjustment of the panel height (if flag
782    * resizePanel is set on), or a reordering of the annotation rows. The former
783    * is dealt with by this method, the latter in mouseReleased.
784    * 
785    * @param evt
786    */
787   @Override
788   public void mouseDragged(MouseEvent evt)
789   {
790     dragEvent = evt;
791
792     if (resizePanel)
793     {
794       Dimension d = ap.annotationScroller.getPreferredSize();
795       int dif = evt.getY() - oldY;
796
797       dif /= ap.av.getCharHeight();
798       dif *= ap.av.getCharHeight();
799
800       if ((d.height - dif) > 20)
801       {
802         ap.annotationScroller
803                 .setPreferredSize(new Dimension(d.width, d.height - dif));
804         d = ap.annotationSpaceFillerHolder.getPreferredSize();
805         ap.annotationSpaceFillerHolder
806                 .setPreferredSize(new Dimension(d.width, d.height - dif));
807         ap.paintAlignment(true, false);
808       }
809
810       ap.addNotify();
811     }
812     else
813     {
814       repaint();
815     }
816   }
817
818   /**
819    * Updates the tooltip as the mouse moves over the labels
820    * 
821    * @param evt
822    */
823   @Override
824   public void mouseMoved(MouseEvent evt)
825   {
826     showOrHideAdjuster(evt);
827
828     getSelectedRow(evt.getY() - getScrollOffset());
829
830     if (selectedRow > -1 && ap.av.getAlignment()
831             .getAlignmentAnnotation().length > selectedRow)
832     {
833       AlignmentAnnotation[] anns = ap.av.getAlignment()
834               .getAlignmentAnnotation();
835       AlignmentAnnotation aa = anns[selectedRow];
836
837       String desc = getTooltip(aa);
838       this.setToolTipText(desc);
839       String msg = getStatusMessage(aa, anns);
840       ap.alignFrame.setStatus(msg);
841     }
842   }
843
844   /**
845    * Constructs suitable text to show in the status bar when over an annotation
846    * label, containing the associated sequence name (if any), and the annotation
847    * labels (or all labels for a graph group annotation)
848    * 
849    * @param aa
850    * @param anns
851    * @return
852    */
853   static String getStatusMessage(AlignmentAnnotation aa,
854           AlignmentAnnotation[] anns)
855   {
856     if (aa == null)
857     {
858       return null;
859     }
860
861     StringBuilder msg = new StringBuilder(32);
862     if (aa.sequenceRef != null)
863     {
864       msg.append(aa.sequenceRef.getName()).append(" : ");
865     }
866
867     if (aa.graphGroup == -1)
868     {
869       msg.append(aa.label);
870     }
871     else if (anns != null)
872     {
873       boolean first = true;
874       for (int i = anns.length - 1; i >= 0; i--)
875       {
876         if (anns[i].graphGroup == aa.graphGroup)
877         {
878           if (!first)
879           {
880             msg.append(", ");
881           }
882           msg.append(anns[i].label);
883           first = false;
884         }
885       }
886     }
887
888     return msg.toString();
889   }
890
891   /**
892    * Answers a tooltip, formatted as html, containing the annotation description
893    * (prefixed by associated sequence id if applicable), and the annotation
894    * (non-positional) score if it has one. Answers null if neither description
895    * nor score is found.
896    * 
897    * @param aa
898    * @return
899    */
900   static String getTooltip(AlignmentAnnotation aa)
901   {
902     if (aa == null)
903     {
904       return null;
905     }
906     StringBuilder tooltip = new StringBuilder();
907     if (aa.description != null && !aa.description.equals("New description"))
908     {
909       // TODO: we could refactor and merge this code with the code in
910       // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
911       // tooltips
912       String desc = aa.getDescription(true).trim();
913       if (!desc.toLowerCase(Locale.ROOT).startsWith(HTML_START_TAG))
914       {
915         tooltip.append(HTML_START_TAG);
916         desc = desc.replace("<", "&lt;");
917       }
918       else if (desc.toLowerCase(Locale.ROOT).endsWith(HTML_END_TAG))
919       {
920         desc = desc.substring(0, desc.length() - HTML_END_TAG.length());
921       }
922       tooltip.append(desc);
923     }
924     else
925     {
926       // begin the tooltip's html fragment
927       tooltip.append(HTML_START_TAG);
928     }
929     if (aa.hasScore())
930     {
931       if (tooltip.length() > HTML_START_TAG.length())
932       {
933         tooltip.append("<br/>");
934       }
935       // TODO: limit precision of score to avoid noise from imprecise
936       // doubles
937       // (64.7 becomes 64.7+/some tiny value).
938       tooltip.append(" Score: ").append(String.valueOf(aa.score));
939     }
940
941     if (tooltip.length() > HTML_START_TAG.length())
942     {
943       return tooltip.append(HTML_END_TAG).toString();
944     }
945
946     /*
947      * nothing in the tooltip (except "<html>")
948      */
949     return null;
950   }
951
952   /**
953    * Shows the height adjuster image if the mouse moves into the top left
954    * region, or hides it if the mouse leaves the regio
955    * 
956    * @param evt
957    */
958   protected void showOrHideAdjuster(MouseEvent evt)
959   {
960     boolean was = resizePanel;
961     resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT
962             && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
963
964     if (resizePanel != was)
965     {
966       setCursor(Cursor
967               .getPredefinedCursor(resizePanel ? Cursor.S_RESIZE_CURSOR
968                       : Cursor.DEFAULT_CURSOR));
969       repaint();
970     }
971   }
972
973   @Override
974   public void mouseClicked(MouseEvent evt)
975   {
976     final AlignmentAnnotation[] aa = ap.av.getAlignment()
977             .getAlignmentAnnotation();
978     if (!evt.isPopupTrigger() && SwingUtilities.isLeftMouseButton(evt))
979     {
980       if (selectedRow > -1 && selectedRow < aa.length)
981       {
982         if (aa[selectedRow].groupRef != null)
983         {
984           if (evt.getClickCount() >= 2)
985           {
986             // todo: make the ap scroll to the selection - not necessary, first
987             // click highlights/scrolls, second selects
988             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
989             // process modifiers
990             SequenceGroup sg = ap.av.getSelectionGroup();
991             if (sg == null || sg == aa[selectedRow].groupRef
992                     || !(Platform.isControlDown(evt) || evt.isShiftDown()))
993             {
994               if (Platform.isControlDown(evt) || evt.isShiftDown())
995               {
996                 // clone a new selection group from the associated group
997                 ap.av.setSelectionGroup(
998                         new SequenceGroup(aa[selectedRow].groupRef));
999               }
1000               else
1001               {
1002                 // set selection to the associated group so it can be edited
1003                 ap.av.setSelectionGroup(aa[selectedRow].groupRef);
1004               }
1005             }
1006             else
1007             {
1008               // modify current selection with associated group
1009               int remainToAdd = aa[selectedRow].groupRef.getSize();
1010               for (SequenceI sgs : aa[selectedRow].groupRef.getSequences())
1011               {
1012                 if (jalview.util.Platform.isControlDown(evt))
1013                 {
1014                   sg.addOrRemove(sgs, --remainToAdd == 0);
1015                 }
1016                 else
1017                 {
1018                   // notionally, we should also add intermediate sequences from
1019                   // last added sequence ?
1020                   sg.addSequence(sgs, --remainToAdd == 0);
1021                 }
1022               }
1023             }
1024
1025             ap.paintAlignment(false, false);
1026             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
1027             ap.av.sendSelection();
1028           }
1029           else
1030           {
1031             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(
1032                     aa[selectedRow].groupRef.getSequences(null));
1033           }
1034           return;
1035         }
1036         else if (aa[selectedRow].sequenceRef != null)
1037         {
1038           if (evt.getClickCount() == 1)
1039           {
1040             ap.getSeqPanel().ap.getIdPanel()
1041                     .highlightSearchResults(Arrays.asList(new SequenceI[]
1042                     { aa[selectedRow].sequenceRef }));
1043           }
1044           else if (evt.getClickCount() >= 2)
1045           {
1046             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
1047             SequenceGroup sg = ap.av.getSelectionGroup();
1048             if (sg != null)
1049             {
1050               // we make a copy rather than edit the current selection if no
1051               // modifiers pressed
1052               // see Enhancement JAL-1557
1053               if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
1054               {
1055                 sg = new SequenceGroup(sg);
1056                 sg.clear();
1057                 sg.addSequence(aa[selectedRow].sequenceRef, false);
1058               }
1059               else
1060               {
1061                 if (Platform.isControlDown(evt))
1062                 {
1063                   sg.addOrRemove(aa[selectedRow].sequenceRef, true);
1064                 }
1065                 else
1066                 {
1067                   // notionally, we should also add intermediate sequences from
1068                   // last added sequence ?
1069                   sg.addSequence(aa[selectedRow].sequenceRef, true);
1070                 }
1071               }
1072             }
1073             else
1074             {
1075               sg = new SequenceGroup();
1076               sg.setStartRes(0);
1077               sg.setEndRes(ap.av.getAlignment().getWidth() - 1);
1078               sg.addSequence(aa[selectedRow].sequenceRef, false);
1079             }
1080             ap.av.setSelectionGroup(sg);
1081             ap.paintAlignment(false, false);
1082             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
1083             ap.av.sendSelection();
1084           }
1085
1086         }
1087       }
1088       return;
1089     }
1090   }
1091
1092   /**
1093    * do a single sequence copy to jalview and the system clipboard
1094    * 
1095    * @param sq
1096    *          sequence to be copied to clipboard
1097    */
1098   protected void copy_annotseqtoclipboard(SequenceI sq)
1099   {
1100     SequenceI[] seqs = new SequenceI[] { sq };
1101     String[] omitHidden = null;
1102     SequenceI[] dseqs = new SequenceI[] { sq.getDatasetSequence() };
1103     if (dseqs[0] == null)
1104     {
1105       dseqs[0] = new Sequence(sq);
1106       dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
1107               sq.getSequenceAsString()));
1108
1109       sq.setDatasetSequence(dseqs[0]);
1110     }
1111     Alignment ds = new Alignment(dseqs);
1112     if (av.hasHiddenColumns())
1113     {
1114       Iterator<int[]> it = av.getAlignment().getHiddenColumns()
1115               .getVisContigsIterator(0, sq.getLength(), false);
1116       omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
1117     }
1118
1119     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
1120     if (av.hasHiddenColumns())
1121     {
1122       alignmentStartEnd = av.getAlignment().getHiddenColumns()
1123               .getVisibleStartAndEndIndex(av.getAlignment().getWidth());
1124     }
1125
1126     String output = new FormatAdapter().formatSequences(FileFormat.Fasta,
1127             seqs, omitHidden, alignmentStartEnd);
1128
1129     Toolkit.getDefaultToolkit().getSystemClipboard()
1130             .setContents(new StringSelection(output), Desktop.instance);
1131
1132     HiddenColumns hiddenColumns = null;
1133
1134     if (av.hasHiddenColumns())
1135     {
1136       hiddenColumns = new HiddenColumns(
1137               av.getAlignment().getHiddenColumns());
1138     }
1139
1140     Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
1141                                                         // of a consensus
1142                                                         // sequence ? need to
1143                                                         // flag
1144         // sequence as special.
1145         hiddenColumns };
1146   }
1147
1148   /**
1149    * DOCUMENT ME!
1150    * 
1151    * @param g1
1152    *          DOCUMENT ME!
1153    */
1154   @Override
1155   public void paintComponent(Graphics g)
1156   {
1157
1158     int width = getWidth();
1159     if (width == 0)
1160     {
1161       width = ap.calculateIdWidth().width;
1162     }
1163
1164     Graphics2D g2 = (Graphics2D) g;
1165     if (av.antiAlias)
1166     {
1167       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1168               RenderingHints.VALUE_ANTIALIAS_ON);
1169     }
1170
1171     drawComponent(g2, true, width, true);
1172   }
1173
1174   /**
1175    * Draw the full set of annotation Labels for the alignment at the given
1176    * cursor
1177    * 
1178    * @param g
1179    *          Graphics2D instance (needed for font scaling)
1180    * @param width
1181    *          Width for scaling labels
1182    * 
1183    */
1184   public void drawComponent(Graphics g, int width)
1185   {
1186     drawComponent(g, false, width, true);
1187   }
1188
1189   /**
1190    * Draw the full set of annotation Labels for the alignment at the given
1191    * cursor
1192    * 
1193    * @param g
1194    *          Graphics2D instance (needed for font scaling)
1195    * @param clip
1196    *          - true indicates that only current visible area needs to be
1197    *          rendered
1198    * @param width
1199    *          Width for scaling labels
1200    */
1201   public void drawComponent(Graphics g, boolean clip, int givenWidth, boolean forGUI)
1202   {
1203     int width = givenWidth;
1204     IdwidthAdjuster iwa = null;
1205     if (ap != null)
1206     {
1207       iwa = ap.idwidthAdjuster;
1208       if ((Cache.getDefault(ADJUST_ANNOTATION_LABELS_WIDTH_PREF, true)
1209               || Jalview.isHeadlessMode()))
1210       {
1211         Graphics2D g2d = (Graphics2D) g;
1212         Graphics dummy = g2d.create();
1213         int newAnnotationIdWidth = drawLabels(dummy, clip, width, false, forGUI,
1214                 null, false);
1215         dummy.dispose();
1216         Dimension d = ap.calculateDefaultAlignmentIdWidth();
1217         int alignmentIdWidth = d.width;
1218         if (iwa != null && !iwa.manuallyAdjusted())
1219         {
1220           // If no manual adjustment to ID column with has been made then adjust
1221           // width match widest of alignment or annotation id widths
1222           boolean allowShrink = Cache.getDefault("ALLOW_SHRINK_ID_WIDTH",
1223                   false);
1224           width = Math.max(alignmentIdWidth, newAnnotationIdWidth);
1225           if (clip && width < givenWidth && !allowShrink)
1226           {
1227             width = givenWidth;
1228           }
1229         }
1230         else if (newAnnotationIdWidth != annotationIdWidth
1231                 && newAnnotationIdWidth > givenWidth
1232                 && newAnnotationIdWidth > alignmentIdWidth)
1233         {
1234           // otherwise if the annotation id width has become larger than the
1235           // current id width, increase
1236           width = newAnnotationIdWidth;
1237           annotationIdWidth = newAnnotationIdWidth;
1238         }
1239         // set the width if it's changed
1240         if (width != ap.av.getIdWidth())
1241         {
1242           iwa.setWidth(width);
1243         }
1244       }
1245     }
1246     else
1247     {
1248       int newAnnotationIdWidth = drawLabels(g, clip, width, false, forGUI, null, false);
1249       width = Math.max(newAnnotationIdWidth, givenWidth);
1250     }
1251     drawLabels(g, clip, width, true, forGUI, null, false);
1252   }
1253
1254   /**
1255    * Render the full set of annotation Labels for the alignment at the given
1256    * cursor. If actuallyDraw is false or g is null then no actual drawing will
1257    * occur, but the widest label width will be returned. If g is null then
1258    * fmetrics must be supplied.
1259    * 
1260    * @param g
1261    *          Graphics2D instance (used for rendering and font scaling if no fmetrics supplied) 
1262    * @param clip
1263    *          - true indicates that only current visible area needs to be
1264    *          rendered
1265    * @param width
1266    *          Width for scaling labels
1267    * @param actuallyDraw - when false, no graphics are rendered to g0
1268    * @param forGUI - when false, GUI relevant marks like indicators for dragging annotation panel height are not rendered
1269    * @param fmetrics
1270    *          FontMetrics if Graphics object g is null
1271    * @param includeHidden - when true returned width includes labels in hidden row width calculation 
1272    * @return the width of the annotation labels.
1273    */
1274   public int drawLabels(Graphics g0, boolean clip, int width,
1275           boolean actuallyDraw, boolean forGUI, FontMetrics fmetrics, boolean includeHidden)
1276   {
1277     if (clip)
1278     {
1279       clip = Cache.getDefault("MOVE_SEQUENCE_ID_WITH_VISIBLE_ANNOTATIONS",
1280               true);
1281     }
1282     Graphics g = null;
1283     // create a dummy Graphics object if not drawing and one is supplied
1284     if (g0 != null)
1285     {
1286       if (!actuallyDraw)
1287       {
1288         Graphics2D g2d = (Graphics2D) g0;
1289         g = g2d.create();
1290       }
1291       else
1292       {
1293         g = g0;
1294       }
1295     }
1296     int actualWidth = 0;
1297     if (g != null)
1298     {
1299       if (av.getFont().getSize() < 10)
1300       {
1301         g.setFont(font);
1302       }
1303       else
1304       {
1305         g.setFont(av.getFont());
1306       }
1307     }
1308
1309     FontMetrics fm = fmetrics == null ? g.getFontMetrics(g.getFont())
1310             : fmetrics;
1311     if (actuallyDraw)
1312     {
1313       g.setColor(Color.white);
1314       g.fillRect(0, 0, getWidth(), getHeight());
1315
1316       if (!Cache.getDefault(RESIZE_MARGINS_MARK_PREF, false)
1317               && !av.getWrapAlignment() && forGUI)
1318       {
1319         g.setColor(Color.LIGHT_GRAY);
1320         g.drawLine(0, HEIGHT_ADJUSTER_HEIGHT / 4, HEIGHT_ADJUSTER_WIDTH / 4,
1321                 HEIGHT_ADJUSTER_HEIGHT / 4);
1322         g.drawLine(0, 3 * HEIGHT_ADJUSTER_HEIGHT / 4,
1323                 HEIGHT_ADJUSTER_WIDTH / 4, 3 * HEIGHT_ADJUSTER_HEIGHT / 4);
1324
1325       }
1326     }
1327
1328     if (actuallyDraw)
1329     {
1330       g.translate(0, getScrollOffset());
1331       g.setColor(Color.black);
1332     }
1333     SequenceI lastSeqRef = null;
1334     String lastLabel = null;
1335     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1336     int fontHeight = g != null ? g.getFont().getSize()
1337             : fm.getFont().getSize();
1338     int y = 0;
1339     int x = 0;
1340     int graphExtras = 0;
1341     int offset = 0;
1342     Font baseFont = g != null ? g.getFont() : fm.getFont();
1343     FontMetrics baseMetrics = fm;
1344     int ofontH = fontHeight;
1345     int sOffset = 0;
1346     int visHeight = 0;
1347     int[] visr = (ap != null && ap.getAnnotationPanel() != null)
1348             ? ap.getAnnotationPanel().getVisibleVRange()
1349             : null;
1350     if (clip && visr != null)
1351     {
1352       sOffset = visr[0];
1353       visHeight = visr[1];
1354     }
1355     boolean visible = true, before = false, after = false;
1356     if (aa != null)
1357     {
1358       hasHiddenRows = false;
1359       int olY = 0;
1360       int nexAA = 0;
1361       for (int i = 0; i < aa.length; i++)
1362       {
1363         visible = true;
1364         if (!aa[i].visible && !includeHidden)
1365         {
1366           hasHiddenRows = true;
1367           continue;
1368         }
1369         olY = y;
1370         // look ahead to next annotation
1371         for (nexAA = i + 1; nexAA < aa.length
1372                 && (!aa[nexAA].visible && includeHidden); nexAA++)
1373           ;
1374         y += aa[i].height;
1375         if (clip)
1376         {
1377           if (y < sOffset)
1378           {
1379             if (!before)
1380             {
1381               if (debugRedraw)
1382               {
1383                 jalview.bin.Console.outPrintln("before vis: " + i);
1384               }
1385               before = true;
1386             }
1387             // don't draw what isn't visible
1388             continue;
1389           }
1390           if (olY > visHeight)
1391           {
1392
1393             if (!after)
1394             {
1395               if (debugRedraw)
1396               {
1397                 jalview.bin.Console.outPrintln(
1398                         "Scroll offset: " + sOffset + " after vis: " + i);
1399               }
1400               after = true;
1401             }
1402             // don't draw what isn't visible
1403             continue;
1404           }
1405         }
1406         if (actuallyDraw && g != null)
1407         {
1408           g.setColor(Color.black);
1409         }
1410         offset = -aa[i].height / 2;
1411
1412         if (aa[i].hasText)
1413         {
1414           offset += fm.getHeight() / 2;
1415           offset -= fm.getDescent();
1416         }
1417         else
1418         {
1419           offset += fm.getDescent();
1420         }
1421         String label = aa[i].label;
1422         boolean vertBar = false;
1423         if ((lastLabel != null && lastLabel.equals(label)))
1424         {
1425           label = aa[i].description;
1426         }
1427         else
1428         {
1429           if (nexAA < aa.length && label.equals(aa[nexAA].label)) // &&
1430                                                                   // aa[nexY].sequenceRef==aa[i].sequenceRef)
1431           {
1432             lastLabel = label;
1433             // next label is the same as this label
1434             label = aa[i].description;
1435           }
1436           else
1437           {
1438             lastLabel = label;
1439           }
1440         }
1441         if (aa[i].sequenceRef != null)
1442         {
1443           if (aa[i].sequenceRef != lastSeqRef)
1444           {
1445             label = aa[i].sequenceRef.getName() + " " + label;
1446             // TODO record relationship between sequence and this annotation and
1447             // display it here
1448           }
1449           else
1450           {
1451             vertBar = true;
1452           }
1453         }
1454
1455         int labelWidth = fm.stringWidth(label) + 3;
1456         x = width - labelWidth;
1457
1458         if (aa[i].graphGroup > -1)
1459         {
1460           int groupSize = 0;
1461           // TODO: JAL-1291 revise rendering model so the graphGroup map is
1462           // computed efficiently for all visible labels
1463           for (int gg = 0; gg < aa.length; gg++)
1464           {
1465             if (aa[gg].graphGroup == aa[i].graphGroup)
1466             {
1467               groupSize++;
1468             }
1469           }
1470           if (groupSize * (fontHeight + 8) < aa[i].height)
1471           {
1472             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1473                     / 2;
1474           }
1475           else
1476           {
1477             // scale font to fit
1478             float h = aa[i].height / (float) groupSize, s;
1479             if (h < 9)
1480             {
1481               visible = false;
1482             }
1483             else
1484             {
1485               fontHeight = -8 + (int) h;
1486               s = ((float) fontHeight) / (float) ofontH;
1487               Font f = baseFont
1488                       .deriveFont(AffineTransform.getScaleInstance(s, s));
1489               Canvas c = new Canvas();
1490               fm = c.getFontMetrics(f);
1491               if (actuallyDraw && g != null)
1492               {
1493                 g.setFont(f);
1494                 // fm = g.getFontMetrics();
1495                 graphExtras = (aa[i].height
1496                         - (groupSize * (fontHeight + 8))) / 2;
1497               }
1498             }
1499           }
1500           if (visible)
1501           {
1502             for (int gg = 0; gg < aa.length; gg++)
1503             {
1504               if (aa[gg].graphGroup == aa[i].graphGroup)
1505               {
1506                 labelWidth = fm.stringWidth(aa[gg].label) + 3;
1507                 x = width - labelWidth;
1508                 if (actuallyDraw && g != null)
1509                 {
1510                   g.drawString(aa[gg].label, x, y - graphExtras);
1511
1512                   if (aa[gg]._linecolour != null)
1513                   {
1514
1515                     g.setColor(aa[gg]._linecolour);
1516                     g.drawLine(x, y - graphExtras + 3,
1517                             x + fm.stringWidth(aa[gg].label),
1518                             y - graphExtras + 3);
1519                   }
1520
1521                   g.setColor(Color.black);
1522                 }
1523                 graphExtras += fontHeight + 8;
1524               }
1525             }
1526           }
1527           if (actuallyDraw && g != null)
1528           {
1529             g.setFont(baseFont);
1530           }
1531           fm = baseMetrics;
1532           fontHeight = ofontH;
1533         }
1534         else
1535         {
1536           if (actuallyDraw && g != null)
1537           {
1538             if (vertBar)
1539             {
1540               g.drawLine(width - 3, y + offset - fontHeight, width - 3,
1541                       (int) (y - 1.5 * aa[i].height - offset - fontHeight));
1542               // g.drawLine(20, y + offset, x - 20, y + offset);
1543
1544             }
1545             g.drawString(label, x, y + offset);
1546           }
1547         }
1548         lastSeqRef = aa[i].sequenceRef;
1549
1550         if (labelWidth > actualWidth)
1551         {
1552           actualWidth = labelWidth;
1553         }
1554       }
1555     }
1556
1557     if (!resizePanel && dragEvent != null && aa != null && selectedRow>-1 && selectedRow<aa.length)
1558     {
1559       if (actuallyDraw && g != null)
1560       {
1561         g.setColor(Color.lightGray);
1562         g.drawString(
1563                 (aa[selectedRow].sequenceRef == null ? ""
1564                         : aa[selectedRow].sequenceRef.getName())
1565                         + aa[selectedRow].label,
1566                 dragEvent.getX(), dragEvent.getY() - getScrollOffset());
1567       }
1568     }
1569
1570     if (!av.getWrapAlignment() && ((aa == null) || (aa.length < 1)))
1571     {
1572       if (actuallyDraw && g != null)
1573       {
1574         g.drawString(MessageManager.getString("label.right_click"), 2, 8);
1575         g.drawString(MessageManager.getString("label.to_add_annotation"), 2,
1576                 18);
1577       }
1578     }
1579
1580     return actualWidth;
1581   }
1582
1583   public int getScrollOffset()
1584   {
1585     return scrollOffset;
1586   }
1587
1588   @Override
1589   public void mouseEntered(MouseEvent e)
1590   {
1591   }
1592
1593   public void drawComponentNotGUI(Graphics idGraphics, int idWidth)
1594   {
1595     drawComponent(idGraphics, false, idWidth, false);
1596   }
1597 }