JAL-2629 tidy unit tests, constants etc
[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     item = new JMenuItem(EDITNAME);
392     item.addActionListener(this);
393     pop.add(item);
394     item = new JMenuItem(HIDE);
395     item.addActionListener(this);
396     pop.add(item);
397     // JAL-1264 hide all sequence-specific annotations of this type
398     if (selectedRow < aa.length)
399     {
400       if (aa[selectedRow].sequenceRef != null)
401       {
402         final String label = aa[selectedRow].label;
403         JMenuItem hideType = new JMenuItem();
404         String text = MessageManager.getString("label.hide_all") + " "
405                 + label;
406         hideType.setText(text);
407         hideType.addActionListener(new ActionListener()
408         {
409           @Override
410           public void actionPerformed(ActionEvent e)
411           {
412             AlignmentUtils.showOrHideSequenceAnnotations(
413                     ap.av.getAlignment(), Collections.singleton(label),
414                     null, false, false);
415             // for (AlignmentAnnotation ann : ap.av.getAlignment()
416             // .getAlignmentAnnotation())
417             // {
418             // if (ann.sequenceRef != null && ann.label != null
419             // && ann.label.equals(label))
420             // {
421             // ann.visible = false;
422             // }
423             // }
424             ap.refresh(true);
425           }
426         });
427         pop.add(hideType);
428       }
429     }
430     item = new JMenuItem(DELETE);
431     item.addActionListener(this);
432     pop.add(item);
433     if (hasHiddenRows)
434     {
435       item = new JMenuItem(SHOWALL);
436       item.addActionListener(this);
437       pop.add(item);
438     }
439     item = new JMenuItem(OUTPUT_TEXT);
440     item.addActionListener(this);
441     pop.add(item);
442     // TODO: annotation object should be typed for autocalculated/derived
443     // property methods
444     if (selectedRow < aa.length)
445     {
446       final String label = aa[selectedRow].label;
447       if (!(aa[selectedRow].autoCalculated)
448               && !(InformationThread.HMM_CALC_ID
449                       .equals(aa[selectedRow].getCalcId())))
450       {
451         if (aa[selectedRow].graph == AlignmentAnnotation.NO_GRAPH)
452         {
453           // display formatting settings for this row.
454           pop.addSeparator();
455           // av and sequencegroup need to implement same interface for
456           item = new JCheckBoxMenuItem(TOGGLE_LABELSCALE,
457                   aa[selectedRow].scaleColLabel);
458           item.addActionListener(this);
459           pop.add(item);
460         }
461       }
462       else if (label.indexOf("Consensus") > -1)
463       {
464
465
466         pop.addSeparator();
467         // av and sequencegroup need to implement same interface for
468
469         final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
470                 MessageManager.getString("label.ignore_gaps_consensus"),
471                 (aa[selectedRow].groupRef != null)
472                         ? aa[selectedRow].groupRef.getIgnoreGapsConsensus()
473                         : ap.av.isIgnoreGapsConsensus());
474         final AlignmentAnnotation aaa = aa[selectedRow];
475         cbmi.addActionListener(new ActionListener()
476         {
477           @Override
478           public void actionPerformed(ActionEvent e)
479           {
480             if (aaa.groupRef != null)
481             {
482               // TODO: pass on reference to ap so the view can be updated.
483               aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
484               ap.getAnnotationPanel()
485                       .paint(ap.getAnnotationPanel().getGraphics());
486             }
487             else
488             {
489               ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
490             }
491             ap.alignmentChanged();
492           }
493         });
494         pop.add(cbmi);
495         // av and sequencegroup need to implement same interface for
496         if (aaa.groupRef != null)
497         {
498           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
499                   MessageManager.getString("label.show_group_histogram"),
500                   aa[selectedRow].groupRef.isShowConsensusHistogram());
501           chist.addActionListener(new ActionListener()
502           {
503             @Override
504             public void actionPerformed(ActionEvent e)
505             {
506               // TODO: pass on reference
507               // to ap
508               // so the
509               // view
510               // can be
511               // updated.
512               aaa.groupRef.setShowConsensusHistogram(chist.getState());
513               ap.repaint();
514               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
515             }
516           });
517           pop.add(chist);
518           final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
519                   MessageManager.getString("label.show_group_logo"),
520                   aa[selectedRow].groupRef.isShowSequenceLogo());
521           cprofl.addActionListener(new ActionListener()
522           {
523             @Override
524             public void actionPerformed(ActionEvent e)
525             {
526               // TODO: pass on reference
527               // to ap
528               // so the
529               // view
530               // can be
531               // updated.
532               aaa.groupRef.setshowSequenceLogo(cprofl.getState());
533               ap.repaint();
534               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
535             }
536           });
537           pop.add(cprofl);
538           final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
539                   MessageManager.getString("label.normalise_group_logo"),
540                   aa[selectedRow].groupRef.isNormaliseSequenceLogo());
541           cproflnorm.addActionListener(new ActionListener()
542           {
543             @Override
544             public void actionPerformed(ActionEvent e)
545             {
546
547               // TODO: pass on reference
548               // to ap
549               // so the
550               // view
551               // can be
552               // updated.
553               aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
554               // automatically enable logo display if we're clicked
555               aaa.groupRef.setshowSequenceLogo(true);
556               ap.repaint();
557               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
558             }
559           });
560           pop.add(cproflnorm);
561         }
562         else
563         {
564           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
565                   MessageManager.getString("label.show_histogram"),
566                   av.isShowConsensusHistogram());
567           chist.addActionListener(new ActionListener()
568           {
569             @Override
570             public void actionPerformed(ActionEvent e)
571             {
572               // TODO: pass on reference
573               // to ap
574               // so the
575               // view
576               // can be
577               // updated.
578               av.setShowConsensusHistogram(chist.getState());
579               ap.repaint();
580               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
581             }
582           });
583           pop.add(chist);
584           final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
585                   MessageManager.getString("label.show_logo"),
586                   av.isShowSequenceLogo());
587           cprof.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(cprof.getState());
599               ap.repaint();
600               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
601             }
602           });
603           pop.add(cprof);
604           final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
605                   MessageManager.getString("label.normalise_logo"),
606                   av.isNormaliseSequenceLogo());
607           cprofnorm.addActionListener(new ActionListener()
608           {
609             @Override
610             public void actionPerformed(ActionEvent e)
611             {
612               // TODO: pass on reference
613               // to ap
614               // so the
615               // view
616               // can be
617               // updated.
618               av.setShowSequenceLogo(true);
619               av.setNormaliseSequenceLogo(cprofnorm.getState());
620               ap.repaint();
621               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
622             }
623           });
624           pop.add(cprofnorm);
625         }
626         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
627         consclipbrd.addActionListener(this);
628         pop.add(consclipbrd);
629       }
630       else if (InformationThread.HMM_CALC_ID
631               .equals(aa[selectedRow].getCalcId()))
632       {
633         pop.addSeparator();
634         final AlignmentAnnotation aaa = aa[selectedRow];
635
636         final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
637                 MessageManager.getString(
638                         "label.ignore_below_background_frequency"),
639                 (aa[selectedRow].groupRef != null)
640                         ? aa[selectedRow].groupRef
641                                 .getIgnoreBelowBackground()
642                         : ap.av.isIgnoreBelowBackground());
643
644         cbmi.addActionListener(new ActionListener()
645         {
646           @Override
647           public void actionPerformed(ActionEvent e)
648           {
649
650             if (aaa.groupRef != null)
651             {
652               // TODO: pass on reference to ap so the view can be updated.
653               if (aaa.groupRef.getInfoLetterHeight() == false)
654               {
655                 aaa.groupRef.setIgnoreBelowBackground(cbmi.getState());
656                 ap.getAnnotationPanel()
657                         .paint(ap.getAnnotationPanel().getGraphics());
658               }
659             }
660             else if (ap.av.isInfoLetterHeight() == false)
661             {
662               ap.av.setIgnoreBelowBackground(cbmi.getState(), ap);
663             }
664             ap.alignmentChanged();
665           }
666         });
667         pop.add(cbmi);
668         final JCheckBoxMenuItem letteHeight = new JCheckBoxMenuItem(
669                 MessageManager.getString("label.use_info_for_height"),
670                 (aa[selectedRow].groupRef != null)
671                         ? aa[selectedRow].groupRef.getInfoLetterHeight()
672                         : ap.av.isInfoLetterHeight());
673
674         letteHeight.addActionListener(new ActionListener()
675         {
676           @Override
677           public void actionPerformed(ActionEvent e)
678           {
679             if (aaa.groupRef != null)
680             {
681               // TODO: pass on reference to ap so the view can be updated.
682               aaa.groupRef.setInfoLetterHeight((letteHeight.getState()));
683               if (aaa.groupRef.getIgnoreBelowBackground() == false)
684               {
685                 aaa.groupRef.setIgnoreBelowBackground(true);
686               }
687               ap.getAnnotationPanel()
688                       .paint(ap.getAnnotationPanel().getGraphics());
689             }
690             else
691             {
692               ap.av.setInfoLetterHeight(letteHeight.getState(), ap);
693               if (ap.av.isIgnoreBelowBackground() == false)
694               {
695                 ap.av.setIgnoreBelowBackground(true, ap);
696               }
697             }
698             ap.alignmentChanged();
699           }
700         });
701         pop.add(letteHeight);
702         if (aaa.groupRef != null)
703         {
704           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
705                   MessageManager.getString("label.show_group_histogram"),
706                   aa[selectedRow].groupRef.isShowInformationHistogram());
707           chist.addActionListener(new ActionListener()
708           {
709             @Override
710             public void actionPerformed(ActionEvent e)
711             {
712               // TODO: pass on reference
713               // to ap
714               // so the
715               // view
716               // can be
717               // updated.
718               aaa.groupRef.setShowInformationHistogram(chist.getState());
719               ap.repaint();
720               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
721             }
722           });
723           pop.add(chist);
724           final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
725                   MessageManager.getString("label.show_group_logo"),
726                   aa[selectedRow].groupRef.isShowHMMSequenceLogo());
727           cprofl.addActionListener(new ActionListener()
728           {
729             @Override
730             public void actionPerformed(ActionEvent e)
731             {
732               // TODO: pass on reference
733               // to ap
734               // so the
735               // view
736               // can be
737               // updated.
738               aaa.groupRef.setshowHMMSequenceLogo(cprofl.getState());
739               ap.repaint();
740               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
741             }
742           });
743           pop.add(cprofl);
744           final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
745                   MessageManager.getString("label.normalise_group_logo"),
746                   aa[selectedRow].groupRef.isNormaliseHMMSequenceLogo());
747           cproflnorm.addActionListener(new ActionListener()
748           {
749             @Override
750             public void actionPerformed(ActionEvent e)
751             {
752
753               // TODO: pass on reference
754               // to ap
755               // so the
756               // view
757               // can be
758               // updated.
759               aaa.groupRef
760                       .setNormaliseHMMSequenceLogo(cproflnorm.getState());
761               // automatically enable logo display if we're clicked
762               aaa.groupRef.setshowHMMSequenceLogo(true);
763               ap.repaint();
764               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
765             }
766           });
767           pop.add(cproflnorm);
768         }
769         else
770         {
771           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
772                   MessageManager.getString("label.show_histogram"),
773                   av.isShowInformationHistogram());
774           chist.addActionListener(new ActionListener()
775           {
776             @Override
777             public void actionPerformed(ActionEvent e)
778             {
779               // TODO: pass on reference
780               // to ap
781               // so the
782               // view
783               // can be
784               // updated.
785               av.setShowInformationHistogram(chist.getState());
786               ap.repaint();
787               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
788             }
789           });
790           pop.add(chist);
791           final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
792                   MessageManager.getString("label.show_logo"),
793                   av.isShowHMMSequenceLogo());
794           cprof.addActionListener(new ActionListener()
795           {
796             @Override
797             public void actionPerformed(ActionEvent e)
798             {
799               // TODO: pass on reference
800               // to ap
801               // so the
802               // view
803               // can be
804               // updated.
805               av.updateInformation(ap);
806               av.setShowHMMSequenceLogo(cprof.getState());
807               ap.repaint();
808               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
809             }
810           });
811           pop.add(cprof);
812           final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
813                   MessageManager.getString("label.normalise_logo"),
814                   av.isNormaliseHMMSequenceLogo());
815           cprofnorm.addActionListener(new ActionListener()
816           {
817             @Override
818             public void actionPerformed(ActionEvent e)
819             {
820               // TODO: pass on reference
821               // to ap
822               // so the
823               // view
824               // can be
825               // updated.
826               av.setShowHMMSequenceLogo(true);
827               av.setNormaliseHMMSequenceLogo(cprofnorm.getState());
828               ap.repaint();
829               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
830             }
831           });
832           pop.add(cprofnorm);
833         }
834       }
835     }
836     pop.show(this, evt.getX(), evt.getY());
837   }
838
839   /**
840    * Reorders annotation rows after a drag of a label
841    * 
842    * @param evt
843    */
844   @Override
845   public void mouseReleased(MouseEvent evt)
846   {
847     if (evt.isPopupTrigger())
848     {
849       showPopupMenu(evt);
850       return;
851     }
852
853     int start = selectedRow;
854     getSelectedRow(evt.getY() - getScrollOffset());
855     int end = selectedRow;
856
857     /*
858      * if dragging to resize instead, start == end
859      */
860     if (start != end)
861     {
862       // Swap these annotations
863       AlignmentAnnotation startAA = ap.av.getAlignment()
864               .getAlignmentAnnotation()[start];
865       if (end == -1)
866       {
867         end = ap.av.getAlignment().getAlignmentAnnotation().length - 1;
868       }
869       AlignmentAnnotation endAA = ap.av.getAlignment()
870               .getAlignmentAnnotation()[end];
871
872       ap.av.getAlignment().getAlignmentAnnotation()[end] = startAA;
873       ap.av.getAlignment().getAlignmentAnnotation()[start] = endAA;
874     }
875
876     resizePanel = false;
877     dragEvent = null;
878     repaint();
879     ap.getAnnotationPanel().repaint();
880   }
881
882   /**
883    * Removes the height adjuster image on leaving the panel, unless currently
884    * dragging it
885    */
886   @Override
887   public void mouseExited(MouseEvent evt)
888   {
889     if (resizePanel && dragEvent == null)
890     {
891       resizePanel = false;
892       repaint();
893     }
894   }
895
896   /**
897    * A mouse drag may be either an adjustment of the panel height (if flag
898    * resizePanel is set on), or a reordering of the annotation rows. The former
899    * is dealt with by this method, the latter in mouseReleased.
900    * 
901    * @param evt
902    */
903   @Override
904   public void mouseDragged(MouseEvent evt)
905   {
906     dragEvent = evt;
907
908     if (resizePanel)
909     {
910       Dimension d = ap.annotationScroller.getPreferredSize();
911       int dif = evt.getY() - oldY;
912
913       dif /= ap.av.getCharHeight();
914       dif *= ap.av.getCharHeight();
915
916       if ((d.height - dif) > 20)
917       {
918         ap.annotationScroller
919                 .setPreferredSize(new Dimension(d.width, d.height - dif));
920         d = ap.annotationSpaceFillerHolder.getPreferredSize();
921         ap.annotationSpaceFillerHolder
922                 .setPreferredSize(new Dimension(d.width, d.height - dif));
923         ap.paintAlignment(true, false);
924       }
925
926       ap.addNotify();
927     }
928     else
929     {
930       repaint();
931     }
932   }
933
934   /**
935    * Updates the tooltip as the mouse moves over the labels
936    * 
937    * @param evt
938    */
939   @Override
940   public void mouseMoved(MouseEvent evt)
941   {
942     showOrHideAdjuster(evt);
943
944     getSelectedRow(evt.getY() - getScrollOffset());
945
946     if (selectedRow > -1 && ap.av.getAlignment()
947             .getAlignmentAnnotation().length > selectedRow)
948     {
949       AlignmentAnnotation aa = ap.av.getAlignment()
950               .getAlignmentAnnotation()[selectedRow];
951
952       StringBuffer desc = new StringBuffer();
953       if (aa.description != null
954               && !aa.description.equals("New description"))
955       {
956         // TODO: we could refactor and merge this code with the code in
957         // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
958         // tooltips
959         desc.append(aa.getDescription(true).trim());
960         // check to see if the description is an html fragment.
961         if (desc.length() < 6 || (desc.substring(0, 6).toLowerCase()
962                 .indexOf("<html>") < 0))
963         {
964           // clean the description ready for embedding in html
965           desc = new StringBuffer(LEFT_ANGLE_BRACKET_PATTERN.matcher(desc)
966                   .replaceAll("&lt;"));
967           desc.insert(0, "<html>");
968         }
969         else
970         {
971           // remove terminating html if any
972           int i = desc.substring(desc.length() - 7).toLowerCase()
973                   .lastIndexOf("</html>");
974           if (i > -1)
975           {
976             desc.setLength(desc.length() - 7 + i);
977           }
978         }
979         if (aa.hasScore())
980         {
981           desc.append("<br/>");
982         }
983         // if (aa.hasProperties())
984         // {
985         // desc.append("<table>");
986         // for (String prop : aa.getProperties())
987         // {
988         // desc.append("<tr><td>" + prop + "</td><td>"
989         // + aa.getProperty(prop) + "</td><tr>");
990         // }
991         // desc.append("</table>");
992         // }
993       }
994       else
995       {
996         // begin the tooltip's html fragment
997         desc.append("<html>");
998         if (aa.hasScore())
999         {
1000           // TODO: limit precision of score to avoid noise from imprecise
1001           // doubles
1002           // (64.7 becomes 64.7+/some tiny value).
1003           desc.append(" Score: " + aa.score);
1004         }
1005       }
1006       if (desc.length() > 6)
1007       {
1008         desc.append("</html>");
1009         this.setToolTipText(desc.toString());
1010       }
1011       else
1012       {
1013         this.setToolTipText(null);
1014       }
1015     }
1016   }
1017
1018   /**
1019    * Shows the height adjuster image if the mouse moves into the top left
1020    * region, or hides it if the mouse leaves the regio
1021    * 
1022    * @param evt
1023    */
1024   protected void showOrHideAdjuster(MouseEvent evt)
1025   {
1026     boolean was = resizePanel;
1027     resizePanel = evt.getY() < adjusterImageHeight && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
1028
1029     if (resizePanel != was)
1030     {
1031       setCursor(Cursor.getPredefinedCursor(
1032               resizePanel ? Cursor.S_RESIZE_CURSOR
1033                       : Cursor.DEFAULT_CURSOR));
1034       repaint();
1035     }
1036   }
1037
1038   @Override
1039   public void mouseClicked(MouseEvent evt)
1040   {
1041     final AlignmentAnnotation[] aa = ap.av.getAlignment()
1042             .getAlignmentAnnotation();
1043     if (!evt.isPopupTrigger() && SwingUtilities.isLeftMouseButton(evt))
1044     {
1045       if (selectedRow > -1 && selectedRow < aa.length)
1046       {
1047         if (aa[selectedRow].groupRef != null)
1048         {
1049           if (evt.getClickCount() >= 2)
1050           {
1051             // todo: make the ap scroll to the selection - not necessary, first
1052             // click highlights/scrolls, second selects
1053             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
1054             // process modifiers
1055             SequenceGroup sg = ap.av.getSelectionGroup();
1056             if (sg == null || sg == aa[selectedRow].groupRef
1057                     || !(Platform.isControlDown(evt) || evt.isShiftDown()))
1058             {
1059               if (Platform.isControlDown(evt) || evt.isShiftDown())
1060               {
1061                 // clone a new selection group from the associated group
1062                 ap.av.setSelectionGroup(
1063                         new SequenceGroup(aa[selectedRow].groupRef));
1064               }
1065               else
1066               {
1067                 // set selection to the associated group so it can be edited
1068                 ap.av.setSelectionGroup(aa[selectedRow].groupRef);
1069               }
1070             }
1071             else
1072             {
1073               // modify current selection with associated group
1074               int remainToAdd = aa[selectedRow].groupRef.getSize();
1075               for (SequenceI sgs : aa[selectedRow].groupRef.getSequences())
1076               {
1077                 if (jalview.util.Platform.isControlDown(evt))
1078                 {
1079                   sg.addOrRemove(sgs, --remainToAdd == 0);
1080                 }
1081                 else
1082                 {
1083                   // notionally, we should also add intermediate sequences from
1084                   // last added sequence ?
1085                   sg.addSequence(sgs, --remainToAdd == 0);
1086                 }
1087               }
1088             }
1089
1090             ap.paintAlignment(false, false);
1091             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
1092             ap.av.sendSelection();
1093           }
1094           else
1095           {
1096             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(
1097                     aa[selectedRow].groupRef.getSequences(null));
1098           }
1099           return;
1100         }
1101         else if (aa[selectedRow].sequenceRef != null)
1102         {
1103           if (evt.getClickCount() == 1)
1104           {
1105             ap.getSeqPanel().ap.getIdPanel()
1106                     .highlightSearchResults(Arrays.asList(new SequenceI[]
1107                     { aa[selectedRow].sequenceRef }));
1108           }
1109           else if (evt.getClickCount() >= 2)
1110           {
1111             ap.getSeqPanel().ap.getIdPanel().highlightSearchResults(null);
1112             SequenceGroup sg = ap.av.getSelectionGroup();
1113             if (sg != null)
1114             {
1115               // we make a copy rather than edit the current selection if no
1116               // modifiers pressed
1117               // see Enhancement JAL-1557
1118               if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
1119               {
1120                 sg = new SequenceGroup(sg);
1121                 sg.clear();
1122                 sg.addSequence(aa[selectedRow].sequenceRef, false);
1123               }
1124               else
1125               {
1126                 if (Platform.isControlDown(evt))
1127                 {
1128                   sg.addOrRemove(aa[selectedRow].sequenceRef, true);
1129                 }
1130                 else
1131                 {
1132                   // notionally, we should also add intermediate sequences from
1133                   // last added sequence ?
1134                   sg.addSequence(aa[selectedRow].sequenceRef, true);
1135                 }
1136               }
1137             }
1138             else
1139             {
1140               sg = new SequenceGroup();
1141               sg.setStartRes(0);
1142               sg.setEndRes(ap.av.getAlignment().getWidth() - 1);
1143               sg.addSequence(aa[selectedRow].sequenceRef, false);
1144             }
1145             ap.av.setSelectionGroup(sg);
1146             ap.paintAlignment(false, false);
1147             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
1148             ap.av.sendSelection();
1149           }
1150
1151         }
1152       }
1153       return;
1154     }
1155   }
1156
1157   /**
1158    * do a single sequence copy to jalview and the system clipboard
1159    * 
1160    * @param sq
1161    *          sequence to be copied to clipboard
1162    */
1163   protected void copy_annotseqtoclipboard(SequenceI sq)
1164   {
1165     SequenceI[] seqs = new SequenceI[] { sq };
1166     String[] omitHidden = null;
1167     SequenceI[] dseqs = new SequenceI[] { sq.getDatasetSequence() };
1168     if (dseqs[0] == null)
1169     {
1170       dseqs[0] = new Sequence(sq);
1171       dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
1172               sq.getSequenceAsString()));
1173
1174       sq.setDatasetSequence(dseqs[0]);
1175     }
1176     Alignment ds = new Alignment(dseqs);
1177     if (av.hasHiddenColumns())
1178     {
1179       omitHidden = av.getAlignment().getHiddenColumns()
1180               .getVisibleSequenceStrings(0, sq.getLength(), seqs);
1181     }
1182
1183     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
1184     if (av.hasHiddenColumns())
1185     {
1186       alignmentStartEnd = av.getAlignment().getHiddenColumns()
1187               .getVisibleStartAndEndIndex(av.getAlignment().getWidth());
1188     }
1189
1190     String output = new FormatAdapter().formatSequences(FileFormat.Fasta,
1191             seqs, omitHidden, alignmentStartEnd);
1192
1193     Toolkit.getDefaultToolkit().getSystemClipboard()
1194             .setContents(new StringSelection(output), Desktop.instance);
1195
1196     ArrayList<int[]> hiddenColumns = null;
1197
1198     if (av.hasHiddenColumns())
1199     {
1200
1201       hiddenColumns = av.getAlignment().getHiddenColumns()
1202               .getHiddenColumnsCopy();
1203
1204     }
1205
1206     Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
1207                                                         // of a consensus
1208                                                         // sequence ? need to
1209                                                         // flag
1210         // sequence as special.
1211         hiddenColumns };
1212   }
1213
1214   /**
1215    * DOCUMENT ME!
1216    * 
1217    * @param g1
1218    *          DOCUMENT ME!
1219    */
1220   @Override
1221   public void paintComponent(Graphics g)
1222   {
1223
1224     int width = getWidth();
1225     if (width == 0)
1226     {
1227       width = ap.calculateIdWidth().width + 4;
1228     }
1229
1230     Graphics2D g2 = (Graphics2D) g;
1231     if (av.antiAlias)
1232     {
1233       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1234               RenderingHints.VALUE_ANTIALIAS_ON);
1235     }
1236
1237     drawComponent(g2, true, width);
1238
1239   }
1240
1241   /**
1242    * Draw the full set of annotation Labels for the alignment at the given
1243    * cursor
1244    * 
1245    * @param g
1246    *          Graphics2D instance (needed for font scaling)
1247    * @param width
1248    *          Width for scaling labels
1249    * 
1250    */
1251   public void drawComponent(Graphics g, int width)
1252   {
1253     drawComponent(g, false, width);
1254   }
1255
1256   /**
1257    * Draw the full set of annotation Labels for the alignment at the given
1258    * cursor
1259    * 
1260    * @param g
1261    *          Graphics2D instance (needed for font scaling)
1262    * @param clip
1263    *          - true indicates that only current visible area needs to be
1264    *          rendered
1265    * @param width
1266    *          Width for scaling labels
1267    */
1268   public void drawComponent(Graphics g, boolean clip, int width)
1269   {
1270     if (av.getFont().getSize() < 10)
1271     {
1272       g.setFont(font);
1273     }
1274     else
1275     {
1276       g.setFont(av.getFont());
1277     }
1278
1279     FontMetrics fm = g.getFontMetrics(g.getFont());
1280     g.setColor(Color.white);
1281     g.fillRect(0, 0, getWidth(), getHeight());
1282
1283     g.translate(0, getScrollOffset());
1284     g.setColor(Color.black);
1285
1286     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
1287     int fontHeight = g.getFont().getSize();
1288     int y = 0;
1289     int x = 0;
1290     int graphExtras = 0;
1291     int offset = 0;
1292     Font baseFont = g.getFont();
1293     FontMetrics baseMetrics = fm;
1294     int ofontH = fontHeight;
1295     int sOffset = 0;
1296     int visHeight = 0;
1297     int[] visr = (ap != null && ap.getAnnotationPanel() != null)
1298             ? ap.getAnnotationPanel().getVisibleVRange()
1299             : null;
1300     if (clip && visr != null)
1301     {
1302       sOffset = visr[0];
1303       visHeight = visr[1];
1304     }
1305     boolean visible = true, before = false, after = false;
1306     if (aa != null)
1307     {
1308       hasHiddenRows = false;
1309       int olY = 0;
1310       for (int i = 0; i < aa.length; i++)
1311       {
1312         visible = true;
1313         if (!aa[i].visible)
1314         {
1315           hasHiddenRows = true;
1316           continue;
1317         }
1318         olY = y;
1319         y += aa[i].height;
1320         if (clip)
1321         {
1322           if (y < sOffset)
1323           {
1324             if (!before)
1325             {
1326               if (debugRedraw)
1327               {
1328                 System.out.println("before vis: " + i);
1329               }
1330               before = true;
1331             }
1332             // don't draw what isn't visible
1333             continue;
1334           }
1335           if (olY > visHeight)
1336           {
1337
1338             if (!after)
1339             {
1340               if (debugRedraw)
1341               {
1342                 System.out.println(
1343                         "Scroll offset: " + sOffset + " after vis: " + i);
1344               }
1345               after = true;
1346             }
1347             // don't draw what isn't visible
1348             continue;
1349           }
1350         }
1351         g.setColor(Color.black);
1352
1353         offset = -aa[i].height / 2;
1354
1355         if (aa[i].hasText)
1356         {
1357           offset += fm.getHeight() / 2;
1358           offset -= fm.getDescent();
1359         }
1360         else
1361         {
1362           offset += fm.getDescent();
1363         }
1364
1365         x = width - fm.stringWidth(aa[i].label) - 3;
1366
1367         if (aa[i].graphGroup > -1)
1368         {
1369           int groupSize = 0;
1370           // TODO: JAL-1291 revise rendering model so the graphGroup map is
1371           // computed efficiently for all visible labels
1372           for (int gg = 0; gg < aa.length; gg++)
1373           {
1374             if (aa[gg].graphGroup == aa[i].graphGroup)
1375             {
1376               groupSize++;
1377             }
1378           }
1379           if (groupSize * (fontHeight + 8) < aa[i].height)
1380           {
1381             graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1382                     / 2;
1383           }
1384           else
1385           {
1386             // scale font to fit
1387             float h = aa[i].height / (float) groupSize, s;
1388             if (h < 9)
1389             {
1390               visible = false;
1391             }
1392             else
1393             {
1394               fontHeight = -8 + (int) h;
1395               s = ((float) fontHeight) / (float) ofontH;
1396               Font f = baseFont
1397                       .deriveFont(AffineTransform.getScaleInstance(s, s));
1398               g.setFont(f);
1399               fm = g.getFontMetrics();
1400               graphExtras = (aa[i].height - (groupSize * (fontHeight + 8)))
1401                       / 2;
1402             }
1403           }
1404           if (visible)
1405           {
1406             for (int gg = 0; gg < aa.length; gg++)
1407             {
1408               if (aa[gg].graphGroup == aa[i].graphGroup)
1409               {
1410                 x = width - fm.stringWidth(aa[gg].label) - 3;
1411                 g.drawString(aa[gg].label, x, y - graphExtras);
1412
1413                 if (aa[gg]._linecolour != null)
1414                 {
1415
1416                   g.setColor(aa[gg]._linecolour);
1417                   g.drawLine(x, y - graphExtras + 3,
1418                           x + fm.stringWidth(aa[gg].label),
1419                           y - graphExtras + 3);
1420                 }
1421
1422                 g.setColor(Color.black);
1423                 graphExtras += fontHeight + 8;
1424               }
1425             }
1426           }
1427           g.setFont(baseFont);
1428           fm = baseMetrics;
1429           fontHeight = ofontH;
1430         }
1431         else
1432         {
1433           g.drawString(aa[i].label, x, y + offset);
1434         }
1435       }
1436     }
1437
1438     if (resizePanel)
1439     {
1440       // g.drawImage(adjusterImage, 2, 0 - getScrollOffset(), this);
1441     }
1442     else if (dragEvent != null && aa != null)
1443     {
1444       g.setColor(Color.lightGray);
1445       g.drawString(aa[selectedRow].label, dragEvent.getX(),
1446               dragEvent.getY() - getScrollOffset());
1447     }
1448
1449     if (!av.getWrapAlignment() && ((aa == null) || (aa.length < 1)))
1450     {
1451       g.drawString(MessageManager.getString("label.right_click"), 2, 8);
1452       g.drawString(MessageManager.getString("label.to_add_annotation"), 2,
1453               18);
1454     }
1455   }
1456
1457   public int getScrollOffset()
1458   {
1459     return scrollOffset;
1460   }
1461
1462   @Override
1463   public void mouseEntered(MouseEvent e)
1464   {
1465   }
1466 }