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