001package javax.swing.text.html; 002 003import gnu.javax.swing.text.html.ImageViewIconFactory; 004import gnu.javax.swing.text.html.css.Length; 005 006import java.awt.Graphics; 007import java.awt.Image; 008import java.awt.MediaTracker; 009import java.awt.Rectangle; 010import java.awt.Shape; 011import java.awt.Toolkit; 012import java.awt.image.ImageObserver; 013import java.net.MalformedURLException; 014import java.net.URL; 015 016import javax.swing.Icon; 017import javax.swing.SwingUtilities; 018import javax.swing.text.AbstractDocument; 019import javax.swing.text.AttributeSet; 020import javax.swing.text.BadLocationException; 021import javax.swing.text.Document; 022import javax.swing.text.Element; 023import javax.swing.text.View; 024import javax.swing.text.Position.Bias; 025import 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 */ 032public 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}