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