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