001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.client.ui;
017:
018: import com.google.gwt.user.client.DOM;
019: import com.google.gwt.user.client.DeferredCommand;
020: import com.google.gwt.user.client.Command;
021: import com.google.gwt.user.client.Element;
022: import com.google.gwt.user.client.Event;
023: import com.google.gwt.user.client.ui.impl.ClippedImageImpl;
024: import com.google.gwt.core.client.GWT;
025:
026: import java.util.HashMap;
027:
028: /**
029: * A widget that displays the image at a given URL. The image can be in
030: * 'unclipped' mode (the default) or 'clipped' mode. In clipped mode, a viewport
031: * is overlaid on top of the image so that a subset of the image will be
032: * displayed. In unclipped mode, there is no viewport - the entire image will be
033: * visible. Whether an image is in clipped or unclipped mode depends on how the
034: * image is constructed, and how it is transformed after construction. Methods
035: * will operate differently depending on the mode that the image is in. These
036: * differences are detailed in the documentation for each method.
037: *
038: * <p>
039: * If an image transitions between clipped mode and unclipped mode, any
040: * {@link Element}-specific attributes added by the user (including style
041: * attributes, style names, and style modifiers), except for event listeners,
042: * will be lost.
043: * </p>
044: *
045: * <h3>CSS Style Rules</h3>
046: * <ul class="css">
047: * <li>.gwt-Image { }</li>
048: * </ul>
049: *
050: * Tranformations between clipped and unclipped state will result in a loss of
051: * any style names that were set/added; the only style names that are preserved
052: * are those that are mentioned in the static CSS style rules. Due to
053: * browser-specific HTML constructions needed to achieve the clipping effect,
054: * certain CSS attributes, such as padding and background, may not work as
055: * expected when an image is in clipped mode. These limitations can usually be
056: * easily worked around by encapsulating the image in a container widget that
057: * can itself be styled.
058: *
059: * <p>
060: * <h3>Example</h3>
061: * {@example com.google.gwt.examples.ImageExample}
062: * </p>
063: */
064: public class Image extends Widget implements SourcesClickEvents,
065: SourcesLoadEvents, SourcesMouseEvents, SourcesMouseWheelEvents {
066:
067: /**
068: * Abstract class which is used to hold the state associated with an image
069: * object.
070: */
071: private abstract static class State {
072:
073: public abstract int getHeight(Image image);
074:
075: public abstract int getOriginLeft();
076:
077: public abstract int getOriginTop();
078:
079: public abstract String getUrl(Image image);
080:
081: public abstract int getWidth(Image image);
082:
083: public abstract void setUrl(Image image, String url);
084:
085: public abstract void setUrlAndVisibleRect(Image image,
086: String url, int left, int top, int width, int height);
087:
088: public abstract void setVisibleRect(Image image, int left,
089: int top, int width, int height);
090:
091: // This method is used only by unit tests.
092: protected abstract String getStateName();
093: }
094:
095: /**
096: * Implementation of behaviors associated with the unclipped state of an
097: * image.
098: */
099: private static class UnclippedState extends State {
100:
101: UnclippedState(Image image) {
102: image.setElement(DOM.createImg());
103: image
104: .sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS
105: | Event.ONLOAD | Event.ONERROR
106: | Event.ONMOUSEWHEEL);
107: }
108:
109: UnclippedState(Image image, String url) {
110: this (image);
111: setUrl(image, url);
112: }
113:
114: @Override
115: public int getHeight(Image image) {
116: return DOM.getElementPropertyInt(image.getElement(),
117: "height");
118: }
119:
120: @Override
121: public int getOriginLeft() {
122: return 0;
123: }
124:
125: @Override
126: public int getOriginTop() {
127: return 0;
128: }
129:
130: @Override
131: public String getUrl(Image image) {
132: return DOM.getImgSrc(image.getElement());
133: }
134:
135: @Override
136: public int getWidth(Image image) {
137: return DOM.getElementPropertyInt(image.getElement(),
138: "width");
139: }
140:
141: @Override
142: public void setUrl(Image image, String url) {
143: DOM.setImgSrc(image.getElement(), url);
144: }
145:
146: @Override
147: public void setUrlAndVisibleRect(Image image, String url,
148: int left, int top, int width, int height) {
149: image.changeState(new ClippedState(image, url, left, top,
150: width, height));
151: }
152:
153: @Override
154: public void setVisibleRect(Image image, int left, int top,
155: int width, int height) {
156: image.changeState(new ClippedState(image, getUrl(image),
157: left, top, width, height));
158: }
159:
160: // This method is used only by unit tests.
161: @Override
162: protected String getStateName() {
163: return "unclipped";
164: }
165: }
166:
167: /**
168: * Implementation of behaviors associated with the clipped state of an image.
169: */
170: private static class ClippedState extends State {
171:
172: private static final ClippedImageImpl impl = GWT
173: .create(ClippedImageImpl.class);
174:
175: private int left = 0;
176: private int top = 0;
177: private int width = 0;
178: private int height = 0;
179: private String url = null;
180:
181: ClippedState(Image image, String url, int left, int top,
182: int width, int height) {
183: this .left = left;
184: this .top = top;
185: this .width = width;
186: this .height = height;
187: this .url = url;
188: image.setElement(impl.createStructure(url, left, top,
189: width, height));
190: image.sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS
191: | Event.ONMOUSEWHEEL);
192: fireSyntheticLoadEvent(image);
193: }
194:
195: private void fireSyntheticLoadEvent(final Image image) {
196: /*
197: * We need to synthesize a load event, because the native events that are
198: * fired would correspond to the loading of clear.cache.gif, which is
199: * incorrect. A native event would not even fire in Internet Explorer,
200: * because the root element is a wrapper element around the <img> element.
201: * Since we are synthesizing a load event, we do not need to sink the
202: * onload event.
203: *
204: * We use a deferred command here to simulate the native version of the
205: * load event as closely as possible. In the native event case, it is
206: * unlikely that a second load event would occur while you are in the load
207: * event handler.
208: */
209: DeferredCommand.addCommand(new Command() {
210: public void execute() {
211: if (image.loadListeners != null) {
212: image.loadListeners.fireLoad(image);
213: }
214: }
215: });
216: }
217:
218: @Override
219: public int getHeight(Image image) {
220: return height;
221: }
222:
223: @Override
224: public int getOriginLeft() {
225: return left;
226: }
227:
228: @Override
229: public int getOriginTop() {
230: return top;
231: }
232:
233: @Override
234: public String getUrl(Image image) {
235: return url;
236: }
237:
238: @Override
239: public int getWidth(Image image) {
240: return width;
241: }
242:
243: @Override
244: public void setUrl(Image image, String url) {
245: image.changeState(new UnclippedState(image, url));
246: }
247:
248: @Override
249: public void setUrlAndVisibleRect(Image image, String url,
250: int left, int top, int width, int height) {
251: if (!this .url.equals(url) || this .left != left
252: || this .top != top || this .width != width
253: || this .height != height) {
254:
255: this .url = url;
256: this .left = left;
257: this .top = top;
258: this .width = width;
259: this .height = height;
260:
261: impl.adjust(image.getElement(), url, left, top, width,
262: height);
263: fireSyntheticLoadEvent(image);
264: }
265: }
266:
267: @Override
268: public void setVisibleRect(Image image, int left, int top,
269: int width, int height) {
270: /*
271: * In the event that the clipping rectangle has not changed, we want to
272: * skip all of the work required with a getImpl().adjust, and we do not
273: * want to fire a load event.
274: */
275: if (this .left != left || this .top != top
276: || this .width != width || this .height != height) {
277:
278: this .left = left;
279: this .top = top;
280: this .width = width;
281: this .height = height;
282:
283: impl.adjust(image.getElement(), url, left, top, width,
284: height);
285: fireSyntheticLoadEvent(image);
286: }
287: }
288:
289: /* This method is used only by unit tests */
290: @Override
291: protected String getStateName() {
292: return "clipped";
293: }
294: }
295:
296: /**
297: * This map is used to store prefetched images. If a reference is not kept to
298: * the prefetched image objects, they can get garbage collected, which
299: * sometimes keeps them from getting fully fetched.
300: */
301: private static HashMap<String, Element> prefetchImages = new HashMap<String, Element>();
302:
303: /**
304: * Causes the browser to pre-fetch the image at a given URL.
305: *
306: * @param url the URL of the image to be prefetched
307: */
308: public static void prefetch(String url) {
309: Element img = DOM.createImg();
310: DOM.setImgSrc(img, url);
311: prefetchImages.put(url, img);
312: }
313:
314: private ClickListenerCollection clickListeners;
315: private LoadListenerCollection loadListeners;
316: private MouseListenerCollection mouseListeners;
317: private MouseWheelListenerCollection mouseWheelListeners;
318:
319: private State state;
320:
321: /**
322: * Creates an empty image.
323: */
324: public Image() {
325: changeState(new UnclippedState(this ));
326: setStyleName("gwt-Image");
327: }
328:
329: /**
330: * Creates an image with a specified URL. The load event will be fired once
331: * the image at the given URL has been retrieved by the browser.
332: *
333: * @param url the URL of the image to be displayed
334: */
335: public Image(String url) {
336: changeState(new UnclippedState(this , url));
337: setStyleName("gwt-Image");
338: }
339:
340: /**
341: * Creates a clipped image with a specified URL and visibility rectangle. The
342: * visibility rectangle is declared relative to the the rectangle which
343: * encompasses the entire image, which has an upper-left vertex of (0,0). The
344: * load event will be fired immediately after the object has been constructed
345: * (i.e. potentially before the image has been loaded in the browser). Since
346: * the width and height are specified explicitly by the user, this behavior
347: * will not cause problems with retrieving the width and height of a clipped
348: * image in a load event handler.
349: *
350: * @param url the URL of the image to be displayed
351: * @param left the horizontal co-ordinate of the upper-left vertex of the
352: * visibility rectangle
353: * @param top the vertical co-ordinate of the upper-left vertex of the
354: * visibility rectangle
355: * @param width the width of the visibility rectangle
356: * @param height the height of the visibility rectangle
357: */
358: public Image(String url, int left, int top, int width, int height) {
359: changeState(new ClippedState(this , url, left, top, width,
360: height));
361: setStyleName("gwt-Image");
362: }
363:
364: public void addClickListener(ClickListener listener) {
365: if (clickListeners == null) {
366: clickListeners = new ClickListenerCollection();
367: }
368: clickListeners.add(listener);
369: }
370:
371: public void addLoadListener(LoadListener listener) {
372: if (loadListeners == null) {
373: loadListeners = new LoadListenerCollection();
374: }
375: loadListeners.add(listener);
376: }
377:
378: public void addMouseListener(MouseListener listener) {
379: if (mouseListeners == null) {
380: mouseListeners = new MouseListenerCollection();
381: }
382: mouseListeners.add(listener);
383: }
384:
385: public void addMouseWheelListener(MouseWheelListener listener) {
386: if (mouseWheelListeners == null) {
387: mouseWheelListeners = new MouseWheelListenerCollection();
388: }
389: mouseWheelListeners.add(listener);
390: }
391:
392: /**
393: * Gets the height of the image. When the image is in the unclipped state, the
394: * height of the image is not known until the image has been loaded (i.e. load
395: * event has been fired for the image).
396: *
397: * @return the height of the image, or 0 if the height is unknown
398: */
399: public int getHeight() {
400: return state.getHeight(this );
401: }
402:
403: /**
404: * Gets the horizontal co-ordinate of the upper-left vertex of the image's
405: * visibility rectangle. If the image is in the unclipped state, then the
406: * visibility rectangle is assumed to be the rectangle which encompasses the
407: * entire image, which has an upper-left vertex of (0,0).
408: *
409: * @return the horizontal co-ordinate of the upper-left vertex of the image's
410: * visibility rectangle
411: */
412: public int getOriginLeft() {
413: return state.getOriginLeft();
414: }
415:
416: /**
417: * Gets the vertical co-ordinate of the upper-left vertex of the image's
418: * visibility rectangle. If the image is in the unclipped state, then the
419: * visibility rectangle is assumed to be the rectangle which encompasses the
420: * entire image, which has an upper-left vertex of (0,0).
421: *
422: * @return the vertical co-ordinate of the upper-left vertex of the image's
423: * visibility rectangle
424: */
425: public int getOriginTop() {
426: return state.getOriginTop();
427: }
428:
429: /**
430: * Gets the URL of the image. The URL that is returned is not necessarily the
431: * URL that was passed in by the user. It may have been transformed to an
432: * absolute URL.
433: *
434: * @return the image URL
435: */
436: public String getUrl() {
437: return state.getUrl(this );
438: }
439:
440: /**
441: * Gets the width of the image. When the image is in the unclipped state, the
442: * width of the image is not known until the image has been loaded (i.e. load
443: * event has been fired for the image).
444: *
445: * @return the width of the image, or 0 if the width is unknown
446: */
447: public int getWidth() {
448: return state.getWidth(this );
449: }
450:
451: @Override
452: public void onBrowserEvent(Event event) {
453: switch (DOM.eventGetType(event)) {
454: case Event.ONCLICK: {
455: if (clickListeners != null) {
456: clickListeners.fireClick(this );
457: }
458: break;
459: }
460: case Event.ONMOUSEDOWN:
461: case Event.ONMOUSEUP:
462: case Event.ONMOUSEMOVE:
463: case Event.ONMOUSEOVER:
464: case Event.ONMOUSEOUT: {
465: if (mouseListeners != null) {
466: mouseListeners.fireMouseEvent(this , event);
467: }
468: break;
469: }
470: case Event.ONMOUSEWHEEL:
471: if (mouseWheelListeners != null) {
472: mouseWheelListeners.fireMouseWheelEvent(this , event);
473: }
474: break;
475: case Event.ONLOAD: {
476: if (loadListeners != null) {
477: loadListeners.fireLoad(this );
478: }
479: break;
480: }
481: case Event.ONERROR: {
482: if (loadListeners != null) {
483: loadListeners.fireError(this );
484: }
485: break;
486: }
487: }
488: }
489:
490: public void removeClickListener(ClickListener listener) {
491: if (clickListeners != null) {
492: clickListeners.remove(listener);
493: }
494: }
495:
496: public void removeLoadListener(LoadListener listener) {
497: if (loadListeners != null) {
498: loadListeners.remove(listener);
499: }
500: }
501:
502: public void removeMouseListener(MouseListener listener) {
503: if (mouseListeners != null) {
504: mouseListeners.remove(listener);
505: }
506: }
507:
508: public void removeMouseWheelListener(MouseWheelListener listener) {
509: if (mouseWheelListeners != null) {
510: mouseWheelListeners.remove(listener);
511: }
512: }
513:
514: /**
515: * Sets the URL of the image to be displayed. If the image is in the clipped
516: * state, a call to this method will cause a transition of the image to the
517: * unclipped state. Regardless of whether or not the image is in the clipped
518: * or unclipped state, a load event will be fired.
519: *
520: * @param url the image URL
521: */
522: public void setUrl(String url) {
523: state.setUrl(this , url);
524: }
525:
526: /**
527: * Sets the url and the visibility rectangle for the image at the same time. A
528: * single load event will be fired if either the incoming url or visiblity
529: * rectangle co-ordinates differ from the image's current url or current
530: * visibility rectangle co-ordinates. If the image is currently in the
531: * unclipped state, a call to this method will cause a transition to the
532: * clipped state.
533: *
534: * @param url the image URL
535: * @param left the horizontal coordinate of the upper-left vertex of the
536: * visibility rectangle
537: * @param top the vertical coordinate of the upper-left vertex of the
538: * visibility rectangle
539: * @param width the width of the visibility rectangle
540: * @param height the height of the visibility rectangle
541: */
542: public void setUrlAndVisibleRect(String url, int left, int top,
543: int width, int height) {
544: state.setUrlAndVisibleRect(this , url, left, top, width, height);
545: }
546:
547: /**
548: * Sets the visibility rectangle of an image. The visibility rectangle is
549: * declared relative to the the rectangle which encompasses the entire image,
550: * which has an upper-left vertex of (0,0). Provided that any of the left,
551: * top, width, and height parameters are different than the those values that
552: * are currently set for the image, a load event will be fired. If the image
553: * is in the unclipped state, a call to this method will cause a transition of
554: * the image to the clipped state. This transition will cause a load event to
555: * fire.
556: *
557: * @param left the horizontal coordinate of the upper-left vertex of the
558: * visibility rectangle
559: * @param top the vertical coordinate of the upper-left vertex of the
560: * visibility rectangle
561: * @param width the width of the visibility rectangle
562: * @param height the height of the visibility rectangle
563: */
564: public void setVisibleRect(int left, int top, int width, int height) {
565: state.setVisibleRect(this , left, top, width, height);
566: }
567:
568: private void changeState(State newState) {
569: state = newState;
570: }
571: }
|