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 }