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