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