tentative merge of EKIT_0_9H and custom patches
[old-projects.git] / ekit / com / hexidec / ekit / component / RelativeImageView.java
1 /*
2 GNU Lesser General Public License
3
4 RelativeImageView
5 Copyright (C) 2001 Frits Jalvingh & Howard Kistler
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 */
21
22 package com.hexidec.ekit.component;
23
24 import java.awt.Color;
25 import java.awt.Component;
26 import java.awt.Container;
27 import java.awt.Dimension;
28 import java.awt.Graphics;
29 import java.awt.Image;
30 import java.awt.Point;
31 import java.awt.Rectangle;
32 import java.awt.Shape;
33 import java.awt.Toolkit;
34 import java.awt.event.MouseEvent;
35 import java.awt.event.MouseListener;
36 import java.awt.event.MouseMotionListener;
37 import java.awt.image.ImageObserver;
38 import java.io.BufferedInputStream;
39 import java.io.ByteArrayOutputStream;
40 import java.io.File;
41 import java.io.InputStream;
42 import java.io.IOException;
43 import java.net.MalformedURLException;
44 import java.net.URL;
45 import java.util.Dictionary;
46 import javax.swing.Icon;
47 import javax.swing.ImageIcon;
48 import javax.swing.JEditorPane;
49 import javax.swing.text.AbstractDocument;
50 import javax.swing.text.AttributeSet;
51 import javax.swing.text.BadLocationException;
52 import javax.swing.text.Document;
53 import javax.swing.text.Element;
54 import javax.swing.text.JTextComponent;
55 import javax.swing.text.MutableAttributeSet;
56 import javax.swing.text.Position;
57 import javax.swing.text.SimpleAttributeSet;
58 import javax.swing.text.StyledDocument;
59 import javax.swing.text.View;
60 import javax.swing.text.ViewFactory;
61 import javax.swing.text.html.HTML;
62 import javax.swing.text.html.HTMLDocument;
63 import javax.swing.text.html.StyleSheet;
64 import javax.swing.event.DocumentEvent;
65
66 /**
67 * @author <a href="mailto:jal@grimor.com">Frits Jalvingh</a>
68 * @version 1.0
69 *
70 * This code was modeled after an artice on
71 * <a href="http://www.javaworld.com/javaworld/javatips/jw-javatip109.html">
72 * JavaWorld</a> by Bob Kenworthy.
73 */
74
75 public class RelativeImageView extends View implements ImageObserver, MouseListener, MouseMotionListener
76 {
77 public static final String TOP = "top";
78 public static final String TEXTTOP = "texttop";
79 public static final String MIDDLE = "middle";
80 public static final String ABSMIDDLE = "absmiddle";
81 public static final String CENTER = "center";
82 public static final String BOTTOM = "bottom";
83 public static final String IMAGE_CACHE_PROPERTY = "imageCache";
84
85 private static Icon sPendingImageIcon;
86 private static Icon sMissingImageIcon;
87 private static final String PENDING_IMAGE_SRC = "icons/ImagePendingHK.gif";
88 private static final String MISSING_IMAGE_SRC = "icons/ImageMissingHK.gif";
89 private static final int DEFAULT_WIDTH = 32;
90 private static final int DEFAULT_HEIGHT = 32;
91 private static final int DEFAULT_BORDER = 1;
92
93 private AttributeSet attr;
94 private Element fElement;
95 private Image fImage;
96 private int fHeight;
97 private int fWidth;
98 private Container fContainer;
99 private Rectangle fBounds;
100 private Component fComponent;
101 private Point fGrowBase; // base of drag while growing image
102 private boolean fGrowProportionally; // should grow be proportional?
103 private String BaseUrl;
104 private boolean bLoading; // set to true while the receiver is locked, to indicate the reciever is loading the image. This is used in imageUpdate.
105
106 /** Constructor
107 * Creates a new view that represents an IMG element.
108 * @param elem the element to create a view for
109 */
110 public RelativeImageView(Element elem,String baseurl)
111 {
112 super(elem);
113 BaseUrl = baseurl;
114 initialize(elem);
115 StyleSheet sheet = getStyleSheet();
116 attr = sheet.getViewAttributes(this);
117 }
118
119 private void initialize(Element elem)
120 {
121 synchronized(this)
122 {
123 bLoading = true;
124 fWidth = 0;
125 fHeight = 0;
126 }
127 int width = 0;
128 int height = 0;
129 boolean customWidth = false;
130 boolean customHeight = false;
131 try
132 {
133 fElement = elem;
134 // request image from document's cache
135 AttributeSet attr = elem.getAttributes();
136 if(true || isURL())
137 {
138 URL src = getSourceURL();
139 if(src != null)
140 {
141 Dictionary cache = (Dictionary)getDocument().getProperty(IMAGE_CACHE_PROPERTY);
142 if(cache != null)
143 {
144 fImage = (Image)cache.get(src);
145 }
146 else
147 {
148 fImage = Toolkit.getDefaultToolkit().getImage(src);
149 }
150 }
151 }
152 else
153 {
154 // load image from relative path
155 String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);
156 src = processSrcPath(src);
157 fImage = Toolkit.getDefaultToolkit().createImage(src);
158 try
159 {
160 waitForImage();
161 }
162 catch(InterruptedException ie)
163 {
164 fImage = null;
165 // possibly replace with the ImageBroken icon, if that's what is happening
166 }
167 }
168
169 // get height & width from params or image or defaults
170 height = getIntAttr(HTML.Attribute.HEIGHT, -1);
171 customHeight = (height > 0);
172 if(!customHeight && fImage != null)
173 {
174 height = fImage.getHeight(this);
175 }
176 if(height <= 0)
177 {
178 height = DEFAULT_HEIGHT;
179 }
180
181 width = getIntAttr(HTML.Attribute.WIDTH, -1);
182 customWidth = (width > 0);
183 if(!customWidth && fImage != null)
184 {
185 width = fImage.getWidth(this);
186 }
187 if(width <= 0)
188 {
189 width = DEFAULT_WIDTH;
190 }
191
192 if(fImage != null)
193 {
194 if(customHeight && customWidth)
195 {
196 Toolkit.getDefaultToolkit().prepareImage(fImage, height, width, this);
197 }
198 else
199 {
200 Toolkit.getDefaultToolkit().prepareImage(fImage, -1, -1, this);
201 }
202 }
203 }
204 finally
205 {
206 synchronized(this)
207 {
208 bLoading = false;
209 if(customHeight || fHeight == 0)
210 {
211 fHeight = height;
212 }
213 if(customWidth || fWidth == 0)
214 {
215 fWidth = width;
216 }
217 }
218 }
219 }
220
221 /** Determines if path is in the form of a URL
222 */
223 private boolean isURL()
224 {
225 String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);
226 return src.toLowerCase().startsWith("file") || src.toLowerCase().startsWith("http");
227 }
228
229 /** Checks to see if the absolute path is availabe thru an application
230 * global static variable or thru a system variable. If so, appends
231 * the relative path to the absolute path and returns the String.
232 */
233 private String processSrcPath(String src)
234 {
235 String val = src;
236 File imageFile = new File(src);
237 if(imageFile.isAbsolute())
238 {
239 return src;
240 }
241 boolean found = false;
242 Document doc = getDocument();
243 if(doc != null)
244 {
245 String pv = (String)doc.getProperty("com.hexidec.ekit.docsource");
246 if(pv != null)
247 {
248 File f = new File(pv);
249 val = (new File(f.getParent(), imageFile.getPath().toString())).toString();
250 found = true;
251 }
252 }
253 if(!found)
254 {
255 String imagePath = System.getProperty("system.image.path.key");
256 if(imagePath != null)
257 {
258 val = (new File(imagePath, imageFile.getPath())).toString();
259 }
260 }
261 return val;
262 }
263
264 /** Method insures that the image is loaded and not a broken reference
265 */
266 private void waitForImage()
267 throws InterruptedException
268 {
269 int w = fImage.getWidth(this);
270 int h = fImage.getHeight(this);
271 while (true)
272 {
273 int flags = Toolkit.getDefaultToolkit().checkImage(fImage, w, h, this);
274 if(((flags & ERROR) != 0) || ((flags & ABORT) != 0 ))
275 {
276 throw new InterruptedException();
277 }
278 else if((flags & (ALLBITS | FRAMEBITS)) != 0)
279 {
280 return;
281 }
282 Thread.sleep(10);
283 }
284 }
285
286 /** Fetches the attributes to use when rendering. This is
287 * implemented to multiplex the attributes specified in the
288 * model with a StyleSheet.
289 */
290 public AttributeSet getAttributes()
291 {
292 return attr;
293 }
294
295 /** Method tests whether the image within a link
296 */
297 boolean isLink()
298 {
299 AttributeSet anchorAttr = (AttributeSet)fElement.getAttributes().getAttribute(HTML.Tag.A);
300 if(anchorAttr != null)
301 {
302 return anchorAttr.isDefined(HTML.Attribute.HREF);
303 }
304 return false;
305 }
306
307 /** Method returns the size of the border to use
308 */
309 int getBorder()
310 {
311 return getIntAttr(HTML.Attribute.BORDER, isLink() ? DEFAULT_BORDER : 0);
312 }
313
314 /** Method returns the amount of extra space to add along an axis
315 */
316 int getSpace(int axis)
317 {
318 return getIntAttr((axis == X_AXIS) ? HTML.Attribute.HSPACE : HTML.Attribute.VSPACE, 0);
319 }
320
321 /** Method returns the border's color, or null if this is not a link
322 */
323 Color getBorderColor()
324 {
325 StyledDocument doc = (StyledDocument)getDocument();
326 return doc.getForeground(getAttributes());
327 }
328
329 /** Method returns the image's vertical alignment
330 */
331 float getVerticalAlignment()
332 {
333 String align = (String)fElement.getAttributes().getAttribute(HTML.Attribute.ALIGN);
334 if(align != null)
335 {
336 align = align.toLowerCase();
337 if(align.equals(TOP) || align.equals(TEXTTOP))
338 {
339 return 0.0f;
340 }
341 else if(align.equals(this.CENTER) || align.equals(MIDDLE) || align.equals(ABSMIDDLE))
342 {
343 return 0.5f;
344 }
345 }
346 return 1.0f; // default alignment is bottom
347 }
348
349 boolean hasPixels(ImageObserver obs)
350 {
351 return ((fImage != null) && (fImage.getHeight(obs) > 0) && (fImage.getWidth(obs) > 0));
352 }
353
354 /** Method returns a URL for the image source, or null if it could not be determined
355 */
356 private URL getSourceURL()
357 {
358 String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);
359 if(src == null)
360 {
361 return null;
362 }
363 try
364 {
365 URL reference = new URL(BaseUrl);
366 URL u = new URL(reference,src);
367 return u;
368 }
369 catch(MalformedURLException mue)
370 {
371 return null;
372 }
373 }
374
375 /** Method looks up an integer-valued attribute (not recursive!)
376 */
377 private int getIntAttr(HTML.Attribute name, int iDefault)
378 {
379 AttributeSet attr = fElement.getAttributes();
380 if(attr.isDefined(name))
381 {
382 int i;
383 String val = (String)attr.getAttribute(name);
384 if(val == null)
385 {
386 i = iDefault;
387 }
388 else
389 {
390 try
391 {
392 i = Math.max(0, Integer.parseInt(val));
393 }
394 catch(NumberFormatException nfe)
395 {
396 i = iDefault;
397 }
398 }
399 return i;
400 }
401 else
402 {
403 return iDefault;
404 }
405 }
406
407 /**
408 * Establishes the parent view for this view.
409 * Seize this moment to cache the AWT Container I'm in.
410 */
411 public void setParent(View parent)
412 {
413 super.setParent(parent);
414 fContainer = ((parent != null) ? getContainer() : null);
415 if((parent == null) && (fComponent != null))
416 {
417 fComponent.getParent().remove(fComponent);
418 fComponent = null;
419 }
420 }
421
422 /** My attributes may have changed. */
423 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f)
424 {
425 super.changedUpdate(e, a, f);
426 float align = getVerticalAlignment();
427
428 int height = fHeight;
429 int width = fWidth;
430
431 initialize(getElement());
432
433 boolean hChanged = fHeight != height;
434 boolean wChanged = fWidth != width;
435 if(hChanged || wChanged || getVerticalAlignment() != align)
436 {
437 getParent().preferenceChanged(this, hChanged, wChanged);
438 }
439 }
440
441
442 /**
443 * Paints the image.
444 *
445 * @param g the rendering surface to use
446 * @param a the allocated region to render into
447 * @see View#paint
448 */
449 public void paint(Graphics g, Shape a)
450 {
451 Color oldColor = g.getColor();
452 fBounds = a.getBounds();
453 int border = getBorder();
454 int x = fBounds.x + border + getSpace(X_AXIS);
455 int y = fBounds.y + border + getSpace(Y_AXIS);
456 int width = fWidth;
457 int height = fHeight;
458 int sel = getSelectionState();
459
460 // If no pixels yet, draw gray outline and icon
461 if(!hasPixels(this))
462 {
463 g.setColor(Color.lightGray);
464 g.drawRect(x, y, width - 1, height - 1);
465 g.setColor(oldColor);
466 loadImageStatusIcons();
467 Icon icon = ((fImage == null) ? sMissingImageIcon : sPendingImageIcon);
468 if(icon != null)
469 {
470 icon.paintIcon(getContainer(), g, x, y);
471 }
472 }
473
474 // Draw image
475 if(fImage != null)
476 {
477 g.drawImage(fImage, x, y, width, height, this);
478 }
479
480 // If selected exactly, we need a black border & grow-box
481 Color bc = getBorderColor();
482 if(sel == 2)
483 {
484 // Make sure there's room for a border
485 int delta = 2 - border;
486 if(delta > 0)
487 {
488 x += delta;
489 y += delta;
490 width -= delta << 1;
491 height -= delta << 1;
492 border = 2;
493 }
494 bc = null;
495 g.setColor(Color.black);
496 // Draw grow box
497 g.fillRect(x + width - 5, y + height - 5, 5, 5);
498 }
499
500 // Draw border
501 if(border > 0)
502 {
503 if(bc != null)
504 {
505 g.setColor(bc);
506 }
507 // Draw a thick rectangle:
508 for(int i = 1; i <= border; i++)
509 {
510 g.drawRect(x - i, y - i, width - 1 + i + i, height - 1 + i + i);
511 }
512 g.setColor(oldColor);
513 }
514 }
515
516 /** Request that this view be repainted. Assumes the view is still at its last-drawn location.
517 */
518 protected void repaint(long delay)
519 {
520 if((fContainer != null) && (fBounds != null))
521 {
522 fContainer.repaint(delay, fBounds.x, fBounds.y, fBounds.width, fBounds.height);
523 }
524 }
525
526 /**
527 * Determines whether the image is selected, and if it's the only thing selected.
528 * @return 0 if not selected, 1 if selected, 2 if exclusively selected.
529 * "Exclusive" selection is only returned when editable.
530 */
531 protected int getSelectionState()
532 {
533 int p0 = fElement.getStartOffset();
534 int p1 = fElement.getEndOffset();
535 if(fContainer instanceof JTextComponent)
536 {
537 JTextComponent textComp = (JTextComponent)fContainer;
538 int start = textComp.getSelectionStart();
539 int end = textComp.getSelectionEnd();
540 if((start <= p0) && (end >= p1))
541 {
542 if((start == p0) && (end == p1) && isEditable())
543 {
544 return 2;
545 }
546 else
547 {
548 return 1;
549 }
550 }
551 }
552 return 0;
553 }
554
555 protected boolean isEditable()
556 {
557 return ((fContainer instanceof JEditorPane) && ((JEditorPane)fContainer).isEditable());
558 }
559
560 /** Returns the text editor's highlight color.
561 */
562 protected Color getHighlightColor()
563 {
564 JTextComponent textComp = (JTextComponent)fContainer;
565 return textComp.getSelectionColor();
566 }
567
568 // Progressive display -------------------------------------------------
569
570 // This can come on any thread. If we are in the process of reloading
571 // the image and determining our state (loading == true) we don't fire
572 // preference changed, or repaint, we just reset the fWidth/fHeight as
573 // necessary and return. This is ok as we know when loading finishes
574 // it will pick up the new height/width, if necessary.
575
576 private static boolean sIsInc = true;
577 private static int sIncRate = 100;
578
579 public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height)
580 {
581 if((fImage == null) || (fImage != img))
582 {
583 return false;
584 }
585
586 // Bail out if there was an error
587 if((flags & (ABORT|ERROR)) != 0)
588 {
589 fImage = null;
590 repaint(0);
591 return false;
592 }
593
594 // Resize image if necessary
595 short changed = 0;
596 if((flags & ImageObserver.HEIGHT) != 0)
597 {
598 if(!getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT))
599 {
600 changed |= 1;
601 }
602 }
603 if((flags & ImageObserver.WIDTH) != 0)
604 {
605 if(!getElement().getAttributes().isDefined(HTML.Attribute.WIDTH))
606 {
607 changed |= 2;
608 }
609 }
610
611 synchronized(this)
612 {
613 if((changed & 1) == 1)
614 {
615 fWidth = width;
616 }
617 if((changed & 2) == 2)
618 {
619 fHeight = height;
620 }
621 if(bLoading)
622 {
623 // No need to resize or repaint, still in the process of loading
624 return true;
625 }
626 }
627
628 if(changed != 0)
629 {
630 // May need to resize myself, asynchronously
631 Document doc = getDocument();
632 try
633 {
634 if(doc instanceof AbstractDocument)
635 {
636 ((AbstractDocument)doc).readLock();
637 }
638 preferenceChanged(this, true, true);
639 }
640 finally
641 {
642 if(doc instanceof AbstractDocument)
643 {
644 ((AbstractDocument)doc).readUnlock();
645 }
646 }
647 return true;
648 }
649
650 // Repaint when done or when new pixels arrive
651 if((flags & (FRAMEBITS|ALLBITS)) != 0)
652 {
653 repaint(0);
654 }
655 else if((flags & SOMEBITS) != 0)
656 {
657 if(sIsInc)
658 {
659 repaint(sIncRate);
660 }
661 }
662 return ((flags & ALLBITS) == 0);
663 }
664
665 // Layout --------------------------------------------------------------
666
667 /** Determines the preferred span for this view along an axis.
668 *
669 * @param axis may be either X_AXIS or Y_AXIS
670 * @returns the span the view would like to be rendered into.
671 * Typically the view is told to render into the span
672 * that is returned, although there is no guarantee.
673 * The parent may choose to resize or break the view.
674 */
675 public float getPreferredSpan(int axis)
676 {
677 int extra = 2 * (getBorder() + getSpace(axis));
678 switch(axis)
679 {
680 case View.X_AXIS:
681 return fWidth+extra;
682 case View.Y_AXIS:
683 return fHeight+extra;
684 default:
685 throw new IllegalArgumentException("Invalid axis in getPreferredSpan() : " + axis);
686 }
687 }
688
689 /** Determines the desired alignment for this view along an
690 * axis. This is implemented to give the alignment to the
691 * bottom of the icon along the y axis, and the default
692 * along the x axis.
693 *
694 * @param axis may be either X_AXIS or Y_AXIS
695 * @returns the desired alignment. This should be a value
696 * between 0.0 and 1.0 where 0 indicates alignment at the
697 * origin and 1.0 indicates alignment to the full span
698 * away from the origin. An alignment of 0.5 would be the
699 * center of the view.
700 */
701 public float getAlignment(int axis)
702 {
703 switch(axis)
704 {
705 case View.Y_AXIS:
706 return getVerticalAlignment();
707 default:
708 return super.getAlignment(axis);
709 }
710 }
711
712 /** Provides a mapping from the document model coordinate space
713 * to the coordinate space of the view mapped to it.
714 *
715 * @param pos the position to convert
716 * @param a the allocated region to render into
717 * @return the bounding box of the given position
718 * @exception BadLocationException if the given position does not represent a
719 * valid location in the associated document
720 * @see View#modelToView
721 */
722 public Shape modelToView(int pos, Shape a, Position.Bias b)
723 throws BadLocationException
724 {
725 int p0 = getStartOffset();
726 int p1 = getEndOffset();
727 if((pos >= p0) && (pos <= p1))
728 {
729 Rectangle r = a.getBounds();
730 if(pos == p1)
731 {
732 r.x += r.width;
733 }
734 r.width = 0;
735 return r;
736 }
737 return null;
738 }
739
740 /** Provides a mapping from the view coordinate space to the logical
741 * coordinate space of the model.
742 *
743 * @param x the X coordinate
744 * @param y the Y coordinate
745 * @param a the allocated region to render into
746 * @return the location within the model that best represents the
747 * given point of view
748 * @see View#viewToModel
749 */
750 public int viewToModel(float x, float y, Shape a, Position.Bias[] bias)
751 {
752 Rectangle alloc = (Rectangle) a;
753 if(x < (alloc.x + alloc.width))
754 {
755 bias[0] = Position.Bias.Forward;
756 return getStartOffset();
757 }
758 bias[0] = Position.Bias.Backward;
759 return getEndOffset();
760 }
761
762 /** Change the size of this image. This alters the HEIGHT and WIDTH
763 * attributes of the Element and causes a re-layout.
764 */
765 protected void resize(int width, int height)
766 {
767 if((width == fWidth) && (height == fHeight))
768 {
769 return;
770 }
771 fWidth = width;
772 fHeight= height;
773 // Replace attributes in document
774 MutableAttributeSet attr = new SimpleAttributeSet();
775 attr.addAttribute(HTML.Attribute.WIDTH ,Integer.toString(width));
776 attr.addAttribute(HTML.Attribute.HEIGHT,Integer.toString(height));
777 ((StyledDocument)getDocument()).setCharacterAttributes(fElement.getStartOffset(), fElement.getEndOffset(), attr, false);
778 }
779
780 // Mouse event handling ------------------------------------------------
781
782 /** Select or grow image when clicked.
783 */
784 public void mousePressed(MouseEvent e)
785 {
786 Dimension size = fComponent.getSize();
787 if((e.getX() >= (size.width - 7)) && (e.getY() >= (size.height - 7)) && (getSelectionState() == 2))
788 {
789 // Click in selected grow-box:
790 Point loc = fComponent.getLocationOnScreen();
791 fGrowBase = new Point(loc.x + e.getX() - fWidth, loc.y + e.getY() - fHeight);
792 fGrowProportionally = e.isShiftDown();
793 }
794 else
795 {
796 // Else select image:
797 fGrowBase = null;
798 JTextComponent comp = (JTextComponent)fContainer;
799 int start = fElement.getStartOffset();
800 int end = fElement.getEndOffset();
801 int mark = comp.getCaret().getMark();
802 int dot = comp.getCaret().getDot();
803 if(e.isShiftDown())
804 {
805 // extend selection if shift key down:
806 if(mark <= start)
807 {
808 comp.moveCaretPosition(end);
809 }
810 else
811 {
812 comp.moveCaretPosition(start);
813 }
814 }
815 else
816 {
817 // just select image, without shift:
818 if(mark != start)
819 {
820 comp.setCaretPosition(start);
821 }
822 if(dot != end)
823 {
824 comp.moveCaretPosition(end);
825 }
826 }
827 }
828 }
829
830 /** Resize image if initial click was in grow-box: */
831 public void mouseDragged(MouseEvent e)
832 {
833 if(fGrowBase != null)
834 {
835 Point loc = fComponent.getLocationOnScreen();
836 int width = Math.max(2, loc.x + e.getX() - fGrowBase.x);
837 int height= Math.max(2, loc.y + e.getY() - fGrowBase.y);
838 if(e.isShiftDown() && fImage != null)
839 {
840 // Make sure size is proportional to actual image size
841 float imgWidth = fImage.getWidth(this);
842 float imgHeight = fImage.getHeight(this);
843 if((imgWidth > 0) && (imgHeight > 0))
844 {
845 float prop = imgHeight / imgWidth;
846 float pwidth = height / prop;
847 float pheight = width * prop;
848 if(pwidth > width)
849 {
850 width = (int)pwidth;
851 }
852 else
853 {
854 height = (int)pheight;
855 }
856 }
857 }
858 resize(width,height);
859 }
860 }
861
862 public void mouseReleased(MouseEvent me)
863 {
864 fGrowBase = null;
865 //! Should post some command to make the action undo-able
866 }
867
868 /** On double-click, open image properties dialog.
869 */
870 public void mouseClicked(MouseEvent me)
871 {
872 if(me.getClickCount() == 2)
873 {
874 //$ IMPLEMENT
875 }
876 }
877
878 public void mouseEntered(MouseEvent me) { ; }
879 public void mouseMoved(MouseEvent me) { ; }
880 public void mouseExited(MouseEvent me) { ; }
881
882 // Static icon accessors -----------------------------------------------
883
884 private Icon makeIcon(final String gifFile)
885 throws IOException
886 {
887 /* Copy resource into a byte array. This is
888 * necessary because several browsers consider
889 * Class.getResource a security risk because it
890 * can be used to load additional classes.
891 * Class.getResourceAsStream just returns raw
892 * bytes, which we can convert to an image.
893 */
894 InputStream resource = RelativeImageView.class.getResourceAsStream(gifFile);
895
896 if(resource == null)
897 {
898 return null;
899 }
900 BufferedInputStream in = new BufferedInputStream(resource);
901 ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
902 byte[] buffer = new byte[1024];
903 int n;
904 while((n = in.read(buffer)) > 0)
905 {
906 out.write(buffer, 0, n);
907 }
908 in.close();
909 out.flush();
910
911 buffer = out.toByteArray();
912 if(buffer.length == 0)
913 {
914 System.err.println("WARNING : " + gifFile + " is zero-length");
915 return null;
916 }
917 return new ImageIcon(buffer);
918 }
919
920 private void loadImageStatusIcons()
921 {
922 try
923 {
924 if(sPendingImageIcon == null)
925 {
926 sPendingImageIcon = makeIcon(PENDING_IMAGE_SRC);
927 }
928 if(sMissingImageIcon == null)
929 {
930 sMissingImageIcon = makeIcon(MISSING_IMAGE_SRC);
931 }
932 }
933 catch(Exception e)
934 {
935 System.err.println("ImageView : Couldn't load image icons");
936 }
937 }
938
939 protected StyleSheet getStyleSheet()
940 {
941 HTMLDocument doc = (HTMLDocument)getDocument();
942 return doc.getStyleSheet();
943 }
944
945 }