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