JAL-3161 limit tooltip and status updates to visible columns
[jalview.git] / src / jalview / appletgui / AnnotationPanel.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.appletgui;
22
23 import jalview.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.Annotation;
25 import jalview.datamodel.SequenceI;
26 import jalview.renderer.AnnotationRenderer;
27 import jalview.renderer.AwtRenderPanelI;
28 import jalview.schemes.ResidueProperties;
29 import jalview.util.Comparison;
30 import jalview.util.MessageManager;
31 import jalview.util.Platform;
32 import jalview.viewmodel.ViewportListenerI;
33 import jalview.viewmodel.ViewportRanges;
34
35 import java.awt.Color;
36 import java.awt.Dimension;
37 import java.awt.Font;
38 import java.awt.FontMetrics;
39 import java.awt.Graphics;
40 import java.awt.Image;
41 import java.awt.MenuItem;
42 import java.awt.Panel;
43 import java.awt.PopupMenu;
44 import java.awt.event.ActionEvent;
45 import java.awt.event.ActionListener;
46 import java.awt.event.AdjustmentEvent;
47 import java.awt.event.AdjustmentListener;
48 import java.awt.event.InputEvent;
49 import java.awt.event.MouseEvent;
50 import java.awt.event.MouseListener;
51 import java.awt.event.MouseMotionListener;
52 import java.beans.PropertyChangeEvent;
53
54 public class AnnotationPanel extends Panel
55         implements AwtRenderPanelI, AdjustmentListener, ActionListener,
56         MouseListener, MouseMotionListener, ViewportListenerI
57 {
58   AlignViewport av;
59
60   AlignmentPanel ap;
61
62   int activeRow = -1;
63
64   final String HELIX = "Helix";
65
66   final String SHEET = "Sheet";
67
68   /**
69    * For RNA secondary structure "stems" aka helices
70    */
71   final String STEM = "RNA Helix";
72
73   final String LABEL = "Label";
74
75   final String REMOVE = "Remove Annotation";
76
77   final String COLOUR = "Colour";
78
79   final Color HELIX_COLOUR = Color.red.darker();
80
81   final Color SHEET_COLOUR = Color.green.darker().darker();
82
83   Image image;
84
85   Graphics gg;
86
87   FontMetrics fm;
88
89   int imgWidth = 0;
90
91   boolean fastPaint = false;
92
93   // Used For mouse Dragging and resizing graphs
94   int graphStretch = -1;
95
96   int graphStretchY = -1;
97
98   boolean mouseDragging = false;
99
100   public static int GRAPH_HEIGHT = 40;
101
102   boolean MAC = false;
103
104   public final AnnotationRenderer renderer;
105
106   public AnnotationPanel(AlignmentPanel ap)
107   {
108     new jalview.util.Platform();
109     MAC = Platform.isAMac();
110     this.ap = ap;
111     av = ap.av;
112     setLayout(null);
113     int height = adjustPanelHeight();
114     ap.apvscroll.setValues(0, getSize().height, 0, height);
115
116     addMouseMotionListener(this);
117
118     addMouseListener(this);
119
120     // ap.annotationScroller.getVAdjustable().addAdjustmentListener( this );
121     renderer = new AnnotationRenderer();
122
123     av.getRanges().addPropertyChangeListener(this);
124   }
125
126   public AnnotationPanel(AlignViewport av)
127   {
128     this.av = av;
129     renderer = new AnnotationRenderer();
130
131   }
132
133   @Override
134   public void adjustmentValueChanged(AdjustmentEvent evt)
135   {
136   }
137
138   /**
139    * DOCUMENT ME!
140    * 
141    * @param evt
142    *          DOCUMENT ME!
143    */
144   @Override
145   public void actionPerformed(ActionEvent evt)
146   {
147     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
148     if (aa == null)
149     {
150       return;
151     }
152     Annotation[] anot = aa[activeRow].annotations;
153
154     if (anot.length < av.getColumnSelection().getMax())
155     {
156       Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
157               + 2];
158       System.arraycopy(anot, 0, temp, 0, anot.length);
159       anot = temp;
160       aa[activeRow].annotations = anot;
161     }
162
163     String label = "";
164     if (av.getColumnSelection() != null
165             && !av.getColumnSelection().isEmpty()
166             && anot[av.getColumnSelection().getMin()] != null)
167     {
168       label = anot[av.getColumnSelection().getMin()].displayCharacter;
169     }
170
171     if (evt.getActionCommand().equals(REMOVE))
172     {
173       for (int index : av.getColumnSelection().getSelected())
174       {
175         if (av.getAlignment().getHiddenColumns().isVisible(index))
176         {
177           anot[index] = null;
178         }
179       }
180     }
181     else if (evt.getActionCommand().equals(LABEL))
182     {
183       label = enterLabel(label, "Enter Label");
184
185       if (label == null)
186       {
187         return;
188       }
189
190       if ((label.length() > 0) && !aa[activeRow].hasText)
191       {
192         aa[activeRow].hasText = true;
193       }
194
195       for (int index : av.getColumnSelection().getSelected())
196       {
197         // TODO: JAL-2001 - provide a fast method to list visible selected
198         // columns
199         if (!av.getAlignment().getHiddenColumns().isVisible(index))
200         {
201           continue;
202         }
203
204         if (anot[index] == null)
205         {
206           anot[index] = new Annotation(label, "", ' ', 0);
207         }
208
209         anot[index].displayCharacter = label;
210       }
211     }
212     else if (evt.getActionCommand().equals(COLOUR))
213     {
214       UserDefinedColours udc = new UserDefinedColours(this, Color.black,
215               ap.alignFrame);
216
217       Color col = udc.getColor();
218
219       for (int index : av.getColumnSelection().getSelected())
220       {
221         if (!av.getAlignment().getHiddenColumns().isVisible(index))
222         {
223           continue;
224         }
225
226         if (anot[index] == null)
227         {
228           anot[index] = new Annotation("", "", ' ', 0);
229         }
230
231         anot[index].colour = col;
232       }
233     }
234     else
235     // HELIX OR SHEET
236     {
237       char type = 0;
238       String symbol = "\u03B1";
239
240       if (evt.getActionCommand().equals(HELIX))
241       {
242         type = 'H';
243       }
244       else if (evt.getActionCommand().equals(SHEET))
245       {
246         type = 'E';
247         symbol = "\u03B2";
248       }
249
250       // Added by LML to color stems
251       else if (evt.getActionCommand().equals(STEM))
252       {
253         type = 'S';
254         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
255         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
256       }
257
258       if (!aa[activeRow].hasIcons)
259       {
260         aa[activeRow].hasIcons = true;
261       }
262
263       label = enterLabel(symbol, "Enter Label");
264
265       if (label == null)
266       {
267         return;
268       }
269
270       if ((label.length() > 0) && !aa[activeRow].hasText)
271       {
272         aa[activeRow].hasText = true;
273         if (evt.getActionCommand().equals(STEM))
274         {
275           aa[activeRow].showAllColLabels = true;
276         }
277       }
278
279       for (int index : av.getColumnSelection().getSelected())
280       {
281         if (!av.getAlignment().getHiddenColumns().isVisible(index))
282         {
283           continue;
284         }
285
286         if (anot[index] == null)
287         {
288           anot[index] = new Annotation(label, "", type, 0);
289         }
290
291         anot[index].secondaryStructure = type != 'S' ? type
292                 : label.length() == 0 ? ' ' : label.charAt(0);
293         anot[index].displayCharacter = label;
294       }
295     }
296
297     av.getAlignment().validateAnnotation(aa[activeRow]);
298
299     ap.alignmentChanged();
300     adjustPanelHeight();
301     repaint();
302
303     return;
304   }
305
306   String enterLabel(String text, String label)
307   {
308     EditNameDialog dialog = new EditNameDialog(text, null, label, null,
309             ap.alignFrame, "Enter Label", 400, 200, true);
310
311     if (dialog.accept)
312     {
313       return dialog.getName();
314     }
315     else
316     {
317       return null;
318     }
319   }
320
321   @Override
322   public void mousePressed(MouseEvent evt)
323   {
324     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
325     if (aa == null)
326     {
327       return;
328     }
329
330     int height = -scrollOffset;
331     activeRow = -1;
332
333     for (int i = 0; i < aa.length; i++)
334     {
335       if (aa[i].visible)
336       {
337         height += aa[i].height;
338       }
339
340       if (evt.getY() < height)
341       {
342         if (aa[i].editable)
343         {
344           activeRow = i;
345         }
346         else if (aa[i].graph > 0)
347         {
348           // Stretch Graph
349           graphStretch = i;
350           graphStretchY = evt.getY();
351         }
352
353         break;
354       }
355     }
356
357     if ((evt.getModifiers()
358             & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK
359             && activeRow != -1)
360     {
361       if (av.getColumnSelection() == null
362               || av.getColumnSelection().isEmpty())
363       {
364         return;
365       }
366
367       PopupMenu pop = new PopupMenu(
368               MessageManager.getString("label.structure_type"));
369       MenuItem item;
370
371       if (av.getAlignment().isNucleotide())
372       {
373         item = new MenuItem(STEM);
374         item.addActionListener(this);
375         pop.add(item);
376       }
377       else
378       {
379         item = new MenuItem(HELIX);
380         item.addActionListener(this);
381         pop.add(item);
382         item = new MenuItem(SHEET);
383         item.addActionListener(this);
384         pop.add(item);
385       }
386       item = new MenuItem(LABEL);
387       item.addActionListener(this);
388       pop.add(item);
389       item = new MenuItem(COLOUR);
390       item.addActionListener(this);
391       pop.add(item);
392       item = new MenuItem(REMOVE);
393       item.addActionListener(this);
394       pop.add(item);
395       ap.alignFrame.add(pop);
396       pop.show(this, evt.getX(), evt.getY());
397
398       return;
399     }
400
401     ap.scalePanel.mousePressed(evt);
402   }
403
404   @Override
405   public void mouseReleased(MouseEvent evt)
406   {
407     graphStretch = -1;
408     graphStretchY = -1;
409     mouseDragging = false;
410     if (needValidating)
411     {
412       ap.validate();
413       needValidating = false;
414     }
415     ap.scalePanel.mouseReleased(evt);
416   }
417
418   @Override
419   public void mouseClicked(MouseEvent evt)
420   {
421   }
422
423   boolean needValidating = false;
424
425   @Override
426   public void mouseDragged(MouseEvent evt)
427   {
428     if (graphStretch > -1)
429     {
430       av.getAlignment()
431               .getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
432                       - evt.getY();
433       if (av.getAlignment()
434               .getAlignmentAnnotation()[graphStretch].graphHeight < 0)
435       {
436         av.getAlignment()
437                 .getAlignmentAnnotation()[graphStretch].graphHeight = 0;
438       }
439       graphStretchY = evt.getY();
440       av.calcPanelHeight();
441       needValidating = true;
442       // TODO: only update overview visible geometry
443       ap.paintAlignment(true, false);
444     }
445     else
446     {
447       ap.scalePanel.mouseDragged(evt);
448     }
449   }
450
451   @Override
452   public void mouseMoved(MouseEvent evt)
453   {
454     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
455     if (aa == null)
456     {
457       return;
458     }
459
460     int row = -1;
461     int height = -scrollOffset;
462     for (int i = 0; i < aa.length; i++)
463     {
464
465       if (aa[i].visible)
466       {
467         height += aa[i].height;
468       }
469
470       if (evt.getY() < height)
471       {
472         row = i;
473         break;
474       }
475     }
476
477     int column = evt.getX() / av.getCharWidth()
478             + av.getRanges().getStartRes();
479
480     if (av.hasHiddenColumns())
481     {
482       column = av.getAlignment().getHiddenColumns()
483               .visibleToAbsoluteColumn(column);
484     }
485
486     if (row > -1 && column < aa[row].annotations.length
487             && aa[row].annotations[column] != null)
488     {
489       StringBuilder text = new StringBuilder();
490       text.append(MessageManager.getString("label.column")).append(" ")
491               .append(column + 1);
492       String description = aa[row].annotations[column].description;
493       if (description != null && description.length() > 0)
494       {
495         text.append("  ").append(description);
496       }
497
498       /*
499        * if the annotation is sequence-specific, show the sequence number
500        * in the alignment, and (if not a gap) the residue and position
501        */
502       SequenceI seqref = aa[row].sequenceRef;
503       if (seqref != null)
504       {
505         int seqIndex = av.getAlignment().findIndex(seqref);
506         if (seqIndex != -1)
507         {
508           text.append(", ")
509                   .append(MessageManager.getString("label.sequence"))
510                   .append(" ").append(seqIndex + 1);
511           char residue = seqref.getCharAt(column);
512           if (!Comparison.isGap(residue))
513           {
514             text.append(" ");
515             String name;
516             if (av.getAlignment().isNucleotide())
517             {
518               name = ResidueProperties.nucleotideName
519                       .get(String.valueOf(residue));
520               text.append(" Nucleotide: ")
521                       .append(name != null ? name : residue);
522             }
523             else
524             {
525               name = 'X' == residue ? "X"
526                       : ('*' == residue ? "STOP"
527                               : ResidueProperties.aa2Triplet
528                                       .get(String.valueOf(residue)));
529               text.append(" Residue: ")
530                       .append(name != null ? name : residue);
531             }
532             int residuePos = seqref.findPosition(column);
533             text.append(" (").append(residuePos).append(")");
534             // int residuePos = seqref.findPosition(column);
535             // text.append(residue).append(" (")
536             // .append(residuePos).append(")");
537           }
538         }
539       }
540
541       ap.alignFrame.statusBar.setText(text.toString());
542     }
543   }
544
545   @Override
546   public void mouseEntered(MouseEvent evt)
547   {
548     ap.scalePanel.mouseEntered(evt);
549   }
550
551   @Override
552   public void mouseExited(MouseEvent evt)
553   {
554     ap.scalePanel.mouseExited(evt);
555   }
556
557   public int adjustPanelHeight()
558   {
559     return adjustPanelHeight(true);
560   }
561
562   public int adjustPanelHeight(boolean repaint)
563   {
564     int height = av.calcPanelHeight();
565     this.setSize(new Dimension(getSize().width, height));
566     if (repaint)
567     {
568       repaint();
569     }
570     return height;
571   }
572
573   /**
574    * calculate the height for visible annotation, revalidating bounds where
575    * necessary ABSTRACT GUI METHOD
576    * 
577    * @return total height of annotation
578    */
579
580   public void addEditableColumn(int i)
581   {
582     if (activeRow == -1)
583     {
584       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
585       if (aa == null)
586       {
587         return;
588       }
589
590       for (int j = 0; j < aa.length; j++)
591       {
592         if (aa[j].editable)
593         {
594           activeRow = j;
595           break;
596         }
597       }
598     }
599   }
600
601   @Override
602   public void update(Graphics g)
603   {
604     paint(g);
605   }
606
607   @Override
608   public void paint(Graphics g)
609   {
610     Dimension d = getSize();
611     imgWidth = d.width;
612     // (av.endRes - av.startRes + 1) * av.charWidth;
613     if (imgWidth < 1 || d.height < 1)
614     {
615       return;
616     }
617     if (image == null || imgWidth != image.getWidth(this)
618             || d.height != image.getHeight(this))
619     {
620       image = createImage(imgWidth, d.height);
621       gg = image.getGraphics();
622       gg.setFont(av.getFont());
623       fm = gg.getFontMetrics();
624       fastPaint = false;
625     }
626
627     if (fastPaint)
628     {
629       g.drawImage(image, 0, 0, this);
630       fastPaint = false;
631       return;
632     }
633
634     gg.setColor(Color.white);
635     gg.fillRect(0, 0, getSize().width, getSize().height);
636     drawComponent(gg, av.getRanges().getStartRes(),
637             av.getRanges().getEndRes() + 1);
638
639     g.drawImage(image, 0, 0, this);
640   }
641
642   public void fastPaint(int horizontal)
643   {
644     if (horizontal == 0 || gg == null
645             || av.getAlignment().getAlignmentAnnotation() == null
646             || av.getAlignment().getAlignmentAnnotation().length < 1)
647     {
648       repaint();
649       return;
650     }
651
652     gg.copyArea(0, 0, imgWidth, getSize().height,
653             -horizontal * av.getCharWidth(), 0);
654     int sr = av.getRanges().getStartRes(),
655             er = av.getRanges().getEndRes() + 1, transX = 0;
656
657     if (horizontal > 0) // scrollbar pulled right, image to the left
658     {
659       transX = (er - sr - horizontal) * av.getCharWidth();
660       sr = er - horizontal;
661     }
662     else if (horizontal < 0)
663     {
664       er = sr - horizontal;
665     }
666
667     gg.translate(transX, 0);
668
669     drawComponent(gg, sr, er);
670
671     gg.translate(-transX, 0);
672
673     fastPaint = true;
674     repaint();
675   }
676
677   /**
678    * DOCUMENT ME!
679    * 
680    * @param g
681    *          DOCUMENT ME!
682    * @param startRes
683    *          DOCUMENT ME!
684    * @param endRes
685    *          DOCUMENT ME!
686    */
687   public void drawComponent(Graphics g, int startRes, int endRes)
688   {
689     Font ofont = av.getFont();
690     g.setFont(ofont);
691
692     g.setColor(Color.white);
693     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(),
694             getSize().height);
695
696     if (fm == null)
697     {
698       fm = g.getFontMetrics();
699     }
700
701     if ((av.getAlignment().getAlignmentAnnotation() == null)
702             || (av.getAlignment().getAlignmentAnnotation().length < 1))
703     {
704       g.setColor(Color.white);
705       g.fillRect(0, 0, getSize().width, getSize().height);
706       g.setColor(Color.black);
707       if (av.validCharWidth)
708       {
709         g.drawString(MessageManager
710                 .getString("label.alignment_has_no_annotations"), 20, 15);
711       }
712
713       return;
714     }
715     g.translate(0, -scrollOffset);
716     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
717     g.translate(0, +scrollOffset);
718   }
719
720   int scrollOffset = 0;
721
722   public void setScrollOffset(int value, boolean repaint)
723   {
724     scrollOffset = value;
725     if (repaint)
726     {
727       repaint();
728     }
729   }
730
731   @Override
732   public FontMetrics getFontMetrics()
733   {
734     return fm;
735   }
736
737   @Override
738   public Image getFadedImage()
739   {
740     return image;
741   }
742
743   @Override
744   public int getFadedImageWidth()
745   {
746     return imgWidth;
747   }
748
749   private int[] bounds = new int[2];
750
751   @Override
752   public int[] getVisibleVRange()
753   {
754     if (ap != null && ap.alabels != null)
755     {
756       int sOffset = -ap.alabels.scrollOffset;
757       int visHeight = sOffset + ap.annotationPanelHolder.getHeight();
758       bounds[0] = sOffset;
759       bounds[1] = visHeight;
760       return bounds;
761     }
762     else
763     {
764       return null;
765     }
766   }
767
768   @Override
769   public void propertyChange(PropertyChangeEvent evt)
770   {
771     // Respond to viewport range changes (e.g. alignment panel was scrolled)
772     // Both scrolling and resizing change viewport ranges: scrolling changes
773     // both start and end points, but resize only changes end values.
774     // Here we only want to fastpaint on a scroll, with resize using a normal
775     // paint, so scroll events are identified as changes to the horizontal or
776     // vertical start value.
777     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
778     {
779       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
780     }
781     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
782     {
783       fastPaint(((int[]) evt.getNewValue())[0]
784               - ((int[]) evt.getOldValue())[0]);
785     }
786     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
787     {
788       repaint();
789     }
790   }
791 }