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