javadoc
[jalview.git] / src / jalview / gui / 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.gui;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import java.awt.image.*;
23 import javax.swing.*;
24
25 import jalview.datamodel.*;
26 import jalview.renderer.AnnotationRenderer;
27 import jalview.renderer.AwtRenderPanelI;
28
29 /**
30  * AnnotationPanel displays visible portion of annotation rows below unwrapped alignment 
31  * @author $author$
32  * @version $Revision$
33  */
34 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
35         MouseListener, MouseMotionListener, ActionListener,
36         AdjustmentListener
37 {
38   final String HELIX = "Helix";
39
40   final String SHEET = "Sheet";
41
42   /**
43    * For RNA secondary structure "stems" aka helices
44    */
45   final String STEM = "RNA Helix";
46
47   final String LABEL = "Label";
48
49   final String REMOVE = "Remove Annotation";
50
51   final String COLOUR = "Colour";
52
53   public final Color HELIX_COLOUR = Color.red.darker();
54
55   public final Color SHEET_COLOUR = Color.green.darker().darker();
56
57   public final Color STEM_COLOUR = Color.blue.darker();
58
59   /** DOCUMENT ME!! */
60   public AlignViewport av;
61
62   AlignmentPanel ap;
63
64   public int activeRow = -1;
65
66   public BufferedImage image;
67
68   public volatile BufferedImage fadedImage;
69
70   Graphics2D gg;
71
72   public FontMetrics fm;
73
74   public int imgWidth = 0;
75
76   boolean fastPaint = false;
77
78   // Used For mouse Dragging and resizing graphs
79   int graphStretch = -1;
80
81   int graphStretchY = -1;
82
83   int min; // used by mouseDragged to see if user
84
85   int max; // used by mouseDragged to see if user
86
87   boolean mouseDragging = false;
88
89   boolean MAC = false;
90
91   // for editing cursor
92   int cursorX = 0;
93
94   int cursorY = 0;
95
96   public final AnnotationRenderer renderer;
97
98   /**
99    * Creates a new AnnotationPanel object.
100    *
101    * @param ap
102    *          DOCUMENT ME!
103    */
104   public AnnotationPanel(AlignmentPanel ap)
105   {
106
107     MAC = new jalview.util.Platform().isAMac();
108
109     ToolTipManager.sharedInstance().registerComponent(this);
110     ToolTipManager.sharedInstance().setInitialDelay(0);
111     ToolTipManager.sharedInstance().setDismissDelay(10000);
112     this.ap = ap;
113     av = ap.av;
114     this.setLayout(null);
115     addMouseListener(this);
116     addMouseMotionListener(this);
117     ap.annotationScroller.getVerticalScrollBar()
118             .addAdjustmentListener(this);
119     renderer = new AnnotationRenderer();
120   }
121
122   public AnnotationPanel(AlignViewport av)
123   {
124     this.av = av;
125     renderer = new AnnotationRenderer();
126   }
127
128   /**
129    * DOCUMENT ME!
130    *
131    * @param evt
132    *          DOCUMENT ME!
133    */
134   @Override
135   public void adjustmentValueChanged(AdjustmentEvent evt)
136   {
137     ap.alabels.setScrollOffset(-evt.getValue());
138   }
139
140   /**
141    * Calculates the height of the annotation displayed in the annotation panel.
142    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
143    * all annotation associated components are updated correctly.
144    *
145    */
146   public int adjustPanelHeight()
147   {
148     int height = av.calcPanelHeight();
149     this.setPreferredSize(new Dimension(1, height));
150     if (ap != null)
151     {
152       // revalidate only when the alignment panel is fully constructed
153       ap.validate();
154     }
155
156     return height;
157   }
158
159   /**
160    * DOCUMENT ME!
161    *
162    * @param evt
163    *          DOCUMENT ME!
164    */
165   @Override
166   public void actionPerformed(ActionEvent evt)
167   {
168     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
169     if (aa == null)
170     {
171       return;
172     }
173     Annotation[] anot = aa[activeRow].annotations;
174
175     if (anot.length < av.getColumnSelection().getMax())
176     {
177       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
178       System.arraycopy(anot, 0, temp, 0, anot.length);
179       anot = temp;
180       aa[activeRow].annotations = anot;
181     }
182
183     if (evt.getActionCommand().equals(REMOVE))
184     {
185       for (int i = 0; i < av.getColumnSelection().size(); i++)
186       {
187         anot[av.getColumnSelection().columnAt(i)] = null;
188       }
189     }
190     else if (evt.getActionCommand().equals(LABEL))
191     {
192       String exMesg = collectAnnotVals(anot, av.getColumnSelection(), LABEL);
193       String label = JOptionPane.showInputDialog(this, "Enter label",
194               exMesg);
195
196       if (label == null)
197       {
198         return;
199       }
200
201       if ((label.length() > 0) && !aa[activeRow].hasText)
202       {
203         aa[activeRow].hasText = true;
204       }
205
206       for (int i = 0; i < av.getColumnSelection().size(); i++)
207       {
208         int index = av.getColumnSelection().columnAt(i);
209
210         if (!av.getColumnSelection().isVisible(index))
211           continue;
212
213         if (anot[index] == null)
214         {
215           anot[index] = new Annotation(label, "", ' ', 0); // TODO: verify that
216           // null exceptions
217           // aren't raised
218           // elsewhere.
219         }
220         else
221         {
222           anot[index].displayCharacter = label;
223         }
224       }
225     }
226     else if (evt.getActionCommand().equals(COLOUR))
227     {
228       Color col = JColorChooser.showDialog(this,
229               "Choose foreground colour", Color.black);
230
231       for (int i = 0; i < av.getColumnSelection().size(); i++)
232       {
233         int index = av.getColumnSelection().columnAt(i);
234
235         if (!av.getColumnSelection().isVisible(index))
236           continue;
237
238         if (anot[index] == null)
239         {
240           anot[index] = new Annotation("", "", ' ', 0);
241         }
242
243         anot[index].colour = col;
244       }
245     }
246     else
247     // HELIX OR SHEET
248     {
249       char type = 0;
250       String symbol = "\u03B1";
251
252       if (evt.getActionCommand().equals(HELIX))
253       {
254         type = 'H';
255       }
256       else if (evt.getActionCommand().equals(SHEET))
257       {
258         type = 'E';
259         symbol = "\u03B2";
260       }
261
262       // Added by LML to color stems
263       else if (evt.getActionCommand().equals(STEM))
264       {
265         type = 'S';
266         symbol = "\u03C3";
267       }
268
269       if (!aa[activeRow].hasIcons)
270       {
271         aa[activeRow].hasIcons = true;
272       }
273
274       String label = JOptionPane.showInputDialog(
275               "Enter a label for the structure?", symbol);
276
277       if (label == null)
278       {
279         return;
280       }
281
282       if ((label.length() > 0) && !aa[activeRow].hasText)
283       {
284         aa[activeRow].hasText = true;
285       }
286
287       for (int i = 0; i < av.getColumnSelection().size(); i++)
288       {
289         int index = av.getColumnSelection().columnAt(i);
290
291         if (!av.getColumnSelection().isVisible(index))
292           continue;
293
294         if (anot[index] == null)
295         {
296           anot[index] = new Annotation(label, "", type, 0);
297         }
298
299         anot[index].secondaryStructure = type;
300         anot[index].displayCharacter = label;
301       }
302     }
303     aa[activeRow].validateRangeAndDisplay();
304
305     adjustPanelHeight();
306     ap.alignmentChanged();
307     repaint();
308
309     return;
310   }
311
312   private String collectAnnotVals(Annotation[] anot,
313           ColumnSelection columnSelection, String label2)
314   {
315     String collatedInput = "";
316     String last = "";
317     ColumnSelection viscols=av.getColumnSelection();
318     // TODO: refactor and save av.getColumnSelection for efficiency
319     for (int i = 0; i < columnSelection.size(); i++)
320     {
321       int index = columnSelection.columnAt(i);
322       // always check for current display state - just in case
323       if (!viscols.isVisible(index))
324         continue;
325       String tlabel = null;
326       if (anot[index] != null)
327       { // LML added stem code
328         if (label2.equals(HELIX) || label2.equals(SHEET)
329                 || label2.equals(STEM) || label2.equals(LABEL))
330         {
331           tlabel = anot[index].description;
332           if (tlabel == null || tlabel.length() < 1)
333           {
334             if (label2.equals(HELIX) || label2.equals(SHEET)
335                     || label2.equals(STEM))
336             {
337               tlabel = "" + anot[index].secondaryStructure;
338             }
339             else
340             {
341               tlabel = "" + anot[index].displayCharacter;
342             }
343           }
344         }
345         if (tlabel != null && !tlabel.equals(last))
346         {
347           if (last.length() > 0)
348           {
349             collatedInput += " ";
350           }
351           collatedInput += tlabel;
352         }
353       }
354     }
355     return collatedInput;
356   }
357
358   /**
359    * DOCUMENT ME!
360    *
361    * @param evt
362    *          DOCUMENT ME!
363    */
364   @Override
365   public void mousePressed(MouseEvent evt)
366   {
367
368     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
369     if (aa == null)
370     {
371       return;
372     }
373
374     int height = 0;
375     activeRow = -1;
376
377     for (int i = 0; i < aa.length; i++)
378     {
379       if (aa[i].visible)
380       {
381         height += aa[i].height;
382       }
383
384       if (evt.getY() < height)
385       {
386         if (aa[i].editable)
387         {
388           activeRow = i;
389         }
390         else if (aa[i].graph > 0)
391         {
392           // Stretch Graph
393           graphStretch = i;
394           graphStretchY = evt.getY();
395         }
396
397         break;
398       }
399     }
400
401     if (SwingUtilities.isRightMouseButton(evt) && activeRow != -1)
402     {
403       if (av.getColumnSelection() == null)
404       {
405         return;
406       }
407
408       JPopupMenu pop = new JPopupMenu("Structure type");
409       JMenuItem item;
410       /*
411        * Just display the needed structure options
412        */
413       if (av.getAlignment().isNucleotide() == true)
414       {
415         item = new JMenuItem(STEM);
416         item.addActionListener(this);
417         pop.add(item);
418       }
419       else
420       {
421         item = new JMenuItem(HELIX);
422         item.addActionListener(this);
423         pop.add(item);
424         item = new JMenuItem(SHEET);
425         item.addActionListener(this);
426         pop.add(item);
427       }
428       item = new JMenuItem(LABEL);
429       item.addActionListener(this);
430       pop.add(item);
431       item = new JMenuItem(COLOUR);
432       item.addActionListener(this);
433       pop.add(item);
434       item = new JMenuItem(REMOVE);
435       item.addActionListener(this);
436       pop.add(item);
437       pop.show(this, evt.getX(), evt.getY());
438
439       return;
440     }
441
442     if (aa == null)
443     {
444       return;
445     }
446
447     ap.scalePanel.mousePressed(evt);
448
449   }
450
451   /**
452    * DOCUMENT ME!
453    *
454    * @param evt
455    *          DOCUMENT ME!
456    */
457   @Override
458   public void mouseReleased(MouseEvent evt)
459   {
460     graphStretch = -1;
461     graphStretchY = -1;
462     mouseDragging = false;
463     ap.scalePanel.mouseReleased(evt);
464   }
465
466   /**
467    * DOCUMENT ME!
468    *
469    * @param evt
470    *          DOCUMENT ME!
471    */
472   @Override
473   public void mouseEntered(MouseEvent evt)
474   {
475     ap.scalePanel.mouseEntered(evt);
476   }
477
478   /**
479    * DOCUMENT ME!
480    *
481    * @param evt
482    *          DOCUMENT ME!
483    */
484   @Override
485   public void mouseExited(MouseEvent evt)
486   {
487     ap.scalePanel.mouseExited(evt);
488   }
489
490   /**
491    * DOCUMENT ME!
492    *
493    * @param evt
494    *          DOCUMENT ME!
495    */
496   @Override
497   public void mouseDragged(MouseEvent evt)
498   {
499     if (graphStretch > -1)
500     {
501       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
502               - evt.getY();
503       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
504       {
505         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
506       }
507       graphStretchY = evt.getY();
508       adjustPanelHeight();
509       ap.paintAlignment(true);
510     }
511     else
512     {
513       ap.scalePanel.mouseDragged(evt);
514     }
515   }
516
517   /**
518    * DOCUMENT ME!
519    *
520    * @param evt
521    *          DOCUMENT ME!
522    */
523   @Override
524   public void mouseMoved(MouseEvent evt)
525   {
526     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
527
528     if (aa == null)
529     {
530       this.setToolTipText(null);
531       return;
532     }
533
534     int row = -1;
535     int height = 0;
536
537     for (int i = 0; i < aa.length; i++)
538     {
539       if (aa[i].visible)
540       {
541         height += aa[i].height;
542       }
543
544       if (evt.getY() < height)
545       {
546         row = i;
547
548         break;
549       }
550     }
551
552     if (row == -1)
553     {
554       this.setToolTipText(null);
555       return;
556     }
557
558     int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
559
560     if (av.hasHiddenColumns())
561     {
562       res = av.getColumnSelection().adjustForHiddenColumns(res);
563     }
564
565     if (row > -1 && aa[row].annotations != null
566             && res < aa[row].annotations.length)
567     {
568       if (aa[row].graphGroup > -1)
569       {
570         StringBuffer tip = new StringBuffer("<html>");
571         for (int gg = 0; gg < aa.length; gg++)
572         {
573           if (aa[gg].graphGroup == aa[row].graphGroup
574                   && aa[gg].annotations[res] != null)
575           {
576             tip.append(aa[gg].label + " "
577                     + aa[gg].annotations[res].description + "<br>");
578           }
579         }
580         if (tip.length() != 6)
581         {
582           tip.setLength(tip.length() - 4);
583           this.setToolTipText(tip.toString() + "</html>");
584         }
585       }
586       else if (aa[row].annotations[res] != null
587               && aa[row].annotations[res].description != null
588               && aa[row].annotations[res].description.length() > 0)
589       {
590         this.setToolTipText(aa[row].annotations[res].description);
591       }
592       else
593       {
594         // clear the tooltip.
595         this.setToolTipText(null);
596       }
597
598       if (aa[row].annotations[res] != null)
599       {
600         StringBuffer text = new StringBuffer("Sequence position "
601                 + (res + 1));
602
603         if (aa[row].annotations[res].description != null)
604         {
605           text.append("  " + aa[row].annotations[res].description);
606         }
607
608         ap.alignFrame.statusBar.setText(text.toString());
609       }
610     }
611     else
612     {
613       this.setToolTipText(null);
614     }
615   }
616
617   /**
618    * DOCUMENT ME!
619    *
620    * @param evt
621    *          DOCUMENT ME!
622    */
623   @Override
624   public void mouseClicked(MouseEvent evt)
625   {
626     if (activeRow != -1)
627     {
628       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
629       AlignmentAnnotation anot = aa[activeRow];
630
631       if (anot.description.equals("secondary structure"))
632       {
633         // System.out.println(anot.description+" "+anot.getRNAStruc());
634       }
635     }
636   }
637
638   // TODO mouseClicked-content and drawCursor are quite experimental!
639   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
640           int y1)
641   {
642     int pady = av.charHeight / 5;
643     int charOffset = 0;
644     graphics.setColor(Color.black);
645     graphics.fillRect(x1, y1, av.charWidth, av.charHeight);
646
647     if (av.validCharWidth)
648     {
649       graphics.setColor(Color.white);
650
651       char s = seq.getCharAt(res);
652
653       charOffset = (av.charWidth - fm.charWidth(s)) / 2;
654       graphics.drawString(String.valueOf(s), charOffset + x1,
655               (y1 + av.charHeight) - pady);
656     }
657
658   }
659   private volatile boolean imageFresh=false;
660   /**
661    * DOCUMENT ME!
662    *
663    * @param g
664    *          DOCUMENT ME!
665    */
666   @Override
667   public void paintComponent(Graphics g)
668   {
669     g.setColor(Color.white);
670     g.fillRect(0, 0, getWidth(), getHeight());
671
672     if (image != null)
673     {
674       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
675               || (getVisibleRect().height != g.getClipBounds().height))
676       {
677         g.drawImage(image, 0, 0, this);
678         fastPaint = false;
679         return;
680       }
681     }
682     imgWidth = (av.endRes - av.startRes + 1) * av.charWidth;
683     if (imgWidth < 1)
684       return;
685     if (image == null || imgWidth != image.getWidth()
686             || image.getHeight(this) != getHeight())
687     {
688       image = new BufferedImage(imgWidth, ap.annotationPanel.getHeight(),
689               BufferedImage.TYPE_INT_RGB);
690       gg = (Graphics2D) image.getGraphics();
691
692       if (av.antiAlias)
693       {
694         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
695                 RenderingHints.VALUE_ANTIALIAS_ON);
696       }
697
698       gg.setFont(av.getFont());
699       fm = gg.getFontMetrics();
700       gg.setColor(Color.white);
701       gg.fillRect(0, 0, imgWidth, image.getHeight());
702       imageFresh=true;
703     }
704
705     drawComponent(gg, av.startRes, av.endRes + 1);
706     imageFresh=false;
707     g.drawImage(image, 0, 0, this);
708   }
709
710   /**
711    * non-Thread safe repaint
712    *
713    * @param horizontal
714    *          repaint with horizontal shift in alignment
715    */
716   public void fastPaint(int horizontal)
717   {
718
719     if ((horizontal == 0) || gg == null
720             || av.getAlignment().getAlignmentAnnotation() == null
721             || av.getAlignment().getAlignmentAnnotation().length < 1
722             || av.isCalcInProgress())
723     {
724       repaint();
725       return;
726     }
727     gg.copyArea(0, 0, imgWidth, getHeight(), -horizontal * av.charWidth, 0);
728
729     int sr = av.startRes;
730     int er = av.endRes + 1;
731     int transX = 0;
732
733     if (horizontal > 0) // scrollbar pulled right, image to the left
734     {
735       transX = (er - sr - horizontal) * av.charWidth;
736       sr = er - horizontal;
737     }
738     else if (horizontal < 0)
739     {
740       er = sr - horizontal;
741     }
742
743     gg.translate(transX, 0);
744
745     drawComponent(gg, sr, er);
746
747     gg.translate(-transX, 0);
748
749     fastPaint = true;
750     repaint();
751
752   }
753   private volatile boolean lastImageGood=false;
754   /**
755    * DOCUMENT ME!
756    *
757    * @param g
758    *          DOCUMENT ME!
759    * @param startRes
760    *          DOCUMENT ME!
761    * @param endRes
762    *          DOCUMENT ME!
763    */
764   public void drawComponent(Graphics g, int startRes, int endRes)
765   {
766     BufferedImage oldFaded=fadedImage;
767     if (av.isCalcInProgress())
768     {
769       if (image == null)
770       {
771         lastImageGood=false;
772         return;
773       }
774       // We'll keep a record of the old image,
775       // and draw a faded image until the calculation
776       // has completed
777       if (lastImageGood && (fadedImage == null || fadedImage.getWidth() != imgWidth
778               || fadedImage.getHeight() != image.getHeight()))
779       {
780 //        System.err.println("redraw faded image ("+(fadedImage==null ? "null image" : "") + " lastGood="+lastImageGood+")");
781         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
782                 BufferedImage.TYPE_INT_RGB);
783
784         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
785
786         fadedG.setColor(Color.white);
787         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
788
789         fadedG.setComposite(AlphaComposite.getInstance(
790                 AlphaComposite.SRC_OVER, .3f));
791         fadedG.drawImage(image, 0, 0, this);
792
793       }
794       // make sure we don't overwrite the last good faded image until all calculations have finished
795       lastImageGood=false;
796
797     }
798     else
799     {
800       if (fadedImage!=null)
801       {
802         oldFaded=fadedImage;
803       }
804       fadedImage = null;
805     }
806
807     g.setColor(Color.white);
808     g.fillRect(0, 0, (endRes - startRes) * av.charWidth, getHeight());
809
810     g.setFont(av.getFont());
811     if (fm == null)
812     {
813       fm = g.getFontMetrics();
814     }
815
816     if ((av.getAlignment().getAlignmentAnnotation() == null)
817             || (av.getAlignment().getAlignmentAnnotation().length < 1))
818     {
819       g.setColor(Color.white);
820       g.fillRect(0, 0, getWidth(), getHeight());
821       g.setColor(Color.black);
822       if (av.validCharWidth)
823       {
824         g.drawString("Alignment has no annotations", 20, 15);
825       }
826
827       return;
828     }
829     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
830     if (!lastImageGood && fadedImage==null)
831     {
832       fadedImage=oldFaded;
833     }
834   }
835
836   @Override
837   public FontMetrics getFontMetrics()
838   {
839     return fm;
840   }
841
842   @Override
843   public Image getFadedImage()
844   {
845     return fadedImage;
846   }
847
848   @Override
849   public int getFadedImageWidth()
850   {
851     return imgWidth;
852   }
853 }