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:     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: }