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