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