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