JAL-2173 don't remove annotation from hidden columns
[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 index : av.getColumnSelection().getSelected())
293       {
294         if (av.getColumnSelection().isVisible(index))
295         {
296           anot[index] = null;
297         }
298       }
299     }
300     else if (evt.getActionCommand().equals(LABEL))
301     {
302       String exMesg = collectAnnotVals(anot, LABEL);
303       String label = JOptionPane.showInputDialog(this,
304               MessageManager.getString("label.enter_label"), exMesg);
305
306       if (label == null)
307       {
308         return;
309       }
310
311       if ((label.length() > 0) && !aa[activeRow].hasText)
312       {
313         aa[activeRow].hasText = true;
314       }
315
316       for (int index : av.getColumnSelection().getSelected())
317       {
318         if (!av.getColumnSelection().isVisible(index))
319         {
320           continue;
321         }
322
323         if (anot[index] == null)
324         {
325           anot[index] = new Annotation(label, "", ' ', 0); // TODO: verify that
326           // null exceptions
327           // aren't raised
328           // elsewhere.
329         }
330         else
331         {
332           anot[index].displayCharacter = label;
333         }
334       }
335     }
336     else if (evt.getActionCommand().equals(COLOUR))
337     {
338       Color col = JColorChooser.showDialog(this,
339               MessageManager.getString("label.select_foreground_colour"),
340               Color.black);
341
342       for (int index : av.getColumnSelection().getSelected())
343       {
344         if (!av.getColumnSelection().isVisible(index))
345         {
346           continue;
347         }
348
349         if (anot[index] == null)
350         {
351           anot[index] = new Annotation("", "", ' ', 0);
352         }
353
354         anot[index].colour = col;
355       }
356     }
357     else
358     // HELIX, SHEET or STEM
359     {
360       char type = 0;
361       String symbol = "\u03B1";
362
363       if (evt.getActionCommand().equals(HELIX))
364       {
365         type = 'H';
366       }
367       else if (evt.getActionCommand().equals(SHEET))
368       {
369         type = 'E';
370         symbol = "\u03B2";
371       }
372
373       // Added by LML to color stems
374       else if (evt.getActionCommand().equals(STEM))
375       {
376         type = 'S';
377         symbol = "\u03C3";
378       }
379
380       if (!aa[activeRow].hasIcons)
381       {
382         aa[activeRow].hasIcons = true;
383       }
384
385       String label = JOptionPane.showInputDialog(MessageManager
386               .getString("label.enter_label_for_the_structure"), symbol);
387
388       if (label == null)
389       {
390         return;
391       }
392
393       if ((label.length() > 0) && !aa[activeRow].hasText)
394       {
395         aa[activeRow].hasText = true;
396         if (evt.getActionCommand().equals(STEM))
397         {
398           aa[activeRow].showAllColLabels = true;
399         }
400       }
401       for (int index : av.getColumnSelection().getSelected())
402       {
403         if (!av.getColumnSelection().isVisible(index))
404         {
405           continue;
406         }
407
408         if (anot[index] == null)
409         {
410           anot[index] = new Annotation(label, "", type, 0);
411         }
412
413         anot[index].secondaryStructure = type != 'S' ? type : label
414                 .length() == 0 ? ' ' : label.charAt(0);
415         anot[index].displayCharacter = label;
416
417       }
418     }
419
420     av.getAlignment().validateAnnotation(aa[activeRow]);
421     ap.alignmentChanged();
422     ap.alignFrame.setMenusForViewport();
423     adjustPanelHeight();
424     repaint();
425
426     return;
427   }
428
429   /**
430    * Returns any existing annotation concatenated as a string. For each
431    * annotation, takes the description, if any, else the secondary structure
432    * character (if type is HELIX, SHEET or STEM), else the display character (if
433    * type is LABEL).
434    * 
435    * @param anots
436    * @param type
437    * @return
438    */
439   private String collectAnnotVals(Annotation[] anots, String type)
440   {
441     // TODO is this method wanted? why? 'last' is not used
442
443     StringBuilder collatedInput = new StringBuilder(64);
444     String last = "";
445     ColumnSelection viscols = av.getColumnSelection();
446     // TODO: refactor and save av.getColumnSelection for efficiency
447     for (int index : viscols.getSelected())
448     {
449       // always check for current display state - just in case
450       if (!viscols.isVisible(index))
451       {
452         continue;
453       }
454       String tlabel = null;
455       if (anots[index] != null)
456       { // LML added stem code
457         if (type.equals(HELIX) || type.equals(SHEET)
458                 || type.equals(STEM) || type.equals(LABEL))
459         {
460           tlabel = anots[index].description;
461           if (tlabel == null || tlabel.length() < 1)
462           {
463             if (type.equals(HELIX) || type.equals(SHEET)
464                     || type.equals(STEM))
465             {
466               tlabel = "" + anots[index].secondaryStructure;
467             }
468             else
469             {
470               tlabel = "" + anots[index].displayCharacter;
471             }
472           }
473         }
474         if (tlabel != null && !tlabel.equals(last))
475         {
476           if (last.length() > 0)
477           {
478             collatedInput.append(" ");
479           }
480           collatedInput.append(tlabel);
481         }
482       }
483     }
484     return collatedInput.toString();
485   }
486
487   /**
488    * DOCUMENT ME!
489    * 
490    * @param evt
491    *          DOCUMENT ME!
492    */
493   @Override
494   public void mousePressed(MouseEvent evt)
495   {
496
497     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
498     if (aa == null)
499     {
500       return;
501     }
502
503     int height = 0;
504     activeRow = -1;
505
506     for (int i = 0; i < aa.length; i++)
507     {
508       if (aa[i].visible)
509       {
510         height += aa[i].height;
511       }
512
513       if (evt.getY() < height)
514       {
515         if (aa[i].editable)
516         {
517           activeRow = i;
518         }
519         else if (aa[i].graph > 0)
520         {
521           // Stretch Graph
522           graphStretch = i;
523           graphStretchY = evt.getY();
524         }
525
526         break;
527       }
528     }
529
530     if (evt.isPopupTrigger() && activeRow != -1)
531     {
532       if (av.getColumnSelection() == null)
533       {
534         return;
535       }
536
537       JPopupMenu pop = new JPopupMenu(
538               MessageManager.getString("label.structure_type"));
539       JMenuItem item;
540       /*
541        * Just display the needed structure options
542        */
543       if (av.getAlignment().isNucleotide() == true)
544       {
545         item = new JMenuItem(STEM);
546         item.addActionListener(this);
547         pop.add(item);
548       }
549       else
550       {
551         item = new JMenuItem(HELIX);
552         item.addActionListener(this);
553         pop.add(item);
554         item = new JMenuItem(SHEET);
555         item.addActionListener(this);
556         pop.add(item);
557       }
558       item = new JMenuItem(LABEL);
559       item.addActionListener(this);
560       pop.add(item);
561       item = new JMenuItem(COLOUR);
562       item.addActionListener(this);
563       pop.add(item);
564       item = new JMenuItem(REMOVE);
565       item.addActionListener(this);
566       pop.add(item);
567       pop.show(this, evt.getX(), evt.getY());
568
569       return;
570     }
571
572     if (aa == null)
573     {
574       return;
575     }
576
577     ap.getScalePanel().mousePressed(evt);
578
579   }
580
581   /**
582    * DOCUMENT ME!
583    * 
584    * @param evt
585    *          DOCUMENT ME!
586    */
587   @Override
588   public void mouseReleased(MouseEvent evt)
589   {
590     graphStretch = -1;
591     graphStretchY = -1;
592     mouseDragging = false;
593     ap.getScalePanel().mouseReleased(evt);
594   }
595
596   /**
597    * DOCUMENT ME!
598    * 
599    * @param evt
600    *          DOCUMENT ME!
601    */
602   @Override
603   public void mouseEntered(MouseEvent evt)
604   {
605     ap.getScalePanel().mouseEntered(evt);
606   }
607
608   /**
609    * DOCUMENT ME!
610    * 
611    * @param evt
612    *          DOCUMENT ME!
613    */
614   @Override
615   public void mouseExited(MouseEvent evt)
616   {
617     ap.getScalePanel().mouseExited(evt);
618   }
619
620   /**
621    * DOCUMENT ME!
622    * 
623    * @param evt
624    *          DOCUMENT ME!
625    */
626   @Override
627   public void mouseDragged(MouseEvent evt)
628   {
629     if (graphStretch > -1)
630     {
631       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
632               - evt.getY();
633       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
634       {
635         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
636       }
637       graphStretchY = evt.getY();
638       adjustPanelHeight();
639       ap.paintAlignment(true);
640     }
641     else
642     {
643       ap.getScalePanel().mouseDragged(evt);
644     }
645   }
646
647   /**
648    * Constructs the tooltip, and constructs and displays a status message, for
649    * the current mouse position
650    * 
651    * @param evt
652    */
653   @Override
654   public void mouseMoved(MouseEvent evt)
655   {
656     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
657
658     if (aa == null)
659     {
660       this.setToolTipText(null);
661       return;
662     }
663
664     int row = -1;
665     int height = 0;
666
667     for (int i = 0; i < aa.length; i++)
668     {
669       if (aa[i].visible)
670       {
671         height += aa[i].height;
672       }
673
674       if (evt.getY() < height)
675       {
676         row = i;
677         break;
678       }
679     }
680
681     if (row == -1)
682     {
683       this.setToolTipText(null);
684       return;
685     }
686
687     int column = (evt.getX() / av.getCharWidth()) + av.getStartRes();
688
689     if (av.hasHiddenColumns())
690     {
691       column = av.getColumnSelection().adjustForHiddenColumns(column);
692     }
693
694     AlignmentAnnotation ann = aa[row];
695     if (row > -1 && ann.annotations != null
696             && column < ann.annotations.length)
697     {
698       buildToolTip(ann, column, aa);
699       setStatusMessage(column, ann);
700     }
701     else
702     {
703       this.setToolTipText(null);
704       ap.alignFrame.statusBar.setText(" ");
705     }
706   }
707
708   /**
709    * Builds a tooltip for the annotation at the current mouse position.
710    * 
711    * @param ann
712    * @param column
713    * @param anns
714    */
715   void buildToolTip(AlignmentAnnotation ann, int column,
716           AlignmentAnnotation[] anns)
717   {
718     if (ann.graphGroup > -1)
719     {
720       StringBuilder tip = new StringBuilder(32);
721       tip.append("<html>");
722       for (int i = 0; i < anns.length; i++)
723       {
724         if (anns[i].graphGroup == ann.graphGroup
725                 && anns[i].annotations[column] != null)
726         {
727           tip.append(anns[i].label);
728           String description = anns[i].annotations[column].description;
729           if (description != null && description.length() > 0)
730           {
731             tip.append(" ").append(description);
732           }
733           tip.append("<br>");
734         }
735       }
736       if (tip.length() != 6)
737       {
738         tip.setLength(tip.length() - 4);
739         this.setToolTipText(tip.toString() + "</html>");
740       }
741     }
742     else if (ann.annotations[column] != null)
743     {
744       String description = ann.annotations[column].description;
745       if (description != null && description.length() > 0)
746       {
747         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
748       }
749     }
750     else
751     {
752       // clear the tooltip.
753       this.setToolTipText(null);
754     }
755   }
756
757   /**
758    * Constructs and displays the status bar message
759    * 
760    * @param column
761    * @param ann
762    */
763   void setStatusMessage(int column, AlignmentAnnotation ann)
764   {
765     /*
766      * show alignment column and annotation description if any
767      */
768     StringBuilder text = new StringBuilder(32);
769     text.append(MessageManager.getString("label.column")).append(" ")
770             .append(column + 1);
771
772     if (ann.annotations[column] != null)
773     {
774       String description = ann.annotations[column].description;
775       if (description != null && description.trim().length() > 0)
776       {
777         text.append("  ").append(description);
778       }
779     }
780
781     /*
782      * if the annotation is sequence-specific, show the sequence number
783      * in the alignment, and (if not a gap) the residue and position
784      */
785     SequenceI seqref = ann.sequenceRef;
786     if (seqref != null)
787     {
788       int seqIndex = av.getAlignment().findIndex(seqref);
789       if (seqIndex != -1)
790       {
791         text.append(", ")
792                 .append(MessageManager.getString("label.sequence"))
793                 .append(" ")
794                 .append(seqIndex + 1);
795         char residue = seqref.getCharAt(column);
796         if (!Comparison.isGap(residue))
797         {
798           int residuePos = seqref.findPosition(column);
799           text.append(": ").append(residue).append(" (")
800                   .append(residuePos).append(")");
801         }
802       }
803     }
804
805     ap.alignFrame.statusBar.setText(text.toString());
806   }
807
808   /**
809    * DOCUMENT ME!
810    * 
811    * @param evt
812    *          DOCUMENT ME!
813    */
814   @Override
815   public void mouseClicked(MouseEvent evt)
816   {
817     // if (activeRow != -1)
818     // {
819     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
820     // AlignmentAnnotation anot = aa[activeRow];
821     // }
822   }
823
824   // TODO mouseClicked-content and drawCursor are quite experimental!
825   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
826           int y1)
827   {
828     int pady = av.getCharHeight() / 5;
829     int charOffset = 0;
830     graphics.setColor(Color.black);
831     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
832
833     if (av.validCharWidth)
834     {
835       graphics.setColor(Color.white);
836
837       char s = seq.getCharAt(res);
838
839       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
840       graphics.drawString(String.valueOf(s), charOffset + x1,
841               (y1 + av.getCharHeight()) - pady);
842     }
843
844   }
845
846   private volatile boolean imageFresh = false;
847
848   /**
849    * DOCUMENT ME!
850    * 
851    * @param g
852    *          DOCUMENT ME!
853    */
854   @Override
855   public void paintComponent(Graphics g)
856   {
857     g.setColor(Color.white);
858     g.fillRect(0, 0, getWidth(), getHeight());
859
860     if (image != null)
861     {
862       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
863               || (getVisibleRect().height != g.getClipBounds().height))
864       {
865         g.drawImage(image, 0, 0, this);
866         fastPaint = false;
867         return;
868       }
869     }
870     imgWidth = (av.endRes - av.startRes + 1) * av.getCharWidth();
871     if (imgWidth < 1)
872     {
873       return;
874     }
875     if (image == null || imgWidth != image.getWidth(this)
876             || image.getHeight(this) != getHeight())
877     {
878       try
879       {
880         image = new BufferedImage(imgWidth, ap.getAnnotationPanel()
881                 .getHeight(), BufferedImage.TYPE_INT_RGB);
882       } catch (OutOfMemoryError oom)
883       {
884         try
885         {
886           System.gc();
887         } catch (Exception x)
888         {
889         }
890         ;
891         new OOMWarning(
892                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
893                 oom);
894         return;
895       }
896       gg = (Graphics2D) image.getGraphics();
897
898       if (av.antiAlias)
899       {
900         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
901                 RenderingHints.VALUE_ANTIALIAS_ON);
902       }
903
904       gg.setFont(av.getFont());
905       fm = gg.getFontMetrics();
906       gg.setColor(Color.white);
907       gg.fillRect(0, 0, imgWidth, image.getHeight());
908       imageFresh = true;
909     }
910
911     drawComponent(gg, av.startRes, av.endRes + 1);
912     imageFresh = false;
913     g.drawImage(image, 0, 0, this);
914   }
915
916   /**
917    * set true to enable redraw timing debug output on stderr
918    */
919   private final boolean debugRedraw = false;
920
921   /**
922    * non-Thread safe repaint
923    * 
924    * @param horizontal
925    *          repaint with horizontal shift in alignment
926    */
927   public void fastPaint(int horizontal)
928   {
929     if ((horizontal == 0) || gg == null
930             || av.getAlignment().getAlignmentAnnotation() == null
931             || av.getAlignment().getAlignmentAnnotation().length < 1
932             || av.isCalcInProgress())
933     {
934       repaint();
935       return;
936     }
937     long stime = System.currentTimeMillis();
938     gg.copyArea(0, 0, imgWidth, getHeight(),
939             -horizontal * av.getCharWidth(), 0);
940     long mtime = System.currentTimeMillis();
941     int sr = av.startRes;
942     int er = av.endRes + 1;
943     int transX = 0;
944
945     if (horizontal > 0) // scrollbar pulled right, image to the left
946     {
947       transX = (er - sr - horizontal) * av.getCharWidth();
948       sr = er - horizontal;
949     }
950     else if (horizontal < 0)
951     {
952       er = sr - horizontal;
953     }
954
955     gg.translate(transX, 0);
956
957     drawComponent(gg, sr, er);
958
959     gg.translate(-transX, 0);
960     long dtime = System.currentTimeMillis();
961     fastPaint = true;
962     repaint();
963     long rtime = System.currentTimeMillis();
964     if (debugRedraw)
965     {
966       System.err.println("Scroll:\t" + horizontal + "\tCopyArea:\t"
967               + (mtime - stime) + "\tDraw component:\t" + (dtime - mtime)
968               + "\tRepaint call:\t" + (rtime - dtime));
969     }
970
971   }
972
973   private volatile boolean lastImageGood = false;
974
975   /**
976    * DOCUMENT ME!
977    * 
978    * @param g
979    *          DOCUMENT ME!
980    * @param startRes
981    *          DOCUMENT ME!
982    * @param endRes
983    *          DOCUMENT ME!
984    */
985   public void drawComponent(Graphics g, int startRes, int endRes)
986   {
987     BufferedImage oldFaded = fadedImage;
988     if (av.isCalcInProgress())
989     {
990       if (image == null)
991       {
992         lastImageGood = false;
993         return;
994       }
995       // We'll keep a record of the old image,
996       // and draw a faded image until the calculation
997       // has completed
998       if (lastImageGood
999               && (fadedImage == null || fadedImage.getWidth() != imgWidth || fadedImage
1000                       .getHeight() != image.getHeight()))
1001       {
1002         // System.err.println("redraw faded image ("+(fadedImage==null ?
1003         // "null image" : "") + " lastGood="+lastImageGood+")");
1004         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1005                 BufferedImage.TYPE_INT_RGB);
1006
1007         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1008
1009         fadedG.setColor(Color.white);
1010         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1011
1012         fadedG.setComposite(AlphaComposite.getInstance(
1013                 AlphaComposite.SRC_OVER, .3f));
1014         fadedG.drawImage(image, 0, 0, this);
1015
1016       }
1017       // make sure we don't overwrite the last good faded image until all
1018       // calculations have finished
1019       lastImageGood = false;
1020
1021     }
1022     else
1023     {
1024       if (fadedImage != null)
1025       {
1026         oldFaded = fadedImage;
1027       }
1028       fadedImage = null;
1029     }
1030
1031     g.setColor(Color.white);
1032     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1033
1034     g.setFont(av.getFont());
1035     if (fm == null)
1036     {
1037       fm = g.getFontMetrics();
1038     }
1039
1040     if ((av.getAlignment().getAlignmentAnnotation() == null)
1041             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1042     {
1043       g.setColor(Color.white);
1044       g.fillRect(0, 0, getWidth(), getHeight());
1045       g.setColor(Color.black);
1046       if (av.validCharWidth)
1047       {
1048         g.drawString(MessageManager
1049                 .getString("label.alignment_has_no_annotations"), 20, 15);
1050       }
1051
1052       return;
1053     }
1054     lastImageGood = renderer.drawComponent(this, av, g, activeRow,
1055             startRes, endRes);
1056     if (!lastImageGood && fadedImage == null)
1057     {
1058       fadedImage = oldFaded;
1059     }
1060   }
1061
1062   @Override
1063   public FontMetrics getFontMetrics()
1064   {
1065     return fm;
1066   }
1067
1068   @Override
1069   public Image getFadedImage()
1070   {
1071     return fadedImage;
1072   }
1073
1074   @Override
1075   public int getFadedImageWidth()
1076   {
1077     return imgWidth;
1078   }
1079
1080   private int[] bounds = new int[2];
1081
1082   @Override
1083   public int[] getVisibleVRange()
1084   {
1085     if (ap != null && ap.getAlabels() != null)
1086     {
1087       int sOffset = -ap.getAlabels().getScrollOffset();
1088       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1089       bounds[0] = sOffset;
1090       bounds[1] = visHeight;
1091       return bounds;
1092     }
1093     else
1094     {
1095       return null;
1096     }
1097   }
1098 }