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