b1a8aff6a863f3756989c1439747b8d3ef36ad01
[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         if (aa.hasScore())
879         {
880           // TODO: limit precision of score to avoid noise from imprecise
881           // doubles
882           // (64.7 becomes 64.7+/some tiny value).
883           desc.append(" Score: " + aa.score);
884         }
885       }
886       if (desc.length() > 6)
887       {
888         desc.append("</html>");
889         this.setToolTipText(desc.toString());
890       }
891       else
892       {
893         this.setToolTipText(null);
894       }
895     }
896   }
897
898   /**
899    * Shows the height adjuster image if the mouse moves into the top left
900    * region, or hides it if the mouse leaves the regio
901    * 
902    * @param evt
903    */
904   protected void showOrHideAdjuster(MouseEvent evt)
905   {
906     boolean was = resizePanel;
907     resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
908
909     if (resizePanel != was)
910     {
911       setCursor(Cursor.getPredefinedCursor(
912               resizePanel ? Cursor.S_RESIZE_CURSOR
913                       : Cursor.DEFAULT_CURSOR));
914       repaint();
915     }
916   }
917
918   @Override
919   public void mouseClicked(MouseEvent evt)
920   {
921     final AlignmentAnnotation[] aa = ap.av.getAlignment()
922             .getAlignmentAnnotation();
923     if (!evt.isPopupTrigger() && SwingUtilities.isLeftMouseButton(evt))
924     {
925       if (selectedRow > -1 && selectedRow < aa.length)
926       {
927         if (aa[selectedRow].groupRef != null)
928         {
929           if (evt.getClickCount() >= 2)
930           {
931             // todo: make the ap scroll to the selection - not necessary, first
932             // click highlights/scrolls, second selects
933             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
934             // process modifiers
935             SequenceGroup sg = ap.av.getSelectionGroup();
936             if (sg == null || sg == aa[selectedRow].groupRef
937                     || !(Platform.isControlDown(evt) || evt.isShiftDown()))
938             {
939               if (Platform.isControlDown(evt) || evt.isShiftDown())
940               {
941                 // clone a new selection group from the associated group
942                 ap.av.setSelectionGroup(
943                         new SequenceGroup(aa[selectedRow].groupRef));
944               }
945               else
946               {
947                 // set selection to the associated group so it can be edited
948                 ap.av.setSelectionGroup(aa[selectedRow].groupRef);
949               }
950             }
951             else
952             {
953               // modify current selection with associated group
954               int remainToAdd = aa[selectedRow].groupRef.getSize();
955               for (SequenceI sgs : aa[selectedRow].groupRef.getSequences())
956               {
957                 if (jalview.util.Platform.isControlDown(evt))
958                 {
959                   sg.addOrRemove(sgs, --remainToAdd == 0);
960                 }
961                 else
962                 {
963                   // notionally, we should also add intermediate sequences from
964                   // last added sequence ?
965                   sg.addSequence(sgs, --remainToAdd == 0);
966                 }
967               }
968             }
969
970             ap.paintAlignment(false, false);
971             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
972             ap.av.sendSelection();
973           }
974           else
975           {
976             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(
977                     aa[selectedRow].groupRef.getSequences(null));
978           }
979           return;
980         }
981         else if (aa[selectedRow].sequenceRef != null)
982         {
983           if (evt.getClickCount() == 1)
984           {
985             ap.getSeqPanel().ap.getIdPanel()
986                     .highlightSearchResults(Arrays.asList(new SequenceI[]
987                     { aa[selectedRow].sequenceRef }));
988           }
989           else if (evt.getClickCount() >= 2)
990           {
991             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
992             SequenceGroup sg = ap.av.getSelectionGroup();
993             if (sg != null)
994             {
995               // we make a copy rather than edit the current selection if no
996               // modifiers pressed
997               // see Enhancement JAL-1557
998               if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
999               {
1000                 sg = new SequenceGroup(sg);
1001                 sg.clear();
1002                 sg.addSequence(aa[selectedRow].sequenceRef, false);
1003               }
1004               else
1005               {
1006                 if (Platform.isControlDown(evt))
1007                 {
1008                   sg.addOrRemove(aa[selectedRow].sequenceRef, true);
1009                 }
1010                 else
1011                 {
1012                   // notionally, we should also add intermediate sequences from
1013                   // last added sequence ?
1014                   sg.addSequence(aa[selectedRow].sequenceRef, true);
1015                 }
1016               }
1017             }
1018             else
1019             {
1020               sg = new SequenceGroup();
1021               sg.setStartRes(0);
1022               sg.setEndRes(ap.av.getAlignment().getWidth() - 1);
1023               sg.addSequence(aa[selectedRow].sequenceRef, false);
1024             }
1025             ap.av.setSelectionGroup(sg);
1026             ap.paintAlignment(false, false);
1027             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
1028             ap.av.sendSelection();
1029           }
1030         }
1031       }
1032       return;
1033     }
1034   }
1035
1036   /**
1037    * do a single sequence copy to jalview and the system clipboard
1038    * 
1039    * @param sq
1040    *          sequence to be copied to clipboard
1041    */
1042   protected void copy_annotseqtoclipboard(SequenceI sq)
1043   {
1044     SequenceI[] seqs = new SequenceI[] { sq };
1045     String[] omitHidden = null;
1046     SequenceI[] dseqs = new SequenceI[] { sq.getDatasetSequence() };
1047     if (dseqs[0] == null)
1048     {
1049       dseqs[0] = new Sequence(sq);
1050       dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
1051               sq.getSequenceAsString()));
1052
1053       sq.setDatasetSequence(dseqs[0]);
1054     }
1055     Alignment ds = new Alignment(dseqs);
1056     if (av.hasHiddenColumns())
1057     {
1058       Iterator<int[]> it = av.getAlignment().getHiddenColumns()
1059               .getVisContigsIterator(0, sq.getLength(), false);
1060       omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
1061     }
1062
1063     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
1064     if (av.hasHiddenColumns())
1065     {
1066       alignmentStartEnd = av.getAlignment().getHiddenColumns()
1067               .getVisibleStartAndEndIndex(av.getAlignment().getWidth());
1068     }
1069
1070     String output = new FormatAdapter().formatSequences(FileFormat.Fasta,
1071             seqs, omitHidden, alignmentStartEnd);
1072
1073     Toolkit.getDefaultToolkit().getSystemClipboard()
1074             .setContents(new StringSelection(output), Desktop.instance);
1075
1076     HiddenColumns hiddenColumns = null;
1077
1078     if (av.hasHiddenColumns())
1079     {
1080       hiddenColumns = new HiddenColumns(
1081               av.getAlignment().getHiddenColumns());
1082     }
1083
1084     Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
1085                                                         // of a consensus
1086                                                         // sequence ? need to
1087                                                         // flag
1088         // sequence as special.
1089         hiddenColumns };
1090   }
1091
1092   /**
1093    * DOCUMENT ME!
1094    * 
1095    * @param g1
1096    *          DOCUMENT ME!
1097    */
1098   @Override
1099   public void paintComponent(Graphics g)
1100   {
1101     int width = getWidth();
1102     if (width == 0)
1103     {
1104       width = ap.calculateIdWidth().width + 4;
1105     }
1106
1107     Graphics2D g2 = (Graphics2D) g;
1108     if (av.antiAlias)
1109     {
1110       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1111               RenderingHints.VALUE_ANTIALIAS_ON);
1112     }
1113
1114     drawComponent(g2, true, width);
1115   }
1116
1117   /**
1118    * Draw the full set of annotation Labels for the alignment at the given
1119    * cursor
1120    * 
1121    * @param g
1122    *          Graphics2D instance (needed for font scaling)
1123    * @param width
1124    *          Width for scaling labels
1125    * 
1126    */
1127   public void drawComponent(Graphics g, int width)
1128   {
1129     drawComponent(g, false, width);
1130   }
1131
1132   /**
1133    * Draw the full set of annotation Labels for the alignment at the given
1134    * cursor
1135    * 
1136    * @param g
1137    *          Graphics2D instance (needed for font scaling)
1138    * @param clip
1139    *          - true indicates that only current visible area needs to be
1140    *          rendered
1141    * @param width
1142    *          Width for scaling labels
1143    */
1144   public void drawComponent(Graphics g, boolean clip, int width)
1145   {
1146     if (av.getFont().getSize() < 10)
1147     {
1148       g.setFont(font);
1149     }
1150     else
1151     {
1152       g.setFont(av.getFont());
1153     }
1154
1155     FontMetrics fm = g.getFontMetrics(g.getFont());
1156     g.setColor(Color.white);
1157     g.fillRect(0, 0, getWidth(), getHeight());
1158
1159     g.translate(0, getScrollOffset());
1160     g.setColor(Color.black);
1161
1162     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1163     int fontHeight = g.getFont().getSize();
1164     int y = 0;
1165     int x = 0;
1166     int graphExtras = 0;
1167     int offset = 0;
1168     Font baseFont = g.getFont();
1169     FontMetrics baseMetrics = fm;
1170     int ofontH = fontHeight;
1171     int sOffset = 0;
1172     int visHeight = 0;
1173     int[] visr = (ap != null && ap.getAnnotationPanel() != null)
1174             ? ap.getAnnotationPanel().getVisibleVRange()
1175             : null;
1176     if (clip && visr != null)
1177     {
1178       sOffset = visr[0];
1179       visHeight = visr[1];
1180     }
1181     boolean visible = true, before = false, after = false;
1182     if (aa != null)
1183     {
1184       hasHiddenRows = false;
1185       int olY = 0;
1186       for (int i = 0; i < aa.length; i++)
1187       {
1188         visible = true;
1189         if (!aa[i].visible)
1190         {
1191           hasHiddenRows = true;
1192           continue;
1193         }
1194         olY = y;
1195         y += aa[i].height;
1196         if (clip)
1197         {
1198           if (y < sOffset)
1199           {
1200             if (!before)
1201             {
1202               if (debugRedraw)
1203               {
1204                 System.out.println("before vis: " + i);
1205               }
1206               before = true;
1207             }
1208             // don't draw what isn't visible
1209             continue;
1210           }
1211           if (olY > visHeight)
1212           {
1213
1214             if (!after)
1215             {
1216               if (debugRedraw)
1217               {
1218                 System.out.println(
1219                         "Scroll offset: " + sOffset + " after vis: " + i);
1220               }
1221               after = true;
1222             }
1223             // don't draw what isn't visible
1224             continue;
1225           }
1226         }
1227         g.setColor(Color.black);
1228
1229         offset = -aa[i].height / 2;
1230
1231         if (aa[i].hasText)
1232         {
1233           offset += fm.getHeight() / 2;
1234           offset -= fm.getDescent();
1235         }
1236         else
1237         {
1238           offset += fm.getDescent();
1239         }
1240
1241         x = width - fm.stringWidth(aa[i].label) - 3;
1242
1243         if (aa[i].graphGroup > -1)
1244         {
1245           int groupSize = 0;
1246           // TODO: JAL-1291 revise rendering model so the graphGroup map is
1247           // computed efficiently for all visible labels
1248           for (int gg = 0; gg < aa.length; gg++)
1249           {
1250             if (aa[gg].graphGroup == aa[i].graphGroup)
1251             {
1252               groupSize++;
1253             }
1254           }
1255           if (groupSize * (fontHeight + 8) < aa[i].height)
1256           {
1257             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1258                     / 2;
1259           }
1260           else
1261           {
1262             // scale font to fit
1263             float h = aa[i].height / (float) groupSize, s;
1264             if (h < 9)
1265             {
1266               visible = false;
1267             }
1268             else
1269             {
1270               fontHeight = -8 + (int) h;
1271               s = ((float) fontHeight) / (float) ofontH;
1272               Font f = baseFont
1273                       .deriveFont(AffineTransform.getScaleInstance(s, s));
1274               g.setFont(f);
1275               fm = g.getFontMetrics();
1276               graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1277                       / 2;
1278             }
1279           }
1280           if (visible)
1281           {
1282             for (int gg = 0; gg < aa.length; gg++)
1283             {
1284               if (aa[gg].graphGroup == aa[i].graphGroup)
1285               {
1286                 x = width - fm.stringWidth(aa[gg].label) - 3;
1287                 g.drawString(aa[gg].label, x, y - graphExtras);
1288
1289                 if (aa[gg]._linecolour != null)
1290                 {
1291
1292                   g.setColor(aa[gg]._linecolour);
1293                   g.drawLine(x, y - graphExtras + 3,
1294                           x + fm.stringWidth(aa[gg].label),
1295                           y - graphExtras + 3);
1296                 }
1297
1298                 g.setColor(Color.black);
1299                 graphExtras += fontHeight + 8;
1300               }
1301             }
1302           }
1303           g.setFont(baseFont);
1304           fm = baseMetrics;
1305           fontHeight = ofontH;
1306         }
1307         else
1308         {
1309           g.drawString(aa[i].label, x, y + offset);
1310         }
1311       }
1312     }
1313
1314     if (!resizePanel && dragEvent != null && aa != null)
1315     {
1316       g.setColor(Color.lightGray);
1317       g.drawString(aa[selectedRow].label, dragEvent.getX(),
1318               dragEvent.getY() - getScrollOffset());
1319     }
1320
1321     if (!av.getWrapAlignment() && ((aa == null) || (aa.length < 1)))
1322     {
1323       g.drawString(MessageManager.getString("label.right_click"), 2, 8);
1324       g.drawString(MessageManager.getString("label.to_add_annotation"), 2,
1325               18);
1326     }
1327   }
1328
1329   public int getScrollOffset()
1330   {
1331     return scrollOffset;
1332   }
1333
1334   @Override
1335   public void mouseEntered(MouseEvent e)
1336   {
1337   }
1338 }