JAL-2146 show Column no [Sequence no [Residue / pos]] on mouseover
[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";
245       }
246
247       if (!aa[activeRow].hasIcons)
248       {
249         aa[activeRow].hasIcons = true;
250       }
251
252       label = enterLabel(symbol, "Enter Label");
253
254       if (label == null)
255       {
256         return;
257       }
258
259       if ((label.length() > 0) && !aa[activeRow].hasText)
260       {
261         aa[activeRow].hasText = true;
262         if (evt.getActionCommand().equals(STEM))
263         {
264           aa[activeRow].showAllColLabels = true;
265         }
266       }
267
268       for (int index : av.getColumnSelection().getSelected())
269       {
270         if (!av.getColumnSelection().isVisible(index))
271         {
272           continue;
273         }
274
275         if (anot[index] == null)
276         {
277           anot[index] = new Annotation(label, "", type, 0);
278         }
279
280         anot[index].secondaryStructure = type != 'S' ? type : label
281                 .length() == 0 ? ' ' : label.charAt(0);
282         anot[index].displayCharacter = label;
283       }
284     }
285
286     av.getAlignment().validateAnnotation(aa[activeRow]);
287
288     ap.alignmentChanged();
289     adjustPanelHeight();
290     repaint();
291
292     return;
293   }
294
295   String enterLabel(String text, String label)
296   {
297     EditNameDialog dialog = new EditNameDialog(text, null, label, null,
298             ap.alignFrame, "Enter Label", 400, 200, true);
299
300     if (dialog.accept)
301     {
302       return dialog.getName();
303     }
304     else
305     {
306       return null;
307     }
308   }
309
310   @Override
311   public void mousePressed(MouseEvent evt)
312   {
313     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
314     if (aa == null)
315     {
316       return;
317     }
318
319     int height = -scrollOffset;
320     activeRow = -1;
321
322     for (int i = 0; i < aa.length; i++)
323     {
324       if (aa[i].visible)
325       {
326         height += aa[i].height;
327       }
328
329       if (evt.getY() < height)
330       {
331         if (aa[i].editable)
332         {
333           activeRow = i;
334         }
335         else if (aa[i].graph > 0)
336         {
337           // Stretch Graph
338           graphStretch = i;
339           graphStretchY = evt.getY();
340         }
341
342         break;
343       }
344     }
345
346     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK
347             && activeRow != -1)
348     {
349       if (av.getColumnSelection() == null)
350       {
351         return;
352       }
353
354       PopupMenu pop = new PopupMenu(
355               MessageManager.getString("label.structure_type"));
356       MenuItem item;
357       /*
358        * Just display the needed structure options
359        */
360       if (av.getAlignment().isNucleotide() == true)
361       {
362         item = new MenuItem(STEM);
363         item.addActionListener(this);
364         pop.add(item);
365       }
366       else
367       {
368         item = new MenuItem(HELIX);
369         item.addActionListener(this);
370         pop.add(item);
371         item = new MenuItem(SHEET);
372         item.addActionListener(this);
373         pop.add(item);
374       }
375       item = new MenuItem(LABEL);
376       item.addActionListener(this);
377       pop.add(item);
378       item = new MenuItem(COLOUR);
379       item.addActionListener(this);
380       pop.add(item);
381       item = new MenuItem(REMOVE);
382       item.addActionListener(this);
383       pop.add(item);
384       ap.alignFrame.add(pop);
385       pop.show(this, evt.getX(), evt.getY());
386
387       return;
388     }
389
390     ap.scalePanel.mousePressed(evt);
391   }
392
393   @Override
394   public void mouseReleased(MouseEvent evt)
395   {
396     graphStretch = -1;
397     graphStretchY = -1;
398     mouseDragging = false;
399     if (needValidating)
400     {
401       ap.validate();
402       needValidating = false;
403     }
404     ap.scalePanel.mouseReleased(evt);
405   }
406
407   @Override
408   public void mouseClicked(MouseEvent evt)
409   {
410   }
411
412   boolean needValidating = false;
413
414   @Override
415   public void mouseDragged(MouseEvent evt)
416   {
417     if (graphStretch > -1)
418     {
419       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
420               - evt.getY();
421       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
422       {
423         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
424       }
425       graphStretchY = evt.getY();
426       av.calcPanelHeight();
427       needValidating = true;
428       ap.paintAlignment(true);
429     }
430     else
431     {
432       ap.scalePanel.mouseDragged(evt);
433     }
434   }
435
436   @Override
437   public void mouseMoved(MouseEvent evt)
438   {
439     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
440     if (aa == null)
441     {
442       return;
443     }
444
445     int row = -1;
446     int height = -scrollOffset;
447     for (int i = 0; i < aa.length; i++)
448     {
449
450       if (aa[i].visible)
451       {
452         height += aa[i].height;
453       }
454
455       if (evt.getY() < height)
456       {
457         row = i;
458         break;
459       }
460     }
461
462     int column = evt.getX() / av.getCharWidth() + av.getStartRes();
463
464     if (av.hasHiddenColumns())
465     {
466       column = av.getColumnSelection().adjustForHiddenColumns(column);
467     }
468
469     if (row > -1 && column < aa[row].annotations.length
470             && aa[row].annotations[column] != null)
471     {
472       StringBuilder text = new StringBuilder();
473       text.append(MessageManager.getString("label.column")).append(" ")
474               .append(column + 1);
475       if (aa[row].annotations[column].description != null)
476       {
477         text.append("  ").append(aa[row].annotations[column].description);
478       }
479
480       /*
481        * if the annotation is sequence-specific, show the sequence number
482        * in the alignment, and (if not a gap) the residue and position
483        */
484       SequenceI seqref = aa[row].sequenceRef;
485       if (seqref != null)
486       {
487         int seqIndex = av.getAlignment().findIndex(seqref);
488         if (seqIndex != -1)
489         {
490           text.append(", ")
491                   .append(MessageManager.getString("label.sequence"))
492                   .append(" ").append(seqIndex + 1);
493           char residue = seqref.getCharAt(column);
494           if (!Comparison.isGap(residue))
495           {
496             int residuePos = seqref.findPosition(column);
497             text.append(": ").append(residue).append(" (")
498                     .append(residuePos).append(")");
499           }
500         }
501       }
502
503       ap.alignFrame.statusBar.setText(text.toString());
504     }
505   }
506
507   @Override
508   public void mouseEntered(MouseEvent evt)
509   {
510     ap.scalePanel.mouseEntered(evt);
511   }
512
513   @Override
514   public void mouseExited(MouseEvent evt)
515   {
516     ap.scalePanel.mouseExited(evt);
517   }
518
519   public int adjustPanelHeight()
520   {
521     return adjustPanelHeight(true);
522   }
523
524   public int adjustPanelHeight(boolean repaint)
525   {
526     int height = av.calcPanelHeight();
527     this.setSize(new Dimension(getSize().width, height));
528     if (repaint)
529     {
530       repaint();
531     }
532     return height;
533   }
534
535   /**
536    * calculate the height for visible annotation, revalidating bounds where
537    * necessary ABSTRACT GUI METHOD
538    * 
539    * @return total height of annotation
540    */
541
542   public void addEditableColumn(int i)
543   {
544     if (activeRow == -1)
545     {
546       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
547       if (aa == null)
548       {
549         return;
550       }
551
552       for (int j = 0; j < aa.length; j++)
553       {
554         if (aa[j].editable)
555         {
556           activeRow = j;
557           break;
558         }
559       }
560     }
561   }
562
563   @Override
564   public void update(Graphics g)
565   {
566     paint(g);
567   }
568
569   @Override
570   public void paint(Graphics g)
571   {
572     Dimension d = getSize();
573     imgWidth = d.width;
574     // (av.endRes - av.startRes + 1) * av.charWidth;
575     if (imgWidth < 1 || d.height < 1)
576     {
577       return;
578     }
579     if (image == null || imgWidth != image.getWidth(this)
580             || d.height != image.getHeight(this))
581     {
582       image = createImage(imgWidth, d.height);
583       gg = image.getGraphics();
584       gg.setFont(av.getFont());
585       fm = gg.getFontMetrics();
586       fastPaint = false;
587     }
588
589     if (fastPaint)
590     {
591       g.drawImage(image, 0, 0, this);
592       fastPaint = false;
593       return;
594     }
595
596     gg.setColor(Color.white);
597     gg.fillRect(0, 0, getSize().width, getSize().height);
598     drawComponent(gg, av.startRes, av.endRes + 1);
599
600     g.drawImage(image, 0, 0, this);
601   }
602
603   public void fastPaint(int horizontal)
604   {
605     if (horizontal == 0
606             || av.getAlignment().getAlignmentAnnotation() == null
607             || av.getAlignment().getAlignmentAnnotation().length < 1)
608     {
609       repaint();
610       return;
611     }
612
613     gg.copyArea(0, 0, imgWidth, getSize().height,
614             -horizontal * av.getCharWidth(), 0);
615     int sr = av.startRes, er = av.endRes + 1, transX = 0;
616
617     if (horizontal > 0) // scrollbar pulled right, image to the left
618     {
619       transX = (er - sr - horizontal) * av.getCharWidth();
620       sr = er - horizontal;
621     }
622     else if (horizontal < 0)
623     {
624       er = sr - horizontal;
625     }
626
627     gg.translate(transX, 0);
628
629     drawComponent(gg, sr, er);
630
631     gg.translate(-transX, 0);
632
633     fastPaint = true;
634     repaint();
635   }
636
637   /**
638    * DOCUMENT ME!
639    * 
640    * @param g
641    *          DOCUMENT ME!
642    * @param startRes
643    *          DOCUMENT ME!
644    * @param endRes
645    *          DOCUMENT ME!
646    */
647   public void drawComponent(Graphics g, int startRes, int endRes)
648   {
649     Font ofont = av.getFont();
650     g.setFont(ofont);
651
652     g.setColor(Color.white);
653     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(),
654             getSize().height);
655
656     if (fm == null)
657     {
658       fm = g.getFontMetrics();
659     }
660
661     if ((av.getAlignment().getAlignmentAnnotation() == null)
662             || (av.getAlignment().getAlignmentAnnotation().length < 1))
663     {
664       g.setColor(Color.white);
665       g.fillRect(0, 0, getSize().width, getSize().height);
666       g.setColor(Color.black);
667       if (av.validCharWidth)
668       {
669         g.drawString(MessageManager
670                 .getString("label.alignment_has_no_annotations"), 20, 15);
671       }
672
673       return;
674     }
675     g.translate(0, -scrollOffset);
676     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
677     g.translate(0, +scrollOffset);
678   }
679
680   int scrollOffset = 0;
681
682   public void setScrollOffset(int value, boolean repaint)
683   {
684     scrollOffset = value;
685     if (repaint)
686     {
687       repaint();
688     }
689   }
690
691   @Override
692   public FontMetrics getFontMetrics()
693   {
694     return fm;
695   }
696
697   @Override
698   public Image getFadedImage()
699   {
700     return image;
701   }
702
703   @Override
704   public int getFadedImageWidth()
705   {
706     return imgWidth;
707   }
708
709   private int[] bounds = new int[2];
710
711   @Override
712   public int[] getVisibleVRange()
713   {
714     if (ap != null && ap.alabels != null)
715     {
716       int sOffset = -ap.alabels.scrollOffset;
717       int visHeight = sOffset + ap.annotationPanelHolder.getHeight();
718       bounds[0] = sOffset;
719       bounds[1] = visHeight;
720       return bounds;
721     }
722     else
723     {
724       return null;
725     }
726   }
727 }