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