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