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