Merge remote-tracking branch 'origin/develop' into bug/JAL-2491
[jalview.git] / src / jalview / gui / 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.gui;
22
23 import jalview.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.Annotation;
25 import jalview.datamodel.ColumnSelection;
26 import jalview.datamodel.HiddenColumns;
27 import jalview.datamodel.SequenceI;
28 import jalview.renderer.AnnotationRenderer;
29 import jalview.renderer.AwtRenderPanelI;
30 import jalview.schemes.ResidueProperties;
31 import jalview.util.Comparison;
32 import jalview.util.MessageManager;
33 import jalview.viewmodel.ViewportListenerI;
34
35 import java.awt.AlphaComposite;
36 import java.awt.Color;
37 import java.awt.Dimension;
38 import java.awt.FontMetrics;
39 import java.awt.Graphics;
40 import java.awt.Graphics2D;
41 import java.awt.Image;
42 import java.awt.Rectangle;
43 import java.awt.RenderingHints;
44 import java.awt.event.ActionEvent;
45 import java.awt.event.ActionListener;
46 import java.awt.event.AdjustmentEvent;
47 import java.awt.event.AdjustmentListener;
48 import java.awt.event.MouseEvent;
49 import java.awt.event.MouseListener;
50 import java.awt.event.MouseMotionListener;
51 import java.awt.event.MouseWheelEvent;
52 import java.awt.event.MouseWheelListener;
53 import java.awt.image.BufferedImage;
54 import java.beans.PropertyChangeEvent;
55 import java.util.ArrayList;
56 import java.util.Collections;
57 import java.util.List;
58
59 import javax.swing.JColorChooser;
60 import javax.swing.JMenuItem;
61 import javax.swing.JPanel;
62 import javax.swing.JPopupMenu;
63 import javax.swing.Scrollable;
64 import javax.swing.ToolTipManager;
65
66 /**
67  * AnnotationPanel displays visible portion of annotation rows below unwrapped
68  * alignment
69  * 
70  * @author $author$
71  * @version $Revision$
72  */
73 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
74         MouseListener, MouseWheelListener, MouseMotionListener,
75         ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
76 {
77   String HELIX = MessageManager.getString("label.helix");
78
79   String SHEET = MessageManager.getString("label.sheet");
80
81   /**
82    * For RNA secondary structure "stems" aka helices
83    */
84   String STEM = MessageManager.getString("label.rna_helix");
85
86   String LABEL = MessageManager.getString("label.label");
87
88   String REMOVE = MessageManager.getString("label.remove_annotation");
89
90   String COLOUR = MessageManager.getString("action.colour");
91
92   public final Color HELIX_COLOUR = Color.red.darker();
93
94   public final Color SHEET_COLOUR = Color.green.darker().darker();
95
96   public final Color STEM_COLOUR = Color.blue.darker();
97
98   /** DOCUMENT ME!! */
99   public AlignViewport av;
100
101   AlignmentPanel ap;
102
103   public int activeRow = -1;
104
105   public BufferedImage image;
106
107   public volatile BufferedImage fadedImage;
108
109   Graphics2D gg;
110
111   public FontMetrics fm;
112
113   public int imgWidth = 0;
114
115   boolean fastPaint = false;
116
117   // Used For mouse Dragging and resizing graphs
118   int graphStretch = -1;
119
120   int graphStretchY = -1;
121
122   int min; // used by mouseDragged to see if user
123
124   int max; // used by mouseDragged to see if user
125
126   boolean mouseDragging = false;
127
128   // for editing cursor
129   int cursorX = 0;
130
131   int cursorY = 0;
132
133   public final AnnotationRenderer renderer;
134
135   private MouseWheelListener[] _mwl;
136
137   /**
138    * Creates a new AnnotationPanel object.
139    * 
140    * @param ap
141    *          DOCUMENT ME!
142    */
143   public AnnotationPanel(AlignmentPanel ap)
144   {
145     ToolTipManager.sharedInstance().registerComponent(this);
146     ToolTipManager.sharedInstance().setInitialDelay(0);
147     ToolTipManager.sharedInstance().setDismissDelay(10000);
148     this.ap = ap;
149     av = ap.av;
150     this.setLayout(null);
151     addMouseListener(this);
152     addMouseMotionListener(this);
153     ap.annotationScroller.getVerticalScrollBar()
154             .addAdjustmentListener(this);
155     // save any wheel listeners on the scroller, so we can propagate scroll
156     // events to them.
157     _mwl = ap.annotationScroller.getMouseWheelListeners();
158     // and then set our own listener to consume all mousewheel events
159     ap.annotationScroller.addMouseWheelListener(this);
160     renderer = new AnnotationRenderer();
161
162     av.getRanges().addPropertyChangeListener(this);
163   }
164
165   public AnnotationPanel(AlignViewport av)
166   {
167     this.av = av;
168     renderer = new AnnotationRenderer();
169   }
170
171   @Override
172   public void mouseWheelMoved(MouseWheelEvent e)
173   {
174     if (e.isShiftDown())
175     {
176       e.consume();
177       if (e.getWheelRotation() > 0)
178       {
179         av.getRanges().scrollRight(true);
180       }
181       else
182       {
183         av.getRanges().scrollRight(false);
184       }
185     }
186     else
187     {
188       // TODO: find the correct way to let the event bubble up to
189       // ap.annotationScroller
190       for (MouseWheelListener mwl : _mwl)
191       {
192         if (mwl != null)
193         {
194           mwl.mouseWheelMoved(e);
195         }
196         if (e.isConsumed())
197         {
198           break;
199         }
200       }
201     }
202   }
203
204   @Override
205   public Dimension getPreferredScrollableViewportSize()
206   {
207     return getPreferredSize();
208   }
209
210   @Override
211   public int getScrollableBlockIncrement(Rectangle visibleRect,
212           int orientation, int direction)
213   {
214     return 30;
215   }
216
217   @Override
218   public boolean getScrollableTracksViewportHeight()
219   {
220     return false;
221   }
222
223   @Override
224   public boolean getScrollableTracksViewportWidth()
225   {
226     return true;
227   }
228
229   @Override
230   public int getScrollableUnitIncrement(Rectangle visibleRect,
231           int orientation, int direction)
232   {
233     return 30;
234   }
235
236   /*
237    * (non-Javadoc)
238    * 
239    * @see
240    * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
241    * .AdjustmentEvent)
242    */
243   @Override
244   public void adjustmentValueChanged(AdjustmentEvent evt)
245   {
246     // update annotation label display
247     ap.getAlabels().setScrollOffset(-evt.getValue());
248   }
249
250   /**
251    * Calculates the height of the annotation displayed in the annotation panel.
252    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
253    * all annotation associated components are updated correctly.
254    * 
255    */
256   public int adjustPanelHeight()
257   {
258     int height = av.calcPanelHeight();
259     this.setPreferredSize(new Dimension(1, height));
260     if (ap != null)
261     {
262       // revalidate only when the alignment panel is fully constructed
263       ap.validate();
264     }
265
266     return height;
267   }
268
269   /**
270    * DOCUMENT ME!
271    * 
272    * @param evt
273    *          DOCUMENT ME!
274    */
275   @Override
276   public void actionPerformed(ActionEvent evt)
277   {
278     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
279     if (aa == null)
280     {
281       return;
282     }
283     Annotation[] anot = aa[activeRow].annotations;
284
285     if (anot.length < av.getColumnSelection().getMax())
286     {
287       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
288       System.arraycopy(anot, 0, temp, 0, anot.length);
289       anot = temp;
290       aa[activeRow].annotations = anot;
291     }
292
293     String action = evt.getActionCommand();
294     if (action.equals(REMOVE))
295     {
296       for (int index : av.getColumnSelection().getSelected())
297       {
298         if (av.getAlignment().getHiddenColumns().isVisible(index))
299         {
300           anot[index] = null;
301         }
302       }
303     }
304     else if (action.equals(LABEL))
305     {
306       String exMesg = collectAnnotVals(anot, LABEL);
307       String label = JvOptionPane.showInputDialog(this,
308               MessageManager.getString("label.enter_label"), exMesg);
309
310       if (label == null)
311       {
312         return;
313       }
314
315       if ((label.length() > 0) && !aa[activeRow].hasText)
316       {
317         aa[activeRow].hasText = true;
318       }
319
320       for (int index : av.getColumnSelection().getSelected())
321       {
322         if (!av.getAlignment().getHiddenColumns().isVisible(index))
323         {
324           continue;
325         }
326
327         if (anot[index] == null)
328         {
329           anot[index] = new Annotation(label, "", ' ', 0);
330         }
331         else
332         {
333           anot[index].displayCharacter = label;
334         }
335       }
336     }
337     else if (action.equals(COLOUR))
338     {
339       Color col = JColorChooser.showDialog(this,
340               MessageManager.getString("label.select_foreground_colour"),
341               Color.black);
342
343       for (int index : av.getColumnSelection().getSelected())
344       {
345         if (!av.getAlignment().getHiddenColumns().isVisible(index))
346         {
347           continue;
348         }
349
350         if (anot[index] == null)
351         {
352           anot[index] = new Annotation("", "", ' ', 0);
353         }
354
355         anot[index].colour = col;
356       }
357     }
358     else
359     // HELIX, SHEET or STEM
360     {
361       char type = 0;
362       String symbol = "\u03B1"; // alpha
363
364       if (action.equals(HELIX))
365       {
366         type = 'H';
367       }
368       else if (action.equals(SHEET))
369       {
370         type = 'E';
371         symbol = "\u03B2"; // beta
372       }
373
374       // Added by LML to color stems
375       else if (action.equals(STEM))
376       {
377         type = 'S';
378         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
379         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
380       }
381
382       if (!aa[activeRow].hasIcons)
383       {
384         aa[activeRow].hasIcons = true;
385       }
386
387       String label = JvOptionPane.showInputDialog(MessageManager
388               .getString("label.enter_label_for_the_structure"), symbol);
389
390       if (label == null)
391       {
392         return;
393       }
394
395       if ((label.length() > 0) && !aa[activeRow].hasText)
396       {
397         aa[activeRow].hasText = true;
398         if (action.equals(STEM))
399         {
400           aa[activeRow].showAllColLabels = true;
401         }
402       }
403       for (int index : av.getColumnSelection().getSelected())
404       {
405         if (!av.getAlignment().getHiddenColumns().isVisible(index))
406         {
407           continue;
408         }
409
410         if (anot[index] == null)
411         {
412           anot[index] = new Annotation(label, "", type, 0);
413         }
414
415         anot[index].secondaryStructure = type != 'S' ? type : label
416                 .length() == 0 ? ' ' : label.charAt(0);
417         anot[index].displayCharacter = label;
418
419       }
420     }
421
422     av.getAlignment().validateAnnotation(aa[activeRow]);
423     ap.alignmentChanged();
424     ap.alignFrame.setMenusForViewport();
425     adjustPanelHeight();
426     repaint();
427
428     return;
429   }
430
431   /**
432    * Returns any existing annotation concatenated as a string. For each
433    * annotation, takes the description, if any, else the secondary structure
434    * character (if type is HELIX, SHEET or STEM), else the display character (if
435    * type is LABEL).
436    * 
437    * @param anots
438    * @param type
439    * @return
440    */
441   private String collectAnnotVals(Annotation[] anots, String type)
442   {
443     // TODO is this method wanted? why? 'last' is not used
444
445     StringBuilder collatedInput = new StringBuilder(64);
446     String last = "";
447     ColumnSelection viscols = av.getColumnSelection();
448     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
449
450     /*
451      * the selection list (read-only view) is in selection order, not
452      * column order; make a copy so we can sort it
453      */
454     List<Integer> selected = new ArrayList<Integer>(viscols.getSelected());
455     Collections.sort(selected);
456     for (int index : selected)
457     {
458       // always check for current display state - just in case
459       if (!hidden.isVisible(index))
460       {
461         continue;
462       }
463       String tlabel = null;
464       if (anots[index] != null)
465       { // LML added stem code
466         if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
467                 || type.equals(LABEL))
468         {
469           tlabel = anots[index].description;
470           if (tlabel == null || tlabel.length() < 1)
471           {
472             if (type.equals(HELIX) || type.equals(SHEET)
473                     || type.equals(STEM))
474             {
475               tlabel = "" + anots[index].secondaryStructure;
476             }
477             else
478             {
479               tlabel = "" + anots[index].displayCharacter;
480             }
481           }
482         }
483         if (tlabel != null && !tlabel.equals(last))
484         {
485           if (last.length() > 0)
486           {
487             collatedInput.append(" ");
488           }
489           collatedInput.append(tlabel);
490         }
491       }
492     }
493     return collatedInput.toString();
494   }
495
496   /**
497    * DOCUMENT ME!
498    * 
499    * @param evt
500    *          DOCUMENT ME!
501    */
502   @Override
503   public void mousePressed(MouseEvent evt)
504   {
505
506     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
507     if (aa == null)
508     {
509       return;
510     }
511
512     int height = 0;
513     activeRow = -1;
514
515     final int y = evt.getY();
516     for (int i = 0; i < aa.length; i++)
517     {
518       if (aa[i].visible)
519       {
520         height += aa[i].height;
521       }
522
523       if (y < height)
524       {
525         if (aa[i].editable)
526         {
527           activeRow = i;
528         }
529         else if (aa[i].graph > 0)
530         {
531           // Stretch Graph
532           graphStretch = i;
533           graphStretchY = y;
534         }
535
536         break;
537       }
538     }
539
540     /*
541      * isPopupTrigger fires in mousePressed on Mac,
542      * not until mouseRelease on Windows
543      */
544     if (evt.isPopupTrigger() && activeRow != -1)
545     {
546       showPopupMenu(y, evt.getX());
547       return;
548     }
549
550     ap.getScalePanel().mousePressed(evt);
551   }
552
553   /**
554    * Construct and display a context menu at the right-click position
555    * 
556    * @param y
557    * @param x
558    */
559   void showPopupMenu(final int y, int x)
560   {
561     if (av.getColumnSelection() == null
562             || av.getColumnSelection().isEmpty())
563     {
564       return;
565     }
566
567     JPopupMenu pop = new JPopupMenu(
568             MessageManager.getString("label.structure_type"));
569     JMenuItem item;
570     /*
571      * Just display the needed structure options
572      */
573     if (av.getAlignment().isNucleotide())
574     {
575       item = new JMenuItem(STEM);
576       item.addActionListener(this);
577       pop.add(item);
578     }
579     else
580     {
581       item = new JMenuItem(HELIX);
582       item.addActionListener(this);
583       pop.add(item);
584       item = new JMenuItem(SHEET);
585       item.addActionListener(this);
586       pop.add(item);
587     }
588     item = new JMenuItem(LABEL);
589     item.addActionListener(this);
590     pop.add(item);
591     item = new JMenuItem(COLOUR);
592     item.addActionListener(this);
593     pop.add(item);
594     item = new JMenuItem(REMOVE);
595     item.addActionListener(this);
596     pop.add(item);
597     pop.show(this, x, y);
598   }
599
600   /**
601    * DOCUMENT ME!
602    * 
603    * @param evt
604    *          DOCUMENT ME!
605    */
606   @Override
607   public void mouseReleased(MouseEvent evt)
608   {
609     graphStretch = -1;
610     graphStretchY = -1;
611     mouseDragging = false;
612     ap.getScalePanel().mouseReleased(evt);
613
614     /*
615      * isPopupTrigger is set in mouseReleased on Windows
616      * (in mousePressed on Mac)
617      */
618     if (evt.isPopupTrigger() && activeRow != -1)
619     {
620       showPopupMenu(evt.getY(), evt.getX());
621     }
622
623   }
624
625   /**
626    * DOCUMENT ME!
627    * 
628    * @param evt
629    *          DOCUMENT ME!
630    */
631   @Override
632   public void mouseEntered(MouseEvent evt)
633   {
634     ap.getScalePanel().mouseEntered(evt);
635   }
636
637   /**
638    * DOCUMENT ME!
639    * 
640    * @param evt
641    *          DOCUMENT ME!
642    */
643   @Override
644   public void mouseExited(MouseEvent evt)
645   {
646     ap.getScalePanel().mouseExited(evt);
647   }
648
649   /**
650    * DOCUMENT ME!
651    * 
652    * @param evt
653    *          DOCUMENT ME!
654    */
655   @Override
656   public void mouseDragged(MouseEvent evt)
657   {
658     if (graphStretch > -1)
659     {
660       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
661               - evt.getY();
662       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
663       {
664         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
665       }
666       graphStretchY = evt.getY();
667       adjustPanelHeight();
668       ap.paintAlignment(true);
669     }
670     else
671     {
672       ap.getScalePanel().mouseDragged(evt);
673     }
674   }
675
676   /**
677    * Constructs the tooltip, and constructs and displays a status message, for
678    * the current mouse position
679    * 
680    * @param evt
681    */
682   @Override
683   public void mouseMoved(MouseEvent evt)
684   {
685     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
686
687     if (aa == null)
688     {
689       this.setToolTipText(null);
690       return;
691     }
692
693     int row = -1;
694     int height = 0;
695
696     for (int i = 0; i < aa.length; i++)
697     {
698       if (aa[i].visible)
699       {
700         height += aa[i].height;
701       }
702
703       if (evt.getY() < height)
704       {
705         row = i;
706         break;
707       }
708     }
709
710     if (row == -1)
711     {
712       this.setToolTipText(null);
713       return;
714     }
715
716     int column = (evt.getX() / av.getCharWidth())
717             + av.getRanges().getStartRes();
718
719     if (av.hasHiddenColumns())
720     {
721       column = av.getAlignment().getHiddenColumns()
722               .adjustForHiddenColumns(column);
723     }
724
725     AlignmentAnnotation ann = aa[row];
726     if (row > -1 && ann.annotations != null
727             && column < ann.annotations.length)
728     {
729       buildToolTip(ann, column, aa);
730       setStatusMessage(column, ann);
731     }
732     else
733     {
734       this.setToolTipText(null);
735       ap.alignFrame.statusBar.setText(" ");
736     }
737   }
738
739   /**
740    * Builds a tooltip for the annotation at the current mouse position.
741    * 
742    * @param ann
743    * @param column
744    * @param anns
745    */
746   void buildToolTip(AlignmentAnnotation ann, int column,
747           AlignmentAnnotation[] anns)
748   {
749     if (ann.graphGroup > -1)
750     {
751       StringBuilder tip = new StringBuilder(32);
752       tip.append("<html>");
753       for (int i = 0; i < anns.length; i++)
754       {
755         if (anns[i].graphGroup == ann.graphGroup
756                 && anns[i].annotations[column] != null)
757         {
758           tip.append(anns[i].label);
759           String description = anns[i].annotations[column].description;
760           if (description != null && description.length() > 0)
761           {
762             tip.append(" ").append(description);
763           }
764           tip.append("<br>");
765         }
766       }
767       if (tip.length() != 6)
768       {
769         tip.setLength(tip.length() - 4);
770         this.setToolTipText(tip.toString() + "</html>");
771       }
772     }
773     else if (ann.annotations[column] != null)
774     {
775       String description = ann.annotations[column].description;
776       if (description != null && description.length() > 0)
777       {
778         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
779       }
780     }
781     else
782     {
783       // clear the tooltip.
784       this.setToolTipText(null);
785     }
786   }
787
788   /**
789    * Constructs and displays the status bar message
790    * 
791    * @param column
792    * @param ann
793    */
794   void setStatusMessage(int column, AlignmentAnnotation ann)
795   {
796     /*
797      * show alignment column and annotation description if any
798      */
799     StringBuilder text = new StringBuilder(32);
800     text.append(MessageManager.getString("label.column")).append(" ")
801             .append(column + 1);
802
803     if (ann.annotations[column] != null)
804     {
805       String description = ann.annotations[column].description;
806       if (description != null && description.trim().length() > 0)
807       {
808         text.append("  ").append(description);
809       }
810     }
811
812     /*
813      * if the annotation is sequence-specific, show the sequence number
814      * in the alignment, and (if not a gap) the residue and position
815      */
816     SequenceI seqref = ann.sequenceRef;
817     if (seqref != null)
818     {
819       int seqIndex = av.getAlignment().findIndex(seqref);
820       if (seqIndex != -1)
821       {
822         text.append(", ")
823                 .append(MessageManager.getString("label.sequence"))
824                 .append(" ").append(seqIndex + 1);
825         char residue = seqref.getCharAt(column);
826         if (!Comparison.isGap(residue))
827         {
828           text.append(" ");
829           String name;
830           if (av.getAlignment().isNucleotide())
831           {
832             name = ResidueProperties.nucleotideName.get(String
833                     .valueOf(residue));
834             text.append(" Nucleotide: ").append(
835                     name != null ? name : residue);
836           }
837           else
838           {
839             name = 'X' == residue ? "X" : ('*' == residue ? "STOP"
840                     : ResidueProperties.aa2Triplet.get(String
841                             .valueOf(residue)));
842             text.append(" Residue: ").append(name != null ? name : residue);
843           }
844           int residuePos = seqref.findPosition(column);
845           text.append(" (").append(residuePos).append(")");
846         }
847       }
848     }
849
850     ap.alignFrame.statusBar.setText(text.toString());
851   }
852
853   /**
854    * DOCUMENT ME!
855    * 
856    * @param evt
857    *          DOCUMENT ME!
858    */
859   @Override
860   public void mouseClicked(MouseEvent evt)
861   {
862     // if (activeRow != -1)
863     // {
864     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
865     // AlignmentAnnotation anot = aa[activeRow];
866     // }
867   }
868
869   // TODO mouseClicked-content and drawCursor are quite experimental!
870   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
871           int y1)
872   {
873     int pady = av.getCharHeight() / 5;
874     int charOffset = 0;
875     graphics.setColor(Color.black);
876     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
877
878     if (av.validCharWidth)
879     {
880       graphics.setColor(Color.white);
881
882       char s = seq.getCharAt(res);
883
884       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
885       graphics.drawString(String.valueOf(s), charOffset + x1,
886               (y1 + av.getCharHeight()) - pady);
887     }
888
889   }
890
891   private volatile boolean imageFresh = false;
892
893   /**
894    * DOCUMENT ME!
895    * 
896    * @param g
897    *          DOCUMENT ME!
898    */
899   @Override
900   public void paintComponent(Graphics g)
901   {
902     g.setColor(Color.white);
903     g.fillRect(0, 0, getWidth(), getHeight());
904
905     if (image != null)
906     {
907       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
908               || (getVisibleRect().height != g.getClipBounds().height))
909       {
910         g.drawImage(image, 0, 0, this);
911         fastPaint = false;
912         return;
913       }
914     }
915     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes() + 1)
916             * av.getCharWidth();
917     if (imgWidth < 1)
918     {
919       return;
920     }
921     if (image == null || imgWidth != image.getWidth(this)
922             || image.getHeight(this) != getHeight())
923     {
924       try
925       {
926         image = new BufferedImage(imgWidth, ap.getAnnotationPanel()
927                 .getHeight(), BufferedImage.TYPE_INT_RGB);
928       } catch (OutOfMemoryError oom)
929       {
930         try
931         {
932           System.gc();
933         } catch (Exception x)
934         {
935         }
936         ;
937         new OOMWarning(
938                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
939                 oom);
940         return;
941       }
942       gg = (Graphics2D) image.getGraphics();
943
944       if (av.antiAlias)
945       {
946         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
947                 RenderingHints.VALUE_ANTIALIAS_ON);
948       }
949
950       gg.setFont(av.getFont());
951       fm = gg.getFontMetrics();
952       gg.setColor(Color.white);
953       gg.fillRect(0, 0, imgWidth, image.getHeight());
954       imageFresh = true;
955     }
956
957     drawComponent(gg, av.getRanges().getStartRes(), av.getRanges()
958             .getEndRes() + 1);
959     imageFresh = false;
960     g.drawImage(image, 0, 0, this);
961   }
962
963   /**
964    * set true to enable redraw timing debug output on stderr
965    */
966   private final boolean debugRedraw = false;
967
968   /**
969    * non-Thread safe repaint
970    * 
971    * @param horizontal
972    *          repaint with horizontal shift in alignment
973    */
974   public void fastPaint(int horizontal)
975   {
976     if ((horizontal == 0) || gg == null
977             || av.getAlignment().getAlignmentAnnotation() == null
978             || av.getAlignment().getAlignmentAnnotation().length < 1
979             || av.isCalcInProgress())
980     {
981       repaint();
982       return;
983     }
984     long stime = System.currentTimeMillis();
985     gg.copyArea(0, 0, imgWidth, getHeight(),
986             -horizontal * av.getCharWidth(), 0);
987     long mtime = System.currentTimeMillis();
988     int sr = av.getRanges().getStartRes();
989     int er = av.getRanges().getEndRes() + 1;
990     int transX = 0;
991
992     if (horizontal > 0) // scrollbar pulled right, image to the left
993     {
994       transX = (er - sr - horizontal) * av.getCharWidth();
995       sr = er - horizontal;
996     }
997     else if (horizontal < 0)
998     {
999       er = sr - horizontal;
1000     }
1001
1002     gg.translate(transX, 0);
1003
1004     drawComponent(gg, sr, er);
1005
1006     gg.translate(-transX, 0);
1007     long dtime = System.currentTimeMillis();
1008     fastPaint = true;
1009     repaint();
1010     long rtime = System.currentTimeMillis();
1011     if (debugRedraw)
1012     {
1013       System.err.println("Scroll:\t" + horizontal + "\tCopyArea:\t"
1014               + (mtime - stime) + "\tDraw component:\t" + (dtime - mtime)
1015               + "\tRepaint call:\t" + (rtime - dtime));
1016     }
1017
1018   }
1019
1020   private volatile boolean lastImageGood = false;
1021
1022   /**
1023    * DOCUMENT ME!
1024    * 
1025    * @param g
1026    *          DOCUMENT ME!
1027    * @param startRes
1028    *          DOCUMENT ME!
1029    * @param endRes
1030    *          DOCUMENT ME!
1031    */
1032   public void drawComponent(Graphics g, int startRes, int endRes)
1033   {
1034     BufferedImage oldFaded = fadedImage;
1035     if (av.isCalcInProgress())
1036     {
1037       if (image == null)
1038       {
1039         lastImageGood = false;
1040         return;
1041       }
1042       // We'll keep a record of the old image,
1043       // and draw a faded image until the calculation
1044       // has completed
1045       if (lastImageGood
1046               && (fadedImage == null || fadedImage.getWidth() != imgWidth || fadedImage
1047                       .getHeight() != image.getHeight()))
1048       {
1049         // System.err.println("redraw faded image ("+(fadedImage==null ?
1050         // "null image" : "") + " lastGood="+lastImageGood+")");
1051         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1052                 BufferedImage.TYPE_INT_RGB);
1053
1054         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1055
1056         fadedG.setColor(Color.white);
1057         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1058
1059         fadedG.setComposite(AlphaComposite.getInstance(
1060                 AlphaComposite.SRC_OVER, .3f));
1061         fadedG.drawImage(image, 0, 0, this);
1062
1063       }
1064       // make sure we don't overwrite the last good faded image until all
1065       // calculations have finished
1066       lastImageGood = false;
1067
1068     }
1069     else
1070     {
1071       if (fadedImage != null)
1072       {
1073         oldFaded = fadedImage;
1074       }
1075       fadedImage = null;
1076     }
1077
1078     g.setColor(Color.white);
1079     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1080
1081     g.setFont(av.getFont());
1082     if (fm == null)
1083     {
1084       fm = g.getFontMetrics();
1085     }
1086
1087     if ((av.getAlignment().getAlignmentAnnotation() == null)
1088             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1089     {
1090       g.setColor(Color.white);
1091       g.fillRect(0, 0, getWidth(), getHeight());
1092       g.setColor(Color.black);
1093       if (av.validCharWidth)
1094       {
1095         g.drawString(MessageManager
1096                 .getString("label.alignment_has_no_annotations"), 20, 15);
1097       }
1098
1099       return;
1100     }
1101     lastImageGood = renderer.drawComponent(this, av, g, activeRow,
1102             startRes, endRes);
1103     if (!lastImageGood && fadedImage == null)
1104     {
1105       fadedImage = oldFaded;
1106     }
1107   }
1108
1109   @Override
1110   public FontMetrics getFontMetrics()
1111   {
1112     return fm;
1113   }
1114
1115   @Override
1116   public Image getFadedImage()
1117   {
1118     return fadedImage;
1119   }
1120
1121   @Override
1122   public int getFadedImageWidth()
1123   {
1124     return imgWidth;
1125   }
1126
1127   private int[] bounds = new int[2];
1128
1129   @Override
1130   public int[] getVisibleVRange()
1131   {
1132     if (ap != null && ap.getAlabels() != null)
1133     {
1134       int sOffset = -ap.getAlabels().getScrollOffset();
1135       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1136       bounds[0] = sOffset;
1137       bounds[1] = visHeight;
1138       return bounds;
1139     }
1140     else
1141     {
1142       return null;
1143     }
1144   }
1145
1146   /**
1147    * Try to ensure any references held are nulled
1148    */
1149   public void dispose()
1150   {
1151     av = null;
1152     ap = null;
1153     image = null;
1154     fadedImage = null;
1155     gg = null;
1156     _mwl = null;
1157
1158     /*
1159      * I created the renderer so I will dispose of it
1160      */
1161     if (renderer != null)
1162     {
1163       renderer.dispose();
1164     }
1165   }
1166
1167   @Override
1168   public void propertyChange(PropertyChangeEvent evt)
1169   {
1170     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1171     if (evt.getPropertyName().equals("startres")
1172             || evt.getPropertyName().equals("endres"))
1173     {
1174       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1175     }
1176   }
1177 }