001    package javax.swing.text.html;
002    
003    import gnu.javax.swing.text.html.ImageViewIconFactory;
004    import gnu.javax.swing.text.html.css.Length;
005    
006    import java.awt.Graphics;
007    import java.awt.Image;
008    import java.awt.MediaTracker;
009    import java.awt.Rectangle;
010    import java.awt.Shape;
011    import java.awt.Toolkit;
012    import java.awt.image.ImageObserver;
013    import java.net.MalformedURLException;
014    import java.net.URL;
015    
016    import javax.swing.Icon;
017    import javax.swing.SwingUtilities;
018    import javax.swing.text.AbstractDocument;
019    import javax.swing.text.AttributeSet;
020    import javax.swing.text.BadLocationException;
021    import javax.swing.text.Document;
022    import javax.swing.text.Element;
023    import javax.swing.text.View;
024    import javax.swing.text.Position.Bias;
025    import javax.swing.text.html.HTML.Attribute;
026    
027    /**
028     * A view, representing a single image, represented by the HTML IMG tag.
029     *
030     * @author Audrius Meskauskas (AudriusA@Bioinformatics.org)
031     */
032    public class ImageView extends View
033    {
034      /**
035       * Tracks image loading state and performs the necessary layout updates.
036       */
037      class Observer
038        implements ImageObserver
039      {
040    
041        public boolean imageUpdate(Image image, int flags, int x, int y, int width, int height)
042        {
043          boolean widthChanged = false;
044          if ((flags & ImageObserver.WIDTH) != 0 && spans[X_AXIS] == null)
045            widthChanged = true;
046          boolean heightChanged = false;
047          if ((flags & ImageObserver.HEIGHT) != 0 && spans[Y_AXIS] == null)
048            heightChanged = true;
049          if (widthChanged || heightChanged)
050            safePreferenceChanged(ImageView.this, widthChanged, heightChanged);
051          boolean ret = (flags & ALLBITS) != 0;
052          return ret;
053        }
054    
055      }
056    
057      /**
058       * True if the image loads synchronuosly (on demand). By default, the image
059       * loads asynchronuosly.
060       */
061      boolean loadOnDemand;
062    
063      /**
064       * The image icon, wrapping the image,
065       */
066      Image image;
067    
068      /**
069       * The image state.
070       */
071      byte imageState = MediaTracker.LOADING;
072    
073      /**
074       * True when the image needs re-loading, false otherwise.
075       */
076      private boolean reloadImage;
077    
078      /**
079       * True when the image properties need re-loading, false otherwise.
080       */
081      private boolean reloadProperties;
082    
083      /**
084       * True when the width is set as CSS/HTML attribute.
085       */
086      private boolean haveWidth;
087    
088      /**
089       * True when the height is set as CSS/HTML attribute.
090       */
091      private boolean haveHeight;
092    
093      /**
094       * True when the image is currently loading.
095       */
096      private boolean loading;
097    
098      /**
099       * The current width of the image.
100       */
101      private int width;
102    
103      /**
104       * The current height of the image.
105       */
106      private int height;
107    
108      /**
109       * Our ImageObserver for tracking the loading state.
110       */
111      private ImageObserver observer;
112    
113      /**
114       * The CSS width and height.
115       *
116       * Package private to avoid synthetic accessor methods.
117       */
118      Length[] spans;
119    
120      /**
121       * The cached attributes.
122       */
123      private AttributeSet attributes;
124    
125      /**
126       * Creates the image view that represents the given element.
127       *
128       * @param element the element, represented by this image view.
129       */
130      public ImageView(Element element)
131      {
132        super(element);
133        spans = new Length[2];
134        observer = new Observer();
135        reloadProperties = true;
136        reloadImage = true;
137        loadOnDemand = false;
138      }
139    
140      /**
141       * Load or reload the image. This method initiates the image reloading. After
142       * the image is ready, the repaint event will be scheduled. The current image,
143       * if it already exists, will be discarded.
144       */
145      private void reloadImage()
146      {
147        loading = true;
148        reloadImage = false;
149        haveWidth = false;
150        haveHeight = false;
151        image = null;
152        width = 0;
153        height = 0;
154        try
155          {
156            loadImage();
157            updateSize();
158          }
159        finally
160          {
161            loading = false;
162          }
163      }
164    
165      /**
166       * Get the image alignment. This method works handling standart alignment
167       * attributes in the HTML IMG tag (align = top bottom middle left right).
168       * Depending from the parameter, either horizontal or vertical alingment
169       * information is returned.
170       *
171       * @param axis -
172       *          either X_AXIS or Y_AXIS
173       */
174      public float getAlignment(int axis)
175      {
176        AttributeSet attrs = getAttributes();
177        Object al = attrs.getAttribute(Attribute.ALIGN);
178    
179        // Default is top left aligned.
180        if (al == null)
181          return 0.0f;
182    
183        String align = al.toString();
184    
185        if (axis == View.X_AXIS)
186          {
187            if (align.equals("middle"))
188              return 0.5f;
189            else if (align.equals("left"))
190              return 0.0f;
191            else if (align.equals("right"))
192              return 1.0f;
193            else
194              return 0.0f;
195          }
196        else if (axis == View.Y_AXIS)
197          {
198            if (align.equals("middle"))
199              return 0.5f;
200            else if (align.equals("top"))
201              return 0.0f;
202            else if (align.equals("bottom"))
203              return 1.0f;
204            else
205              return 0.0f;
206          }
207        else
208          throw new IllegalArgumentException("axis " + axis);
209      }
210    
211      /**
212       * Get the text that should be shown as the image replacement and also as the
213       * image tool tip text. The method returns the value of the attribute, having
214       * the name {@link Attribute#ALT}. If there is no such attribute, the image
215       * name from the url is returned. If the URL is not available, the empty
216       * string is returned.
217       */
218      public String getAltText()
219      {
220        Object rt = getAttributes().getAttribute(Attribute.ALT);
221        if (rt != null)
222          return rt.toString();
223        else
224          {
225            URL u = getImageURL();
226            if (u == null)
227              return "";
228            else
229              return u.getFile();
230          }
231      }
232    
233      /**
234       * Returns the combination of the document and the style sheet attributes.
235       */
236      public AttributeSet getAttributes()
237      {
238        if (attributes == null)
239          attributes = getStyleSheet().getViewAttributes(this);
240        return attributes;
241      }
242    
243      /**
244       * Get the image to render. May return null if the image is not yet loaded.
245       */
246      public Image getImage()
247      {
248        updateState();
249        return image;
250      }
251    
252      /**
253       * Get the URL location of the image to render. If this method returns null,
254       * the "no image" icon is rendered instead. By defaul, url must be present as
255       * the "src" property of the IMG tag. If it is missing, null is returned and
256       * the "no image" icon is rendered.
257       *
258       * @return the URL location of the image to render.
259       */
260      public URL getImageURL()
261      {
262        Element el = getElement();
263        String src = (String) el.getAttributes().getAttribute(Attribute.SRC);
264        URL url = null;
265        if (src != null)
266          {
267            URL base = ((HTMLDocument) getDocument()).getBase();
268            try
269              {
270                url = new URL(base, src);
271              }
272            catch (MalformedURLException ex)
273              {
274                // Return null.
275              }
276          }
277        return url;
278      }
279    
280      /**
281       * Get the icon that should be displayed while the image is loading and hence
282       * not yet available.
283       *
284       * @return an icon, showing a non broken sheet of paper with image.
285       */
286      public Icon getLoadingImageIcon()
287      {
288        return ImageViewIconFactory.getLoadingImageIcon();
289      }
290    
291      /**
292       * Get the image loading strategy.
293       *
294       * @return false (default) if the image is loaded when the view is
295       *         constructed, true if the image is only loaded on demand when
296       *         rendering.
297       */
298      public boolean getLoadsSynchronously()
299      {
300        return loadOnDemand;
301      }
302    
303      /**
304       * Get the icon that should be displayed when the image is not available.
305       *
306       * @return an icon, showing a broken sheet of paper with image.
307       */
308      public Icon getNoImageIcon()
309      {
310        return ImageViewIconFactory.getNoImageIcon();
311      }
312    
313      /**
314       * Get the preferred span of the image along the axis. The image size is first
315       * requested to the attributes {@link Attribute#WIDTH} and
316       * {@link Attribute#HEIGHT}. If they are missing, and the image is already
317       * loaded, the image size is returned. If there are no attributes, and the
318       * image is not loaded, zero is returned.
319       *
320       * @param axis -
321       *          either X_AXIS or Y_AXIS
322       * @return either width of height of the image, depending on the axis.
323       */
324      public float getPreferredSpan(int axis)
325      {
326        Image image = getImage();
327    
328        if (axis == View.X_AXIS)
329          {
330            if (spans[axis] != null)
331              return spans[axis].getValue();
332            else if (image != null)
333              return image.getWidth(getContainer());
334            else
335              return getNoImageIcon().getIconWidth();
336          }
337        else if (axis == View.Y_AXIS)
338          {
339            if (spans[axis] != null)
340              return spans[axis].getValue();
341            else if (image != null)
342              return image.getHeight(getContainer());
343            else
344              return getNoImageIcon().getIconHeight();
345          }
346        else
347          throw new IllegalArgumentException("axis " + axis);
348      }
349    
350      /**
351       * Get the associated style sheet from the document.
352       *
353       * @return the associated style sheet.
354       */
355      protected StyleSheet getStyleSheet()
356      {
357        HTMLDocument doc = (HTMLDocument) getDocument();
358        return doc.getStyleSheet();
359      }
360    
361      /**
362       * Get the tool tip text. This is overridden to return the value of the
363       * {@link #getAltText()}. The parameters are ignored.
364       *
365       * @return that is returned by getAltText().
366       */
367      public String getToolTipText(float x, float y, Shape shape)
368      {
369        return getAltText();
370      }
371    
372      /**
373       * Paints the image or one of the two image state icons. The image is resized
374       * to the shape bounds. If there is no image available, the alternative text
375       * is displayed besides the image state icon.
376       *
377       * @param g
378       *          the Graphics, used for painting.
379       * @param bounds
380       *          the bounds of the region where the image or replacing icon must be
381       *          painted.
382       */
383      public void paint(Graphics g, Shape bounds)
384      {
385        updateState();
386        Rectangle r = bounds instanceof Rectangle ? (Rectangle) bounds
387                                                  : bounds.getBounds();
388        Image image = getImage();
389        if (image != null)
390          {
391            g.drawImage(image, r.x, r.y, r.width, r.height, observer);
392          }
393        else
394          {
395            Icon icon = getNoImageIcon();
396            if (icon != null)
397              icon.paintIcon(getContainer(), g, r.x, r.y);
398          }
399      }
400    
401      /**
402       * Set if the image should be loaded only when needed (synchronuosly). By
403       * default, the image loads asynchronuosly. If the image is not yet ready, the
404       * icon, returned by the {@link #getLoadingImageIcon()}, is displayed.
405       */
406      public void setLoadsSynchronously(boolean load_on_demand)
407      {
408        loadOnDemand = load_on_demand;
409      }
410    
411      /**
412       * Update all cached properties from the attribute set, returned by the
413       * {@link #getAttributes}.
414       */
415      protected void setPropertiesFromAttributes()
416      {
417        AttributeSet atts = getAttributes();
418        StyleSheet ss = getStyleSheet();
419        float emBase = ss.getEMBase(atts);
420        float exBase = ss.getEXBase(atts);
421        spans[X_AXIS] = (Length) atts.getAttribute(CSS.Attribute.WIDTH);
422        if (spans[X_AXIS] != null)
423          {
424            spans[X_AXIS].setFontBases(emBase, exBase);
425          }
426        spans[Y_AXIS] = (Length) atts.getAttribute(CSS.Attribute.HEIGHT);
427        if (spans[Y_AXIS] != null)
428          {
429            spans[Y_AXIS].setFontBases(emBase, exBase);
430          }
431      }
432    
433      /**
434       * Maps the picture co-ordinates into the image position in the model. As the
435       * image is not divideable, this is currently implemented always to return the
436       * start offset.
437       */
438      public int viewToModel(float x, float y, Shape shape, Bias[] bias)
439      {
440        return getStartOffset();
441      }
442    
443      /**
444       * This is currently implemented always to return the area of the image view,
445       * as the image is not divideable by character positions.
446       *
447       * @param pos character position
448       * @param area of the image view
449       * @param bias bias
450       *
451       * @return the shape, where the given character position should be mapped.
452       */
453      public Shape modelToView(int pos, Shape area, Bias bias)
454          throws BadLocationException
455      {
456        return area;
457      }
458    
459      /**
460       * Starts loading the image asynchronuosly. If the image must be loaded
461       * synchronuosly instead, the {@link #setLoadsSynchronously} must be
462       * called before calling this method. The passed parameters are not used.
463       */
464      public void setSize(float width, float height)
465      {
466        updateState();
467        // TODO: Implement this when we have an alt view for the alt=... attribute.
468      }
469    
470      /**
471       * This makes sure that the image and properties have been loaded.
472       */
473      private void updateState()
474      {
475        if (reloadImage)
476          reloadImage();
477        if (reloadProperties)
478          setPropertiesFromAttributes();
479      }
480    
481      /**
482       * Actually loads the image.
483       */
484      private void loadImage()
485      {
486        URL src = getImageURL();
487        Image newImage = null;
488        if (src != null)
489          {
490            // Call getImage(URL) to allow the toolkit caching of that image URL.
491            Toolkit tk = Toolkit.getDefaultToolkit();
492            newImage = tk.getImage(src);
493            tk.prepareImage(newImage, -1, -1, observer);
494            if (newImage != null && getLoadsSynchronously())
495              {
496                // Load image synchronously.
497                MediaTracker tracker = new MediaTracker(getContainer());
498                tracker.addImage(newImage, 0);
499                try
500                  {
501                    tracker.waitForID(0);
502                  }
503                catch (InterruptedException ex)
504                  {
505                    Thread.interrupted();
506                  }
507    
508              }
509          }
510        image = newImage;
511      }
512    
513      /**
514       * Updates the size parameters of the image.
515       */
516      private void updateSize()
517      {
518        int newW = 0;
519        int newH = 0;
520        Image newIm = getImage();
521        if (newIm != null)
522          {
523            // Fetch width.
524            Length l = spans[X_AXIS];
525            if (l != null)
526              {
527                newW = (int) l.getValue();
528                haveWidth = true;
529              }
530            else
531              {
532                newW = newIm.getWidth(observer);
533              }
534            // Fetch height.
535            l = spans[Y_AXIS];
536            if (l != null)
537              {
538                newH = (int) l.getValue();
539                haveHeight = true;
540              }
541            else
542              {
543                newW = newIm.getWidth(observer);
544              }
545            // Go and trigger loading.
546            Toolkit tk = Toolkit.getDefaultToolkit();
547            if (haveWidth || haveHeight)
548              tk.prepareImage(newIm, width, height, observer);
549            else
550              tk.prepareImage(newIm, -1, -1, observer);
551          }
552      }
553    
554      /**
555       * Calls preferenceChanged from the event dispatch thread and within
556       * a read lock to protect us from threading issues.
557       *
558       * @param v the view
559       * @param width true when the width changed
560       * @param height true when the height changed
561       */
562      void safePreferenceChanged(final View v, final boolean width,
563                                 final boolean height)
564      {
565        if (SwingUtilities.isEventDispatchThread())
566          {
567            Document doc = getDocument();
568            if (doc instanceof AbstractDocument)
569              ((AbstractDocument) doc).readLock();
570            try
571              {
572                preferenceChanged(v, width, height);
573              }
574            finally
575              {
576                if (doc instanceof AbstractDocument)
577                  ((AbstractDocument) doc).readUnlock();
578              }
579          }
580        else
581          {
582            SwingUtilities.invokeLater(new Runnable()
583            {
584              public void run()
585              {
586                safePreferenceChanged(v, width, height);
587              }
588            });
589          }
590      }
591    }