Source for javax.swing.text.html.ImageView

   1: package javax.swing.text.html;
   2: 
   3: import gnu.javax.swing.text.html.ImageViewIconFactory;
   4: import gnu.javax.swing.text.html.css.Length;
   5: 
   6: import java.awt.Graphics;
   7: import java.awt.Image;
   8: import java.awt.MediaTracker;
   9: import java.awt.Rectangle;
  10: import java.awt.Shape;
  11: import java.awt.Toolkit;
  12: import java.awt.image.ImageObserver;
  13: import java.net.MalformedURLException;
  14: import java.net.URL;
  15: 
  16: import javax.swing.Icon;
  17: import javax.swing.SwingUtilities;
  18: import javax.swing.text.AbstractDocument;
  19: import javax.swing.text.AttributeSet;
  20: import javax.swing.text.BadLocationException;
  21: import javax.swing.text.Document;
  22: import javax.swing.text.Element;
  23: import javax.swing.text.View;
  24: import javax.swing.text.Position.Bias;
  25: import javax.swing.text.html.HTML.Attribute;
  26: 
  27: /**
  28:  * A view, representing a single image, represented by the HTML IMG tag.
  29:  *
  30:  * @author Audrius Meskauskas (AudriusA@Bioinformatics.org)
  31:  */
  32: public class ImageView extends View
  33: {
  34:   /**
  35:    * Tracks image loading state and performs the necessary layout updates.
  36:    */
  37:   class Observer
  38:     implements ImageObserver
  39:   {
  40: 
  41:     public boolean imageUpdate(Image image, int flags, int x, int y, int width, int height)
  42:     {
  43:       boolean widthChanged = false;
  44:       if ((flags & ImageObserver.WIDTH) != 0 && spans[X_AXIS] == null)
  45:         widthChanged = true;
  46:       boolean heightChanged = false;
  47:       if ((flags & ImageObserver.HEIGHT) != 0 && spans[Y_AXIS] == null)
  48:         heightChanged = true;
  49:       if (widthChanged || heightChanged)
  50:         safePreferenceChanged(ImageView.this, widthChanged, heightChanged);
  51:       boolean ret = (flags & ALLBITS) != 0;
  52:       return ret;
  53:     }
  54: 
  55:   }
  56: 
  57:   /**
  58:    * True if the image loads synchronuosly (on demand). By default, the image
  59:    * loads asynchronuosly.
  60:    */
  61:   boolean loadOnDemand;
  62: 
  63:   /**
  64:    * The image icon, wrapping the image,
  65:    */
  66:   Image image;
  67: 
  68:   /**
  69:    * The image state.
  70:    */
  71:   byte imageState = MediaTracker.LOADING;
  72: 
  73:   /**
  74:    * True when the image needs re-loading, false otherwise.
  75:    */
  76:   private boolean reloadImage;
  77: 
  78:   /**
  79:    * True when the image properties need re-loading, false otherwise.
  80:    */
  81:   private boolean reloadProperties;
  82: 
  83:   /**
  84:    * True when the width is set as CSS/HTML attribute.
  85:    */
  86:   private boolean haveWidth;
  87: 
  88:   /**
  89:    * True when the height is set as CSS/HTML attribute.
  90:    */
  91:   private boolean haveHeight;
  92: 
  93:   /**
  94:    * True when the image is currently loading.
  95:    */
  96:   private boolean loading;
  97: 
  98:   /**
  99:    * 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: }