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