(JAL-970) interactive editing of RNA helix annotation in applet
[jalview.git] / src / jalview / appletgui / AnnotationPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.appletgui;
19
20 import java.util.*;
21
22 import java.awt.*;
23 import java.awt.event.*;
24 import java.awt.font.LineMetrics;
25 import java.awt.geom.AffineTransform;
26
27 import jalview.analysis.AAFrequency;
28 import jalview.datamodel.*;
29 import jalview.renderer.AnnotationRenderer;
30 import jalview.renderer.AwtRenderPanelI;
31 import jalview.schemes.ColourSchemeI;
32
33 public class AnnotationPanel extends Panel implements AwtRenderPanelI, AdjustmentListener,
34         ActionListener, MouseListener, MouseMotionListener
35 {
36   AlignViewport av;
37
38   AlignmentPanel ap;
39
40   int activeRow = -1;
41
42   Vector activeRes;
43
44   final String HELIX = "Helix";
45
46   final String SHEET = "Sheet";
47   
48   /**
49    * For RNA secondary structure "stems" aka helices
50    */
51   final String STEM = "RNA Helix";
52
53   final String LABEL = "Label";
54
55   final String REMOVE = "Remove Annotation";
56
57   final String COLOUR = "Colour";
58
59   final Color HELIX_COLOUR = Color.red.darker();
60
61   final Color SHEET_COLOUR = Color.green.darker().darker();
62
63   Image image;
64
65   Graphics gg;
66
67   FontMetrics fm;
68
69   int imgWidth = 0;
70
71   boolean fastPaint = false;
72
73   // Used For mouse Dragging and resizing graphs
74   int graphStretch = -1;
75
76   int graphStretchY = -1;
77
78   boolean mouseDragging = false;
79
80   public static int GRAPH_HEIGHT = 40;
81
82   boolean MAC = false;
83
84   public final AnnotationRenderer renderer;
85
86   public AnnotationPanel(AlignmentPanel ap)
87   {
88     MAC = new jalview.util.Platform().isAMac();
89     this.ap = ap;
90     av = ap.av;
91     setLayout(null);
92     int height = adjustPanelHeight();
93     ap.apvscroll.setValues(0, getSize().height, 0, height);
94
95     addMouseMotionListener(this);
96
97     addMouseListener(this);
98
99     // ap.annotationScroller.getVAdjustable().addAdjustmentListener( this );
100     renderer = new AnnotationRenderer();
101   }
102
103   public AnnotationPanel(AlignViewport av)
104   {
105     this.av = av;
106     renderer = new AnnotationRenderer();
107   }
108
109   public void adjustmentValueChanged(AdjustmentEvent evt)
110   {
111   }
112
113   /**
114    * DOCUMENT ME!
115    * 
116    * @param evt
117    *          DOCUMENT ME!
118    */
119   public void actionPerformed(ActionEvent evt)
120   {
121     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
122     if (aa == null)
123     {
124       return;
125     }
126     Annotation[] anot = aa[activeRow].annotations;
127
128     if (anot.length < av.getColumnSelection().getMax())
129     {
130       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
131       System.arraycopy(anot, 0, temp, 0, anot.length);
132       anot = temp;
133       aa[activeRow].annotations = anot;
134     }
135
136     String label = "";
137     if (av.getColumnSelection() != null && av.getColumnSelection().size() > 0
138             && anot[av.getColumnSelection().getMin()] != null)
139       label = anot[av.getColumnSelection().getMin()].displayCharacter;
140
141     if (evt.getActionCommand().equals(REMOVE))
142     {
143       for (int i = 0; i < av.getColumnSelection().size(); i++)
144       {
145         anot[av.getColumnSelection().columnAt(i)] = null;
146       }
147     }
148     else if (evt.getActionCommand().equals(LABEL))
149     {
150       label = enterLabel(label, "Enter Label");
151
152       if (label == null)
153       {
154         return;
155       }
156
157       if ((label.length() > 0) && !aa[activeRow].hasText)
158       {
159         aa[activeRow].hasText = true;
160       }
161
162       for (int i = 0; i < av.getColumnSelection().size(); i++)
163       {
164         int index = av.getColumnSelection().columnAt(i);
165
166         if (!av.getColumnSelection().isVisible(index))
167           continue;
168
169         if (anot[index] == null)
170         {
171           anot[index] = new Annotation(label, "", ' ', 0);
172         }
173
174         anot[index].displayCharacter = label;
175       }
176     }
177     else if (evt.getActionCommand().equals(COLOUR))
178     {
179       UserDefinedColours udc = new UserDefinedColours(this, Color.black,
180               ap.alignFrame);
181
182       Color col = udc.getColor();
183
184       for (int i = 0; i < av.getColumnSelection().size(); i++)
185       {
186         int index = av.getColumnSelection().columnAt(i);
187
188         if (!av.getColumnSelection().isVisible(index))
189           continue;
190
191         if (anot[index] == null)
192         {
193           anot[index] = new Annotation("", "", ' ', 0);
194         }
195
196         anot[index].colour = col;
197       }
198     }
199     else
200     // HELIX OR SHEET
201     {
202       char type = 0;
203       String symbol = "\u03B1";
204
205       if (evt.getActionCommand().equals(HELIX))
206       {
207         type = 'H';
208       }
209       else if (evt.getActionCommand().equals(SHEET))
210       {
211         type = 'E';
212         symbol = "\u03B2";
213       }
214
215       // Added by LML to color stems
216       else if (evt.getActionCommand().equals(STEM))
217       {
218         type = 'S';
219         symbol = "\u03C3";
220       }
221
222       if (!aa[activeRow].hasIcons)
223       {
224         aa[activeRow].hasIcons = true;
225       }
226
227       label = enterLabel(symbol, "Enter Label");
228
229       if (label == null)
230       {
231         return;
232       }
233
234       if ((label.length() > 0) && !aa[activeRow].hasText)
235       {
236         aa[activeRow].hasText = true;
237       }
238
239       for (int i = 0; i < av.getColumnSelection().size(); i++)
240       {
241         int index = av.getColumnSelection().columnAt(i);
242
243         if (!av.getColumnSelection().isVisible(index))
244           continue;
245
246         if (anot[index] == null)
247         {
248           anot[index] = new Annotation(label, "", type, 0);
249         }
250
251         anot[index].secondaryStructure = type;
252         anot[index].displayCharacter = label;
253       }
254     }
255
256     aa[activeRow].validateRangeAndDisplay();
257
258     adjustPanelHeight();
259     repaint();
260
261     return;
262   }
263
264   String enterLabel(String text, String label)
265   {
266     EditNameDialog dialog = new EditNameDialog(text, null, label, null,
267             ap.alignFrame, "Enter Label", 400, 200, true);
268
269     if (dialog.accept)
270       return dialog.getName();
271     else
272       return null;
273   }
274
275   public void mousePressed(MouseEvent evt)
276   {
277     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
278     if (aa == null)
279     {
280       return;
281     }
282
283     int height = -scrollOffset;
284     activeRow = -1;
285
286     for (int i = 0; i < aa.length; i++)
287     {
288       if (aa[i].visible)
289       {
290         height += aa[i].height;
291       }
292
293       if (evt.getY() < height)
294       {
295         if (aa[i].editable)
296         {
297           activeRow = i;
298         }
299         else if (aa[i].graph > 0)
300         {
301           // Stretch Graph
302           graphStretch = i;
303           graphStretchY = evt.getY();
304         }
305
306         break;
307       }
308     }
309
310     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK
311             && activeRow != -1)
312     {
313       if (av.getColumnSelection() == null)
314       {
315         return;
316       }
317
318       PopupMenu pop = new PopupMenu("Structure type");
319       MenuItem item;
320       /*
321        * Just display the needed structure options
322        */
323       if (av.getAlignment().isNucleotide() == true)
324       {
325         item = new MenuItem(STEM);
326         item.addActionListener(this);
327         pop.add(item);
328       } else {
329         item = new MenuItem(HELIX);
330         item.addActionListener(this);
331         pop.add(item);
332         item = new MenuItem(SHEET);
333         item.addActionListener(this);
334         pop.add(item);
335       }
336       item = new MenuItem(LABEL);
337       item.addActionListener(this);
338       pop.add(item);
339       item = new MenuItem(COLOUR);
340       item.addActionListener(this);
341       pop.add(item);
342       item = new MenuItem(REMOVE);
343       item.addActionListener(this);
344       pop.add(item);
345       ap.alignFrame.add(pop);
346       pop.show(this, evt.getX(), evt.getY());
347
348       return;
349     }
350
351     if (aa == null)
352     {
353       return;
354     }
355
356     ap.scalePanel.mousePressed(evt);
357   }
358
359   public void mouseReleased(MouseEvent evt)
360   {
361     graphStretch = -1;
362     graphStretchY = -1;
363     mouseDragging = false;
364     if (needValidating)
365     {
366       ap.validate();
367       needValidating = false;
368     }
369     ap.scalePanel.mouseReleased(evt);
370   }
371
372   public void mouseClicked(MouseEvent evt)
373   {
374   }
375
376   boolean needValidating = false;
377
378   public void mouseDragged(MouseEvent evt)
379   {
380     if (graphStretch > -1)
381     {
382       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
383               - evt.getY();
384       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
385       {
386         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
387       }
388       graphStretchY = evt.getY();
389       calcPanelHeight();
390       needValidating = true;
391       ap.paintAlignment(true);
392     }
393     else
394     {
395       ap.scalePanel.mouseDragged(evt);
396     }
397   }
398
399   public void mouseMoved(MouseEvent evt)
400   {
401     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
402     if (aa == null)
403     {
404       return;
405     }
406
407     int row = -1;
408     int height = -scrollOffset;
409     for (int i = 0; i < aa.length; i++)
410     {
411
412       if (aa[i].visible)
413       {
414         height += aa[i].height;
415       }
416
417       if (evt.getY() < height)
418       {
419         row = i;
420         break;
421       }
422     }
423
424     int res = evt.getX() / av.getCharWidth() + av.getStartRes();
425
426     if (av.hasHiddenColumns())
427     {
428       res = av.getColumnSelection().adjustForHiddenColumns(res);
429     }
430
431     if (row > -1 && res < aa[row].annotations.length
432             && aa[row].annotations[res] != null)
433     {
434       StringBuffer text = new StringBuffer("Sequence position " + (res + 1));
435       if (aa[row].annotations[res].description != null)
436       {
437         text.append("  " + aa[row].annotations[res].description);
438       }
439       ap.alignFrame.statusBar.setText(text.toString());
440     }
441   }
442
443   public void mouseEntered(MouseEvent evt)
444   {
445     ap.scalePanel.mouseEntered(evt);
446   }
447
448   public void mouseExited(MouseEvent evt)
449   {
450     ap.scalePanel.mouseExited(evt);
451   }
452
453   public int adjustPanelHeight()
454   {
455     return adjustPanelHeight(true);
456   }
457
458   public int adjustPanelHeight(boolean repaint)
459   {
460     int height = calcPanelHeight();
461     this.setSize(new Dimension(getSize().width, height));
462     if (repaint)
463     {
464       repaint();
465     }
466     return height;
467   }
468   /**
469    * calculate the height for visible annotation, revalidating bounds where necessary
470    * ABSTRACT GUI METHOD
471    * @return total height of annotation
472    */
473   public int calcPanelHeight()
474   {
475     // setHeight of panels
476     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
477     int height = 0;
478
479     if (aa != null)
480     {
481       for (int i = 0; i < aa.length; i++)
482       {
483         if (!aa[i].visible)
484         {
485           continue;
486         }
487
488         aa[i].height = 0;
489
490         if (aa[i].hasText)
491         {
492           aa[i].height += av.charHeight;
493         }
494
495         if (aa[i].hasIcons)
496         {
497           aa[i].height += 16;
498         }
499
500         if (aa[i].graph > 0)
501         {
502           aa[i].height += aa[i].graphHeight;
503         }
504
505         if (aa[i].height == 0)
506         {
507           aa[i].height = 20;
508         }
509
510         height += aa[i].height;
511       }
512     }
513     if (height == 0)
514     {
515       height = 20;
516     }
517
518     return height;
519
520   }
521
522   public void addEditableColumn(int i)
523   {
524     if (activeRow == -1)
525     {
526       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
527       if (aa == null)
528       {
529         return;
530       }
531
532       for (int j = 0; j < aa.length; j++)
533       {
534         if (aa[j].editable)
535         {
536           activeRow = j;
537           break;
538         }
539       }
540     }
541
542     if (activeRes == null)
543     {
544       activeRes = new Vector();
545       activeRes.addElement(String.valueOf(i));
546       return;
547     }
548
549     activeRes.addElement(String.valueOf(i));
550   }
551
552   public void update(Graphics g)
553   {
554     paint(g);
555   }
556
557   public void paint(Graphics g)
558   {
559     Dimension d = getSize();
560     imgWidth = d.width;
561     // (av.endRes - av.startRes + 1) * av.charWidth;
562     if (imgWidth<1 || d.height<1)
563     {
564       return;
565     }
566     if (image == null || imgWidth != image.getWidth(this) || d.height != image.getHeight(this))
567     {
568       image = createImage(imgWidth, d.height);
569       gg = image.getGraphics();
570       gg.setFont(av.getFont());
571       fm = gg.getFontMetrics();
572       fastPaint = false;
573     }
574
575     if (fastPaint)
576     {
577       g.drawImage(image, 0, 0, this);
578       fastPaint = false;
579       return;
580     }
581
582     gg.setColor(Color.white);
583     gg.fillRect(0, 0, getSize().width, getSize().height);
584     drawComponent(gg, av.startRes, av.endRes + 1);
585
586     g.drawImage(image, 0, 0, this);
587   }
588
589   public void fastPaint(int horizontal)
590   {
591     if (horizontal == 0 || av.getAlignment().getAlignmentAnnotation() == null
592             || av.getAlignment().getAlignmentAnnotation().length < 1)
593     {
594       repaint();
595       return;
596     }
597
598     gg.copyArea(0, 0, imgWidth, getSize().height, -horizontal
599             * av.charWidth, 0);
600     int sr = av.startRes, er = av.endRes + 1, transX = 0;
601
602     if (horizontal > 0) // scrollbar pulled right, image to the left
603     {
604       transX = (er - sr - horizontal) * av.charWidth;
605       sr = er - horizontal;
606     }
607     else if (horizontal < 0)
608     {
609       er = sr - horizontal;
610     }
611
612     gg.translate(transX, 0);
613
614     drawComponent(gg, sr, er);
615
616     gg.translate(-transX, 0);
617
618     fastPaint = true;
619     repaint();
620   }
621
622   /**
623    * DOCUMENT ME!
624    * 
625    * @param g
626    *          DOCUMENT ME!
627    * @param startRes
628    *          DOCUMENT ME!
629    * @param endRes
630    *          DOCUMENT ME!
631    */
632   public void drawComponent(Graphics g, int startRes, int endRes)
633   {
634     Font ofont = av.getFont();
635     g.setFont(ofont);
636
637     g.setColor(Color.white);
638     g.fillRect(0, 0, (endRes - startRes) * av.charWidth, getSize().height);
639
640     if (fm == null)
641     {
642       fm = g.getFontMetrics();
643     }
644
645     if ((av.getAlignment().getAlignmentAnnotation() == null)
646             || (av.getAlignment().getAlignmentAnnotation().length < 1))
647     {
648       g.setColor(Color.white);
649       g.fillRect(0, 0, getSize().width, getSize().height);
650       g.setColor(Color.black);
651       if (av.validCharWidth)
652       {
653         g.drawString("Alignment has no annotations", 20, 15);
654       }
655
656       return;
657     }
658     g.translate(0, -scrollOffset);
659     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
660     g.translate(0, +scrollOffset);
661   }
662   
663   int scrollOffset = 0;
664
665   public void setScrollOffset(int value)
666   {
667     scrollOffset = value;
668     repaint();
669   }
670
671   @Override
672   public FontMetrics getFontMetrics()
673   {
674     return fm;
675   }
676
677   @Override
678   public Image getFadedImage()
679   {
680     return image;
681   }
682
683   @Override
684   public int getFadedImageWidth()
685   {
686     return imgWidth;
687   }
688 }