JAL-2172 test and code updates for readonly selection list
[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.SequenceI;
27 import jalview.renderer.AnnotationRenderer;
28 import jalview.renderer.AwtRenderPanelI;
29 import jalview.schemes.ResidueProperties;
30 import jalview.util.Comparison;
31 import jalview.util.MessageManager;
32
33 import java.awt.AlphaComposite;
34 import java.awt.Color;
35 import java.awt.Dimension;
36 import java.awt.FontMetrics;
37 import java.awt.Graphics;
38 import java.awt.Graphics2D;
39 import java.awt.Image;
40 import java.awt.Rectangle;
41 import java.awt.RenderingHints;
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.MouseEvent;
47 import java.awt.event.MouseListener;
48 import java.awt.event.MouseMotionListener;
49 import java.awt.event.MouseWheelEvent;
50 import java.awt.event.MouseWheelListener;
51 import java.awt.image.BufferedImage;
52 import java.util.ArrayList;
53 import java.util.Collections;
54 import java.util.List;
55
56 import javax.swing.JColorChooser;
57 import javax.swing.JMenuItem;
58 import javax.swing.JOptionPane;
59 import javax.swing.JPanel;
60 import javax.swing.JPopupMenu;
61 import javax.swing.Scrollable;
62 import javax.swing.ToolTipManager;
63
64 /**
65  * AnnotationPanel displays visible portion of annotation rows below unwrapped
66  * alignment
67  * 
68  * @author $author$
69  * @version $Revision$
70  */
71 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
72         MouseListener, MouseWheelListener, MouseMotionListener,
73         ActionListener, AdjustmentListener, Scrollable
74 {
75   String HELIX = MessageManager.getString("label.helix");
76
77   String SHEET = MessageManager.getString("label.sheet");
78
79   /**
80    * For RNA secondary structure "stems" aka helices
81    */
82   String STEM = MessageManager.getString("label.rna_helix");
83
84   String LABEL = MessageManager.getString("label.label");
85
86   String REMOVE = MessageManager.getString("label.remove_annotation");
87
88   String COLOUR = MessageManager.getString("action.colour");
89
90   public final Color HELIX_COLOUR = Color.red.darker();
91
92   public final Color SHEET_COLOUR = Color.green.darker().darker();
93
94   public final Color STEM_COLOUR = Color.blue.darker();
95
96   /** DOCUMENT ME!! */
97   public AlignViewport av;
98
99   AlignmentPanel ap;
100
101   public int activeRow = -1;
102
103   public BufferedImage image;
104
105   public volatile BufferedImage fadedImage;
106
107   Graphics2D gg;
108
109   public FontMetrics fm;
110
111   public int imgWidth = 0;
112
113   boolean fastPaint = false;
114
115   // Used For mouse Dragging and resizing graphs
116   int graphStretch = -1;
117
118   int graphStretchY = -1;
119
120   int min; // used by mouseDragged to see if user
121
122   int max; // used by mouseDragged to see if user
123
124   boolean mouseDragging = false;
125
126   boolean MAC = 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
146     MAC = jalview.util.Platform.isAMac();
147
148     ToolTipManager.sharedInstance().registerComponent(this);
149     ToolTipManager.sharedInstance().setInitialDelay(0);
150     ToolTipManager.sharedInstance().setDismissDelay(10000);
151     this.ap = ap;
152     av = ap.av;
153     this.setLayout(null);
154     addMouseListener(this);
155     addMouseMotionListener(this);
156     ap.annotationScroller.getVerticalScrollBar()
157             .addAdjustmentListener(this);
158     // save any wheel listeners on the scroller, so we can propagate scroll
159     // events to them.
160     _mwl = ap.annotationScroller.getMouseWheelListeners();
161     // and then set our own listener to consume all mousewheel events
162     ap.annotationScroller.addMouseWheelListener(this);
163     renderer = new AnnotationRenderer();
164   }
165
166   public AnnotationPanel(AlignViewport av)
167   {
168     this.av = av;
169     renderer = new AnnotationRenderer();
170   }
171
172   @Override
173   public void mouseWheelMoved(MouseWheelEvent e)
174   {
175     if (e.isShiftDown())
176     {
177       e.consume();
178       if (e.getWheelRotation() > 0)
179       {
180         ap.scrollRight(true);
181       }
182       else
183       {
184         ap.scrollRight(false);
185       }
186     }
187     else
188     {
189       // TODO: find the correct way to let the event bubble up to
190       // ap.annotationScroller
191       for (MouseWheelListener mwl : _mwl)
192       {
193         if (mwl != null)
194         {
195           mwl.mouseWheelMoved(e);
196         }
197         if (e.isConsumed())
198         {
199           break;
200         }
201       }
202     }
203   }
204
205   @Override
206   public Dimension getPreferredScrollableViewportSize()
207   {
208     return getPreferredSize();
209   }
210
211   @Override
212   public int getScrollableBlockIncrement(Rectangle visibleRect,
213           int orientation, int direction)
214   {
215     return 30;
216   }
217
218   @Override
219   public boolean getScrollableTracksViewportHeight()
220   {
221     return false;
222   }
223
224   @Override
225   public boolean getScrollableTracksViewportWidth()
226   {
227     return true;
228   }
229
230   @Override
231   public int getScrollableUnitIncrement(Rectangle visibleRect,
232           int orientation, int direction)
233   {
234     return 30;
235   }
236
237   /*
238    * (non-Javadoc)
239    * 
240    * @see
241    * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
242    * .AdjustmentEvent)
243    */
244   @Override
245   public void adjustmentValueChanged(AdjustmentEvent evt)
246   {
247     // update annotation label display
248     ap.getAlabels().setScrollOffset(-evt.getValue());
249   }
250
251   /**
252    * Calculates the height of the annotation displayed in the annotation panel.
253    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
254    * all annotation associated components are updated correctly.
255    * 
256    */
257   public int adjustPanelHeight()
258   {
259     int height = av.calcPanelHeight();
260     this.setPreferredSize(new Dimension(1, height));
261     if (ap != null)
262     {
263       // revalidate only when the alignment panel is fully constructed
264       ap.validate();
265     }
266
267     return height;
268   }
269
270   /**
271    * DOCUMENT ME!
272    * 
273    * @param evt
274    *          DOCUMENT ME!
275    */
276   @Override
277   public void actionPerformed(ActionEvent evt)
278   {
279     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
280     if (aa == null)
281     {
282       return;
283     }
284     Annotation[] anot = aa[activeRow].annotations;
285
286     if (anot.length < av.getColumnSelection().getMax())
287     {
288       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
289       System.arraycopy(anot, 0, temp, 0, anot.length);
290       anot = temp;
291       aa[activeRow].annotations = anot;
292     }
293
294     String action = evt.getActionCommand();
295     if (action.equals(REMOVE))
296     {
297       for (int index : av.getColumnSelection().getSelected())
298       {
299         if (av.getColumnSelection().isVisible(index))
300         {
301           anot[index] = null;
302         }
303       }
304     }
305     else if (action.equals(LABEL))
306     {
307       String exMesg = collectAnnotVals(anot, LABEL);
308       String label = JOptionPane.showInputDialog(this,
309               MessageManager.getString("label.enter_label"), exMesg);
310
311       if (label == null)
312       {
313         return;
314       }
315
316       if ((label.length() > 0) && !aa[activeRow].hasText)
317       {
318         aa[activeRow].hasText = true;
319       }
320
321       for (int index : av.getColumnSelection().getSelected())
322       {
323         if (!av.getColumnSelection().isVisible(index))
324         {
325           continue;
326         }
327
328         if (anot[index] == null)
329         {
330           anot[index] = new Annotation(label, "", ' ', 0);
331         }
332         else
333         {
334           anot[index].displayCharacter = label;
335         }
336       }
337     }
338     else if (action.equals(COLOUR))
339     {
340       Color col = JColorChooser.showDialog(this,
341               MessageManager.getString("label.select_foreground_colour"),
342               Color.black);
343
344       for (int index : av.getColumnSelection().getSelected())
345       {
346         if (!av.getColumnSelection().isVisible(index))
347         {
348           continue;
349         }
350
351         if (anot[index] == null)
352         {
353           anot[index] = new Annotation("", "", ' ', 0);
354         }
355
356         anot[index].colour = col;
357       }
358     }
359     else
360     // HELIX, SHEET or STEM
361     {
362       char type = 0;
363       String symbol = "\u03B1"; // alpha
364
365       if (action.equals(HELIX))
366       {
367         type = 'H';
368       }
369       else if (action.equals(SHEET))
370       {
371         type = 'E';
372         symbol = "\u03B2"; // beta
373       }
374
375       // Added by LML to color stems
376       else if (action.equals(STEM))
377       {
378         type = 'S';
379         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
380         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
381       }
382
383       if (!aa[activeRow].hasIcons)
384       {
385         aa[activeRow].hasIcons = true;
386       }
387
388       String label = JOptionPane.showInputDialog(MessageManager
389               .getString("label.enter_label_for_the_structure"), symbol);
390
391       if (label == null)
392       {
393         return;
394       }
395
396       if ((label.length() > 0) && !aa[activeRow].hasText)
397       {
398         aa[activeRow].hasText = true;
399         if (action.equals(STEM))
400         {
401           aa[activeRow].showAllColLabels = true;
402         }
403       }
404       for (int index : av.getColumnSelection().getSelected())
405       {
406         if (!av.getColumnSelection().isVisible(index))
407         {
408           continue;
409         }
410
411         if (anot[index] == null)
412         {
413           anot[index] = new Annotation(label, "", type, 0);
414         }
415
416         anot[index].secondaryStructure = type != 'S' ? type : label
417                 .length() == 0 ? ' ' : label.charAt(0);
418         anot[index].displayCharacter = label;
419
420       }
421     }
422
423     av.getAlignment().validateAnnotation(aa[activeRow]);
424     ap.alignmentChanged();
425     ap.alignFrame.setMenusForViewport();
426     adjustPanelHeight();
427     repaint();
428
429     return;
430   }
431
432   /**
433    * Returns any existing annotation concatenated as a string. For each
434    * annotation, takes the description, if any, else the secondary structure
435    * character (if type is HELIX, SHEET or STEM), else the display character (if
436    * type is LABEL).
437    * 
438    * @param anots
439    * @param type
440    * @return
441    */
442   private String collectAnnotVals(Annotation[] anots, String type)
443   {
444     // TODO is this method wanted? why? 'last' is not used
445
446     StringBuilder collatedInput = new StringBuilder(64);
447     String last = "";
448     ColumnSelection viscols = av.getColumnSelection();
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 (!viscols.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)
467                 || type.equals(STEM) || 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()) + av.getStartRes();
717
718     if (av.hasHiddenColumns())
719     {
720       column = av.getColumnSelection().adjustForHiddenColumns(column);
721     }
722
723     AlignmentAnnotation ann = aa[row];
724     if (row > -1 && ann.annotations != null
725             && column < ann.annotations.length)
726     {
727       buildToolTip(ann, column, aa);
728       setStatusMessage(column, ann);
729     }
730     else
731     {
732       this.setToolTipText(null);
733       ap.alignFrame.statusBar.setText(" ");
734     }
735   }
736
737   /**
738    * Builds a tooltip for the annotation at the current mouse position.
739    * 
740    * @param ann
741    * @param column
742    * @param anns
743    */
744   void buildToolTip(AlignmentAnnotation ann, int column,
745           AlignmentAnnotation[] anns)
746   {
747     if (ann.graphGroup > -1)
748     {
749       StringBuilder tip = new StringBuilder(32);
750       tip.append("<html>");
751       for (int i = 0; i < anns.length; i++)
752       {
753         if (anns[i].graphGroup == ann.graphGroup
754                 && anns[i].annotations[column] != null)
755         {
756           tip.append(anns[i].label);
757           String description = anns[i].annotations[column].description;
758           if (description != null && description.length() > 0)
759           {
760             tip.append(" ").append(description);
761           }
762           tip.append("<br>");
763         }
764       }
765       if (tip.length() != 6)
766       {
767         tip.setLength(tip.length() - 4);
768         this.setToolTipText(tip.toString() + "</html>");
769       }
770     }
771     else if (ann.annotations[column] != null)
772     {
773       String description = ann.annotations[column].description;
774       if (description != null && description.length() > 0)
775       {
776         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
777       }
778     }
779     else
780     {
781       // clear the tooltip.
782       this.setToolTipText(null);
783     }
784   }
785
786   /**
787    * Constructs and displays the status bar message
788    * 
789    * @param column
790    * @param ann
791    */
792   void setStatusMessage(int column, AlignmentAnnotation ann)
793   {
794     /*
795      * show alignment column and annotation description if any
796      */
797     StringBuilder text = new StringBuilder(32);
798     text.append(MessageManager.getString("label.column")).append(" ")
799             .append(column + 1);
800
801     if (ann.annotations[column] != null)
802     {
803       String description = ann.annotations[column].description;
804       if (description != null && description.trim().length() > 0)
805       {
806         text.append("  ").append(description);
807       }
808     }
809
810     /*
811      * if the annotation is sequence-specific, show the sequence number
812      * in the alignment, and (if not a gap) the residue and position
813      */
814     SequenceI seqref = ann.sequenceRef;
815     if (seqref != null)
816     {
817       int seqIndex = av.getAlignment().findIndex(seqref);
818       if (seqIndex != -1)
819       {
820         text.append(", ")
821                 .append(MessageManager.getString("label.sequence"))
822                 .append(" ")
823                 .append(seqIndex + 1);
824         char residue = seqref.getCharAt(column);
825         if (!Comparison.isGap(residue))
826         {
827           text.append(" ");
828           String name;
829           if (av.getAlignment().isNucleotide())
830           {
831             name = ResidueProperties.nucleotideName.get(String
832                     .valueOf(residue));
833             text.append(" Nucleotide: ").append(
834                     name != null ? name : residue);
835           }
836           else
837           {
838             name = 'X' == residue ? "X" : ('*' == residue ? "STOP"
839                     : ResidueProperties.aa2Triplet.get(String
840                             .valueOf(residue)));
841             text.append(" Residue: ").append(name != null ? name : residue);
842           }
843           int residuePos = seqref.findPosition(column);
844           text.append(" (").append(residuePos).append(")");
845         }
846       }
847     }
848
849     ap.alignFrame.statusBar.setText(text.toString());
850   }
851
852   /**
853    * DOCUMENT ME!
854    * 
855    * @param evt
856    *          DOCUMENT ME!
857    */
858   @Override
859   public void mouseClicked(MouseEvent evt)
860   {
861     // if (activeRow != -1)
862     // {
863     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
864     // AlignmentAnnotation anot = aa[activeRow];
865     // }
866   }
867
868   // TODO mouseClicked-content and drawCursor are quite experimental!
869   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
870           int y1)
871   {
872     int pady = av.getCharHeight() / 5;
873     int charOffset = 0;
874     graphics.setColor(Color.black);
875     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
876
877     if (av.validCharWidth)
878     {
879       graphics.setColor(Color.white);
880
881       char s = seq.getCharAt(res);
882
883       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
884       graphics.drawString(String.valueOf(s), charOffset + x1,
885               (y1 + av.getCharHeight()) - pady);
886     }
887
888   }
889
890   private volatile boolean imageFresh = false;
891
892   /**
893    * DOCUMENT ME!
894    * 
895    * @param g
896    *          DOCUMENT ME!
897    */
898   @Override
899   public void paintComponent(Graphics g)
900   {
901     g.setColor(Color.white);
902     g.fillRect(0, 0, getWidth(), getHeight());
903
904     if (image != null)
905     {
906       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
907               || (getVisibleRect().height != g.getClipBounds().height))
908       {
909         g.drawImage(image, 0, 0, this);
910         fastPaint = false;
911         return;
912       }
913     }
914     imgWidth = (av.endRes - av.startRes + 1) * av.getCharWidth();
915     if (imgWidth < 1)
916     {
917       return;
918     }
919     if (image == null || imgWidth != image.getWidth(this)
920             || image.getHeight(this) != getHeight())
921     {
922       try
923       {
924         image = new BufferedImage(imgWidth, ap.getAnnotationPanel()
925                 .getHeight(), BufferedImage.TYPE_INT_RGB);
926       } catch (OutOfMemoryError oom)
927       {
928         try
929         {
930           System.gc();
931         } catch (Exception x)
932         {
933         }
934         ;
935         new OOMWarning(
936                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
937                 oom);
938         return;
939       }
940       gg = (Graphics2D) image.getGraphics();
941
942       if (av.antiAlias)
943       {
944         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
945                 RenderingHints.VALUE_ANTIALIAS_ON);
946       }
947
948       gg.setFont(av.getFont());
949       fm = gg.getFontMetrics();
950       gg.setColor(Color.white);
951       gg.fillRect(0, 0, imgWidth, image.getHeight());
952       imageFresh = true;
953     }
954
955     drawComponent(gg, av.startRes, av.endRes + 1);
956     imageFresh = false;
957     g.drawImage(image, 0, 0, this);
958   }
959
960   /**
961    * set true to enable redraw timing debug output on stderr
962    */
963   private final boolean debugRedraw = false;
964
965   /**
966    * non-Thread safe repaint
967    * 
968    * @param horizontal
969    *          repaint with horizontal shift in alignment
970    */
971   public void fastPaint(int horizontal)
972   {
973     if ((horizontal == 0) || gg == null
974             || av.getAlignment().getAlignmentAnnotation() == null
975             || av.getAlignment().getAlignmentAnnotation().length < 1
976             || av.isCalcInProgress())
977     {
978       repaint();
979       return;
980     }
981     long stime = System.currentTimeMillis();
982     gg.copyArea(0, 0, imgWidth, getHeight(),
983             -horizontal * av.getCharWidth(), 0);
984     long mtime = System.currentTimeMillis();
985     int sr = av.startRes;
986     int er = av.endRes + 1;
987     int transX = 0;
988
989     if (horizontal > 0) // scrollbar pulled right, image to the left
990     {
991       transX = (er - sr - horizontal) * av.getCharWidth();
992       sr = er - horizontal;
993     }
994     else if (horizontal < 0)
995     {
996       er = sr - horizontal;
997     }
998
999     gg.translate(transX, 0);
1000
1001     drawComponent(gg, sr, er);
1002
1003     gg.translate(-transX, 0);
1004     long dtime = System.currentTimeMillis();
1005     fastPaint = true;
1006     repaint();
1007     long rtime = System.currentTimeMillis();
1008     if (debugRedraw)
1009     {
1010       System.err.println("Scroll:\t" + horizontal + "\tCopyArea:\t"
1011               + (mtime - stime) + "\tDraw component:\t" + (dtime - mtime)
1012               + "\tRepaint call:\t" + (rtime - dtime));
1013     }
1014
1015   }
1016
1017   private volatile boolean lastImageGood = false;
1018
1019   /**
1020    * DOCUMENT ME!
1021    * 
1022    * @param g
1023    *          DOCUMENT ME!
1024    * @param startRes
1025    *          DOCUMENT ME!
1026    * @param endRes
1027    *          DOCUMENT ME!
1028    */
1029   public void drawComponent(Graphics g, int startRes, int endRes)
1030   {
1031     BufferedImage oldFaded = fadedImage;
1032     if (av.isCalcInProgress())
1033     {
1034       if (image == null)
1035       {
1036         lastImageGood = false;
1037         return;
1038       }
1039       // We'll keep a record of the old image,
1040       // and draw a faded image until the calculation
1041       // has completed
1042       if (lastImageGood
1043               && (fadedImage == null || fadedImage.getWidth() != imgWidth || fadedImage
1044                       .getHeight() != image.getHeight()))
1045       {
1046         // System.err.println("redraw faded image ("+(fadedImage==null ?
1047         // "null image" : "") + " lastGood="+lastImageGood+")");
1048         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1049                 BufferedImage.TYPE_INT_RGB);
1050
1051         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1052
1053         fadedG.setColor(Color.white);
1054         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1055
1056         fadedG.setComposite(AlphaComposite.getInstance(
1057                 AlphaComposite.SRC_OVER, .3f));
1058         fadedG.drawImage(image, 0, 0, this);
1059
1060       }
1061       // make sure we don't overwrite the last good faded image until all
1062       // calculations have finished
1063       lastImageGood = false;
1064
1065     }
1066     else
1067     {
1068       if (fadedImage != null)
1069       {
1070         oldFaded = fadedImage;
1071       }
1072       fadedImage = null;
1073     }
1074
1075     g.setColor(Color.white);
1076     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1077
1078     g.setFont(av.getFont());
1079     if (fm == null)
1080     {
1081       fm = g.getFontMetrics();
1082     }
1083
1084     if ((av.getAlignment().getAlignmentAnnotation() == null)
1085             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1086     {
1087       g.setColor(Color.white);
1088       g.fillRect(0, 0, getWidth(), getHeight());
1089       g.setColor(Color.black);
1090       if (av.validCharWidth)
1091       {
1092         g.drawString(MessageManager
1093                 .getString("label.alignment_has_no_annotations"), 20, 15);
1094       }
1095
1096       return;
1097     }
1098     lastImageGood = renderer.drawComponent(this, av, g, activeRow,
1099             startRes, endRes);
1100     if (!lastImageGood && fadedImage == null)
1101     {
1102       fadedImage = oldFaded;
1103     }
1104   }
1105
1106   @Override
1107   public FontMetrics getFontMetrics()
1108   {
1109     return fm;
1110   }
1111
1112   @Override
1113   public Image getFadedImage()
1114   {
1115     return fadedImage;
1116   }
1117
1118   @Override
1119   public int getFadedImageWidth()
1120   {
1121     return imgWidth;
1122   }
1123
1124   private int[] bounds = new int[2];
1125
1126   @Override
1127   public int[] getVisibleVRange()
1128   {
1129     if (ap != null && ap.getAlabels() != null)
1130     {
1131       int sOffset = -ap.getAlabels().getScrollOffset();
1132       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1133       bounds[0] = sOffset;
1134       bounds[1] = visHeight;
1135       return bounds;
1136     }
1137     else
1138     {
1139       return null;
1140     }
1141   }
1142 }