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