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 }