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