2 GNU Lesser General Public License
5 Copyright (C) 2001-2002 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
);
363 //URL reference = ((HTMLDocument)getDocument()).getBase();
366 URL reference
= new URL(BaseUrl
);
367 URL u
= new URL(reference
,src
);
370 catch(MalformedURLException mue
)
376 /** Method looks up an integer-valued attribute (not recursive!)
378 private int getIntAttr(HTML
.Attribute name
, int iDefault
)
380 AttributeSet attr
= fElement
.getAttributes();
381 if(attr
.isDefined(name
))
384 String val
= (String
)attr
.getAttribute(name
);
393 i
= Math
.max(0, Integer
.parseInt(val
));
395 catch(NumberFormatException nfe
)
409 * Establishes the parent view for this view.
410 * Seize this moment to cache the AWT Container I'm in.
412 public void setParent(View parent
)
414 super.setParent(parent
);
415 fContainer
= ((parent
!= null
) ?
getContainer() : null
);
416 if((parent
== null
) && (fComponent
!= null
))
418 fComponent
.getParent().remove(fComponent
);
423 /** My attributes may have changed. */
424 public void changedUpdate(DocumentEvent e
, Shape a
, ViewFactory f
)
426 super.changedUpdate(e
, a
, f
);
427 float align
= getVerticalAlignment();
429 int height
= fHeight
;
432 initialize(getElement());
434 boolean hChanged
= fHeight
!= height
;
435 boolean wChanged
= fWidth
!= width
;
436 if(hChanged
|| wChanged
|| getVerticalAlignment() != align
)
438 getParent().preferenceChanged(this, hChanged
, wChanged
);
446 * @param g the rendering surface to use
447 * @param a the allocated region to render into
450 public void paint(Graphics g
, Shape a
)
452 Color oldColor
= g
.getColor();
453 fBounds
= a
.getBounds();
454 int border
= getBorder();
455 int x
= fBounds
.x
+ border
+ getSpace(X_AXIS
);
456 int y
= fBounds
.y
+ border
+ getSpace(Y_AXIS
);
458 int height
= fHeight
;
459 int sel
= getSelectionState();
461 // If no pixels yet, draw gray outline and icon
464 g
.setColor(Color
.lightGray
);
465 g
.drawRect(x
, y
, width
- 1, height
- 1);
466 g
.setColor(oldColor
);
467 loadImageStatusIcons();
468 Icon icon
= ((fImage
== null
) ? sMissingImageIcon
: sPendingImageIcon
);
471 icon
.paintIcon(getContainer(), g
, x
, y
);
478 g
.drawImage(fImage
, x
, y
, width
, height
, this);
481 // If selected exactly, we need a black border & grow-box
482 Color bc
= getBorderColor();
485 // Make sure there's room for a border
486 int delta
= 2 - border
;
492 height
-= delta
<< 1;
496 g
.setColor(Color
.black
);
498 g
.fillRect(x
+ width
- 5, y
+ height
- 5, 5, 5);
508 // Draw a thick rectangle:
509 for(int i
= 1; i
<= border
; i
++)
511 g
.drawRect(x
- i
, y
- i
, width
- 1 + i
+ i
, height
- 1 + i
+ i
);
513 g
.setColor(oldColor
);
517 /** Request that this view be repainted. Assumes the view is still at its last-drawn location.
519 protected void repaint(long delay
)
521 if((fContainer
!= null
) && (fBounds
!= null
))
523 fContainer
.repaint(delay
, fBounds
.x
, fBounds
.y
, fBounds
.width
, fBounds
.height
);
528 * Determines whether the image is selected, and if it's the only thing selected.
529 * @return 0 if not selected, 1 if selected, 2 if exclusively selected.
530 * "Exclusive" selection is only returned when editable.
532 protected int getSelectionState()
534 int p0
= fElement
.getStartOffset();
535 int p1
= fElement
.getEndOffset();
536 if(fContainer
instanceof JTextComponent
)
538 JTextComponent textComp
= (JTextComponent
)fContainer
;
539 int start
= textComp
.getSelectionStart();
540 int end
= textComp
.getSelectionEnd();
541 if((start
<= p0
) && (end
>= p1
))
543 if((start
== p0
) && (end
== p1
) && isEditable())
556 protected boolean isEditable()
558 return ((fContainer
instanceof JEditorPane
) && ((JEditorPane
)fContainer
).isEditable());
561 /** Returns the text editor's highlight color.
563 protected Color
getHighlightColor()
565 JTextComponent textComp
= (JTextComponent
)fContainer
;
566 return textComp
.getSelectionColor();
569 // Progressive display -------------------------------------------------
571 // This can come on any thread. If we are in the process of reloading
572 // the image and determining our state (loading == true) we don't fire
573 // preference changed, or repaint, we just reset the fWidth/fHeight as
574 // necessary and return. This is ok as we know when loading finishes
575 // it will pick up the new height/width, if necessary.
577 private static boolean sIsInc
= true
;
578 private static int sIncRate
= 100;
580 public boolean imageUpdate(Image img
, int flags
, int x
, int y
, int width
, int height
)
582 if((fImage
== null
) || (fImage
!= img
))
587 // Bail out if there was an error
588 if((flags
& (ABORT
|ERROR
)) != 0)
595 // Resize image if necessary
597 if((flags
& ImageObserver
.HEIGHT
) != 0)
599 if(!getElement().getAttributes().isDefined(HTML
.Attribute
.HEIGHT
))
604 if((flags
& ImageObserver
.WIDTH
) != 0)
606 if(!getElement().getAttributes().isDefined(HTML
.Attribute
.WIDTH
))
614 if((changed
& 1) == 1)
618 if((changed
& 2) == 2)
624 // No need to resize or repaint, still in the process of loading
631 // May need to resize myself, asynchronously
632 Document doc
= getDocument();
635 if(doc
instanceof AbstractDocument
)
637 ((AbstractDocument
)doc
).readLock();
639 preferenceChanged(this, true
, true
);
643 if(doc
instanceof AbstractDocument
)
645 ((AbstractDocument
)doc
).readUnlock();
651 // Repaint when done or when new pixels arrive
652 if((flags
& (FRAMEBITS
|ALLBITS
)) != 0)
656 else if((flags
& SOMEBITS
) != 0)
663 return ((flags
& ALLBITS
) == 0);
666 // Layout --------------------------------------------------------------
668 /** Determines the preferred span for this view along an axis.
670 * @param axis may be either X_AXIS or Y_AXIS
671 * @returns the span the view would like to be rendered into.
672 * Typically the view is told to render into the span
673 * that is returned, although there is no guarantee.
674 * The parent may choose to resize or break the view.
676 public float getPreferredSpan(int axis
)
678 int extra
= 2 * (getBorder() + getSpace(axis
));
684 return fHeight
+extra
;
686 throw new IllegalArgumentException("Invalid axis in getPreferredSpan() : " + axis
);
690 /** Determines the desired alignment for this view along an
691 * axis. This is implemented to give the alignment to the
692 * bottom of the icon along the y axis, and the default
695 * @param axis may be either X_AXIS or Y_AXIS
696 * @returns the desired alignment. This should be a value
697 * between 0.0 and 1.0 where 0 indicates alignment at the
698 * origin and 1.0 indicates alignment to the full span
699 * away from the origin. An alignment of 0.5 would be the
700 * center of the view.
702 public float getAlignment(int axis
)
707 return getVerticalAlignment();
709 return super.getAlignment(axis
);
713 /** Provides a mapping from the document model coordinate space
714 * to the coordinate space of the view mapped to it.
716 * @param pos the position to convert
717 * @param a the allocated region to render into
718 * @return the bounding box of the given position
719 * @exception BadLocationException if the given position does not represent a
720 * valid location in the associated document
721 * @see View#modelToView
723 public Shape
modelToView(int pos
, Shape a
, Position
.Bias b
)
724 throws BadLocationException
726 int p0
= getStartOffset();
727 int p1
= getEndOffset();
728 if((pos
>= p0
) && (pos
<= p1
))
730 Rectangle r
= a
.getBounds();
741 /** Provides a mapping from the view coordinate space to the logical
742 * coordinate space of the model.
744 * @param x the X coordinate
745 * @param y the Y coordinate
746 * @param a the allocated region to render into
747 * @return the location within the model that best represents the
748 * given point of view
749 * @see View#viewToModel
751 public int viewToModel(float x
, float y
, Shape a
, Position
.Bias
[] bias
)
753 Rectangle alloc
= (Rectangle
) a
;
754 if(x
< (alloc
.x
+ alloc
.width
))
756 bias
[0] = Position
.Bias
.Forward
;
757 return getStartOffset();
759 bias
[0] = Position
.Bias
.Backward
;
760 return getEndOffset();
763 /** Change the size of this image. This alters the HEIGHT and WIDTH
764 * attributes of the Element and causes a re-layout.
766 protected void resize(int width
, int height
)
768 if((width
== fWidth
) && (height
== fHeight
))
774 // Replace attributes in document
775 MutableAttributeSet attr
= new SimpleAttributeSet();
776 attr
.addAttribute(HTML
.Attribute
.WIDTH
,Integer
.toString(width
));
777 attr
.addAttribute(HTML
.Attribute
.HEIGHT
,Integer
.toString(height
));
778 ((StyledDocument
)getDocument()).setCharacterAttributes(fElement
.getStartOffset(), fElement
.getEndOffset(), attr
, false
);
781 // Mouse event handling ------------------------------------------------
783 /** Select or grow image when clicked.
785 public void mousePressed(MouseEvent e
)
787 Dimension size
= fComponent
.getSize();
788 if((e
.getX() >= (size
.width
- 7)) && (e
.getY() >= (size
.height
- 7)) && (getSelectionState() == 2))
790 // Click in selected grow-box:
791 Point loc
= fComponent
.getLocationOnScreen();
792 fGrowBase
= new Point(loc
.x
+ e
.getX() - fWidth
, loc
.y
+ e
.getY() - fHeight
);
793 fGrowProportionally
= e
.isShiftDown();
797 // Else select image:
799 JTextComponent comp
= (JTextComponent
)fContainer
;
800 int start
= fElement
.getStartOffset();
801 int end
= fElement
.getEndOffset();
802 int mark
= comp
.getCaret().getMark();
803 int dot
= comp
.getCaret().getDot();
806 // extend selection if shift key down:
809 comp
.moveCaretPosition(end
);
813 comp
.moveCaretPosition(start
);
818 // just select image, without shift:
821 comp
.setCaretPosition(start
);
825 comp
.moveCaretPosition(end
);
831 /** Resize image if initial click was in grow-box: */
832 public void mouseDragged(MouseEvent e
)
834 if(fGrowBase
!= null
)
836 Point loc
= fComponent
.getLocationOnScreen();
837 int width
= Math
.max(2, loc
.x
+ e
.getX() - fGrowBase
.x
);
838 int height
= Math
.max(2, loc
.y
+ e
.getY() - fGrowBase
.y
);
839 if(e
.isShiftDown() && fImage
!= null
)
841 // Make sure size is proportional to actual image size
842 float imgWidth
= fImage
.getWidth(this);
843 float imgHeight
= fImage
.getHeight(this);
844 if((imgWidth
> 0) && (imgHeight
> 0))
846 float prop
= imgHeight
/ imgWidth
;
847 float pwidth
= height
/ prop
;
848 float pheight
= width
* prop
;
855 height
= (int)pheight
;
859 resize(width
,height
);
863 public void mouseReleased(MouseEvent me
)
866 //! Should post some command to make the action undo-able
869 /** On double-click, open image properties dialog.
871 public void mouseClicked(MouseEvent me
)
873 if(me
.getClickCount() == 2)
879 public void mouseEntered(MouseEvent me
) { ; }
880 public void mouseMoved(MouseEvent me
) { ; }
881 public void mouseExited(MouseEvent me
) { ; }
883 // Static icon accessors -----------------------------------------------
885 private Icon
makeIcon(final String gifFile
)
888 /* Copy resource into a byte array. This is
889 * necessary because several browsers consider
890 * Class.getResource a security risk because it
891 * can be used to load additional classes.
892 * Class.getResourceAsStream just returns raw
893 * bytes, which we can convert to an image.
895 InputStream resource
= RelativeImageView
.class.getResourceAsStream(gifFile
);
901 BufferedInputStream
in = new BufferedInputStream(resource
);
902 ByteArrayOutputStream out
= new ByteArrayOutputStream(1024);
903 byte[] buffer
= new byte[1024];
905 while((n
= in.read(buffer
)) > 0)
907 out
.write(buffer
, 0, n
);
912 buffer
= out
.toByteArray();
913 if(buffer
.length
== 0)
915 System
.err
.println("WARNING : " + gifFile
+ " is zero-length");
918 return new ImageIcon(buffer
);
921 private void loadImageStatusIcons()
925 if(sPendingImageIcon
== null
)
927 sPendingImageIcon
= makeIcon(PENDING_IMAGE_SRC
);
929 if(sMissingImageIcon
== null
)
931 sMissingImageIcon
= makeIcon(MISSING_IMAGE_SRC
);
936 System
.err
.println("ImageView : Couldn't load image icons");
940 protected StyleSheet
getStyleSheet()
942 HTMLDocument doc
= (HTMLDocument
)getDocument();
943 return doc
.getStyleSheet();