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