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 AttributeSet attrs = getAttributes();
327
328 Image image = getImage();
329
330 if (axis == View.X_AXIS)
331 {
332 if (spans[axis] != null)
333 return spans[axis].getValue();
334 else if (image != null)
335 return image.getWidth(getContainer());
336 else
337 return getNoImageIcon().getIconWidth();
338 }
339 else if (axis == View.Y_AXIS)
340 {
341 if (spans[axis] != null)
342 return spans[axis].getValue();
343 else if (image != null)
344 return image.getHeight(getContainer());
345 else
346 return getNoImageIcon().getIconHeight();
347 }
348 else
349 throw new IllegalArgumentException("axis " + axis);
350 }
351
352 /**
353 * Get the associated style sheet from the document.
354 *
355 * @return the associated style sheet.
356 */
357 protected StyleSheet getStyleSheet()
358 {
359 HTMLDocument doc = (HTMLDocument) getDocument();
360 return doc.getStyleSheet();
361 }
362
363 /**
364 * Get the tool tip text. This is overridden to return the value of the
365 * {@link #getAltText()}. The parameters are ignored.
366 *
367 * @return that is returned by getAltText().
368 */
369 public String getToolTipText(float x, float y, Shape shape)
370 {
371 return getAltText();
372 }
373
374 /**
375 * Paints the image or one of the two image state icons. The image is resized
376 * to the shape bounds. If there is no image available, the alternative text
377 * is displayed besides the image state icon.
378 *
379 * @param g
380 * the Graphics, used for painting.
381 * @param bounds
382 * the bounds of the region where the image or replacing icon must be
383 * painted.
384 */
385 public void paint(Graphics g, Shape bounds)
386 {
387 updateState();
388 Rectangle r = bounds instanceof Rectangle ? (Rectangle) bounds
389 : bounds.getBounds();
390 Image image = getImage();
391 if (image != null)
392 {
393 g.drawImage(image, r.x, r.y, r.width, r.height, observer);
394 }
395 else
396 {
397 Icon icon = getNoImageIcon();
398 if (icon != null)
399 icon.paintIcon(getContainer(), g, r.x, r.y);
400 }
401 }
402
403 /**
404 * Set if the image should be loaded only when needed (synchronuosly). By
405 * default, the image loads asynchronuosly. If the image is not yet ready, the
406 * icon, returned by the {@link #getLoadingImageIcon()}, is displayed.
407 */
408 public void setLoadsSynchronously(boolean load_on_demand)
409 {
410 loadOnDemand = load_on_demand;
411 }
412
413 /**
414 * Update all cached properties from the attribute set, returned by the
415 * {@link #getAttributes}.
416 */
417 protected void setPropertiesFromAttributes()
418 {
419 AttributeSet atts = getAttributes();
420 StyleSheet ss = getStyleSheet();
421 float emBase = ss.getEMBase(atts);
422 float exBase = ss.getEXBase(atts);
423 spans[X_AXIS] = (Length) atts.getAttribute(CSS.Attribute.WIDTH);
424 if (spans[X_AXIS] != null)
425 {
426 spans[X_AXIS].setFontBases(emBase, exBase);
427 }
428 spans[Y_AXIS] = (Length) atts.getAttribute(CSS.Attribute.HEIGHT);
429 if (spans[Y_AXIS] != null)
430 {
431 spans[Y_AXIS].setFontBases(emBase, exBase);
432 }
433 }
434
435 /**
436 * Maps the picture co-ordinates into the image position in the model. As the
437 * image is not divideable, this is currently implemented always to return the
438 * start offset.
439 */
440 public int viewToModel(float x, float y, Shape shape, Bias[] bias)
441 {
442 return getStartOffset();
443 }
444
445 /**
446 * This is currently implemented always to return the area of the image view,
447 * as the image is not divideable by character positions.
448 *
449 * @param pos character position
450 * @param area of the image view
451 * @param bias bias
452 *
453 * @return the shape, where the given character position should be mapped.
454 */
455 public Shape modelToView(int pos, Shape area, Bias bias)
456 throws BadLocationException
457 {
458 return area;
459 }
460
461 /**
462 * Starts loading the image asynchronuosly. If the image must be loaded
463 * synchronuosly instead, the {@link #setLoadsSynchronously} must be
464 * called before calling this method. The passed parameters are not used.
465 */
466 public void setSize(float width, float height)
467 {
468 updateState();
469 // TODO: Implement this when we have an alt view for the alt=... attribute.
470 }
471
472 /**
473 * This makes sure that the image and properties have been loaded.
474 */
475 private void updateState()
476 {
477 if (reloadImage)
478 reloadImage();
479 if (reloadProperties)
480 setPropertiesFromAttributes();
481 }
482
483 /**
484 * Actually loads the image.
485 */
486 private void loadImage()
487 {
488 URL src = getImageURL();
489 Image newImage = null;
490 if (src != null)
491 {
492 // Call getImage(URL) to allow the toolkit caching of that image URL.
493 Toolkit tk = Toolkit.getDefaultToolkit();
494 newImage = tk.getImage(src);
495 tk.prepareImage(newImage, -1, -1, observer);
496 if (newImage != null && getLoadsSynchronously())
497 {
498 // Load image synchronously.
499 MediaTracker tracker = new MediaTracker(getContainer());
500 tracker.addImage(newImage, 0);
501 try
502 {
503 tracker.waitForID(0);
504 }
505 catch (InterruptedException ex)
506 {
507 Thread.interrupted();
508 }
509
510 }
511 }
512 image = newImage;
513 }
514
515 /**
516 * Updates the size parameters of the image.
517 */
518 private void updateSize()
519 {
520 int newW = 0;
521 int newH = 0;
522 Image newIm = getImage();
523 if (newIm != null)
524 {
525 AttributeSet atts = getAttributes();
526 // Fetch width.
527 Length l = spans[X_AXIS];
528 if (l != null)
529 {
530 newW = (int) l.getValue();
531 haveWidth = true;
532 }
533 else
534 {
535 newW = newIm.getWidth(observer);
536 }
537 // Fetch height.
538 l = spans[Y_AXIS];
539 if (l != null)
540 {
541 newH = (int) l.getValue();
542 haveHeight = true;
543 }
544 else
545 {
546 newW = newIm.getWidth(observer);
547 }
548 // Go and trigger loading.
549 Toolkit tk = Toolkit.getDefaultToolkit();
550 if (haveWidth || haveHeight)
551 tk.prepareImage(newIm, width, height, observer);
552 else
553 tk.prepareImage(newIm, -1, -1, observer);
554 }
555 }
556
557 /**
558 * Calls preferenceChanged from the event dispatch thread and within
559 * a read lock to protect us from threading issues.
560 *
561 * @param v the view
562 * @param width true when the width changed
563 * @param height true when the height changed
564 */
565 void safePreferenceChanged(final View v, final boolean width,
566 final boolean height)
567 {
568 if (SwingUtilities.isEventDispatchThread())
569 {
570 Document doc = getDocument();
571 if (doc instanceof AbstractDocument)
572 ((AbstractDocument) doc).readLock();
573 try
574 {
575 preferenceChanged(v, width, height);
576 }
577 finally
578 {
579 if (doc instanceof AbstractDocument)
580 ((AbstractDocument) doc).readUnlock();
581 }
582 }
583 else
584 {
585 SwingUtilities.invokeLater(new Runnable()
586 {
587 public void run()
588 {
589 safePreferenceChanged(v, width, height);
590 }
591 });
592 }
593 }
594 }