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