1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package fr.ens.transcriptome.doelan.gui;
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.IOException;
41 import java.io.InputStream;
42 import java.net.MalformedURLException;
43 import java.net.URL;
44 import java.util.Dictionary;
45 import java.util.Map;
46
47 import javax.swing.Icon;
48 import javax.swing.ImageIcon;
49 import javax.swing.JEditorPane;
50 import javax.swing.event.DocumentEvent;
51 import javax.swing.text.AbstractDocument;
52 import javax.swing.text.AttributeSet;
53 import javax.swing.text.BadLocationException;
54 import javax.swing.text.Document;
55 import javax.swing.text.Element;
56 import javax.swing.text.JTextComponent;
57 import javax.swing.text.MutableAttributeSet;
58 import javax.swing.text.Position;
59 import javax.swing.text.SimpleAttributeSet;
60 import javax.swing.text.StyledDocument;
61 import javax.swing.text.View;
62 import javax.swing.text.ViewFactory;
63 import javax.swing.text.html.HTML;
64 import javax.swing.text.html.HTMLDocument;
65 import javax.swing.text.html.StyleSheet;
66
67 import org.apache.log4j.Logger;
68
69 /***
70 * Image viewer
71 * @author Laurent Jourdren
72 */
73 public class MyImageView extends View implements ImageObserver, MouseListener,
74 MouseMotionListener {
75
76 private Logger log = Logger.getLogger(MyImageView.class);
77
78
79
80 public static final String TOP = "top", TEXTTOP = "texttop",
81 MIDDLE = "middle", ABSMIDDLE = "absmiddle", CENTER = "center",
82 BOTTOM = "bottom";
83
84
85
86 /***
87 * Creates a new view that represents an IMG element.
88 * @param elem the element to create a view for
89 * @param mapImage The map with all the images
90 */
91 public MyImageView(final Element elem, final Map mapImage) {
92 super(elem);
93 setMapImages(mapImage);
94 initialize(elem);
95 StyleSheet sheet = getStyleSheet();
96 attr = sheet.getViewAttributes(this);
97 }
98
99 private void initialize(final Element elem) {
100 synchronized (this) {
101 loading = true;
102 fWidth = 0;
103 fHeight = 0;
104 }
105 int width = 0;
106 int height = 0;
107 boolean customWidth = false;
108 boolean customHeight = false;
109 try {
110 fElement = elem;
111
112
113
114 if (isURL()) {
115 URL src = getSourceURL();
116 if (src != null) {
117 Dictionary cache = (Dictionary) getDocument().getProperty(
118 IMAGE_CACHE_PROPERTY);
119 if (cache != null)
120 fImage = (Image) cache.get(src);
121 else
122 fImage = Toolkit.getDefaultToolkit().getImage(src);
123 }
124 } else {
125
126 /*** ****** Code to load from relative path ************ */
127 String src = (String) fElement.getAttributes().getAttribute(
128 HTML.Attribute.SRC);
129 log.debug("src = " + src);
130
131 if (getMapImages() == null) {
132 fImage = null;
133 } else
134 fImage = (Image) getMapImages().get(src);
135
136 log.debug("fimage:" + fImage);
137 /*** *************************************************** */
138
139 }
140
141
142 height = getIntAttr(HTML.Attribute.HEIGHT, -1);
143 customHeight = (height > 0);
144 if (!customHeight && fImage != null)
145 height = fImage.getHeight(this);
146 if (height <= 0)
147 height = DEFAULT_HEIGHT;
148
149 width = getIntAttr(HTML.Attribute.WIDTH, -1);
150 customWidth = (width > 0);
151 if (!customWidth && fImage != null)
152 width = fImage.getWidth(this);
153 if (width <= 0)
154 width = DEFAULT_WIDTH;
155
156
157 if (fImage != null)
158 if (customWidth && customHeight)
159 Toolkit.getDefaultToolkit().prepareImage(fImage, height, width, this);
160 else
161 Toolkit.getDefaultToolkit().prepareImage(fImage, -1, -1, this);
162
163 } finally {
164 synchronized (this) {
165 loading = false;
166 if (customWidth || fWidth == 0) {
167 fWidth = width;
168 }
169 if (customHeight || fHeight == 0) {
170 fHeight = height;
171 }
172 }
173 }
174 }
175
176 /*** Determines if path is in the form of a URL */
177 private boolean isURL() {
178 String src = (String) fElement.getAttributes().getAttribute(
179 HTML.Attribute.SRC);
180 return src.toLowerCase().startsWith("file")
181 || src.toLowerCase().startsWith("http");
182 }
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 /***
200 * Fetches the attributes to use when rendering. This is implemented to
201 * multiplex the attributes specified in the model with a StyleSheet.
202 * @return The attributes
203 */
204 public AttributeSet getAttributes() {
205 return attr;
206 }
207
208 /*** Is this image within a link? */
209 boolean isLink() {
210
211
212 AttributeSet anchorAttr = (AttributeSet) fElement.getAttributes()
213 .getAttribute(HTML.Tag.A);
214 if (anchorAttr != null) {
215 return anchorAttr.isDefined(HTML.Attribute.HREF);
216 }
217 return false;
218 }
219
220 /*** Returns the size of the border to use. */
221 int getBorder() {
222 return getIntAttr(HTML.Attribute.BORDER, isLink() ? DEFAULT_BORDER : 0);
223 }
224
225 /*** Returns the amount of extra space to add along an axis. */
226 int getSpace(final int axis) {
227 return getIntAttr(axis == X_AXIS ? HTML.Attribute.HSPACE
228 : HTML.Attribute.VSPACE, 0);
229 }
230
231 /*** Returns the border's color, or null if this is not a link. */
232 Color getBorderColor() {
233 StyledDocument doc = (StyledDocument) getDocument();
234 return doc.getForeground(getAttributes());
235 }
236
237 /*** Returns the image's vertical alignment. */
238 float getVerticalAlignment() {
239 String align = (String) fElement.getAttributes().getAttribute(
240 HTML.Attribute.ALIGN);
241 if (align != null) {
242 align = align.toLowerCase();
243 if (align.equals(TOP) || align.equals(TEXTTOP))
244 return 0.0f;
245 else if (align.equals(CENTER) || align.equals(MIDDLE)
246 || align.equals(ABSMIDDLE))
247 return 0.5f;
248 }
249 return 1.0f;
250 }
251
252 boolean hasPixels(final ImageObserver obs) {
253 return fImage != null && fImage.getHeight(obs) > 0
254 && fImage.getWidth(obs) > 0;
255 }
256
257 /***
258 * Return a URL for the image source, or null if it could not be determined.
259 */
260 private URL getSourceURL() {
261 String src = (String) fElement.getAttributes().getAttribute(
262 HTML.Attribute.SRC);
263 if (src == null)
264 return null;
265
266 URL reference = ((HTMLDocument) getDocument()).getBase();
267 try {
268 URL u = new URL(reference, src);
269 return u;
270 } catch (MalformedURLException e) {
271 return null;
272 }
273 }
274
275 /*** Look up an integer-valued attribute. <b>Not </b> recursive. */
276 private int getIntAttr(final HTML.Attribute name, final int deflt) {
277 AttributeSet attr = fElement.getAttributes();
278 if (attr.isDefined(name)) {
279 int i;
280 String val = (String) attr.getAttribute(name);
281 if (val == null)
282 i = deflt;
283 else
284 try {
285 i = Math.max(0, Integer.parseInt(val));
286 } catch (NumberFormatException x) {
287 i = deflt;
288 }
289 return i;
290 }
291 return deflt;
292 }
293
294 /***
295 * Establishes the parent view for this view. Seize this moment to cache the
296 * AWT Container I'm in.
297 * @param parent The parent of this view
298 */
299 public void setParent(final View parent) {
300 super.setParent(parent);
301 fContainer = parent != null ? getContainer() : null;
302 if (parent == null && fComponent != null) {
303 fComponent.getParent().remove(fComponent);
304 fComponent = null;
305 }
306 }
307
308 /***
309 * My attributes may have changed.
310 * @param e DocumentEvent
311 * @param a Shape
312 * @param f View factory
313 */
314 public void changedUpdate(final DocumentEvent e, final Shape a,
315 final ViewFactory f) {
316
317 log.debug("ImageView: changedUpdate begin...");
318 super.changedUpdate(e, a, f);
319 float align = getVerticalAlignment();
320
321 int height = fHeight;
322 int width = fWidth;
323
324 initialize(getElement());
325
326 boolean hChanged = fHeight != height;
327 boolean wChanged = fWidth != width;
328 if (hChanged || wChanged || getVerticalAlignment() != align) {
329
330 log.debug("ImageView: calling preferenceChanged");
331 getParent().preferenceChanged(this, hChanged, wChanged);
332 }
333 log.debug("ImageView: changedUpdate end; valign=" + getVerticalAlignment());
334 }
335
336
337
338 /***
339 * Paints the image.
340 * @param g the rendering surface to use
341 * @param a the allocated region to render into
342 * @see View#paint
343 */
344 public void paint(final Graphics g, final Shape a) {
345 Color oldColor = g.getColor();
346 fBounds = a.getBounds();
347 int border = getBorder();
348 int x = fBounds.x + border + getSpace(X_AXIS);
349 int y = fBounds.y + border + getSpace(Y_AXIS);
350 int width = fWidth;
351 int height = fHeight;
352 int sel = getSelectionState();
353
354
355
356
357
358
359
360
361
362
363 if (!hasPixels(this)) {
364 g.setColor(Color.lightGray);
365 g.drawRect(x, y, width - 1, height - 1);
366 g.setColor(oldColor);
367 loadIcons();
368 Icon icon = fImage == null ? sMissingImageIcon : sPendingImageIcon;
369 if (icon != null)
370 icon.paintIcon(getContainer(), g, x, y);
371 }
372
373
374 if (fImage != null) {
375 g.drawImage(fImage, x, y, width, height, this);
376
377
378
379
380
381
382
383
384
385 }
386
387
388 Color bc = getBorderColor();
389 if (sel == 2) {
390
391 int delta = 2 - border;
392 if (delta > 0) {
393 x += delta;
394 y += delta;
395 width -= delta << 1;
396 height -= delta << 1;
397 border = 2;
398 }
399 bc = null;
400 g.setColor(Color.black);
401
402 g.fillRect(x + width - 5, y + height - 5, 5, 5);
403 }
404
405
406 if (border > 0) {
407 if (bc != null)
408 g.setColor(bc);
409
410 for (int i = 1; i <= border; i++)
411 g.drawRect(x - i, y - i, width - 1 + i + i, height - 1 + i + i);
412 g.setColor(oldColor);
413 }
414 }
415
416 /***
417 * Request that this view be repainted. Assumes the view is still at its
418 * last-drawn location.
419 * @param delay Delay to repaint
420 */
421 protected void repaint(final long delay) {
422 if (fContainer != null && fBounds != null) {
423 fContainer.repaint(delay, fBounds.x, fBounds.y, fBounds.width,
424 fBounds.height);
425 }
426 }
427
428 /***
429 * Determines whether the image is selected, and if it's the only thing
430 * selected.
431 * @return 0 if not selected, 1 if selected, 2 if exclusively selected.
432 * "Exclusive" selection is only returned when editable.
433 */
434 protected int getSelectionState() {
435 int p0 = fElement.getStartOffset();
436 int p1 = fElement.getEndOffset();
437 if (fContainer instanceof JTextComponent) {
438 JTextComponent textComp = (JTextComponent) fContainer;
439 int start = textComp.getSelectionStart();
440 int end = textComp.getSelectionEnd();
441 if (start <= p0 && end >= p1) {
442 if (start == p0 && end == p1 && isEditable())
443 return 2;
444
445 return 1;
446 }
447 }
448 return 0;
449 }
450
451 protected boolean isEditable() {
452 return fContainer instanceof JEditorPane
453 && ((JEditorPane) fContainer).isEditable();
454 }
455
456 /*** Returns the text editor's highlight color. */
457 protected Color getHighlightColor() {
458 JTextComponent textComp = (JTextComponent) fContainer;
459 return textComp.getSelectionColor();
460 }
461
462
463
464
465
466
467
468
469 /***
470 * Update the image.
471 * @param img Image
472 * @param flags Flags
473 * @param x X coordinate
474 * @param y Y coordinate
475 * @param width Width
476 * @param height Height
477 * @return true if no need to repaint
478 */
479 public boolean imageUpdate(final Image img, final int flags, final int x,
480 final int y, final int width, final int height) {
481 if (fImage == null || fImage != img)
482 return false;
483
484
485 if ((flags & (ABORT | ERROR)) != 0) {
486 fImage = null;
487 repaint(0);
488 return false;
489 }
490
491
492 short changed = 0;
493 if ((flags & ImageObserver.HEIGHT) != 0)
494 if (!getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT)) {
495 changed |= 1;
496 }
497 if ((flags & ImageObserver.WIDTH) != 0)
498 if (!getElement().getAttributes().isDefined(HTML.Attribute.WIDTH)) {
499 changed |= 2;
500 }
501 synchronized (this) {
502 if ((changed & 1) == 1) {
503 fWidth = width;
504 }
505 if ((changed & 2) == 2) {
506 fHeight = height;
507 }
508 if (loading) {
509
510
511 return true;
512 }
513 }
514 if (changed != 0) {
515
516 log.debug("ImageView: resized to " + fWidth + "x" + fHeight);
517
518 Document doc = getDocument();
519 try {
520 if (doc instanceof AbstractDocument) {
521 ((AbstractDocument) doc).readLock();
522 }
523 preferenceChanged(this, true, true);
524 } finally {
525 if (doc instanceof AbstractDocument) {
526 ((AbstractDocument) doc).readUnlock();
527 }
528 }
529
530 return true;
531 }
532
533
534 if ((flags & (FRAMEBITS | ALLBITS)) != 0)
535 repaint(0);
536 else if ((flags & SOMEBITS) != 0)
537 if (sIsInc)
538 repaint(sIncRate);
539
540 return ((flags & ALLBITS) == 0);
541 }
542
543
544
545
546
547 private static boolean sIsInc = true;
548 private static int sIncRate = 100;
549
550
551
552 /***
553 * Determines the preferred span for this view along an axis.
554 * @param axis may be either X_AXIS or Y_AXIS
555 * @return the span the view would like to be rendered into. Typically the
556 * view is told to render into the span that is returned, although
557 * there is no guarantee. The parent may choose to resize or break the
558 * view.
559 */
560 public float getPreferredSpan(final int axis) {
561
562 int extra = 2 * (getBorder() + getSpace(axis));
563 switch (axis) {
564 case View.X_AXIS:
565 return fWidth + extra;
566 case View.Y_AXIS:
567 return fHeight + extra;
568 default:
569 throw new IllegalArgumentException("Invalid axis: " + axis);
570 }
571 }
572
573 /***
574 * Determines the desired alignment for this view along an axis. This is
575 * implemented to give the alignment to the bottom of the icon along the y
576 * axis, and the default along the x axis.
577 * @param axis may be either X_AXIS or Y_AXIS
578 * @return the desired alignment. This should be a value between 0.0 and 1.0
579 * where 0 indicates alignment at the origin and 1.0 indicates
580 * alignment to the full span away from the origin. An alignment of
581 * 0.5 would be the center of the view.
582 */
583 public float getAlignment(final int axis) {
584 switch (axis) {
585 case View.Y_AXIS:
586 return getVerticalAlignment();
587 default:
588 return super.getAlignment(axis);
589 }
590 }
591
592 /***
593 * Provides a mapping from the document model coordinate space to the
594 * coordinate space of the view mapped to it.
595 * @param pos the position to convert
596 * @param a the allocated region to render into
597 * @param b ???
598 * @return the bounding box of the given position
599 * @exception BadLocationException if the given position does not represent a
600 * valid location in the associated document
601 * @see View modelToView
602 */
603 public Shape modelToView(final int pos, final Shape a, final Position.Bias b)
604 throws BadLocationException {
605 int p0 = getStartOffset();
606 int p1 = getEndOffset();
607 if ((pos >= p0) && (pos <= p1)) {
608 Rectangle r = a.getBounds();
609 if (pos == p1) {
610 r.x += r.width;
611 }
612 r.width = 0;
613 return r;
614 }
615 return null;
616 }
617
618 /***
619 * Provides a mapping from the view coordinate space to the logical coordinate
620 * space of the model.
621 * @param x the X coordinate
622 * @param y the Y coordinate
623 * @param a the allocated region to render into
624 * @param bias ???
625 * @return the location within the model that best represents the given point
626 * of view
627 * @see View viewToModel
628 */
629 public int viewToModel(final float x, final float y, final Shape a,
630 final Position.Bias[] bias) {
631 Rectangle alloc = (Rectangle) a;
632 if (x < alloc.x + alloc.width) {
633 bias[0] = Position.Bias.Forward;
634 return getStartOffset();
635 }
636 bias[0] = Position.Bias.Backward;
637 return getEndOffset();
638 }
639
640 /***
641 * Set the size of the view. (Ignored.)
642 * @param width the width
643 * @param height the height
644 */
645 public void setSize(final float width, final float height) {
646
647
648 }
649
650 /***
651 * Change the size of this image. This alters the HEIGHT and WIDTH attributes
652 * of the Element and causes a re-layout.
653 */
654 protected void resize(final int width, final int height) {
655 if (width == fWidth && height == fHeight)
656 return;
657
658 fWidth = width;
659 fHeight = height;
660
661
662 MutableAttributeSet attr = new SimpleAttributeSet();
663 attr.addAttribute(HTML.Attribute.WIDTH, Integer.toString(width));
664 attr.addAttribute(HTML.Attribute.HEIGHT, Integer.toString(height));
665 ((StyledDocument) getDocument()).setCharacterAttributes(fElement
666 .getStartOffset(), fElement.getEndOffset(), attr, false);
667 }
668
669
670
671 /***
672 * Select or grow image when clicked.
673 * @param e Mouse event.
674 */
675 public void mousePressed(final MouseEvent e) {
676 Dimension size = fComponent.getSize();
677 if (e.getX() >= size.width - 7 && e.getY() >= size.height - 7
678 && getSelectionState() == 2) {
679
680 log.debug("ImageView: grow!!! Size=" + fWidth + "x" + fHeight);
681 Point loc = fComponent.getLocationOnScreen();
682 fGrowBase = new Point(loc.x + e.getX() - fWidth, loc.y + e.getY()
683 - fHeight);
684 this.fGrowProportionally = e.isShiftDown();
685 } else {
686
687 fGrowBase = null;
688 JTextComponent comp = (JTextComponent) fContainer;
689 int start = fElement.getStartOffset();
690 int end = fElement.getEndOffset();
691 int mark = comp.getCaret().getMark();
692 int dot = comp.getCaret().getDot();
693 if (e.isShiftDown()) {
694
695 if (mark <= start)
696 comp.moveCaretPosition(end);
697 else
698 comp.moveCaretPosition(start);
699 } else {
700
701 if (mark != start)
702 comp.setCaretPosition(start);
703 if (dot != end)
704 comp.moveCaretPosition(end);
705 }
706 }
707 }
708
709 /***
710 * Resize image if initial click was in grow-box:
711 * @param e Mouse event
712 */
713 public void mouseDragged(final MouseEvent e) {
714 if (fGrowBase != null) {
715 Point loc = fComponent.getLocationOnScreen();
716 int width = Math.max(2, loc.x + e.getX() - fGrowBase.x);
717 int height = Math.max(2, loc.y + e.getY() - fGrowBase.y);
718
719 if (e.isShiftDown() && fImage != null) {
720
721 float imgWidth = fImage.getWidth(this);
722 float imgHeight = fImage.getHeight(this);
723 if (imgWidth > 0 && imgHeight > 0) {
724 float prop = imgHeight / imgWidth;
725 float pwidth = height / prop;
726 float pheight = width * prop;
727 if (pwidth > width)
728 width = (int) pwidth;
729 else
730 height = (int) pheight;
731 }
732 }
733
734 resize(width, height);
735 }
736 }
737
738 /***
739 * Mouse event
740 * @param e Mouse event
741 */
742 public void mouseReleased(final MouseEvent e) {
743 fGrowBase = null;
744
745 }
746
747 /***
748 * On double-click, open image properties dialog.
749 * @param e Mouse event
750 */
751 public void mouseClicked(final MouseEvent e) {
752 if (e.getClickCount() == 2) {
753
754 }
755 }
756
757 /***
758 * Mouse event
759 * @param e Mouse event
760 */
761 public void mouseEntered(final MouseEvent e) {
762 }
763
764 /***
765 * Mouse event
766 * @param e Mouse event
767 */
768 public void mouseMoved(final MouseEvent e) {
769 }
770
771 /***
772 * Mouse event
773 * @param e Mouse event
774 */
775 public void mouseExited(final MouseEvent e) {
776 }
777
778
779
780 private Icon makeIcon(final String gifFile) throws IOException {
781
782
783
784
785
786
787 InputStream resource = MyImageView.class.getResourceAsStream(gifFile);
788
789 if (resource == null) {
790 log.error(MyImageView.class.getName() + "/" + gifFile + " not found.");
791 return null;
792 }
793 BufferedInputStream in = new BufferedInputStream(resource);
794 ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
795 byte[] buffer = new byte[1024];
796 int n;
797 while ((n = in.read(buffer)) > 0) {
798 out.write(buffer, 0, n);
799 }
800 in.close();
801 out.flush();
802
803 buffer = out.toByteArray();
804 if (buffer.length == 0) {
805 log.warn("warning: " + gifFile + " is zero-length");
806 return null;
807 }
808 return new ImageIcon(buffer);
809 }
810
811 private void loadIcons() {
812 try {
813 if (sPendingImageIcon == null)
814 sPendingImageIcon = makeIcon(PENDING_IMAGE_SRC);
815 if (sMissingImageIcon == null)
816 sMissingImageIcon = makeIcon(MISSING_IMAGE_SRC);
817 } catch (IOException x) {
818 log.error("ImageView: Couldn't load image icons");
819 }
820 }
821
822 protected StyleSheet getStyleSheet() {
823 HTMLDocument doc = (HTMLDocument) getDocument();
824 return doc.getStyleSheet();
825 }
826
827
828
829 /***
830 * Get the map of the images.
831 * @return Returns the mapImages
832 */
833 public Map getMapImages() {
834 return mapImages;
835 }
836
837 /***
838 * Set the map of the images.
839 * @param mapImages The mapImages to set
840 */
841 public void setMapImages(final Map mapImages) {
842 this.mapImages = mapImages;
843 }
844
845
846
847 private Map mapImages;
848
849 private AttributeSet attr;
850 private Element fElement;
851 private Image fImage;
852 private int fHeight, fWidth;
853 private Container fContainer;
854 private Rectangle fBounds;
855 private Component fComponent;
856 private Point fGrowBase;
857 private boolean fGrowProportionally;
858
859 /***
860 * Set to true, while the receiver is locked, to indicate the reciever is
861 * loading the image. This is used in imageUpdate.
862 */
863 private boolean loading;
864
865
866
867 private static Icon sPendingImageIcon, sMissingImageIcon;
868 private static final String PENDING_IMAGE_SRC = "icons/image-delayed.gif",
869
870
871
872 MISSING_IMAGE_SRC = "icons/image-failed.gif";
873
874 private static final boolean DEBUG = false;
875
876
877 static final String IMAGE_CACHE_PROPERTY = "imageCache";
878
879
880 private static final int DEFAULT_WIDTH = 32, DEFAULT_HEIGHT = 32,
881
882 DEFAULT_BORDER = 2;
883
884 }
885