Merge branch 'develop' into trialMerge
[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           }
461         }
462         else if (!ap.av.isInfoLetterHeight())
463         {
464           ap.av.setIgnoreBelowBackground(cbmi.getState(), ap);
465         }
466         ap.alignmentChanged();
467       }
468     });
469     pop.add(cbmi);
470     final JCheckBoxMenuItem letterHeight = new JCheckBoxMenuItem(
471             MessageManager.getString("label.use_info_for_height"),
472             isGroupAnnotation ? ann.groupRef.isUseInfoLetterHeight()
473                     : ap.av.isInfoLetterHeight());
474     letterHeight.addActionListener(new ActionListener()
475     {
476       @Override
477       public void actionPerformed(ActionEvent e)
478       {
479         if (isGroupAnnotation)
480         {
481           ann.groupRef.setInfoLetterHeight((letterHeight.getState()));
482           ann.groupRef.setIgnoreBelowBackground(true);
483         }
484         else
485         {
486           ap.av.setInfoLetterHeight(letterHeight.getState(), ap);
487           ap.av.setIgnoreBelowBackground(true, ap);
488         }
489         ap.alignmentChanged();
490       }
491     });
492     pop.add(letterHeight);
493     if (isGroupAnnotation)
494     {
495       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
496               MessageManager.getString("label.show_group_histogram"),
497               ann.groupRef.isShowInformationHistogram());
498       chist.addActionListener(new ActionListener()
499       {
500         @Override
501         public void actionPerformed(ActionEvent e)
502         {
503           ann.groupRef.setShowInformationHistogram(chist.getState());
504           ap.repaint();
505         }
506       });
507       pop.add(chist);
508       final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
509               MessageManager.getString("label.show_group_logo"),
510               ann.groupRef.isShowHMMSequenceLogo());
511       cprofl.addActionListener(new ActionListener()
512       {
513         @Override
514         public void actionPerformed(ActionEvent e)
515         {
516           ann.groupRef.setshowHMMSequenceLogo(cprofl.getState());
517           ap.repaint();
518         }
519       });
520       pop.add(cprofl);
521       final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
522               MessageManager.getString("label.normalise_group_logo"),
523               ann.groupRef.isNormaliseHMMSequenceLogo());
524       cproflnorm.addActionListener(new ActionListener()
525       {
526         @Override
527         public void actionPerformed(ActionEvent e)
528         {
529           ann.groupRef
530                   .setNormaliseHMMSequenceLogo(cproflnorm.getState());
531           // automatically enable logo display if we're clicked
532           ann.groupRef.setshowHMMSequenceLogo(true);
533           ap.repaint();
534         }
535       });
536       pop.add(cproflnorm);
537     }
538     else
539     {
540       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
541               MessageManager.getString("label.show_histogram"),
542               av.isShowInformationHistogram());
543       chist.addActionListener(new ActionListener()
544       {
545         @Override
546         public void actionPerformed(ActionEvent e)
547         {
548           av.setShowInformationHistogram(chist.getState());
549           ap.repaint();
550         }
551       });
552       pop.add(chist);
553       final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
554               MessageManager.getString("label.show_logo"),
555               av.isShowHMMSequenceLogo());
556       cprof.addActionListener(new ActionListener()
557       {
558         @Override
559         public void actionPerformed(ActionEvent e)
560         {
561           av.updateInformation(ap);
562           av.setShowHMMSequenceLogo(cprof.getState());
563           ap.repaint();
564         }
565       });
566       pop.add(cprof);
567       final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
568               MessageManager.getString("label.normalise_logo"),
569               av.isNormaliseHMMSequenceLogo());
570       cprofnorm.addActionListener(new ActionListener()
571       {
572         @Override
573         public void actionPerformed(ActionEvent e)
574         {
575           av.setShowHMMSequenceLogo(true);
576           av.setNormaliseHMMSequenceLogo(cprofnorm.getState());
577           ap.repaint();
578         }
579       });
580       pop.add(cprofnorm);
581     }
582   }
583
584   /**
585    * Adds context menu options for (alignment or group) Consensus annotation
586    * 
587    * @param pop
588    * @param ann
589    */
590   protected void addConsensusMenu(JPopupMenu pop,
591           final AlignmentAnnotation ann)
592   {
593     final boolean isGroupAnnotation = ann.groupRef != null;
594     pop.addSeparator();
595
596     final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
597             MessageManager.getString("label.ignore_gaps_consensus"),
598             (ann.groupRef != null)
599                     ? ann.groupRef.isIgnoreGapsConsensus()
600                     : ap.av.isIgnoreGapsConsensus());
601     cbmi.addActionListener(new ActionListener()
602     {
603       @Override
604       public void actionPerformed(ActionEvent e)
605       {
606         if (isGroupAnnotation)
607         {
608           ann.groupRef.setIgnoreGapsConsensus(cbmi.getState());
609         }
610         else
611         {
612           ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
613         }
614         ap.alignmentChanged();
615       }
616     });
617     pop.add(cbmi);
618     if (isGroupAnnotation)
619     {
620       /*
621        * group consensus options
622        */
623       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
624               MessageManager.getString("label.show_group_histogram"),
625               ann.groupRef.isShowConsensusHistogram());
626       chist.addActionListener(new ActionListener()
627       {
628         @Override
629         public void actionPerformed(ActionEvent e)
630         {
631           ann.groupRef.setShowConsensusHistogram(chist.getState());
632           ap.repaint();
633         }
634       });
635       pop.add(chist);
636       final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
637               MessageManager.getString("label.show_group_logo"),
638               ann.groupRef.isShowSequenceLogo());
639       cprofl.addActionListener(new ActionListener()
640       {
641         @Override
642         public void actionPerformed(ActionEvent e)
643         {
644           ann.groupRef.setshowSequenceLogo(cprofl.getState());
645           ap.repaint();
646         }
647       });
648       pop.add(cprofl);
649       final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
650               MessageManager.getString("label.normalise_group_logo"),
651               ann.groupRef.isNormaliseSequenceLogo());
652       cproflnorm.addActionListener(new ActionListener()
653       {
654         @Override
655         public void actionPerformed(ActionEvent e)
656         {
657           ann.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
658           // automatically enable logo display if we're clicked
659           ann.groupRef.setshowSequenceLogo(true);
660           ap.repaint();
661         }
662       });
663       pop.add(cproflnorm);
664     }
665     else
666     {
667       /*
668        * alignment consensus options
669        */
670       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
671               MessageManager.getString("label.show_histogram"),
672               av.isShowConsensusHistogram());
673       chist.addActionListener(new ActionListener()
674       {
675         @Override
676         public void actionPerformed(ActionEvent e)
677         {
678           av.setShowConsensusHistogram(chist.getState());
679           ap.repaint();
680         }
681       });
682       pop.add(chist);
683       final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
684               MessageManager.getString("label.show_logo"),
685               av.isShowSequenceLogo());
686       cprof.addActionListener(new ActionListener()
687       {
688         @Override
689         public void actionPerformed(ActionEvent e)
690         {
691           av.setShowSequenceLogo(cprof.getState());
692           ap.repaint();
693         }
694       });
695       pop.add(cprof);
696       final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
697               MessageManager.getString("label.normalise_logo"),
698               av.isNormaliseSequenceLogo());
699       cprofnorm.addActionListener(new ActionListener()
700       {
701         @Override
702         public void actionPerformed(ActionEvent e)
703         {
704           av.setShowSequenceLogo(true);
705           av.setNormaliseSequenceLogo(cprofnorm.getState());
706           ap.repaint();
707         }
708       });
709       pop.add(cprofnorm);
710     }
711     final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
712     consclipbrd.addActionListener(this);
713     pop.add(consclipbrd);
714   }
715
716   /**
717    * Reorders annotation rows after a drag of a label
718    * 
719    * @param evt
720    */
721   @Override
722   public void mouseReleased(MouseEvent evt)
723   {
724     if (evt.isPopupTrigger())
725     {
726       showPopupMenu(evt);
727       return;
728     }
729
730     int start = selectedRow;
731     getSelectedRow(evt.getY() - getScrollOffset());
732     int end = selectedRow;
733
734     /*
735      * if dragging to resize instead, start == end
736      */
737     if (start != end)
738     {
739       // Swap these annotations
740       AlignmentAnnotation startAA = ap.av.getAlignment()
741               .getAlignmentAnnotation()[start];
742       if (end == -1)
743       {
744         end = ap.av.getAlignment().getAlignmentAnnotation().length - 1;
745       }
746       AlignmentAnnotation endAA = ap.av.getAlignment()
747               .getAlignmentAnnotation()[end];
748
749       ap.av.getAlignment().getAlignmentAnnotation()[end] = startAA;
750       ap.av.getAlignment().getAlignmentAnnotation()[start] = endAA;
751     }
752
753     resizePanel = false;
754     dragEvent = null;
755     repaint();
756     ap.getAnnotationPanel().repaint();
757   }
758
759   /**
760    * Removes the height adjuster image on leaving the panel, unless currently
761    * dragging it
762    */
763   @Override
764   public void mouseExited(MouseEvent evt)
765   {
766     if (resizePanel && dragEvent == null)
767     {
768       resizePanel = false;
769       repaint();
770     }
771   }
772
773   /**
774    * A mouse drag may be either an adjustment of the panel height (if flag
775    * resizePanel is set on), or a reordering of the annotation rows. The former
776    * is dealt with by this method, the latter in mouseReleased.
777    * 
778    * @param evt
779    */
780   @Override
781   public void mouseDragged(MouseEvent evt)
782   {
783     dragEvent = evt;
784
785     if (resizePanel)
786     {
787       Dimension d = ap.annotationScroller.getPreferredSize();
788       int dif = evt.getY() - oldY;
789
790       dif /= ap.av.getCharHeight();
791       dif *= ap.av.getCharHeight();
792
793       if ((d.height - dif) > 20)
794       {
795         ap.annotationScroller
796                 .setPreferredSize(new Dimension(d.width, d.height - dif));
797         d = ap.annotationSpaceFillerHolder.getPreferredSize();
798         ap.annotationSpaceFillerHolder
799                 .setPreferredSize(new Dimension(d.width, d.height - dif));
800         ap.paintAlignment(true, false);
801       }
802
803       ap.addNotify();
804     }
805     else
806     {
807       repaint();
808     }
809   }
810
811   /**
812    * Updates the tooltip as the mouse moves over the labels
813    * 
814    * @param evt
815    */
816   @Override
817   public void mouseMoved(MouseEvent evt)
818   {
819     showOrHideAdjuster(evt);
820
821     getSelectedRow(evt.getY() - getScrollOffset());
822
823     if (selectedRow > -1 && ap.av.getAlignment()
824             .getAlignmentAnnotation().length > selectedRow)
825     {
826       AlignmentAnnotation aa = ap.av.getAlignment()
827               .getAlignmentAnnotation()[selectedRow];
828
829       StringBuffer desc = new StringBuffer();
830       if (aa.description != null
831               && !aa.description.equals("New description"))
832       {
833         // TODO: we could refactor and merge this code with the code in
834         // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
835         // tooltips
836         desc.append(aa.getDescription(true).trim());
837         // check to see if the description is an html fragment.
838         if (desc.length() < 6 || (desc.substring(0, 6).toLowerCase()
839                 .indexOf("<html>") < 0))
840         {
841           // clean the description ready for embedding in html
842           desc = new StringBuffer(LEFT_ANGLE_BRACKET_PATTERN.matcher(desc)
843                   .replaceAll("&lt;"));
844           desc.insert(0, "<html>");
845         }
846         else
847         {
848           // remove terminating html if any
849           int i = desc.substring(desc.length() - 7).toLowerCase()
850                   .lastIndexOf("</html>");
851           if (i > -1)
852           {
853             desc.setLength(desc.length() - 7 + i);
854           }
855         }
856         if (aa.hasScore())
857         {
858           desc.append("<br/>");
859         }
860         // if (aa.hasProperties())
861         // {
862         // desc.append("<table>");
863         // for (String prop : aa.getProperties())
864         // {
865         // desc.append("<tr><td>" + prop + "</td><td>"
866         // + aa.getProperty(prop) + "</td><tr>");
867         // }
868         // desc.append("</table>");
869         // }
870       }
871       else
872       {
873         // begin the tooltip's html fragment
874         desc.append("<html>");
875         if (aa.hasScore())
876         {
877           // TODO: limit precision of score to avoid noise from imprecise
878           // doubles
879           // (64.7 becomes 64.7+/some tiny value).
880           desc.append(" Score: " + aa.score);
881         }
882       }
883       if (desc.length() > 6)
884       {
885         desc.append("</html>");
886         this.setToolTipText(desc.toString());
887       }
888       else
889       {
890         this.setToolTipText(null);
891       }
892     }
893   }
894
895   /**
896    * Shows the height adjuster image if the mouse moves into the top left
897    * region, or hides it if the mouse leaves the regio
898    * 
899    * @param evt
900    */
901   protected void showOrHideAdjuster(MouseEvent evt)
902   {
903     boolean was = resizePanel;
904     resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
905
906     if (resizePanel != was)
907     {
908       setCursor(Cursor.getPredefinedCursor(
909               resizePanel ? Cursor.S_RESIZE_CURSOR
910                       : Cursor.DEFAULT_CURSOR));
911       repaint();
912     }
913   }
914
915   @Override
916   public void mouseClicked(MouseEvent evt)
917   {
918     final AlignmentAnnotation[] aa = ap.av.getAlignment()
919             .getAlignmentAnnotation();
920     if (!evt.isPopupTrigger() && SwingUtilities.isLeftMouseButton(evt))
921     {
922       if (selectedRow > -1 && selectedRow < aa.length)
923       {
924         if (aa[selectedRow].groupRef != null)
925         {
926           if (evt.getClickCount() >= 2)
927           {
928             // todo: make the ap scroll to the selection - not necessary, first
929             // click highlights/scrolls, second selects
930             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
931             // process modifiers
932             SequenceGroup sg = ap.av.getSelectionGroup();
933             if (sg == null || sg == aa[selectedRow].groupRef
934                     || !(Platform.isControlDown(evt) || evt.isShiftDown()))
935             {
936               if (Platform.isControlDown(evt) || evt.isShiftDown())
937               {
938                 // clone a new selection group from the associated group
939                 ap.av.setSelectionGroup(
940                         new SequenceGroup(aa[selectedRow].groupRef));
941               }
942               else
943               {
944                 // set selection to the associated group so it can be edited
945                 ap.av.setSelectionGroup(aa[selectedRow].groupRef);
946               }
947             }
948             else
949             {
950               // modify current selection with associated group
951               int remainToAdd = aa[selectedRow].groupRef.getSize();
952               for (SequenceI sgs : aa[selectedRow].groupRef.getSequences())
953               {
954                 if (jalview.util.Platform.isControlDown(evt))
955                 {
956                   sg.addOrRemove(sgs, --remainToAdd == 0);
957                 }
958                 else
959                 {
960                   // notionally, we should also add intermediate sequences from
961                   // last added sequence ?
962                   sg.addSequence(sgs, --remainToAdd == 0);
963                 }
964               }
965             }
966
967             ap.paintAlignment(false, false);
968             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
969             ap.av.sendSelection();
970           }
971           else
972           {
973             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(
974                     aa[selectedRow].groupRef.getSequences(null));
975           }
976           return;
977         }
978         else if (aa[selectedRow].sequenceRef != null)
979         {
980           if (evt.getClickCount() == 1)
981           {
982             ap.getSeqPanel().ap.getIdPanel()
983                     .highlightSearchResults(Arrays.asList(new SequenceI[]
984                     { aa[selectedRow].sequenceRef }));
985           }
986           else if (evt.getClickCount() >= 2)
987           {
988             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
989             SequenceGroup sg = ap.av.getSelectionGroup();
990             if (sg != null)
991             {
992               // we make a copy rather than edit the current selection if no
993               // modifiers pressed
994               // see Enhancement JAL-1557
995               if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
996               {
997                 sg = new SequenceGroup(sg);
998                 sg.clear();
999                 sg.addSequence(aa[selectedRow].sequenceRef, false);
1000               }
1001               else
1002               {
1003                 if (Platform.isControlDown(evt))
1004                 {
1005                   sg.addOrRemove(aa[selectedRow].sequenceRef, true);
1006                 }
1007                 else
1008                 {
1009                   // notionally, we should also add intermediate sequences from
1010                   // last added sequence ?
1011                   sg.addSequence(aa[selectedRow].sequenceRef, true);
1012                 }
1013               }
1014             }
1015             else
1016             {
1017               sg = new SequenceGroup();
1018               sg.setStartRes(0);
1019               sg.setEndRes(ap.av.getAlignment().getWidth() - 1);
1020               sg.addSequence(aa[selectedRow].sequenceRef, false);
1021             }
1022             ap.av.setSelectionGroup(sg);
1023             ap.paintAlignment(false, false);
1024             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
1025             ap.av.sendSelection();
1026           }
1027         }
1028       }
1029       return;
1030     }
1031   }
1032
1033   /**
1034    * do a single sequence copy to jalview and the system clipboard
1035    * 
1036    * @param sq
1037    *          sequence to be copied to clipboard
1038    */
1039   protected void copy_annotseqtoclipboard(SequenceI sq)
1040   {
1041     SequenceI[] seqs = new SequenceI[] { sq };
1042     String[] omitHidden = null;
1043     SequenceI[] dseqs = new SequenceI[] { sq.getDatasetSequence() };
1044     if (dseqs[0] == null)
1045     {
1046       dseqs[0] = new Sequence(sq);
1047       dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
1048               sq.getSequenceAsString()));
1049
1050       sq.setDatasetSequence(dseqs[0]);
1051     }
1052     Alignment ds = new Alignment(dseqs);
1053     if (av.hasHiddenColumns())
1054     {
1055       Iterator<int[]> it = av.getAlignment().getHiddenColumns()
1056               .getVisContigsIterator(0, sq.getLength(), false);
1057       omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
1058     }
1059
1060     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
1061     if (av.hasHiddenColumns())
1062     {
1063       alignmentStartEnd = av.getAlignment().getHiddenColumns()
1064               .getVisibleStartAndEndIndex(av.getAlignment().getWidth());
1065     }
1066
1067     String output = new FormatAdapter().formatSequences(FileFormat.Fasta,
1068             seqs, omitHidden, alignmentStartEnd);
1069
1070     Toolkit.getDefaultToolkit().getSystemClipboard()
1071             .setContents(new StringSelection(output), Desktop.instance);
1072
1073     HiddenColumns hiddenColumns = null;
1074
1075     if (av.hasHiddenColumns())
1076     {
1077       hiddenColumns = new HiddenColumns(
1078               av.getAlignment().getHiddenColumns());
1079     }
1080
1081     Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
1082                                                         // of a consensus
1083                                                         // sequence ? need to
1084                                                         // flag
1085         // sequence as special.
1086         hiddenColumns };
1087   }
1088
1089   /**
1090    * DOCUMENT ME!
1091    * 
1092    * @param g1
1093    *          DOCUMENT ME!
1094    */
1095   @Override
1096   public void paintComponent(Graphics g)
1097   {
1098     int width = getWidth();
1099     if (width == 0)
1100     {
1101       width = ap.calculateIdWidth().width + 4;
1102     }
1103
1104     Graphics2D g2 = (Graphics2D) g;
1105     if (av.antiAlias)
1106     {
1107       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1108               RenderingHints.VALUE_ANTIALIAS_ON);
1109     }
1110
1111     drawComponent(g2, true, width);
1112   }
1113
1114   /**
1115    * Draw the full set of annotation Labels for the alignment at the given
1116    * cursor
1117    * 
1118    * @param g
1119    *          Graphics2D instance (needed for font scaling)
1120    * @param width
1121    *          Width for scaling labels
1122    * 
1123    */
1124   public void drawComponent(Graphics g, int width)
1125   {
1126     drawComponent(g, false, width);
1127   }
1128
1129   /**
1130    * Draw the full set of annotation Labels for the alignment at the given
1131    * cursor
1132    * 
1133    * @param g
1134    *          Graphics2D instance (needed for font scaling)
1135    * @param clip
1136    *          - true indicates that only current visible area needs to be
1137    *          rendered
1138    * @param width
1139    *          Width for scaling labels
1140    */
1141   public void drawComponent(Graphics g, boolean clip, int width)
1142   {
1143     if (av.getFont().getSize() < 10)
1144     {
1145       g.setFont(font);
1146     }
1147     else
1148     {
1149       g.setFont(av.getFont());
1150     }
1151
1152     FontMetrics fm = g.getFontMetrics(g.getFont());
1153     g.setColor(Color.white);
1154     g.fillRect(0, 0, getWidth(), getHeight());
1155
1156     g.translate(0, getScrollOffset());
1157     g.setColor(Color.black);
1158
1159     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1160     int fontHeight = g.getFont().getSize();
1161     int y = 0;
1162     int x = 0;
1163     int graphExtras = 0;
1164     int offset = 0;
1165     Font baseFont = g.getFont();
1166     FontMetrics baseMetrics = fm;
1167     int ofontH = fontHeight;
1168     int sOffset = 0;
1169     int visHeight = 0;
1170     int[] visr = (ap != null && ap.getAnnotationPanel() != null)
1171             ? ap.getAnnotationPanel().getVisibleVRange()
1172             : null;
1173     if (clip && visr != null)
1174     {
1175       sOffset = visr[0];
1176       visHeight = visr[1];
1177     }
1178     boolean visible = true, before = false, after = false;
1179     if (aa != null)
1180     {
1181       hasHiddenRows = false;
1182       int olY = 0;
1183       for (int i = 0; i < aa.length; i++)
1184       {
1185         visible = true;
1186         if (!aa[i].visible)
1187         {
1188           hasHiddenRows = true;
1189           continue;
1190         }
1191         olY = y;
1192         y += aa[i].height;
1193         if (clip)
1194         {
1195           if (y < sOffset)
1196           {
1197             if (!before)
1198             {
1199               if (debugRedraw)
1200               {
1201                 System.out.println("before vis: " + i);
1202               }
1203               before = true;
1204             }
1205             // don't draw what isn't visible
1206             continue;
1207           }
1208           if (olY > visHeight)
1209           {
1210
1211             if (!after)
1212             {
1213               if (debugRedraw)
1214               {
1215                 System.out.println(
1216                         "Scroll offset: " + sOffset + " after vis: " + i);
1217               }
1218               after = true;
1219             }
1220             // don't draw what isn't visible
1221             continue;
1222           }
1223         }
1224         g.setColor(Color.black);
1225
1226         offset = -aa[i].height / 2;
1227
1228         if (aa[i].hasText)
1229         {
1230           offset += fm.getHeight() / 2;
1231           offset -= fm.getDescent();
1232         }
1233         else
1234         {
1235           offset += fm.getDescent();
1236         }
1237
1238         x = width - fm.stringWidth(aa[i].label) - 3;
1239
1240         if (aa[i].graphGroup > -1)
1241         {
1242           int groupSize = 0;
1243           // TODO: JAL-1291 revise rendering model so the graphGroup map is
1244           // computed efficiently for all visible labels
1245           for (int gg = 0; gg < aa.length; gg++)
1246           {
1247             if (aa[gg].graphGroup == aa[i].graphGroup)
1248             {
1249               groupSize++;
1250             }
1251           }
1252           if (groupSize * (fontHeight + 8) < aa[i].height)
1253           {
1254             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1255                     / 2;
1256           }
1257           else
1258           {
1259             // scale font to fit
1260             float h = aa[i].height / (float) groupSize, s;
1261             if (h < 9)
1262             {
1263               visible = false;
1264             }
1265             else
1266             {
1267               fontHeight = -8 + (int) h;
1268               s = ((float) fontHeight) / (float) ofontH;
1269               Font f = baseFont
1270                       .deriveFont(AffineTransform.getScaleInstance(s, s));
1271               g.setFont(f);
1272               fm = g.getFontMetrics();
1273               graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1274                       / 2;
1275             }
1276           }
1277           if (visible)
1278           {
1279             for (int gg = 0; gg < aa.length; gg++)
1280             {
1281               if (aa[gg].graphGroup == aa[i].graphGroup)
1282               {
1283                 x = width - fm.stringWidth(aa[gg].label) - 3;
1284                 g.drawString(aa[gg].label, x, y - graphExtras);
1285
1286                 if (aa[gg]._linecolour != null)
1287                 {
1288
1289                   g.setColor(aa[gg]._linecolour);
1290                   g.drawLine(x, y - graphExtras + 3,
1291                           x + fm.stringWidth(aa[gg].label),
1292                           y - graphExtras + 3);
1293                 }
1294
1295                 g.setColor(Color.black);
1296                 graphExtras += fontHeight + 8;
1297               }
1298             }
1299           }
1300           g.setFont(baseFont);
1301           fm = baseMetrics;
1302           fontHeight = ofontH;
1303         }
1304         else
1305         {
1306           g.drawString(aa[i].label, x, y + offset);
1307         }
1308       }
1309     }
1310
1311     if (!resizePanel && dragEvent != null && aa != null)
1312     {
1313       g.setColor(Color.lightGray);
1314       g.drawString(aa[selectedRow].label, dragEvent.getX(),
1315               dragEvent.getY() - getScrollOffset());
1316     }
1317
1318     if (!av.getWrapAlignment() && ((aa == null) || (aa.length < 1)))
1319     {
1320       g.drawString(MessageManager.getString("label.right_click"), 2, 8);
1321       g.drawString(MessageManager.getString("label.to_add_annotation"), 2,
1322               18);
1323     }
1324   }
1325
1326   public int getScrollOffset()
1327   {
1328     return scrollOffset;
1329   }
1330
1331   @Override
1332   public void mouseEntered(MouseEvent e)
1333   {
1334   }
1335 }