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