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