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 boolean bLoading
; // set to true while the receiver is locked, to indicate the reciever is loading the image. This is used in imageUpdate.
106 * Creates a new view that represents an IMG element.
107 * @param elem the element to create a view for
109 public RelativeImageView(Element elem
)
113 StyleSheet sheet
= getStyleSheet();
114 attr
= sheet
.getViewAttributes(this);
117 private void initialize(Element elem
)
127 boolean customWidth
= false
;
128 boolean customHeight
= false
;
132 // request image from document's cache
133 AttributeSet attr
= elem
.getAttributes();
136 URL src
= getSourceURL();
139 Dictionary cache
= (Dictionary
)getDocument().getProperty(IMAGE_CACHE_PROPERTY
);
142 fImage
= (Image
)cache
.get(src
);
146 fImage
= Toolkit
.getDefaultToolkit().getImage(src
);
152 // load image from relative path
153 String src
= (String
)fElement
.getAttributes().getAttribute(HTML
.Attribute
.SRC
);
154 src
= processSrcPath(src
);
155 fImage
= Toolkit
.getDefaultToolkit().createImage(src
);
160 catch(InterruptedException ie
)
163 // possibly replace with the ImageBroken icon, if that's what is happening
167 // get height & width from params or image or defaults
168 height
= getIntAttr(HTML
.Attribute
.HEIGHT
, -1);
169 customHeight
= (height
> 0);
170 if(!customHeight
&& fImage
!= null
)
172 height
= fImage
.getHeight(this);
176 height
= DEFAULT_HEIGHT
;
179 width
= getIntAttr(HTML
.Attribute
.WIDTH
, -1);
180 customWidth
= (width
> 0);
181 if(!customWidth
&& fImage
!= null
)
183 width
= fImage
.getWidth(this);
187 width
= DEFAULT_WIDTH
;
192 if(customHeight
&& customWidth
)
194 Toolkit
.getDefaultToolkit().prepareImage(fImage
, height
, width
, this);
198 Toolkit
.getDefaultToolkit().prepareImage(fImage
, -1, -1, this);
207 if(customHeight
|| fHeight
== 0)
211 if(customWidth
|| fWidth
== 0)
219 /** Determines if path is in the form of a URL
221 private boolean isURL()
223 String src
= (String
)fElement
.getAttributes().getAttribute(HTML
.Attribute
.SRC
);
224 return src
.toLowerCase().startsWith("file") || src
.toLowerCase().startsWith("http");
227 /** Checks to see if the absolute path is availabe thru an application
228 * global static variable or thru a system variable. If so, appends
229 * the relative path to the absolute path and returns the String.
231 private String
processSrcPath(String src
)
234 File imageFile
= new File(src
);
235 if(imageFile
.isAbsolute())
239 boolean found
= false
;
240 Document doc
= getDocument();
243 String pv
= (String
)doc
.getProperty("com.hexidec.ekit.docsource");
246 File f
= new File(pv
);
247 val
= (new File(f
.getParent(), imageFile
.getPath().toString())).toString();
253 String imagePath
= System
.getProperty("system.image.path.key");
254 if(imagePath
!= null
)
256 val
= (new File(imagePath
, imageFile
.getPath())).toString();
262 /** Method insures that the image is loaded and not a broken reference
264 private void waitForImage()
265 throws InterruptedException
267 int w
= fImage
.getWidth(this);
268 int h
= fImage
.getHeight(this);
271 int flags
= Toolkit
.getDefaultToolkit().checkImage(fImage
, w
, h
, this);
272 if(((flags
& ERROR
) != 0) || ((flags
& ABORT
) != 0 ))
274 throw new InterruptedException();
276 else if((flags
& (ALLBITS
| FRAMEBITS
)) != 0)
284 /** Fetches the attributes to use when rendering. This is
285 * implemented to multiplex the attributes specified in the
286 * model with a StyleSheet.
288 public AttributeSet
getAttributes()
293 /** Method tests whether the image within a link
297 AttributeSet anchorAttr
= (AttributeSet
)fElement
.getAttributes().getAttribute(HTML
.Tag
.A
);
298 if(anchorAttr
!= null
)
300 return anchorAttr
.isDefined(HTML
.Attribute
.HREF
);
305 /** Method returns the size of the border to use
309 return getIntAttr(HTML
.Attribute
.BORDER
, isLink() ? DEFAULT_BORDER
: 0);
312 /** Method returns the amount of extra space to add along an axis
314 int getSpace(int axis
)
316 return getIntAttr((axis
== X_AXIS
) ? HTML
.Attribute
.HSPACE
: HTML
.Attribute
.VSPACE
, 0);
319 /** Method returns the border's color, or null if this is not a link
321 Color
getBorderColor()
323 StyledDocument doc
= (StyledDocument
)getDocument();
324 return doc
.getForeground(getAttributes());
327 /** Method returns the image's vertical alignment
329 float getVerticalAlignment()
331 String align
= (String
)fElement
.getAttributes().getAttribute(HTML
.Attribute
.ALIGN
);
334 align
= align
.toLowerCase();
335 if(align
.equals(TOP
) || align
.equals(TEXTTOP
))
339 else if(align
.equals(this.CENTER
) || align
.equals(MIDDLE
) || align
.equals(ABSMIDDLE
))
344 return 1.0f
; // default alignment is bottom
347 boolean hasPixels(ImageObserver obs
)
349 return ((fImage
!= null
) && (fImage
.getHeight(obs
) > 0) && (fImage
.getWidth(obs
) > 0));
352 /** Method returns a URL for the image source, or null if it could not be determined
354 private URL
getSourceURL()
356 String src
= (String
)fElement
.getAttributes().getAttribute(HTML
.Attribute
.SRC
);
361 URL reference
= ((HTMLDocument
)getDocument()).getBase();
364 URL u
= new URL(reference
,src
);
367 catch(MalformedURLException mue
)
373 /** Method looks up an integer-valued attribute (not recursive!)
375 private int getIntAttr(HTML
.Attribute name
, int iDefault
)
377 AttributeSet attr
= fElement
.getAttributes();
378 if(attr
.isDefined(name
))
381 String val
= (String
)attr
.getAttribute(name
);
390 i
= Math
.max(0, Integer
.parseInt(val
));
392 catch(NumberFormatException nfe
)
406 * Establishes the parent view for this view.
407 * Seize this moment to cache the AWT Container I'm in.
409 public void setParent(View parent
)
411 super.setParent(parent
);
412 fContainer
= ((parent
!= null
) ?
getContainer() : null
);
413 if((parent
== null
) && (fComponent
!= null
))
415 fComponent
.getParent().remove(fComponent
);
420 /** My attributes may have changed. */
421 public void changedUpdate(DocumentEvent e
, Shape a
, ViewFactory f
)
423 super.changedUpdate(e
, a
, f
);
424 float align
= getVerticalAlignment();
426 int height
= fHeight
;
429 initialize(getElement());
431 boolean hChanged
= fHeight
!= height
;
432 boolean wChanged
= fWidth
!= width
;
433 if(hChanged
|| wChanged
|| getVerticalAlignment() != align
)
435 getParent().preferenceChanged(this, hChanged
, wChanged
);
443 * @param g the rendering surface to use
444 * @param a the allocated region to render into
447 public void paint(Graphics g
, Shape a
)
449 Color oldColor
= g
.getColor();
450 fBounds
= a
.getBounds();
451 int border
= getBorder();
452 int x
= fBounds
.x
+ border
+ getSpace(X_AXIS
);
453 int y
= fBounds
.y
+ border
+ getSpace(Y_AXIS
);
455 int height
= fHeight
;
456 int sel
= getSelectionState();
458 // If no pixels yet, draw gray outline and icon
461 g
.setColor(Color
.lightGray
);
462 g
.drawRect(x
, y
, width
- 1, height
- 1);
463 g
.setColor(oldColor
);
464 loadImageStatusIcons();
465 Icon icon
= ((fImage
== null
) ? sMissingImageIcon
: sPendingImageIcon
);
468 icon
.paintIcon(getContainer(), g
, x
, y
);
475 g
.drawImage(fImage
, x
, y
, width
, height
, this);
478 // If selected exactly, we need a black border & grow-box
479 Color bc
= getBorderColor();
482 // Make sure there's room for a border
483 int delta
= 2 - border
;
489 height
-= delta
<< 1;
493 g
.setColor(Color
.black
);
495 g
.fillRect(x
+ width
- 5, y
+ height
- 5, 5, 5);
505 // Draw a thick rectangle:
506 for(int i
= 1; i
<= border
; i
++)
508 g
.drawRect(x
- i
, y
- i
, width
- 1 + i
+ i
, height
- 1 + i
+ i
);
510 g
.setColor(oldColor
);
514 /** Request that this view be repainted. Assumes the view is still at its last-drawn location.
516 protected void repaint(long delay
)
518 if((fContainer
!= null
) && (fBounds
!= null
))
520 fContainer
.repaint(delay
, fBounds
.x
, fBounds
.y
, fBounds
.width
, fBounds
.height
);
525 * Determines whether the image is selected, and if it's the only thing selected.
526 * @return 0 if not selected, 1 if selected, 2 if exclusively selected.
527 * "Exclusive" selection is only returned when editable.
529 protected int getSelectionState()
531 int p0
= fElement
.getStartOffset();
532 int p1
= fElement
.getEndOffset();
533 if(fContainer
instanceof JTextComponent
)
535 JTextComponent textComp
= (JTextComponent
)fContainer
;
536 int start
= textComp
.getSelectionStart();
537 int end
= textComp
.getSelectionEnd();
538 if((start
<= p0
) && (end
>= p1
))
540 if((start
== p0
) && (end
== p1
) && isEditable())
553 protected boolean isEditable()
555 return ((fContainer
instanceof JEditorPane
) && ((JEditorPane
)fContainer
).isEditable());
558 /** Returns the text editor's highlight color.
560 protected Color
getHighlightColor()
562 JTextComponent textComp
= (JTextComponent
)fContainer
;
563 return textComp
.getSelectionColor();
566 // Progressive display -------------------------------------------------
568 // This can come on any thread. If we are in the process of reloading
569 // the image and determining our state (loading == true) we don't fire
570 // preference changed, or repaint, we just reset the fWidth/fHeight as
571 // necessary and return. This is ok as we know when loading finishes
572 // it will pick up the new height/width, if necessary.
574 private static boolean sIsInc
= true
;
575 private static int sIncRate
= 100;
577 public boolean imageUpdate(Image img
, int flags
, int x
, int y
, int width
, int height
)
579 if((fImage
== null
) || (fImage
!= img
))
584 // Bail out if there was an error
585 if((flags
& (ABORT
|ERROR
)) != 0)
592 // Resize image if necessary
594 if((flags
& ImageObserver
.HEIGHT
) != 0)
596 if(!getElement().getAttributes().isDefined(HTML
.Attribute
.HEIGHT
))
601 if((flags
& ImageObserver
.WIDTH
) != 0)
603 if(!getElement().getAttributes().isDefined(HTML
.Attribute
.WIDTH
))
611 if((changed
& 1) == 1)
615 if((changed
& 2) == 2)
621 // No need to resize or repaint, still in the process of loading
628 // May need to resize myself, asynchronously
629 Document doc
= getDocument();
632 if(doc
instanceof AbstractDocument
)
634 ((AbstractDocument
)doc
).readLock();
636 preferenceChanged(this, true
, true
);
640 if(doc
instanceof AbstractDocument
)
642 ((AbstractDocument
)doc
).readUnlock();
648 // Repaint when done or when new pixels arrive
649 if((flags
& (FRAMEBITS
|ALLBITS
)) != 0)
653 else if((flags
& SOMEBITS
) != 0)
660 return ((flags
& ALLBITS
) == 0);
663 // Layout --------------------------------------------------------------
665 /** Determines the preferred span for this view along an axis.
667 * @param axis may be either X_AXIS or Y_AXIS
668 * @returns the span the view would like to be rendered into.
669 * Typically the view is told to render into the span
670 * that is returned, although there is no guarantee.
671 * The parent may choose to resize or break the view.
673 public float getPreferredSpan(int axis
)
675 int extra
= 2 * (getBorder() + getSpace(axis
));
681 return fHeight
+extra
;
683 throw new IllegalArgumentException("Invalid axis in getPreferredSpan() : " + axis
);
687 /** Determines the desired alignment for this view along an
688 * axis. This is implemented to give the alignment to the
689 * bottom of the icon along the y axis, and the default
692 * @param axis may be either X_AXIS or Y_AXIS
693 * @returns the desired alignment. This should be a value
694 * between 0.0 and 1.0 where 0 indicates alignment at the
695 * origin and 1.0 indicates alignment to the full span
696 * away from the origin. An alignment of 0.5 would be the
697 * center of the view.
699 public float getAlignment(int axis
)
704 return getVerticalAlignment();
706 return super.getAlignment(axis
);
710 /** Provides a mapping from the document model coordinate space
711 * to the coordinate space of the view mapped to it.
713 * @param pos the position to convert
714 * @param a the allocated region to render into
715 * @return the bounding box of the given position
716 * @exception BadLocationException if the given position does not represent a
717 * valid location in the associated document
718 * @see View#modelToView
720 public Shape
modelToView(int pos
, Shape a
, Position
.Bias b
)
721 throws BadLocationException
723 int p0
= getStartOffset();
724 int p1
= getEndOffset();
725 if((pos
>= p0
) && (pos
<= p1
))
727 Rectangle r
= a
.getBounds();
738 /** Provides a mapping from the view coordinate space to the logical
739 * coordinate space of the model.
741 * @param x the X coordinate
742 * @param y the Y coordinate
743 * @param a the allocated region to render into
744 * @return the location within the model that best represents the
745 * given point of view
746 * @see View#viewToModel
748 public int viewToModel(float x
, float y
, Shape a
, Position
.Bias
[] bias
)
750 Rectangle alloc
= (Rectangle
) a
;
751 if(x
< (alloc
.x
+ alloc
.width
))
753 bias
[0] = Position
.Bias
.Forward
;
754 return getStartOffset();
756 bias
[0] = Position
.Bias
.Backward
;
757 return getEndOffset();
760 /** Change the size of this image. This alters the HEIGHT and WIDTH
761 * attributes of the Element and causes a re-layout.
763 protected void resize(int width
, int height
)
765 if((width
== fWidth
) && (height
== fHeight
))
771 // Replace attributes in document
772 MutableAttributeSet attr
= new SimpleAttributeSet();
773 attr
.addAttribute(HTML
.Attribute
.WIDTH
,Integer
.toString(width
));
774 attr
.addAttribute(HTML
.Attribute
.HEIGHT
,Integer
.toString(height
));
775 ((StyledDocument
)getDocument()).setCharacterAttributes(fElement
.getStartOffset(), fElement
.getEndOffset(), attr
, false
);
778 // Mouse event handling ------------------------------------------------
780 /** Select or grow image when clicked.
782 public void mousePressed(MouseEvent e
)
784 Dimension size
= fComponent
.getSize();
785 if((e
.getX() >= (size
.width
- 7)) && (e
.getY() >= (size
.height
- 7)) && (getSelectionState() == 2))
787 // Click in selected grow-box:
788 Point loc
= fComponent
.getLocationOnScreen();
789 fGrowBase
= new Point(loc
.x
+ e
.getX() - fWidth
, loc
.y
+ e
.getY() - fHeight
);
790 fGrowProportionally
= e
.isShiftDown();
794 // Else select image:
796 JTextComponent comp
= (JTextComponent
)fContainer
;
797 int start
= fElement
.getStartOffset();
798 int end
= fElement
.getEndOffset();
799 int mark
= comp
.getCaret().getMark();
800 int dot
= comp
.getCaret().getDot();
803 // extend selection if shift key down:
806 comp
.moveCaretPosition(end
);
810 comp
.moveCaretPosition(start
);
815 // just select image, without shift:
818 comp
.setCaretPosition(start
);
822 comp
.moveCaretPosition(end
);
828 /** Resize image if initial click was in grow-box: */
829 public void mouseDragged(MouseEvent e
)
831 if(fGrowBase
!= null
)
833 Point loc
= fComponent
.getLocationOnScreen();
834 int width
= Math
.max(2, loc
.x
+ e
.getX() - fGrowBase
.x
);
835 int height
= Math
.max(2, loc
.y
+ e
.getY() - fGrowBase
.y
);
836 if(e
.isShiftDown() && fImage
!= null
)
838 // Make sure size is proportional to actual image size
839 float imgWidth
= fImage
.getWidth(this);
840 float imgHeight
= fImage
.getHeight(this);
841 if((imgWidth
> 0) && (imgHeight
> 0))
843 float prop
= imgHeight
/ imgWidth
;
844 float pwidth
= height
/ prop
;
845 float pheight
= width
* prop
;
852 height
= (int)pheight
;
856 resize(width
,height
);
860 public void mouseReleased(MouseEvent me
)
863 //! Should post some command to make the action undo-able
866 /** On double-click, open image properties dialog.
868 public void mouseClicked(MouseEvent me
)
870 if(me
.getClickCount() == 2)
876 public void mouseEntered(MouseEvent me
) { ; }
877 public void mouseMoved(MouseEvent me
) { ; }
878 public void mouseExited(MouseEvent me
) { ; }
880 // Static icon accessors -----------------------------------------------
882 private Icon
makeIcon(final String gifFile
)
885 /* Copy resource into a byte array. This is
886 * necessary because several browsers consider
887 * Class.getResource a security risk because it
888 * can be used to load additional classes.
889 * Class.getResourceAsStream just returns raw
890 * bytes, which we can convert to an image.
892 InputStream resource
= RelativeImageView
.class.getResourceAsStream(gifFile
);
898 BufferedInputStream
in = new BufferedInputStream(resource
);
899 ByteArrayOutputStream out
= new ByteArrayOutputStream(1024);
900 byte[] buffer
= new byte[1024];
902 while((n
= in.read(buffer
)) > 0)
904 out
.write(buffer
, 0, n
);
909 buffer
= out
.toByteArray();
910 if(buffer
.length
== 0)
912 System
.err
.println("WARNING : " + gifFile
+ " is zero-length");
915 return new ImageIcon(buffer
);
918 private void loadImageStatusIcons()
922 if(sPendingImageIcon
== null
)
924 sPendingImageIcon
= makeIcon(PENDING_IMAGE_SRC
);
926 if(sMissingImageIcon
== null
)
928 sMissingImageIcon
= makeIcon(MISSING_IMAGE_SRC
);
933 System
.err
.println("ImageView : Couldn't load image icons");
937 protected StyleSheet
getStyleSheet()
939 HTMLDocument doc
= (HTMLDocument
)getDocument();
940 return doc
.getStyleSheet();