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