2 GNU Lesser General Public License
5 Copyright (C) 2001 Frits Jalvingh & Howard Kistler
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.
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.
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
22 package com
.hexidec
.ekit
.component
;
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
;
41 import java
.io
.InputStream
;
42 import java
.io
.IOException
;
43 import java
.net
.MalformedURLException
;
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
;
67 * @author <a href="mailto:jal@grimor.com">Frits Jalvingh</a>
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.
75 public class RelativeImageView
extends View
implements ImageObserver
, MouseListener
, MouseMotionListener
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";
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;
93 private AttributeSet attr
;
94 private Element fElement
;
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.
107 * Creates a new view that represents an IMG element.
108 * @param elem the element to create a view for
110 public RelativeImageView(Element elem
,String baseurl
)
115 StyleSheet sheet
= getStyleSheet();
116 attr
= sheet
.getViewAttributes(this);
119 private void initialize(Element elem
)
129 boolean customWidth
= false
;
130 boolean customHeight
= false
;
134 // request image from document's cache
135 AttributeSet attr
= elem
.getAttributes();
138 URL src
= getSourceURL();
141 Dictionary cache
= (Dictionary
)getDocument().getProperty(IMAGE_CACHE_PROPERTY
);
144 fImage
= (Image
)cache
.get(src
);
148 fImage
= Toolkit
.getDefaultToolkit().getImage(src
);
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
);
162 catch(InterruptedException ie
)
165 // possibly replace with the ImageBroken icon, if that's what is happening
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
)
174 height
= fImage
.getHeight(this);
178 height
= DEFAULT_HEIGHT
;
181 width
= getIntAttr(HTML
.Attribute
.WIDTH
, -1);
182 customWidth
= (width
> 0);
183 if(!customWidth
&& fImage
!= null
)
185 width
= fImage
.getWidth(this);
189 width
= DEFAULT_WIDTH
;
194 if(customHeight
&& customWidth
)
196 Toolkit
.getDefaultToolkit().prepareImage(fImage
, height
, width
, this);
200 Toolkit
.getDefaultToolkit().prepareImage(fImage
, -1, -1, this);
209 if(customHeight
|| fHeight
== 0)
213 if(customWidth
|| fWidth
== 0)
221 /** Determines if path is in the form of a URL
223 private boolean isURL()
225 String src
= (String
)fElement
.getAttributes().getAttribute(HTML
.Attribute
.SRC
);
226 return src
.toLowerCase().startsWith("file") || src
.toLowerCase().startsWith("http");
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.
233 private String
processSrcPath(String src
)
236 File imageFile
= new File(src
);
237 if(imageFile
.isAbsolute())
241 boolean found
= false
;
242 Document doc
= getDocument();
245 String pv
= (String
)doc
.getProperty("com.hexidec.ekit.docsource");
248 File f
= new File(pv
);
249 val
= (new File(f
.getParent(), imageFile
.getPath().toString())).toString();
255 String imagePath
= System
.getProperty("system.image.path.key");
256 if(imagePath
!= null
)
258 val
= (new File(imagePath
, imageFile
.getPath())).toString();
264 /** Method insures that the image is loaded and not a broken reference
266 private void waitForImage()
267 throws InterruptedException
269 int w
= fImage
.getWidth(this);
270 int h
= fImage
.getHeight(this);
273 int flags
= Toolkit
.getDefaultToolkit().checkImage(fImage
, w
, h
, this);
274 if(((flags
& ERROR
) != 0) || ((flags
& ABORT
) != 0 ))
276 throw new InterruptedException();
278 else if((flags
& (ALLBITS
| FRAMEBITS
)) != 0)
286 /** Fetches the attributes to use when rendering. This is
287 * implemented to multiplex the attributes specified in the
288 * model with a StyleSheet.
290 public AttributeSet
getAttributes()
295 /** Method tests whether the image within a link
299 AttributeSet anchorAttr
= (AttributeSet
)fElement
.getAttributes().getAttribute(HTML
.Tag
.A
);
300 if(anchorAttr
!= null
)
302 return anchorAttr
.isDefined(HTML
.Attribute
.HREF
);
307 /** Method returns the size of the border to use
311 return getIntAttr(HTML
.Attribute
.BORDER
, isLink() ? DEFAULT_BORDER
: 0);
314 /** Method returns the amount of extra space to add along an axis
316 int getSpace(int axis
)
318 return getIntAttr((axis
== X_AXIS
) ? HTML
.Attribute
.HSPACE
: HTML
.Attribute
.VSPACE
, 0);
321 /** Method returns the border's color, or null if this is not a link
323 Color
getBorderColor()
325 StyledDocument doc
= (StyledDocument
)getDocument();
326 return doc
.getForeground(getAttributes());
329 /** Method returns the image's vertical alignment
331 float getVerticalAlignment()
333 String align
= (String
)fElement
.getAttributes().getAttribute(HTML
.Attribute
.ALIGN
);
336 align
= align
.toLowerCase();
337 if(align
.equals(TOP
) || align
.equals(TEXTTOP
))
341 else if(align
.equals(this.CENTER
) || align
.equals(MIDDLE
) || align
.equals(ABSMIDDLE
))
346 return 1.0f
; // default alignment is bottom
349 boolean hasPixels(ImageObserver obs
)
351 return ((fImage
!= null
) && (fImage
.getHeight(obs
) > 0) && (fImage
.getWidth(obs
) > 0));
354 /** Method returns a URL for the image source, or null if it could not be determined
356 private URL
getSourceURL()
358 String src
= (String
)fElement
.getAttributes().getAttribute(HTML
.Attribute
.SRC
);
365 URL reference
= new URL(BaseUrl
);
366 URL u
= new URL(reference
,src
);
369 catch(MalformedURLException mue
)
375 /** Method looks up an integer-valued attribute (not recursive!)
377 private int getIntAttr(HTML
.Attribute name
, int iDefault
)
379 AttributeSet attr
= fElement
.getAttributes();
380 if(attr
.isDefined(name
))
383 String val
= (String
)attr
.getAttribute(name
);
392 i
= Math
.max(0, Integer
.parseInt(val
));
394 catch(NumberFormatException nfe
)
408 * Establishes the parent view for this view.
409 * Seize this moment to cache the AWT Container I'm in.
411 public void setParent(View parent
)
413 super.setParent(parent
);
414 fContainer
= ((parent
!= null
) ?
getContainer() : null
);
415 if((parent
== null
) && (fComponent
!= null
))
417 fComponent
.getParent().remove(fComponent
);
422 /** My attributes may have changed. */
423 public void changedUpdate(DocumentEvent e
, Shape a
, ViewFactory f
)
425 super.changedUpdate(e
, a
, f
);
426 float align
= getVerticalAlignment();
428 int height
= fHeight
;
431 initialize(getElement());
433 boolean hChanged
= fHeight
!= height
;
434 boolean wChanged
= fWidth
!= width
;
435 if(hChanged
|| wChanged
|| getVerticalAlignment() != align
)
437 getParent().preferenceChanged(this, hChanged
, wChanged
);
445 * @param g the rendering surface to use
446 * @param a the allocated region to render into
449 public void paint(Graphics g
, Shape a
)
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
);
457 int height
= fHeight
;
458 int sel
= getSelectionState();
460 // If no pixels yet, draw gray outline and icon
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
);
470 icon
.paintIcon(getContainer(), g
, x
, y
);
477 g
.drawImage(fImage
, x
, y
, width
, height
, this);
480 // If selected exactly, we need a black border & grow-box
481 Color bc
= getBorderColor();
484 // Make sure there's room for a border
485 int delta
= 2 - border
;
491 height
-= delta
<< 1;
495 g
.setColor(Color
.black
);
497 g
.fillRect(x
+ width
- 5, y
+ height
- 5, 5, 5);
507 // Draw a thick rectangle:
508 for(int i
= 1; i
<= border
; i
++)
510 g
.drawRect(x
- i
, y
- i
, width
- 1 + i
+ i
, height
- 1 + i
+ i
);
512 g
.setColor(oldColor
);
516 /** Request that this view be repainted. Assumes the view is still at its last-drawn location.
518 protected void repaint(long delay
)
520 if((fContainer
!= null
) && (fBounds
!= null
))
522 fContainer
.repaint(delay
, fBounds
.x
, fBounds
.y
, fBounds
.width
, fBounds
.height
);
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.
531 protected int getSelectionState()
533 int p0
= fElement
.getStartOffset();
534 int p1
= fElement
.getEndOffset();
535 if(fContainer
instanceof JTextComponent
)
537 JTextComponent textComp
= (JTextComponent
)fContainer
;
538 int start
= textComp
.getSelectionStart();
539 int end
= textComp
.getSelectionEnd();
540 if((start
<= p0
) && (end
>= p1
))
542 if((start
== p0
) && (end
== p1
) && isEditable())
555 protected boolean isEditable()
557 return ((fContainer
instanceof JEditorPane
) && ((JEditorPane
)fContainer
).isEditable());
560 /** Returns the text editor's highlight color.
562 protected Color
getHighlightColor()
564 JTextComponent textComp
= (JTextComponent
)fContainer
;
565 return textComp
.getSelectionColor();
568 // Progressive display -------------------------------------------------
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.
576 private static boolean sIsInc
= true
;
577 private static int sIncRate
= 100;
579 public boolean imageUpdate(Image img
, int flags
, int x
, int y
, int width
, int height
)
581 if((fImage
== null
) || (fImage
!= img
))
586 // Bail out if there was an error
587 if((flags
& (ABORT
|ERROR
)) != 0)
594 // Resize image if necessary
596 if((flags
& ImageObserver
.HEIGHT
) != 0)
598 if(!getElement().getAttributes().isDefined(HTML
.Attribute
.HEIGHT
))
603 if((flags
& ImageObserver
.WIDTH
) != 0)
605 if(!getElement().getAttributes().isDefined(HTML
.Attribute
.WIDTH
))
613 if((changed
& 1) == 1)
617 if((changed
& 2) == 2)
623 // No need to resize or repaint, still in the process of loading
630 // May need to resize myself, asynchronously
631 Document doc
= getDocument();
634 if(doc
instanceof AbstractDocument
)
636 ((AbstractDocument
)doc
).readLock();
638 preferenceChanged(this, true
, true
);
642 if(doc
instanceof AbstractDocument
)
644 ((AbstractDocument
)doc
).readUnlock();
650 // Repaint when done or when new pixels arrive
651 if((flags
& (FRAMEBITS
|ALLBITS
)) != 0)
655 else if((flags
& SOMEBITS
) != 0)
662 return ((flags
& ALLBITS
) == 0);
665 // Layout --------------------------------------------------------------
667 /** Determines the preferred span for this view along an axis.
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.
675 public float getPreferredSpan(int axis
)
677 int extra
= 2 * (getBorder() + getSpace(axis
));
683 return fHeight
+extra
;
685 throw new IllegalArgumentException("Invalid axis in getPreferredSpan() : " + axis
);
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
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.
701 public float getAlignment(int axis
)
706 return getVerticalAlignment();
708 return super.getAlignment(axis
);
712 /** Provides a mapping from the document model coordinate space
713 * to the coordinate space of the view mapped to it.
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
722 public Shape
modelToView(int pos
, Shape a
, Position
.Bias b
)
723 throws BadLocationException
725 int p0
= getStartOffset();
726 int p1
= getEndOffset();
727 if((pos
>= p0
) && (pos
<= p1
))
729 Rectangle r
= a
.getBounds();
740 /** Provides a mapping from the view coordinate space to the logical
741 * coordinate space of the model.
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
750 public int viewToModel(float x
, float y
, Shape a
, Position
.Bias
[] bias
)
752 Rectangle alloc
= (Rectangle
) a
;
753 if(x
< (alloc
.x
+ alloc
.width
))
755 bias
[0] = Position
.Bias
.Forward
;
756 return getStartOffset();
758 bias
[0] = Position
.Bias
.Backward
;
759 return getEndOffset();
762 /** Change the size of this image. This alters the HEIGHT and WIDTH
763 * attributes of the Element and causes a re-layout.
765 protected void resize(int width
, int height
)
767 if((width
== fWidth
) && (height
== fHeight
))
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
);
780 // Mouse event handling ------------------------------------------------
782 /** Select or grow image when clicked.
784 public void mousePressed(MouseEvent e
)
786 Dimension size
= fComponent
.getSize();
787 if((e
.getX() >= (size
.width
- 7)) && (e
.getY() >= (size
.height
- 7)) && (getSelectionState() == 2))
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();
796 // Else select image:
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();
805 // extend selection if shift key down:
808 comp
.moveCaretPosition(end
);
812 comp
.moveCaretPosition(start
);
817 // just select image, without shift:
820 comp
.setCaretPosition(start
);
824 comp
.moveCaretPosition(end
);
830 /** Resize image if initial click was in grow-box: */
831 public void mouseDragged(MouseEvent e
)
833 if(fGrowBase
!= null
)
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
)
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))
845 float prop
= imgHeight
/ imgWidth
;
846 float pwidth
= height
/ prop
;
847 float pheight
= width
* prop
;
854 height
= (int)pheight
;
858 resize(width
,height
);
862 public void mouseReleased(MouseEvent me
)
865 //! Should post some command to make the action undo-able
868 /** On double-click, open image properties dialog.
870 public void mouseClicked(MouseEvent me
)
872 if(me
.getClickCount() == 2)
878 public void mouseEntered(MouseEvent me
) { ; }
879 public void mouseMoved(MouseEvent me
) { ; }
880 public void mouseExited(MouseEvent me
) { ; }
882 // Static icon accessors -----------------------------------------------
884 private Icon
makeIcon(final String gifFile
)
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.
894 InputStream resource
= RelativeImageView
.class.getResourceAsStream(gifFile
);
900 BufferedInputStream
in = new BufferedInputStream(resource
);
901 ByteArrayOutputStream out
= new ByteArrayOutputStream(1024);
902 byte[] buffer
= new byte[1024];
904 while((n
= in.read(buffer
)) > 0)
906 out
.write(buffer
, 0, n
);
911 buffer
= out
.toByteArray();
912 if(buffer
.length
== 0)
914 System
.err
.println("WARNING : " + gifFile
+ " is zero-length");
917 return new ImageIcon(buffer
);
920 private void loadImageStatusIcons()
924 if(sPendingImageIcon
== null
)
926 sPendingImageIcon
= makeIcon(PENDING_IMAGE_SRC
);
928 if(sMissingImageIcon
== null
)
930 sMissingImageIcon
= makeIcon(MISSING_IMAGE_SRC
);
935 System
.err
.println("ImageView : Couldn't load image icons");
939 protected StyleSheet
getStyleSheet()
941 HTMLDocument doc
= (HTMLDocument
)getDocument();
942 return doc
.getStyleSheet();