JAL-2446 merged to spike branch
[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.getAlignment().getHiddenColumns().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.getAlignment().getHiddenColumns().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.getAlignment().getHiddenColumns().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.getAlignment().getHiddenColumns().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.getAlignment().getHiddenColumns()
474               .adjustForHiddenColumns(column);
475     }
476
477     if (row > -1 && column < aa[row].annotations.length
478             && aa[row].annotations[column] != null)
479     {
480       StringBuilder text = new StringBuilder();
481       text.append(MessageManager.getString("label.column")).append(" ")
482               .append(column + 1);
483       String description = aa[row].annotations[column].description;
484       if (description != null && description.length() > 0)
485       {
486         text.append("  ").append(description);
487       }
488
489       /*
490        * if the annotation is sequence-specific, show the sequence number
491        * in the alignment, and (if not a gap) the residue and position
492        */
493       SequenceI seqref = aa[row].sequenceRef;
494       if (seqref != null)
495       {
496         int seqIndex = av.getAlignment().findIndex(seqref);
497         if (seqIndex != -1)
498         {
499           text.append(", ")
500                   .append(MessageManager.getString("label.sequence"))
501                   .append(" ").append(seqIndex + 1);
502           char residue = seqref.getCharAt(column);
503           if (!Comparison.isGap(residue))
504           {
505             text.append(" ");
506             String name;
507             if (av.getAlignment().isNucleotide())
508             {
509               name = ResidueProperties.nucleotideName.get(String
510                       .valueOf(residue));
511               text.append(" Nucleotide: ").append(
512                       name != null ? name : residue);
513             }
514             else
515             {
516               name = 'X' == residue ? "X" : ('*' == residue ? "STOP"
517                       : ResidueProperties.aa2Triplet.get(String
518                               .valueOf(residue)));
519               text.append(" Residue: ").append(
520                       name != null ? name : residue);
521             }
522             int residuePos = seqref.findPosition(column);
523             text.append(" (").append(residuePos).append(")");
524             // int residuePos = seqref.findPosition(column);
525             // text.append(residue).append(" (")
526             // .append(residuePos).append(")");
527           }
528         }
529       }
530
531       ap.alignFrame.statusBar.setText(text.toString());
532     }
533   }
534
535   @Override
536   public void mouseEntered(MouseEvent evt)
537   {
538     ap.scalePanel.mouseEntered(evt);
539   }
540
541   @Override
542   public void mouseExited(MouseEvent evt)
543   {
544     ap.scalePanel.mouseExited(evt);
545   }
546
547   public int adjustPanelHeight()
548   {
549     return adjustPanelHeight(true);
550   }
551
552   public int adjustPanelHeight(boolean repaint)
553   {
554     int height = av.calcPanelHeight();
555     this.setSize(new Dimension(getSize().width, height));
556     if (repaint)
557     {
558       repaint();
559     }
560     return height;
561   }
562
563   /**
564    * calculate the height for visible annotation, revalidating bounds where
565    * necessary ABSTRACT GUI METHOD
566    * 
567    * @return total height of annotation
568    */
569
570   public void addEditableColumn(int i)
571   {
572     if (activeRow == -1)
573     {
574       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
575       if (aa == null)
576       {
577         return;
578       }
579
580       for (int j = 0; j < aa.length; j++)
581       {
582         if (aa[j].editable)
583         {
584           activeRow = j;
585           break;
586         }
587       }
588     }
589   }
590
591   @Override
592   public void update(Graphics g)
593   {
594     paint(g);
595   }
596
597   @Override
598   public void paint(Graphics g)
599   {
600     Dimension d = getSize();
601     imgWidth = d.width;
602     // (av.endRes - av.startRes + 1) * av.charWidth;
603     if (imgWidth < 1 || d.height < 1)
604     {
605       return;
606     }
607     if (image == null || imgWidth != image.getWidth(this)
608             || d.height != image.getHeight(this))
609     {
610       image = createImage(imgWidth, d.height);
611       gg = image.getGraphics();
612       gg.setFont(av.getFont());
613       fm = gg.getFontMetrics();
614       fastPaint = false;
615     }
616
617     if (fastPaint)
618     {
619       g.drawImage(image, 0, 0, this);
620       fastPaint = false;
621       return;
622     }
623
624     gg.setColor(Color.white);
625     gg.fillRect(0, 0, getSize().width, getSize().height);
626     drawComponent(gg, av.getRanges().getStartRes(), av.getRanges()
627             .getEndRes() + 1);
628
629     g.drawImage(image, 0, 0, this);
630   }
631
632   public void fastPaint(int horizontal)
633   {
634     if (horizontal == 0
635             || av.getAlignment().getAlignmentAnnotation() == null
636             || av.getAlignment().getAlignmentAnnotation().length < 1)
637     {
638       repaint();
639       return;
640     }
641
642     gg.copyArea(0, 0, imgWidth, getSize().height,
643             -horizontal * av.getCharWidth(), 0);
644     int sr = av.getRanges().getStartRes(), er = av.getRanges().getEndRes() + 1, transX = 0;
645
646     if (horizontal > 0) // scrollbar pulled right, image to the left
647     {
648       transX = (er - sr - horizontal) * av.getCharWidth();
649       sr = er - horizontal;
650     }
651     else if (horizontal < 0)
652     {
653       er = sr - horizontal;
654     }
655
656     gg.translate(transX, 0);
657
658     drawComponent(gg, sr, er);
659
660     gg.translate(-transX, 0);
661
662     fastPaint = true;
663     repaint();
664   }
665
666   /**
667    * DOCUMENT ME!
668    * 
669    * @param g
670    *          DOCUMENT ME!
671    * @param startRes
672    *          DOCUMENT ME!
673    * @param endRes
674    *          DOCUMENT ME!
675    */
676   public void drawComponent(Graphics g, int startRes, int endRes)
677   {
678     Font ofont = av.getFont();
679     g.setFont(ofont);
680
681     g.setColor(Color.white);
682     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(),
683             getSize().height);
684
685     if (fm == null)
686     {
687       fm = g.getFontMetrics();
688     }
689
690     if ((av.getAlignment().getAlignmentAnnotation() == null)
691             || (av.getAlignment().getAlignmentAnnotation().length < 1))
692     {
693       g.setColor(Color.white);
694       g.fillRect(0, 0, getSize().width, getSize().height);
695       g.setColor(Color.black);
696       if (av.validCharWidth)
697       {
698         g.drawString(MessageManager
699                 .getString("label.alignment_has_no_annotations"), 20, 15);
700       }
701
702       return;
703     }
704     g.translate(0, -scrollOffset);
705     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
706     g.translate(0, +scrollOffset);
707   }
708
709   int scrollOffset = 0;
710
711   public void setScrollOffset(int value, boolean repaint)
712   {
713     scrollOffset = value;
714     if (repaint)
715     {
716       repaint();
717     }
718   }
719
720   @Override
721   public FontMetrics getFontMetrics()
722   {
723     return fm;
724   }
725
726   @Override
727   public Image getFadedImage()
728   {
729     return image;
730   }
731
732   @Override
733   public int getFadedImageWidth()
734   {
735     return imgWidth;
736   }
737
738   private int[] bounds = new int[2];
739
740   @Override
741   public int[] getVisibleVRange()
742   {
743     if (ap != null && ap.alabels != null)
744     {
745       int sOffset = -ap.alabels.scrollOffset;
746       int visHeight = sOffset + ap.annotationPanelHolder.getHeight();
747       bounds[0] = sOffset;
748       bounds[1] = visHeight;
749       return bounds;
750     }
751     else
752     {
753       return null;
754     }
755   }
756
757   @Override
758   public void propertyChange(PropertyChangeEvent evt)
759   {
760     // Respond to viewport range changes (e.g. alignment panel was scrolled)
761     if (evt.getPropertyName().equals("startres")
762             || evt.getPropertyName().equals("endres"))
763     {
764       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
765     }
766   }
767 }