JAL-4134 popup menu and annotation row property to control display of display of...
[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.hasGroups())
436           {
437             JCheckBoxMenuItem chitem = new JCheckBoxMenuItem("Show Groups on Matrix");
438             boolean showGroups = aa[selectedRow].isShowGroupsForContactMatrix();
439             final AlignmentAnnotation sel_row=aa[selectedRow];
440             chitem.setState(showGroups);
441             chitem.addActionListener(new ActionListener()
442             {
443
444               @Override
445               public void actionPerformed(ActionEvent e)
446               {
447                 sel_row.setShowGroupsForContactMatrix(chitem.getState());
448                 ap.getAnnotationPanel()
449                 .paint(ap.getAnnotationPanel().getGraphics());
450               }
451             });
452             pop.add(chitem);
453           }
454           if (cm.hasTree())
455           {
456             item = new JMenuItem("Show Tree for Matrix");
457             item.addActionListener(new ActionListener()
458             {
459
460               @Override
461               public void actionPerformed(ActionEvent e)
462               {
463
464                 ap.alignFrame.showContactMapTree(aa[selectedRow], cm);
465
466               }
467             });
468             pop.add(item);
469           }
470           else
471           {
472             item = new JMenuItem("Calculate Tree for Matrix");
473             item.addActionListener(new ActionListener()
474             {
475               // TODO - refactor to analysis background thread
476               @Override
477               public void actionPerformed(ActionEvent e)
478               {
479                 new Thread(new Runnable()
480                 {
481                   @Override
482                   public void run()
483                   {
484                     AlignmentAnnotation alan = aa[selectedRow];
485                     cm.setGroupSet(GroupSet.makeGroups(cm, 5f, true));
486                     ap.alignFrame.showContactMapTree(alan, cm);
487                   }
488                 }).start();
489               }
490             });
491             pop.add(item);
492
493           }
494           // Show/Hide group shading on matrix view
495           // Set/adjust threshold for grouping ?
496           // colour alignment by this [type]
497           // select/hide columns by this row
498           
499         }
500       }
501     }
502     pop.show(this, evt.getX(), evt.getY());
503   }
504
505   /**
506    * A helper method that adds menu options for calculation and visualisation of
507    * group and/or alignment consensus annotation to a popup menu. This is
508    * designed to be reusable for either unwrapped mode (popup menu is shown on
509    * component AnnotationLabels), or wrapped mode (popup menu is shown on
510    * IdPanel when the mouse is over an annotation label).
511    * 
512    * @param ap
513    * @param ann
514    * @param pop
515    */
516   static void addConsensusMenuOptions(AlignmentPanel ap,
517           AlignmentAnnotation ann, JPopupMenu pop)
518   {
519     pop.addSeparator();
520
521     final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
522             MessageManager.getString("label.ignore_gaps_consensus"),
523             (ann.groupRef != null) ? ann.groupRef.getIgnoreGapsConsensus()
524                     : ap.av.isIgnoreGapsConsensus());
525     final AlignmentAnnotation aaa = ann;
526     cbmi.addActionListener(new ActionListener()
527     {
528       @Override
529       public void actionPerformed(ActionEvent e)
530       {
531         if (aaa.groupRef != null)
532         {
533           aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
534           ap.getAnnotationPanel()
535                   .paint(ap.getAnnotationPanel().getGraphics());
536         }
537         else
538         {
539           ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
540         }
541         ap.alignmentChanged();
542       }
543     });
544     pop.add(cbmi);
545
546     if (aaa.groupRef != null)
547     {
548       /*
549        * group consensus options
550        */
551       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
552               MessageManager.getString("label.show_group_histogram"),
553               ann.groupRef.isShowConsensusHistogram());
554       chist.addActionListener(new ActionListener()
555       {
556         @Override
557         public void actionPerformed(ActionEvent e)
558         {
559           aaa.groupRef.setShowConsensusHistogram(chist.getState());
560           ap.repaint();
561         }
562       });
563       pop.add(chist);
564       final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
565               MessageManager.getString("label.show_group_logo"),
566               ann.groupRef.isShowSequenceLogo());
567       cprofl.addActionListener(new ActionListener()
568       {
569         @Override
570         public void actionPerformed(ActionEvent e)
571         {
572           aaa.groupRef.setshowSequenceLogo(cprofl.getState());
573           ap.repaint();
574         }
575       });
576       pop.add(cprofl);
577       final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
578               MessageManager.getString("label.normalise_group_logo"),
579               ann.groupRef.isNormaliseSequenceLogo());
580       cproflnorm.addActionListener(new ActionListener()
581       {
582         @Override
583         public void actionPerformed(ActionEvent e)
584         {
585           aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
586           // automatically enable logo display if we're clicked
587           aaa.groupRef.setshowSequenceLogo(true);
588           ap.repaint();
589         }
590       });
591       pop.add(cproflnorm);
592     }
593     else
594     {
595       /*
596        * alignment consensus options
597        */
598       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
599               MessageManager.getString("label.show_histogram"),
600               ap.av.isShowConsensusHistogram());
601       chist.addActionListener(new ActionListener()
602       {
603         @Override
604         public void actionPerformed(ActionEvent e)
605         {
606           ap.av.setShowConsensusHistogram(chist.getState());
607           ap.alignFrame.setMenusForViewport();
608           ap.repaint();
609         }
610       });
611       pop.add(chist);
612       final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
613               MessageManager.getString("label.show_logo"),
614               ap.av.isShowSequenceLogo());
615       cprof.addActionListener(new ActionListener()
616       {
617         @Override
618         public void actionPerformed(ActionEvent e)
619         {
620           ap.av.setShowSequenceLogo(cprof.getState());
621           ap.alignFrame.setMenusForViewport();
622           ap.repaint();
623         }
624       });
625       pop.add(cprof);
626       final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
627               MessageManager.getString("label.normalise_logo"),
628               ap.av.isNormaliseSequenceLogo());
629       cprofnorm.addActionListener(new ActionListener()
630       {
631         @Override
632         public void actionPerformed(ActionEvent e)
633         {
634           ap.av.setShowSequenceLogo(true);
635           ap.av.setNormaliseSequenceLogo(cprofnorm.getState());
636           ap.alignFrame.setMenusForViewport();
637           ap.repaint();
638         }
639       });
640       pop.add(cprofnorm);
641     }
642   }
643
644   /**
645    * Reorders annotation rows after a drag of a label
646    * 
647    * @param evt
648    */
649   @Override
650   public void mouseReleased(MouseEvent evt)
651   {
652     if (evt.isPopupTrigger())
653     {
654       showPopupMenu(evt);
655       return;
656     }
657
658     int start = selectedRow;
659     getSelectedRow(evt.getY() - getScrollOffset());
660     int end = selectedRow;
661
662     /*
663      * if dragging to resize instead, start == end
664      */
665     if (start != end)
666     {
667       // Swap these annotations
668       AlignmentAnnotation startAA = ap.av.getAlignment()
669               .getAlignmentAnnotation()[start];
670       if (end == -1)
671       {
672         end = ap.av.getAlignment().getAlignmentAnnotation().length - 1;
673       }
674       AlignmentAnnotation endAA = ap.av.getAlignment()
675               .getAlignmentAnnotation()[end];
676
677       ap.av.getAlignment().getAlignmentAnnotation()[end] = startAA;
678       ap.av.getAlignment().getAlignmentAnnotation()[start] = endAA;
679     }
680
681     resizePanel = false;
682     dragEvent = null;
683     repaint();
684     ap.getAnnotationPanel().repaint();
685   }
686
687   /**
688    * Removes the height adjuster image on leaving the panel, unless currently
689    * dragging it
690    */
691   @Override
692   public void mouseExited(MouseEvent evt)
693   {
694     if (resizePanel && dragEvent == null)
695     {
696       resizePanel = false;
697       repaint();
698     }
699   }
700
701   /**
702    * A mouse drag may be either an adjustment of the panel height (if flag
703    * resizePanel is set on), or a reordering of the annotation rows. The former
704    * is dealt with by this method, the latter in mouseReleased.
705    * 
706    * @param evt
707    */
708   @Override
709   public void mouseDragged(MouseEvent evt)
710   {
711     dragEvent = evt;
712
713     if (resizePanel)
714     {
715       Dimension d = ap.annotationScroller.getPreferredSize();
716       int dif = evt.getY() - oldY;
717
718       dif /= ap.av.getCharHeight();
719       dif *= ap.av.getCharHeight();
720
721       if ((d.height - dif) > 20)
722       {
723         ap.annotationScroller
724                 .setPreferredSize(new Dimension(d.width, d.height - dif));
725         d = ap.annotationSpaceFillerHolder.getPreferredSize();
726         ap.annotationSpaceFillerHolder
727                 .setPreferredSize(new Dimension(d.width, d.height - dif));
728         ap.paintAlignment(true, false);
729       }
730
731       ap.addNotify();
732     }
733     else
734     {
735       repaint();
736     }
737   }
738
739   /**
740    * Updates the tooltip as the mouse moves over the labels
741    * 
742    * @param evt
743    */
744   @Override
745   public void mouseMoved(MouseEvent evt)
746   {
747     showOrHideAdjuster(evt);
748
749     getSelectedRow(evt.getY() - getScrollOffset());
750
751     if (selectedRow > -1 && ap.av.getAlignment()
752             .getAlignmentAnnotation().length > selectedRow)
753     {
754       AlignmentAnnotation[] anns = ap.av.getAlignment()
755               .getAlignmentAnnotation();
756       AlignmentAnnotation aa = anns[selectedRow];
757
758       String desc = getTooltip(aa);
759       this.setToolTipText(desc);
760       String msg = getStatusMessage(aa, anns);
761       ap.alignFrame.setStatus(msg);
762     }
763   }
764
765   /**
766    * Constructs suitable text to show in the status bar when over an annotation
767    * label, containing the associated sequence name (if any), and the annotation
768    * labels (or all labels for a graph group annotation)
769    * 
770    * @param aa
771    * @param anns
772    * @return
773    */
774   static String getStatusMessage(AlignmentAnnotation aa,
775           AlignmentAnnotation[] anns)
776   {
777     if (aa == null)
778     {
779       return null;
780     }
781
782     StringBuilder msg = new StringBuilder(32);
783     if (aa.sequenceRef != null)
784     {
785       msg.append(aa.sequenceRef.getName()).append(" : ");
786     }
787
788     if (aa.graphGroup == -1)
789     {
790       msg.append(aa.label);
791     }
792     else if (anns != null)
793     {
794       boolean first = true;
795       for (int i = anns.length - 1; i >= 0; i--)
796       {
797         if (anns[i].graphGroup == aa.graphGroup)
798         {
799           if (!first)
800           {
801             msg.append(", ");
802           }
803           msg.append(anns[i].label);
804           first = false;
805         }
806       }
807     }
808
809     return msg.toString();
810   }
811
812   /**
813    * Answers a tooltip, formatted as html, containing the annotation description
814    * (prefixed by associated sequence id if applicable), and the annotation
815    * (non-positional) score if it has one. Answers null if neither description
816    * nor score is found.
817    * 
818    * @param aa
819    * @return
820    */
821   static String getTooltip(AlignmentAnnotation aa)
822   {
823     if (aa == null)
824     {
825       return null;
826     }
827     StringBuilder tooltip = new StringBuilder();
828     if (aa.description != null && !aa.description.equals("New description"))
829     {
830       // TODO: we could refactor and merge this code with the code in
831       // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
832       // tooltips
833       String desc = aa.getDescription(true).trim();
834       if (!desc.toLowerCase(Locale.ROOT).startsWith(HTML_START_TAG))
835       {
836         tooltip.append(HTML_START_TAG);
837         desc = desc.replace("<", "&lt;");
838       }
839       else if (desc.toLowerCase(Locale.ROOT).endsWith(HTML_END_TAG))
840       {
841         desc = desc.substring(0, desc.length() - HTML_END_TAG.length());
842       }
843       tooltip.append(desc);
844     }
845     else
846     {
847       // begin the tooltip's html fragment
848       tooltip.append(HTML_START_TAG);
849     }
850     if (aa.hasScore())
851     {
852       if (tooltip.length() > HTML_START_TAG.length())
853       {
854         tooltip.append("<br/>");
855       }
856       // TODO: limit precision of score to avoid noise from imprecise
857       // doubles
858       // (64.7 becomes 64.7+/some tiny value).
859       tooltip.append(" Score: ").append(String.valueOf(aa.score));
860     }
861
862     if (tooltip.length() > HTML_START_TAG.length())
863     {
864       return tooltip.append(HTML_END_TAG).toString();
865     }
866
867     /*
868      * nothing in the tooltip (except "<html>")
869      */
870     return null;
871   }
872
873   /**
874    * Shows the height adjuster image if the mouse moves into the top left
875    * region, or hides it if the mouse leaves the regio
876    * 
877    * @param evt
878    */
879   protected void showOrHideAdjuster(MouseEvent evt)
880   {
881     boolean was = resizePanel;
882     resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT
883             && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
884
885     if (resizePanel != was)
886     {
887       setCursor(Cursor
888               .getPredefinedCursor(resizePanel ? Cursor.S_RESIZE_CURSOR
889                       : Cursor.DEFAULT_CURSOR));
890       repaint();
891     }
892   }
893
894   @Override
895   public void mouseClicked(MouseEvent evt)
896   {
897     final AlignmentAnnotation[] aa = ap.av.getAlignment()
898             .getAlignmentAnnotation();
899     if (!evt.isPopupTrigger() && SwingUtilities.isLeftMouseButton(evt))
900     {
901       if (selectedRow > -1 && selectedRow < aa.length)
902       {
903         if (aa[selectedRow].groupRef != null)
904         {
905           if (evt.getClickCount() >= 2)
906           {
907             // todo: make the ap scroll to the selection - not necessary, first
908             // click highlights/scrolls, second selects
909             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
910             // process modifiers
911             SequenceGroup sg = ap.av.getSelectionGroup();
912             if (sg == null || sg == aa[selectedRow].groupRef
913                     || !(Platform.isControlDown(evt) || evt.isShiftDown()))
914             {
915               if (Platform.isControlDown(evt) || evt.isShiftDown())
916               {
917                 // clone a new selection group from the associated group
918                 ap.av.setSelectionGroup(
919                         new SequenceGroup(aa[selectedRow].groupRef));
920               }
921               else
922               {
923                 // set selection to the associated group so it can be edited
924                 ap.av.setSelectionGroup(aa[selectedRow].groupRef);
925               }
926             }
927             else
928             {
929               // modify current selection with associated group
930               int remainToAdd = aa[selectedRow].groupRef.getSize();
931               for (SequenceI sgs : aa[selectedRow].groupRef.getSequences())
932               {
933                 if (jalview.util.Platform.isControlDown(evt))
934                 {
935                   sg.addOrRemove(sgs, --remainToAdd == 0);
936                 }
937                 else
938                 {
939                   // notionally, we should also add intermediate sequences from
940                   // last added sequence ?
941                   sg.addSequence(sgs, --remainToAdd == 0);
942                 }
943               }
944             }
945
946             ap.paintAlignment(false, false);
947             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
948             ap.av.sendSelection();
949           }
950           else
951           {
952             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(
953                     aa[selectedRow].groupRef.getSequences(null));
954           }
955           return;
956         }
957         else if (aa[selectedRow].sequenceRef != null)
958         {
959           if (evt.getClickCount() == 1)
960           {
961             ap.getSeqPanel().ap.getIdPanel()
962                     .highlightSearchResults(Arrays.asList(new SequenceI[]
963                     { aa[selectedRow].sequenceRef }));
964           }
965           else if (evt.getClickCount() >= 2)
966           {
967             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
968             SequenceGroup sg = ap.av.getSelectionGroup();
969             if (sg != null)
970             {
971               // we make a copy rather than edit the current selection if no
972               // modifiers pressed
973               // see Enhancement JAL-1557
974               if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
975               {
976                 sg = new SequenceGroup(sg);
977                 sg.clear();
978                 sg.addSequence(aa[selectedRow].sequenceRef, false);
979               }
980               else
981               {
982                 if (Platform.isControlDown(evt))
983                 {
984                   sg.addOrRemove(aa[selectedRow].sequenceRef, true);
985                 }
986                 else
987                 {
988                   // notionally, we should also add intermediate sequences from
989                   // last added sequence ?
990                   sg.addSequence(aa[selectedRow].sequenceRef, true);
991                 }
992               }
993             }
994             else
995             {
996               sg = new SequenceGroup();
997               sg.setStartRes(0);
998               sg.setEndRes(ap.av.getAlignment().getWidth() - 1);
999               sg.addSequence(aa[selectedRow].sequenceRef, false);
1000             }
1001             ap.av.setSelectionGroup(sg);
1002             ap.paintAlignment(false, false);
1003             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
1004             ap.av.sendSelection();
1005           }
1006
1007         }
1008       }
1009       return;
1010     }
1011   }
1012
1013   /**
1014    * do a single sequence copy to jalview and the system clipboard
1015    * 
1016    * @param sq
1017    *          sequence to be copied to clipboard
1018    */
1019   protected void copy_annotseqtoclipboard(SequenceI sq)
1020   {
1021     SequenceI[] seqs = new SequenceI[] { sq };
1022     String[] omitHidden = null;
1023     SequenceI[] dseqs = new SequenceI[] { sq.getDatasetSequence() };
1024     if (dseqs[0] == null)
1025     {
1026       dseqs[0] = new Sequence(sq);
1027       dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
1028               sq.getSequenceAsString()));
1029
1030       sq.setDatasetSequence(dseqs[0]);
1031     }
1032     Alignment ds = new Alignment(dseqs);
1033     if (av.hasHiddenColumns())
1034     {
1035       Iterator<int[]> it = av.getAlignment().getHiddenColumns()
1036               .getVisContigsIterator(0, sq.getLength(), false);
1037       omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
1038     }
1039
1040     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
1041     if (av.hasHiddenColumns())
1042     {
1043       alignmentStartEnd = av.getAlignment().getHiddenColumns()
1044               .getVisibleStartAndEndIndex(av.getAlignment().getWidth());
1045     }
1046
1047     String output = new FormatAdapter().formatSequences(FileFormat.Fasta,
1048             seqs, omitHidden, alignmentStartEnd);
1049
1050     Toolkit.getDefaultToolkit().getSystemClipboard()
1051             .setContents(new StringSelection(output), Desktop.instance);
1052
1053     HiddenColumns hiddenColumns = null;
1054
1055     if (av.hasHiddenColumns())
1056     {
1057       hiddenColumns = new HiddenColumns(
1058               av.getAlignment().getHiddenColumns());
1059     }
1060
1061     Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
1062                                                         // of a consensus
1063                                                         // sequence ? need to
1064                                                         // flag
1065         // sequence as special.
1066         hiddenColumns };
1067   }
1068
1069   /**
1070    * DOCUMENT ME!
1071    * 
1072    * @param g1
1073    *          DOCUMENT ME!
1074    */
1075   @Override
1076   public void paintComponent(Graphics g)
1077   {
1078
1079     int width = getWidth();
1080     if (width == 0)
1081     {
1082       width = ap.calculateIdWidth().width;
1083     }
1084
1085     Graphics2D g2 = (Graphics2D) g;
1086     if (av.antiAlias)
1087     {
1088       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1089               RenderingHints.VALUE_ANTIALIAS_ON);
1090     }
1091
1092     drawComponent(g2, true, width);
1093
1094   }
1095
1096   /**
1097    * Draw the full set of annotation Labels for the alignment at the given
1098    * cursor
1099    * 
1100    * @param g
1101    *          Graphics2D instance (needed for font scaling)
1102    * @param width
1103    *          Width for scaling labels
1104    * 
1105    */
1106   public void drawComponent(Graphics g, int width)
1107   {
1108     drawComponent(g, false, width);
1109   }
1110
1111   /**
1112    * Draw the full set of annotation Labels for the alignment at the given
1113    * cursor
1114    * 
1115    * @param g
1116    *          Graphics2D instance (needed for font scaling)
1117    * @param clip
1118    *          - true indicates that only current visible area needs to be
1119    *          rendered
1120    * @param width
1121    *          Width for scaling labels
1122    */
1123   public void drawComponent(Graphics g, boolean clip, int width)
1124   {
1125     if (av.getFont().getSize() < 10)
1126     {
1127       g.setFont(font);
1128     }
1129     else
1130     {
1131       g.setFont(av.getFont());
1132     }
1133
1134     FontMetrics fm = g.getFontMetrics(g.getFont());
1135     g.setColor(Color.white);
1136     g.fillRect(0, 0, getWidth(), getHeight());
1137
1138     g.translate(0, getScrollOffset());
1139     g.setColor(Color.black);
1140     SequenceI lastSeqRef = null;
1141     String lastLabel = null;
1142     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1143     int fontHeight = g.getFont().getSize();
1144     int y = 0;
1145     int x = 0;
1146     int graphExtras = 0;
1147     int offset = 0;
1148     Font baseFont = g.getFont();
1149     FontMetrics baseMetrics = fm;
1150     int ofontH = fontHeight;
1151     int sOffset = 0;
1152     int visHeight = 0;
1153     int[] visr = (ap != null && ap.getAnnotationPanel() != null)
1154             ? ap.getAnnotationPanel().getVisibleVRange()
1155             : null;
1156     if (clip && visr != null)
1157     {
1158       sOffset = visr[0];
1159       visHeight = visr[1];
1160     }
1161     boolean visible = true, before = false, after = false;
1162     if (aa != null)
1163     {
1164       hasHiddenRows = false;
1165       int olY = 0;
1166       int nexAA = 0;
1167       for (int i = 0; i < aa.length; i++)
1168       {
1169         visible = true;
1170         if (!aa[i].visible)
1171         {
1172           hasHiddenRows = true;
1173           continue;
1174         }
1175         olY = y;
1176         // look ahead to next annotation
1177         for (nexAA = i + 1; nexAA < aa.length
1178                 && !aa[nexAA].visible; nexAA++)
1179           ;
1180         y += aa[i].height;
1181         if (clip)
1182         {
1183           if (y < sOffset)
1184           {
1185             if (!before)
1186             {
1187               if (debugRedraw)
1188               {
1189                 System.out.println("before vis: " + i);
1190               }
1191               before = true;
1192             }
1193             // don't draw what isn't visible
1194             continue;
1195           }
1196           if (olY > visHeight)
1197           {
1198
1199             if (!after)
1200             {
1201               if (debugRedraw)
1202               {
1203                 System.out.println(
1204                         "Scroll offset: " + sOffset + " after vis: " + i);
1205               }
1206               after = true;
1207             }
1208             // don't draw what isn't visible
1209             continue;
1210           }
1211         }
1212         g.setColor(Color.black);
1213
1214         offset = -aa[i].height / 2;
1215
1216         if (aa[i].hasText)
1217         {
1218           offset += fm.getHeight() / 2;
1219           offset -= fm.getDescent();
1220         }
1221         else
1222         {
1223           offset += fm.getDescent();
1224         }
1225         String label = aa[i].label;
1226         boolean vertBar = false;
1227         if ((lastLabel != null && lastLabel.equals(label)))
1228         {
1229           label = aa[i].description;
1230         }
1231         else
1232         {
1233           if (nexAA < aa.length && label.equals(aa[nexAA].label)) // &&
1234                                                                   // aa[nexY].sequenceRef==aa[i].sequenceRef)
1235           {
1236             lastLabel = label;
1237             // next label is the same as this label
1238             label = aa[i].description;
1239           }
1240           else
1241           {
1242             lastLabel = label;
1243           }
1244         }
1245         if (aa[i].sequenceRef != null)
1246         {
1247           if (aa[i].sequenceRef != lastSeqRef)
1248           {
1249             label = aa[i].sequenceRef.getName() + " " + label;
1250             // TODO record relationship between sequence and this annotation and
1251             // display it here
1252           }
1253           else
1254           {
1255             vertBar = true;
1256           }
1257         }
1258         x = width - fm.stringWidth(label) - 3;
1259
1260         if (aa[i].graphGroup > -1)
1261         {
1262           int groupSize = 0;
1263           // TODO: JAL-1291 revise rendering model so the graphGroup map is
1264           // computed efficiently for all visible labels
1265           for (int gg = 0; gg < aa.length; gg++)
1266           {
1267             if (aa[gg].graphGroup == aa[i].graphGroup)
1268             {
1269               groupSize++;
1270             }
1271           }
1272           if (groupSize * (fontHeight + 8) < aa[i].height)
1273           {
1274             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1275                     / 2;
1276           }
1277           else
1278           {
1279             // scale font to fit
1280             float h = aa[i].height / (float) groupSize, s;
1281             if (h < 9)
1282             {
1283               visible = false;
1284             }
1285             else
1286             {
1287               fontHeight = -8 + (int) h;
1288               s = ((float) fontHeight) / (float) ofontH;
1289               Font f = baseFont
1290                       .deriveFont(AffineTransform.getScaleInstance(s, s));
1291               g.setFont(f);
1292               fm = g.getFontMetrics();
1293               graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1294                       / 2;
1295             }
1296           }
1297           if (visible)
1298           {
1299             for (int gg = 0; gg < aa.length; gg++)
1300             {
1301               if (aa[gg].graphGroup == aa[i].graphGroup)
1302               {
1303                 x = width - fm.stringWidth(aa[gg].label) - 3;
1304                 g.drawString(aa[gg].label, x, y - graphExtras);
1305
1306                 if (aa[gg]._linecolour != null)
1307                 {
1308
1309                   g.setColor(aa[gg]._linecolour);
1310                   g.drawLine(x, y - graphExtras + 3,
1311                           x + fm.stringWidth(aa[gg].label),
1312                           y - graphExtras + 3);
1313                 }
1314
1315                 g.setColor(Color.black);
1316                 graphExtras += fontHeight + 8;
1317               }
1318             }
1319           }
1320           g.setFont(baseFont);
1321           fm = baseMetrics;
1322           fontHeight = ofontH;
1323         }
1324         else
1325         {
1326           if (vertBar)
1327           {
1328             g.drawLine(width - 3, y + offset - fontHeight, width - 3,
1329                     (int) (y - 1.5 * aa[i].height - offset - fontHeight));
1330             // g.drawLine(20, y + offset, x - 20, y + offset);
1331
1332           }
1333           g.drawString(label, x, y + offset);
1334         }
1335         lastSeqRef = aa[i].sequenceRef;
1336       }
1337     }
1338
1339     if (!resizePanel && dragEvent != null && aa != null)
1340     {
1341       g.setColor(Color.lightGray);
1342       g.drawString(
1343               (aa[selectedRow].sequenceRef == null ? ""
1344                       : aa[selectedRow].sequenceRef.getName())
1345                       + aa[selectedRow].label,
1346               dragEvent.getX(), dragEvent.getY() - getScrollOffset());
1347     }
1348
1349     if (!av.getWrapAlignment() && ((aa == null) || (aa.length < 1)))
1350     {
1351       g.drawString(MessageManager.getString("label.right_click"), 2, 8);
1352       g.drawString(MessageManager.getString("label.to_add_annotation"), 2,
1353               18);
1354     }
1355   }
1356
1357   public int getScrollOffset()
1358   {
1359     return scrollOffset;
1360   }
1361
1362   @Override
1363   public void mouseEntered(MouseEvent e)
1364   {
1365   }
1366 }