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