Showing the changes to java/pkg/javax/swing/text/html/ImageView.java (1 of 18352 files). Show all
Unified · Split
| @@ -0,0 +1,1008 @@ | |||
| 1 | /* | ||
| 2 | * Copyright (c) 1997, 2005, Oracle and/or its affiliates. All rights reserved. | ||
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
| 4 | * | ||
| 5 | * This code is free software; you can redistribute it and/or modify it | ||
| 6 | * under the terms of the GNU General Public License version 2 only, as | ||
| 7 | * published by the Free Software Foundation. Oracle designates this | ||
| 8 | * particular file as subject to the "Classpath" exception as provided | ||
| 9 | * by Oracle in the LICENSE file that accompanied this code. | ||
| 10 | * | ||
| 11 | * This code is distributed in the hope that it will be useful, but WITHOUT | ||
| 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
| 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
| 14 | * version 2 for more details (a copy is included in the LICENSE file that | ||
| 15 | * accompanied this code). | ||
| 16 | * | ||
| 17 | * You should have received a copy of the GNU General Public License version | ||
| 18 | * 2 along with this work; if not, write to the Free Software Foundation, | ||
| 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| 20 | * | ||
| 21 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | ||
| 22 | * or visit www.oracle.com if you need additional information or have any | ||
| 23 | * questions. | ||
| 24 | */ | ||
| 25 | package javax.swing.text.html; | ||
| 26 | |||
| 27 | import java.awt.*; | ||
| 28 | import java.awt.image.ImageObserver; | ||
| 29 | import java.net.*; | ||
| 30 | import java.util.Dictionary; | ||
| 31 | import javax.swing.*; | ||
| 32 | import javax.swing.text.*; | ||
| 33 | import javax.swing.event.*; | ||
| 34 | |||
| 35 | /** | ||
| 36 | * View of an Image, intended to support the HTML <IMG> tag. | ||
| 37 | * Supports scaling via the HEIGHT and WIDTH attributes of the tag. | ||
| 38 | * If the image is unable to be loaded any text specified via the | ||
| 39 | * <code>ALT</code> attribute will be rendered. | ||
| 40 | * <p> | ||
| 41 | * While this class has been part of swing for a while now, it is public | ||
| 42 | * as of 1.4. | ||
| 43 | * | ||
| 44 | * @author Scott Violet | ||
| 45 | * @see IconView | ||
| 46 | * @since 1.4 | ||
| 47 | */ | ||
| 48 | public class ImageView extends View { | ||
| 49 | /** | ||
| 50 | * If true, when some of the bits are available a repaint is done. | ||
| 51 | * <p> | ||
| 52 | * This is set to false as swing does not offer a repaint that takes a | ||
| 53 | * delay. If this were true, a bunch of immediate repaints would get | ||
| 54 | * generated that end up significantly delaying the loading of the image | ||
| 55 | * (or anything else going on for that matter). | ||
| 56 | */ | ||
| 57 | private static boolean sIsInc = false; | ||
| 58 | /** | ||
| 59 | * Repaint delay when some of the bits are available. | ||
| 60 | */ | ||
| 61 | private static int sIncRate = 100; | ||
| 62 | /** | ||
| 63 | * Property name for pending image icon | ||
| 64 | */ | ||
| 65 | private static final String PENDING_IMAGE = "html.pendingImage"; | ||
| 66 | /** | ||
| 67 | * Property name for missing image icon | ||
| 68 | */ | ||
| 69 | private static final String MISSING_IMAGE = "html.missingImage"; | ||
| 70 | |||
| 71 | /** | ||
| 72 | * Document property for image cache. | ||
| 73 | */ | ||
| 74 | private static final String IMAGE_CACHE_PROPERTY = "imageCache"; | ||
| 75 | |||
| 76 | // Height/width to use before we know the real size, these should at least | ||
| 77 | // the size of <code>sMissingImageIcon</code> and | ||
| 78 | // <code>sPendingImageIcon</code> | ||
| 79 | private static final int DEFAULT_WIDTH = 38; | ||
| 80 | private static final int DEFAULT_HEIGHT= 38; | ||
| 81 | |||
| 82 | /** | ||
| 83 | * Default border to use if one is not specified. | ||
| 84 | */ | ||
| 85 | private static final int DEFAULT_BORDER = 2; | ||
| 86 | |||
| 87 | // Bitmask values | ||
| 88 | private static final int LOADING_FLAG = 1; | ||
| 89 | private static final int LINK_FLAG = 2; | ||
| 90 | private static final int WIDTH_FLAG = 4; | ||
| 91 | private static final int HEIGHT_FLAG = 8; | ||
| 92 | private static final int RELOAD_FLAG = 16; | ||
| 93 | private static final int RELOAD_IMAGE_FLAG = 32; | ||
| 94 | private static final int SYNC_LOAD_FLAG = 64; | ||
| 95 | |||
| 96 | private AttributeSet attr; | ||
| 97 | private Image image; | ||
| 98 | private Image disabledImage; | ||
| 99 | private int width; | ||
| 100 | private int height; | ||
| 101 | /** Bitmask containing some of the above bitmask values. Because the | ||
| 102 | * image loading notification can happen on another thread access to | ||
| 103 | * this is synchronized (at least for modifying it). */ | ||
| 104 | private int state; | ||
| 105 | private Container container; | ||
| 106 | private Rectangle fBounds; | ||
| 107 | private Color borderColor; | ||
| 108 | // Size of the border, the insets contains this valid. For example, if | ||
| 109 | // the HSPACE attribute was 4 and BORDER 2, leftInset would be 6. | ||
| 110 | private short borderSize; | ||
| 111 | // Insets, obtained from the painter. | ||
| 112 | private short leftInset; | ||
| 113 | private short rightInset; | ||
| 114 | private short topInset; | ||
| 115 | private short bottomInset; | ||
| 116 | /** | ||
| 117 | * We don't directly implement ImageObserver, instead we use an instance | ||
| 118 | * that calls back to us. | ||
| 119 | */ | ||
| 120 | private ImageObserver imageObserver; | ||
| 121 | /** | ||
| 122 | * Used for alt text. Will be non-null if the image couldn't be found, | ||
| 123 | * and there is valid alt text. | ||
| 124 | */ | ||
| 125 | private View altView; | ||
| 126 | /** Alignment along the vertical (Y) axis. */ | ||
| 127 | private float vAlign; | ||
| 128 | |||
| 129 | |||
| 130 | |||
| 131 | /** | ||
| 132 | * Creates a new view that represents an IMG element. | ||
| 133 | * | ||
| 134 | * @param elem the element to create a view for | ||
| 135 | */ | ||
| 136 | public ImageView(Element elem) { | ||
| 137 | super(elem); | ||
| 138 | fBounds = new Rectangle(); | ||
| 139 | imageObserver = new ImageHandler(); | ||
| 140 | state = RELOAD_FLAG | RELOAD_IMAGE_FLAG; | ||
| 141 | } | ||
| 142 | |||
| 143 | /** | ||
| 144 | * Returns the text to display if the image can't be loaded. This is | ||
| 145 | * obtained from the Elements attribute set with the attribute name | ||
| 146 | * <code>HTML.Attribute.ALT</code>. | ||
| 147 | */ | ||
| 148 | public String getAltText() { | ||
| 149 | return (String)getElement().getAttributes().getAttribute | ||
| 150 | (HTML.Attribute.ALT); | ||
| 151 | } | ||
| 152 | |||
| 153 | /** | ||
| 154 | * Return a URL for the image source, | ||
| 155 | * or null if it could not be determined. | ||
| 156 | */ | ||
| 157 | public URL getImageURL() { | ||
| 158 | String src = (String)getElement().getAttributes(). | ||
| 159 | getAttribute(HTML.Attribute.SRC); | ||
| 160 | if (src == null) { | ||
| 161 | return null; | ||
| 162 | } | ||
| 163 | |||
| 164 | URL reference = ((HTMLDocument)getDocument()).getBase(); | ||
| 165 | try { | ||
| 166 | URL u = new URL(reference,src); | ||
| 167 | return u; | ||
| 168 | } catch (MalformedURLException e) { | ||
| 169 | return null; | ||
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Returns the icon to use if the image couldn't be found. | ||
| 175 | */ | ||
| 176 | public Icon getNoImageIcon() { | ||
| 177 | return (Icon) UIManager.getLookAndFeelDefaults().get(MISSING_IMAGE); | ||
| 178 | } | ||
| 179 | |||
| 180 | /** | ||
| 181 | * Returns the icon to use while in the process of loading the image. | ||
| 182 | */ | ||
| 183 | public Icon getLoadingImageIcon() { | ||
| 184 | return (Icon) UIManager.getLookAndFeelDefaults().get(PENDING_IMAGE); | ||
| 185 | } | ||
| 186 | |||
| 187 | /** | ||
| 188 | * Returns the image to render. | ||
| 189 | */ | ||
| 190 | public Image getImage() { | ||
| 191 | sync(); | ||
| 192 | return image; | ||
| 193 | } | ||
| 194 | |||
| 195 | private Image getImage(boolean enabled) { | ||
| 196 | Image img = getImage(); | ||
| 197 | if (! enabled) { | ||
| 198 | if (disabledImage == null) { | ||
| 199 | disabledImage = GrayFilter.createDisabledImage(img); | ||
| 200 | } | ||
| 201 | img = disabledImage; | ||
| 202 | } | ||
| 203 | return img; | ||
| 204 | } | ||
| 205 | |||
| 206 | /** | ||
| 207 | * Sets how the image is loaded. If <code>newValue</code> is true, | ||
| 208 | * the image we be loaded when first asked for, otherwise it will | ||
| 209 | * be loaded asynchronously. The default is to not load synchronously, | ||
| 210 | * that is to load the image asynchronously. | ||
| 211 | */ | ||
| 212 | public void setLoadsSynchronously(boolean newValue) { | ||
| 213 | synchronized(this) { | ||
| 214 | if (newValue) { | ||
| 215 | state |= SYNC_LOAD_FLAG; | ||
| 216 | } | ||
| 217 | else { | ||
| 218 | state = (state | SYNC_LOAD_FLAG) ^ SYNC_LOAD_FLAG; | ||
| 219 | } | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | /** | ||
| 224 | * Returns true if the image should be loaded when first asked for. | ||
| 225 | */ | ||
| 226 | public boolean getLoadsSynchronously() { | ||
| 227 | return ((state & SYNC_LOAD_FLAG) != 0); | ||
| 228 | } | ||
| 229 | |||
| 230 | /** | ||
| 231 | * Convenience method to get the StyleSheet. | ||
| 232 | */ | ||
| 233 | protected StyleSheet getStyleSheet() { | ||
| 234 | HTMLDocument doc = (HTMLDocument) getDocument(); | ||
| 235 | return doc.getStyleSheet(); | ||
| 236 | } | ||
| 237 | |||
| 238 | /** | ||
| 239 | * Fetches the attributes to use when rendering. This is | ||
| 240 | * implemented to multiplex the attributes specified in the | ||
| 241 | * model with a StyleSheet. | ||
| 242 | */ | ||
| 243 | public AttributeSet getAttributes() { | ||
| 244 | sync(); | ||
| 245 | return attr; | ||
| 246 | } | ||
| 247 | |||
| 248 | /** | ||
| 249 | * For images the tooltip text comes from text specified with the | ||
| 250 | * <code>ALT</code> attribute. This is overriden to return | ||
| 251 | * <code>getAltText</code>. | ||
| 252 | * | ||
| 253 | * @see JTextComponent#getToolTipText | ||
| 254 | */ | ||
| 255 | public String getToolTipText(float x, float y, Shape allocation) { | ||
| 256 | return getAltText(); | ||
| 257 | } | ||
| 258 | |||
| 259 | /** | ||
| 260 | * Update any cached values that come from attributes. | ||
| 261 | */ | ||
| 262 | protected void setPropertiesFromAttributes() { | ||
| 263 | StyleSheet sheet = getStyleSheet(); | ||
| 264 | this.attr = sheet.getViewAttributes(this); | ||
| 265 | |||
| 266 | // Gutters | ||
| 267 | borderSize = (short)getIntAttr(HTML.Attribute.BORDER, isLink() ? | ||
| 268 | DEFAULT_BORDER : 0); | ||
| 269 | |||
| 270 | leftInset = rightInset = (short)(getIntAttr(HTML.Attribute.HSPACE, | ||
| 271 | 0) + borderSize); | ||
| 272 | topInset = bottomInset = (short)(getIntAttr(HTML.Attribute.VSPACE, | ||
| 273 | 0) + borderSize); | ||
| 274 | |||
| 275 | borderColor = ((StyledDocument)getDocument()).getForeground | ||
| 276 | (getAttributes()); | ||
| 277 | |||
| 278 | AttributeSet attr = getElement().getAttributes(); | ||
| 279 | |||
| 280 | // Alignment. | ||
| 281 | // PENDING: This needs to be changed to support the CSS versions | ||
| 282 | // when conversion from ALIGN to VERTICAL_ALIGN is complete. | ||
| 283 | Object alignment = attr.getAttribute(HTML.Attribute.ALIGN); | ||
| 284 | |||
| 285 | vAlign = 1.0f; | ||
| 286 | if (alignment != null) { | ||
| 287 | alignment = alignment.toString(); | ||
| 288 | if ("top".equals(alignment)) { | ||
| 289 | vAlign = 0f; | ||
| 290 | } | ||
| 291 | else if ("middle".equals(alignment)) { | ||
| 292 | vAlign = .5f; | ||
| 293 | } | ||
| 294 | } | ||
| 295 | |||
| 296 | AttributeSet anchorAttr = (AttributeSet)attr.getAttribute(HTML.Tag.A); | ||
| 297 | if (anchorAttr != null && anchorAttr.isDefined | ||
| 298 | (HTML.Attribute.HREF)) { | ||
| 299 | synchronized(this) { | ||
| 300 | state |= LINK_FLAG; | ||
| 301 | } | ||
| 302 | } | ||
| 303 | else { | ||
| 304 | synchronized(this) { | ||
| 305 | state = (state | LINK_FLAG) ^ LINK_FLAG; | ||
| 306 | } | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | /** | ||
| 311 | * Establishes the parent view for this view. | ||
| 312 | * Seize this moment to cache the AWT Container I'm in. | ||
| 313 | */ | ||
| 314 | public void setParent(View parent) { | ||
| 315 | View oldParent = getParent(); | ||
| 316 | super.setParent(parent); | ||
| 317 | container = (parent != null) ? getContainer() : null; | ||
| 318 | if (oldParent != parent) { | ||
| 319 | synchronized(this) { | ||
| 320 | state |= RELOAD_FLAG; | ||
| 321 | } | ||
| 322 | } | ||
| 323 | } | ||
| 324 | |||
| 325 | /** | ||
| 326 | * Invoked when the Elements attributes have changed. Recreates the image. | ||
| 327 | */ | ||
| 328 | public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { | ||
| 329 | super.changedUpdate(e,a,f); | ||
| 330 | |||
| 331 | synchronized(this) { | ||
| 332 | state |= RELOAD_FLAG | RELOAD_IMAGE_FLAG; | ||
| 333 | } | ||
| 334 | |||
| 335 | // Assume the worst. | ||
| 336 | preferenceChanged(null, true, true); | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * Paints the View. | ||
| 341 | * | ||
| 342 | * @param g the rendering surface to use | ||
| 343 | * @param a the allocated region to render into | ||
| 344 | * @see View#paint | ||
| 345 | */ | ||
| 346 | public void paint(Graphics g, Shape a) { | ||
| 347 | sync(); | ||
| 348 | |||
| 349 | Rectangle rect = (a instanceof Rectangle) ? (Rectangle)a : | ||
| 350 | a.getBounds(); | ||
| 351 | Rectangle clip = g.getClipBounds(); | ||
| 352 | |||
| 353 | fBounds.setBounds(rect); | ||
| 354 | paintHighlights(g, a); | ||
| 355 | paintBorder(g, rect); | ||
| 356 | if (clip != null) { | ||
| 357 | g.clipRect(rect.x + leftInset, rect.y + topInset, | ||
| 358 | rect.width - leftInset - rightInset, | ||
| 359 | rect.height - topInset - bottomInset); | ||
| 360 | } | ||
| 361 | |||
| 362 | Container host = getContainer(); | ||
| 363 | Image img = getImage(host == null || host.isEnabled()); | ||
| 364 | if (img != null) { | ||
| 365 | if (! hasPixels(img)) { | ||
| 366 | // No pixels yet, use the default | ||
| 367 | Icon icon = getLoadingImageIcon(); | ||
| 368 | if (icon != null) { | ||
| 369 | icon.paintIcon(host, g, | ||
| 370 | rect.x + leftInset, rect.y + topInset); | ||
| 371 | } | ||
| 372 | } | ||
| 373 | else { | ||
| 374 | // Draw the image | ||
| 375 | g.drawImage(img, rect.x + leftInset, rect.y + topInset, | ||
| 376 | width, height, imageObserver); | ||
| 377 | } | ||
| 378 | } | ||
| 379 | else { | ||
| 380 | Icon icon = getNoImageIcon(); | ||
| 381 | if (icon != null) { | ||
| 382 | icon.paintIcon(host, g, | ||
| 383 | rect.x + leftInset, rect.y + topInset); | ||
| 384 | } | ||
| 385 | View view = getAltView(); | ||
| 386 | // Paint the view representing the alt text, if its non-null | ||
| 387 | if (view != null && ((state & WIDTH_FLAG) == 0 || | ||
| 388 | width > DEFAULT_WIDTH)) { | ||
| 389 | // Assume layout along the y direction | ||
| 390 | Rectangle altRect = new Rectangle | ||
| 391 | (rect.x + leftInset + DEFAULT_WIDTH, rect.y + topInset, | ||
| 392 | rect.width - leftInset - rightInset - DEFAULT_WIDTH, | ||
| 393 | rect.height - topInset - bottomInset); | ||
| 394 | |||
| 395 | view.paint(g, altRect); | ||
| 396 | } | ||
| 397 | } | ||
| 398 | if (clip != null) { | ||
| 399 | // Reset clip. | ||
| 400 | g.setClip(clip.x, clip.y, clip.width, clip.height); | ||
| 401 | } | ||
| 402 | } | ||
| 403 | |||
| 404 | private void paintHighlights(Graphics g, Shape shape) { | ||
| 405 | if (container instanceof JTextComponent) { | ||
| 406 | JTextComponent tc = (JTextComponent)container; | ||
| 407 | Highlighter h = tc.getHighlighter(); | ||
| 408 | if (h instanceof LayeredHighlighter) { | ||
| 409 | ((LayeredHighlighter)h).paintLayeredHighlights | ||
| 410 | (g, getStartOffset(), getEndOffset(), shape, tc, this); | ||
| 411 | } | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 415 | private void paintBorder(Graphics g, Rectangle rect) { | ||
| 416 | Color color = borderColor; | ||
| 417 | |||
| 418 | if ((borderSize > 0 || image == null) && color != null) { | ||
| 419 | int xOffset = leftInset - borderSize; | ||
| 420 | int yOffset = topInset - borderSize; | ||
| 421 | g.setColor(color); | ||
| 422 | int n = (image == null) ? 1 : borderSize; | ||
| 423 | for (int counter = 0; counter < n; counter++) { | ||
| 424 | g.drawRect(rect.x + xOffset + counter, | ||
| 425 | rect.y + yOffset + counter, | ||
| 426 | rect.width - counter - counter - xOffset -xOffset-1, | ||
| 427 | rect.height - counter - counter -yOffset-yOffset-1); | ||
| 428 | } | ||
| 429 | } | ||
| 430 | } | ||
| 431 | |||
| 432 | /** | ||
| 433 | * Determines the preferred span for this view along an | ||
| 434 | * axis. | ||
| 435 | * | ||
| 436 | * @param axis may be either X_AXIS or Y_AXIS | ||
| 437 | * @return the span the view would like to be rendered into; | ||
| 438 | * typically the view is told to render into the span | ||
| 439 | * that is returned, although there is no guarantee; | ||
| 440 | * the parent may choose to resize or break the view | ||
| 441 | */ | ||
| 442 | public float getPreferredSpan(int axis) { | ||
| 443 | sync(); | ||
| 444 | |||
| 445 | // If the attributes specified a width/height, always use it! | ||
| 446 | if (axis == View.X_AXIS && (state & WIDTH_FLAG) == WIDTH_FLAG) { | ||
| 447 | getPreferredSpanFromAltView(axis); | ||
| 448 | return width + leftInset + rightInset; | ||
| 449 | } | ||
| 450 | if (axis == View.Y_AXIS && (state & HEIGHT_FLAG) == HEIGHT_FLAG) { | ||
| 451 | getPreferredSpanFromAltView(axis); | ||
| 452 | return height + topInset + bottomInset; | ||
| 453 | } | ||
| 454 | |||
| 455 | Image image = getImage(); | ||
| 456 | |||
| 457 | if (image != null) { | ||
| 458 | switch (axis) { | ||
| 459 | case View.X_AXIS: | ||
| 460 | return width + leftInset + rightInset; | ||
| 461 | case View.Y_AXIS: | ||
| 462 | return height + topInset + bottomInset; | ||
| 463 | default: | ||
| 464 | throw new IllegalArgumentException("Invalid axis: " + axis); | ||
| 465 | } | ||
| 466 | } | ||
| 467 | else { | ||
| 468 | View view = getAltView(); | ||
| 469 | float retValue = 0f; | ||
| 470 | |||
| 471 | if (view != null) { | ||
| 472 | retValue = view.getPreferredSpan(axis); | ||
| 473 | } | ||
| 474 | switch (axis) { | ||
| 475 | case View.X_AXIS: | ||
| 476 | return retValue + (float)(width + leftInset + rightInset); | ||
| 477 | case View.Y_AXIS: | ||
| 478 | return retValue + (float)(height + topInset + bottomInset); | ||
| 479 | default: | ||
| 480 | throw new IllegalArgumentException("Invalid axis: " + axis); | ||
| 481 | } | ||
| 482 | } | ||
| 483 | } | ||
| 484 | |||
| 485 | /** | ||
| 486 | * Determines the desired alignment for this view along an | ||
| 487 | * axis. This is implemented to give the alignment to the | ||
| 488 | * bottom of the icon along the y axis, and the default | ||
| 489 | * along the x axis. | ||
| 490 | * | ||
| 491 | * @param axis may be either X_AXIS or Y_AXIS | ||
| 492 | * @return the desired alignment; this should be a value | ||
| 493 | * between 0.0 and 1.0 where 0 indicates alignment at the | ||
| 494 | * origin and 1.0 indicates alignment to the full span | ||
| 495 | * away from the origin; an alignment of 0.5 would be the | ||
| 496 | * center of the view | ||
| 497 | */ | ||
| 498 | public float getAlignment(int axis) { | ||
| 499 | switch (axis) { | ||
| 500 | case View.Y_AXIS: | ||
| 501 | return vAlign; | ||
| 502 | default: | ||
| 503 | return super.getAlignment(axis); | ||
| 504 | } | ||
| 505 | } | ||
| 506 | |||
| 507 | /** | ||
| 508 | * Provides a mapping from the document model coordinate space | ||
| 509 | * to the coordinate space of the view mapped to it. | ||
| 510 | * | ||
| 511 | * @param pos the position to convert | ||
| 512 | * @param a the allocated region to render into | ||
| 513 | * @return the bounding box of the given position | ||
| 514 | * @exception BadLocationException if the given position does not represent a | ||
| 515 | * valid location in the associated document | ||
| 516 | * @see View#modelToView | ||
| 517 | */ | ||
| 518 | public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { | ||
| 519 | int p0 = getStartOffset(); | ||
| 520 | int p1 = getEndOffset(); | ||
| 521 | if ((pos >= p0) && (pos <= p1)) { | ||
| 522 | Rectangle r = a.getBounds(); | ||
| 523 | if (pos == p1) { | ||
| 524 | r.x += r.width; | ||
| 525 | } | ||
| 526 | r.width = 0; | ||
| 527 | return r; | ||
| 528 | } | ||
| 529 | return null; | ||
| 530 | } | ||
| 531 | |||
| 532 | /** | ||
| 533 | * Provides a mapping from the view coordinate space to the logical | ||
| 534 | * coordinate space of the model. | ||
| 535 | * | ||
| 536 | * @param x the X coordinate | ||
| 537 | * @param y the Y coordinate | ||
| 538 | * @param a the allocated region to render into | ||
| 539 | * @return the location within the model that best represents the | ||
| 540 | * given point of view | ||
| 541 | * @see View#viewToModel | ||
| 542 | */ | ||
| 543 | public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { | ||
| 544 | Rectangle alloc = (Rectangle) a; | ||
| 545 | if (x < alloc.x + alloc.width) { | ||
| 546 | bias[0] = Position.Bias.Forward; | ||
| 547 | return getStartOffset(); | ||
| 548 | } | ||
| 549 | bias[0] = Position.Bias.Backward; | ||
| 550 | return getEndOffset(); | ||
| 551 | } | ||
| 552 | |||
| 553 | /** | ||
| 554 | * Sets the size of the view. This should cause | ||
| 555 | * layout of the view if it has any layout duties. | ||
| 556 | * | ||
| 557 | * @param width the width >= 0 | ||
| 558 | * @param height the height >= 0 | ||
| 559 | */ | ||
| 560 | public void setSize(float width, float height) { | ||
| 561 | sync(); | ||
| 562 | |||
| 563 | if (getImage() == null) { | ||
| 564 | View view = getAltView(); | ||
| 565 | |||
| 566 | if (view != null) { | ||
| 567 | view.setSize(Math.max(0f, width - (float)(DEFAULT_WIDTH + leftInset + rightInset)), | ||
| 568 | Math.max(0f, height - (float)(topInset + bottomInset))); | ||
| 569 | } | ||
| 570 | } | ||
| 571 | } | ||
| 572 | |||
| 573 | /** | ||
| 574 | * Returns true if this image within a link? | ||
| 575 | */ | ||
| 576 | private boolean isLink() { | ||
| 577 | return ((state & LINK_FLAG) == LINK_FLAG); | ||
| 578 | } | ||
| 579 | |||
| 580 | /** | ||
| 581 | * Returns true if the passed in image has a non-zero width and height. | ||
| 582 | */ | ||
| 583 | private boolean hasPixels(Image image) { | ||
| 584 | return image != null && | ||
| 585 | (image.getHeight(imageObserver) > 0) && | ||
| 586 | (image.getWidth(imageObserver) > 0); | ||
| 587 | } | ||
| 588 | |||
| 589 | /** | ||
| 590 | * Returns the preferred span of the View used to display the alt text, | ||
| 591 | * or 0 if the view does not exist. | ||
| 592 | */ | ||
| 593 | private float getPreferredSpanFromAltView(int axis) { | ||
| 594 | if (getImage() == null) { | ||
| 595 | View view = getAltView(); | ||
| 596 | |||
| 597 | if (view != null) { | ||
| 598 | return view.getPreferredSpan(axis); | ||
| 599 | } | ||
| 600 | } | ||
| 601 | return 0f; | ||
| 602 | } | ||
| 603 | |||
| 604 | /** | ||
| 605 | * Request that this view be repainted. | ||
| 606 | * Assumes the view is still at its last-drawn location. | ||
| 607 | */ | ||
| 608 | private void repaint(long delay) { | ||
| 609 | if (container != null && fBounds != null) { | ||
| 610 | container.repaint(delay, fBounds.x, fBounds.y, fBounds.width, | ||
| 611 | fBounds.height); | ||
| 612 | } | ||
| 613 | } | ||
| 614 | |||
| 615 | /** | ||
| 616 | * Convenience method for getting an integer attribute from the elements | ||
| 617 | * AttributeSet. | ||
| 618 | */ | ||
| 619 | private int getIntAttr(HTML.Attribute name, int deflt) { | ||
| 620 | AttributeSet attr = getElement().getAttributes(); | ||
| 621 | if (attr.isDefined(name)) { // does not check parents! | ||
| 622 | int i; | ||
| 623 | String val = (String)attr.getAttribute(name); | ||
| 624 | if (val == null) { | ||
| 625 | i = deflt; | ||
| 626 | } | ||
| 627 | else { | ||
| 628 | try{ | ||
| 629 | i = Math.max(0, Integer.parseInt(val)); | ||
| 630 | }catch( NumberFormatException x ) { | ||
| 631 | i = deflt; | ||
| 632 | } | ||
| 633 | } | ||
| 634 | return i; | ||
| 635 | } else | ||
| 636 | return deflt; | ||
| 637 | } | ||
| 638 | |||
| 639 | /** | ||
| 640 | * Makes sure the necessary properties and image is loaded. | ||
| 641 | */ | ||
| 642 | private void sync() { | ||
| 643 | int s = state; | ||
| 644 | if ((s & RELOAD_IMAGE_FLAG) != 0) { | ||
| 645 | refreshImage(); | ||
| 646 | } | ||
| 647 | s = state; | ||
| 648 | if ((s & RELOAD_FLAG) != 0) { | ||
| 649 | synchronized(this) { | ||
| 650 | state = (state | RELOAD_FLAG) ^ RELOAD_FLAG; | ||
| 651 | } | ||
| 652 | setPropertiesFromAttributes(); | ||
| 653 | } | ||
| 654 | } | ||
| 655 | |||
| 656 | /** | ||
| 657 | * Loads the image and updates the size accordingly. This should be | ||
| 658 | * invoked instead of invoking <code>loadImage</code> or | ||
| 659 | * <code>updateImageSize</code> directly. | ||
| 660 | */ | ||
| 661 | private void refreshImage() { | ||
| 662 | synchronized(this) { | ||
| 663 | // clear out width/height/realoadimage flag and set loading flag | ||
| 664 | state = (state | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG | | ||
| 665 | HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG | | ||
| 666 | RELOAD_IMAGE_FLAG); | ||
| 667 | image = null; | ||
| 668 | width = height = 0; | ||
| 669 | } | ||
| 670 | |||
| 671 | try { | ||
| 672 | // Load the image | ||
| 673 | loadImage(); | ||
| 674 | |||
| 675 | // And update the size params | ||
| 676 | updateImageSize(); | ||
| 677 | } | ||
| 678 | finally { | ||
| 679 | synchronized(this) { | ||
| 680 | // Clear out state in case someone threw an exception. | ||
| 681 | state = (state | LOADING_FLAG) ^ LOADING_FLAG; | ||
| 682 | } | ||
| 683 | } | ||
| 684 | } | ||
| 685 | |||
| 686 | /** | ||
| 687 | * Loads the image from the URL <code>getImageURL</code>. This should | ||
| 688 | * only be invoked from <code>refreshImage</code>. | ||
| 689 | */ | ||
| 690 | private void loadImage() { | ||
| 691 | URL src = getImageURL(); | ||
| 692 | Image newImage = null; | ||
| 693 | if (src != null) { | ||
| 694 | Dictionary cache = (Dictionary)getDocument(). | ||
| 695 | getProperty(IMAGE_CACHE_PROPERTY); | ||
| 696 | if (cache != null) { | ||
| 697 | newImage = (Image)cache.get(src); | ||
| 698 | } | ||
| 699 | else { | ||
| 700 | newImage = Toolkit.getDefaultToolkit().createImage(src); | ||
| 701 | if (newImage != null && getLoadsSynchronously()) { | ||
| 702 | // Force the image to be loaded by using an ImageIcon. | ||
| 703 | ImageIcon ii = new ImageIcon(); | ||
| 704 | ii.setImage(newImage); | ||
| 705 | } | ||
| 706 | } | ||
| 707 | } | ||
| 708 | image = newImage; | ||
| 709 | } | ||
| 710 | |||
| 711 | /** | ||
| 712 | * Recreates and reloads the image. This should | ||
| 713 | * only be invoked from <code>refreshImage</code>. | ||
| 714 | */ | ||
| 715 | private void updateImageSize() { | ||
| 716 | int newWidth = 0; | ||
| 717 | int newHeight = 0; | ||
| 718 | int newState = 0; | ||
| 719 | Image newImage = getImage(); | ||
| 720 | |||
| 721 | if (newImage != null) { | ||
| 722 | Element elem = getElement(); | ||
| 723 | AttributeSet attr = elem.getAttributes(); | ||
| 724 | |||
| 725 | // Get the width/height and set the state ivar before calling | ||
| 726 | // anything that might cause the image to be loaded, and thus the | ||
| 727 | // ImageHandler to be called. | ||
| 728 | newWidth = getIntAttr(HTML.Attribute.WIDTH, -1); | ||
| 729 | if (newWidth > 0) { | ||
| 730 | newState |= WIDTH_FLAG; | ||
| 731 | } | ||
| 732 | newHeight = getIntAttr(HTML.Attribute.HEIGHT, -1); | ||
| 733 | if (newHeight > 0) { | ||
| 734 | newState |= HEIGHT_FLAG; | ||
| 735 | } | ||
| 736 | |||
| 737 | if (newWidth <= 0) { | ||
| 738 | newWidth = newImage.getWidth(imageObserver); | ||
| 739 | if (newWidth <= 0) { | ||
| 740 | newWidth = DEFAULT_WIDTH; | ||
| 741 | } | ||
| 742 | } | ||
| 743 | |||
| 744 | if (newHeight <= 0) { | ||
| 745 | newHeight = newImage.getHeight(imageObserver); | ||
| 746 | if (newHeight <= 0) { | ||
| 747 | newHeight = DEFAULT_HEIGHT; | ||
| 748 | } | ||
| 749 | } | ||
| 750 | |||
| 751 | // Make sure the image starts loading: | ||
| 752 | if ((newState & (WIDTH_FLAG | HEIGHT_FLAG)) != 0) { | ||
| 753 | Toolkit.getDefaultToolkit().prepareImage(newImage, newWidth, | ||
| 754 | newHeight, | ||
| 755 | imageObserver); | ||
| 756 | } | ||
| 757 | else { | ||
| 758 | Toolkit.getDefaultToolkit().prepareImage(newImage, -1, -1, | ||
| 759 | imageObserver); | ||
| 760 | } | ||
| 761 | |||
| 762 | boolean createText = false; | ||
| 763 | synchronized(this) { | ||
| 764 | // If imageloading failed, other thread may have called | ||
| 765 | // ImageLoader which will null out image, hence we check | ||
| 766 | // for it. | ||
| 767 | if (image != null) { | ||
| 768 | if ((newState & WIDTH_FLAG) == WIDTH_FLAG || width == 0) { | ||
| 769 | width = newWidth; | ||
| 770 | } | ||
| 771 | if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG || | ||
| 772 | height == 0) { | ||
| 773 | height = newHeight; | ||
| 774 | } | ||
| 775 | } | ||
| 776 | else { | ||
| 777 | createText = true; | ||
| 778 | if ((newState & WIDTH_FLAG) == WIDTH_FLAG) { | ||
| 779 | width = newWidth; | ||
| 780 | } | ||
| 781 | if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG) { | ||
| 782 | height = newHeight; | ||
| 783 | } | ||
| 784 | } | ||
| 785 | state = state | newState; | ||
| 786 | state = (state | LOADING_FLAG) ^ LOADING_FLAG; | ||
| 787 | } | ||
| 788 | if (createText) { | ||
| 789 | // Only reset if this thread determined image is null | ||
| 790 | updateAltTextView(); | ||
| 791 | } | ||
| 792 | } | ||
| 793 | else { | ||
| 794 | width = height = DEFAULT_HEIGHT; | ||
| 795 | updateAltTextView(); | ||
| 796 | } | ||
| 797 | } | ||
| 798 | |||
| 799 | /** | ||
| 800 | * Updates the view representing the alt text. | ||
| 801 | */ | ||
| 802 | private void updateAltTextView() { | ||
| 803 | String text = getAltText(); | ||
| 804 | |||
| 805 | if (text != null) { | ||
| 806 | ImageLabelView newView; | ||
| 807 | |||
| 808 | newView = new ImageLabelView(getElement(), text); | ||
| 809 | synchronized(this) { | ||
| 810 | altView = newView; | ||
| 811 | } | ||
| 812 | } | ||
| 813 | } | ||
| 814 | |||
| 815 | /** | ||
| 816 | * Returns the view to use for alternate text. This may be null. | ||
| 817 | */ | ||
| 818 | private View getAltView() { | ||
| 819 | View view; | ||
| 820 | |||
| 821 | synchronized(this) { | ||
| 822 | view = altView; | ||
| 823 | } | ||
| 824 | if (view != null && view.getParent() == null) { | ||
| 825 | view.setParent(getParent()); | ||
| 826 | } | ||
| 827 | return view; | ||
| 828 | } | ||
| 829 | |||
| 830 | /** | ||
| 831 | * Invokes <code>preferenceChanged</code> on the event displatching | ||
| 832 | * thread. | ||
| 833 | */ | ||
| 834 | private void safePreferenceChanged() { | ||
| 835 | if (SwingUtilities.isEventDispatchThread()) { | ||
| 836 | Document doc = getDocument(); | ||
| 837 | if (doc instanceof AbstractDocument) { | ||
| 838 | ((AbstractDocument)doc).readLock(); | ||
| 839 | } | ||
| 840 | preferenceChanged(null, true, true); | ||
| 841 | if (doc instanceof AbstractDocument) { | ||
| 842 | ((AbstractDocument)doc).readUnlock(); | ||
| 843 | } | ||
| 844 | } | ||
| 845 | else { | ||
| 846 | SwingUtilities.invokeLater(new Runnable() { | ||
| 847 | public void run() { | ||
| 848 | safePreferenceChanged(); | ||
| 849 | } | ||
| 850 | }); | ||
| 851 | } | ||
| 852 | } | ||
| 853 | |||
| 854 | /** | ||
| 855 | * ImageHandler implements the ImageObserver to correctly update the | ||
| 856 | * display as new parts of the image become available. | ||
| 857 | */ | ||
| 858 | private class ImageHandler implements ImageObserver { | ||
| 859 | // This can come on any thread. If we are in the process of reloading | ||
| 860 | // the image and determining our state (loading == true) we don't fire | ||
| 861 | // preference changed, or repaint, we just reset the fWidth/fHeight as | ||
| 862 | // necessary and return. This is ok as we know when loading finishes | ||
| 863 | // it will pick up the new height/width, if necessary. | ||
| 864 | public boolean imageUpdate(Image img, int flags, int x, int y, | ||
| 865 | int newWidth, int newHeight ) { | ||
| 866 | if (img != image && img != disabledImage || | ||
| 867 | image == null || getParent() == null) { | ||
| 868 | |||
| 869 | return false; | ||
| 870 | } | ||
| 871 | |||
| 872 | // Bail out if there was an error: | ||
| 873 | if ((flags & (ABORT|ERROR)) != 0) { | ||
| 874 | repaint(0); | ||
| 875 | synchronized(ImageView.this) { | ||
| 876 | if (image == img) { | ||
| 877 | // Be sure image hasn't changed since we don't | ||
| 878 | // initialy synchronize | ||
| 879 | image = null; | ||
| 880 | if ((state & WIDTH_FLAG) != WIDTH_FLAG) { | ||
| 881 | width = DEFAULT_WIDTH; | ||
| 882 | } | ||
| 883 | if ((state & HEIGHT_FLAG) != HEIGHT_FLAG) { | ||
| 884 | height = DEFAULT_HEIGHT; | ||
| 885 | } | ||
| 886 | } else { | ||
| 887 | disabledImage = null; | ||
| 888 | } | ||
| 889 | if ((state & LOADING_FLAG) == LOADING_FLAG) { | ||
| 890 | // No need to resize or repaint, still in the process | ||
| 891 | // of loading. | ||
| 892 | return false; | ||
| 893 | } | ||
| 894 | } | ||
| 895 | updateAltTextView(); | ||
| 896 | safePreferenceChanged(); | ||
| 897 | return false; | ||
| 898 | } | ||
| 899 | |||
| 900 | if (image == img) { | ||
| 901 | // Resize image if necessary: | ||
| 902 | short changed = 0; | ||
| 903 | if ((flags & ImageObserver.HEIGHT) != 0 && !getElement(). | ||
| 904 | getAttributes().isDefined(HTML.Attribute.HEIGHT)) { | ||
| 905 | changed |= 1; | ||
| 906 | } | ||
| 907 | if ((flags & ImageObserver.WIDTH) != 0 && !getElement(). | ||
| 908 | getAttributes().isDefined(HTML.Attribute.WIDTH)) { | ||
| 909 | changed |= 2; | ||
| 910 | } | ||
| 911 | |||
| 912 | synchronized(ImageView.this) { | ||
| 913 | if ((changed & 1) == 1 && (state & WIDTH_FLAG) == 0) { | ||
| 914 | width = newWidth; | ||
| 915 | } | ||
| 916 | if ((changed & 2) == 2 && (state & HEIGHT_FLAG) == 0) { | ||
| 917 | height = newHeight; | ||
| 918 | } | ||
| 919 | if ((state & LOADING_FLAG) == LOADING_FLAG) { | ||
| 920 | // No need to resize or repaint, still in the process of | ||
| 921 | // loading. | ||
| 922 | return true; | ||
| 923 | } | ||
| 924 | } | ||
| 925 | if (changed != 0) { | ||
| 926 | // May need to resize myself, asynchronously: | ||
| 927 | safePreferenceChanged(); | ||
| 928 | return true; | ||
| 929 | } | ||
| 930 | } | ||
| 931 | |||
| 932 | // Repaint when done or when new pixels arrive: | ||
| 933 | if ((flags & (FRAMEBITS|ALLBITS)) != 0) { | ||
| 934 | repaint(0); | ||
| 935 | } | ||
| 936 | else if ((flags & SOMEBITS) != 0 && sIsInc) { | ||
| 937 | repaint(sIncRate); | ||
| 938 | } | ||
| 939 | return ((flags & ALLBITS) == 0); | ||
| 940 | } | ||
| 941 | } | ||
| 942 | |||
| 943 | |||
| 944 | /** | ||
| 945 | * ImageLabelView is used if the image can't be loaded, and | ||
| 946 | * the attribute specified an alt attribute. It overriden a handle of | ||
| 947 | * methods as the text is hardcoded and does not come from the document. | ||
| 948 | */ | ||
| 949 | private class ImageLabelView extends InlineView { | ||
| 950 | private Segment segment; | ||
| 951 | private Color fg; | ||
| 952 | |||
| 953 | ImageLabelView(Element e, String text) { | ||
| 954 | super(e); | ||
| 955 | reset(text); | ||
| 956 | } | ||
| 957 | |||
| 958 | public void reset(String text) { | ||
| 959 | segment = new Segment(text.toCharArray(), 0, text.length()); | ||
| 960 | } | ||
| 961 | |||
| 962 | public void paint(Graphics g, Shape a) { | ||
| 963 | // Don't use supers paint, otherwise selection will be wrong | ||
| 964 | // as our start/end offsets are fake. | ||
| 965 | GlyphPainter painter = getGlyphPainter(); | ||
| 966 | |||
| 967 | if (painter != null) { | ||
| 968 | g.setColor(getForeground()); | ||
| 969 | painter.paint(this, g, a, getStartOffset(), getEndOffset()); | ||
| 970 | } | ||
| 971 | } | ||
| 972 | |||
| 973 | public Segment getText(int p0, int p1) { | ||
| 974 | if (p0 < 0 || p1 > segment.array.length) { | ||
| 975 | throw new RuntimeException("ImageLabelView: Stale view"); | ||
| 976 | } | ||
| 977 | segment.offset = p0; | ||
| 978 | segment.count = p1 - p0; | ||
| 979 | return segment; | ||
| 980 | } | ||
| 981 | |||
| 982 | public int getStartOffset() { | ||
| 983 | return 0; | ||
| 984 | } | ||
| 985 | |||
| 986 | public int getEndOffset() { | ||
| 987 | return segment.array.length; | ||
| 988 | } | ||
| 989 | |||
| 990 | public View breakView(int axis, int p0, float pos, float len) { | ||
| 991 | // Don't allow a break | ||
| 992 | return this; | ||
| 993 | } | ||
| 994 | |||
| 995 | public Color getForeground() { | ||
| 996 | View parent; | ||
| 997 | if (fg == null && (parent = getParent()) != null) { | ||
| 998 | Document doc = getDocument(); | ||
| 999 | AttributeSet attr = parent.getAttributes(); | ||
| 1000 | |||
| 1001 | if (attr != null && (doc instanceof StyledDocument)) { | ||
| 1002 | fg = ((StyledDocument)doc).getForeground(attr); | ||
| 1003 | } | ||
| 1004 | } | ||
| 1005 | return fg; | ||
| 1006 | } | ||
| 1007 | } | ||
| 1008 | } | ||