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