blob: 003121489f4c2ab883e8b72e7d28ea8d2eaf9f6c [file] [log] [blame]
/*
* Copyright 2007 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.user.client.ui;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.impl.ClippedImageImpl;
import com.google.gwt.core.client.GWT;
import java.util.HashMap;
/**
* A widget that displays the image at a given URL. The image can be in
* 'unclipped' mode (the default) or 'clipped' mode. In clipped mode, a viewport
* is overlaid on top of the image so that a subset of the image will be
* displayed. In unclipped mode, there is no viewport - the entire image will be
* visible. Whether an image is in clipped or unclipped mode depends on how the
* image is constructed, and how it is transformed after construction. Methods
* will operate differently depending on the mode that the image is in. These
* differences are detailed in the documentation for each method.
*
* <p>
* If an image transitions between clipped mode and unclipped mode, any
* {@link Element}-specific attributes added by the user (including style
* attributes, style names, and style modifiers), except for event listeners,
* will be lost.
* </p>
*
* <h3>CSS Style Rules</h3>
* <ul class="css">
* <li>.gwt-Image { }</li>
* </ul>
*
* Tranformations between clipped and unclipped state will result in a loss of
* any style names that were set/added; the only style names that are preserved
* are those that are mentioned in the static CSS style rules. Due to
* browser-specific HTML constructions needed to achieve the clipping effect,
* certain CSS attributes, such as padding and background, may not work as
* expected when an image is in clipped mode. These limitations can usually be
* easily worked around by encapsulating the image in a container widget that
* can itself be styled.
*
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.ImageExample}
* </p>
*/
public class Image extends Widget implements SourcesClickEvents,
SourcesLoadEvents, SourcesMouseEvents, SourcesMouseWheelEvents {
/**
* Implementation of behaviors associated with the clipped state of an image.
*/
private static class ClippedState extends State {
private static final ClippedImageImpl impl = GWT.create(ClippedImageImpl.class);
private int height = 0;
private int left = 0;
private int top = 0;
private String url = null;
private int width = 0;
ClippedState(Image image, String url, int left, int top, int width,
int height) {
this.left = left;
this.top = top;
this.width = width;
this.height = height;
this.url = url;
image.replaceElement(impl.createStructure(url, left, top, width, height));
image.sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONMOUSEWHEEL);
fireSyntheticLoadEvent(image);
}
@Override
public int getHeight(Image image) {
return height;
}
@Override
public int getOriginLeft() {
return left;
}
@Override
public int getOriginTop() {
return top;
}
@Override
public String getUrl(Image image) {
return url;
}
@Override
public int getWidth(Image image) {
return width;
}
@Override
public void setUrl(Image image, String url) {
image.changeState(new UnclippedState(image, url));
}
@Override
public void setUrlAndVisibleRect(Image image, String url, int left,
int top, int width, int height) {
if (!this.url.equals(url) || this.left != left || this.top != top
|| this.width != width || this.height != height) {
this.url = url;
this.left = left;
this.top = top;
this.width = width;
this.height = height;
impl.adjust(image.getElement(), url, left, top, width, height);
fireSyntheticLoadEvent(image);
}
}
@Override
public void setVisibleRect(Image image, int left, int top, int width,
int height) {
/*
* In the event that the clipping rectangle has not changed, we want to
* skip all of the work required with a getImpl().adjust, and we do not
* want to fire a load event.
*/
if (this.left != left || this.top != top || this.width != width
|| this.height != height) {
this.left = left;
this.top = top;
this.width = width;
this.height = height;
impl.adjust(image.getElement(), url, left, top, width, height);
fireSyntheticLoadEvent(image);
}
}
/* This method is used only by unit tests */
@Override
protected String getStateName() {
return "clipped";
}
private void fireSyntheticLoadEvent(final Image image) {
/*
* We need to synthesize a load event, because the native events that are
* fired would correspond to the loading of clear.cache.gif, which is
* incorrect. A native event would not even fire in Internet Explorer,
* because the root element is a wrapper element around the <img> element.
* Since we are synthesizing a load event, we do not need to sink the
* onload event.
*
* We use a deferred command here to simulate the native version of the
* load event as closely as possible. In the native event case, it is
* unlikely that a second load event would occur while you are in the load
* event handler.
*/
DeferredCommand.addCommand(new Command() {
public void execute() {
if (image.loadListeners != null) {
image.loadListeners.fireLoad(image);
}
}
});
}
}
/**
* Abstract class which is used to hold the state associated with an image
* object.
*/
private abstract static class State {
public abstract int getHeight(Image image);
public abstract int getOriginLeft();
public abstract int getOriginTop();
public abstract String getUrl(Image image);
public abstract int getWidth(Image image);
public abstract void setUrl(Image image, String url);
public abstract void setUrlAndVisibleRect(Image image, String url,
int left, int top, int width, int height);
public abstract void setVisibleRect(Image image, int left, int top,
int width, int height);
// This method is used only by unit tests.
protected abstract String getStateName();
}
/**
* Implementation of behaviors associated with the unclipped state of an
* image.
*/
private static class UnclippedState extends State {
UnclippedState(Image image) {
image.replaceElement(DOM.createImg());
image.sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONLOAD
| Event.ONERROR | Event.ONMOUSEWHEEL);
}
UnclippedState(Image image, String url) {
this(image);
setUrl(image, url);
}
@Override
public int getHeight(Image image) {
return DOM.getElementPropertyInt(image.getElement(), "height");
}
@Override
public int getOriginLeft() {
return 0;
}
@Override
public int getOriginTop() {
return 0;
}
@Override
public String getUrl(Image image) {
return DOM.getImgSrc(image.getElement());
}
@Override
public int getWidth(Image image) {
return DOM.getElementPropertyInt(image.getElement(), "width");
}
@Override
public void setUrl(Image image, String url) {
DOM.setImgSrc(image.getElement(), url);
}
@Override
public void setUrlAndVisibleRect(Image image, String url, int left,
int top, int width, int height) {
image.changeState(new ClippedState(image, url, left, top, width, height));
}
@Override
public void setVisibleRect(Image image, int left, int top, int width,
int height) {
image.changeState(new ClippedState(image, getUrl(image), left, top,
width, height));
}
// This method is used only by unit tests.
@Override
protected String getStateName() {
return "unclipped";
}
}
/**
* This map is used to store prefetched images. If a reference is not kept to
* the prefetched image objects, they can get garbage collected, which
* sometimes keeps them from getting fully fetched.
*/
private static HashMap<String, Element> prefetchImages = new HashMap<String, Element>();
/**
* Causes the browser to pre-fetch the image at a given URL.
*
* @param url the URL of the image to be prefetched
*/
public static void prefetch(String url) {
Element img = DOM.createImg();
DOM.setImgSrc(img, url);
prefetchImages.put(url, img);
}
private ClickListenerCollection clickListeners;
private LoadListenerCollection loadListeners;
private MouseListenerCollection mouseListeners;
private MouseWheelListenerCollection mouseWheelListeners;
private State state;
/**
* Creates an empty image.
*/
public Image() {
changeState(new UnclippedState(this));
setStyleName("gwt-Image");
}
/**
* Creates an image with a specified URL. The load event will be fired once
* the image at the given URL has been retrieved by the browser.
*
* @param url the URL of the image to be displayed
*/
public Image(String url) {
changeState(new UnclippedState(this, url));
setStyleName("gwt-Image");
}
/**
* Creates a clipped image with a specified URL and visibility rectangle. The
* visibility rectangle is declared relative to the the rectangle which
* encompasses the entire image, which has an upper-left vertex of (0,0). The
* load event will be fired immediately after the object has been constructed
* (i.e. potentially before the image has been loaded in the browser). Since
* the width and height are specified explicitly by the user, this behavior
* will not cause problems with retrieving the width and height of a clipped
* image in a load event handler.
*
* @param url the URL of the image to be displayed
* @param left the horizontal co-ordinate of the upper-left vertex of the
* visibility rectangle
* @param top the vertical co-ordinate of the upper-left vertex of the
* visibility rectangle
* @param width the width of the visibility rectangle
* @param height the height of the visibility rectangle
*/
public Image(String url, int left, int top, int width, int height) {
changeState(new ClippedState(this, url, left, top, width, height));
setStyleName("gwt-Image");
}
public void addClickListener(ClickListener listener) {
if (clickListeners == null) {
clickListeners = new ClickListenerCollection();
}
clickListeners.add(listener);
}
public void addLoadListener(LoadListener listener) {
if (loadListeners == null) {
loadListeners = new LoadListenerCollection();
}
loadListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
if (mouseListeners == null) {
mouseListeners = new MouseListenerCollection();
}
mouseListeners.add(listener);
}
public void addMouseWheelListener(MouseWheelListener listener) {
if (mouseWheelListeners == null) {
mouseWheelListeners = new MouseWheelListenerCollection();
}
mouseWheelListeners.add(listener);
}
/**
* Gets the height of the image. When the image is in the unclipped state, the
* height of the image is not known until the image has been loaded (i.e. load
* event has been fired for the image).
*
* @return the height of the image, or 0 if the height is unknown
*/
public int getHeight() {
return state.getHeight(this);
}
/**
* Gets the horizontal co-ordinate of the upper-left vertex of the image's
* visibility rectangle. If the image is in the unclipped state, then the
* visibility rectangle is assumed to be the rectangle which encompasses the
* entire image, which has an upper-left vertex of (0,0).
*
* @return the horizontal co-ordinate of the upper-left vertex of the image's
* visibility rectangle
*/
public int getOriginLeft() {
return state.getOriginLeft();
}
/**
* Gets the vertical co-ordinate of the upper-left vertex of the image's
* visibility rectangle. If the image is in the unclipped state, then the
* visibility rectangle is assumed to be the rectangle which encompasses the
* entire image, which has an upper-left vertex of (0,0).
*
* @return the vertical co-ordinate of the upper-left vertex of the image's
* visibility rectangle
*/
public int getOriginTop() {
return state.getOriginTop();
}
/**
* Gets the URL of the image. The URL that is returned is not necessarily the
* URL that was passed in by the user. It may have been transformed to an
* absolute URL.
*
* @return the image URL
*/
public String getUrl() {
return state.getUrl(this);
}
/**
* Gets the width of the image. When the image is in the unclipped state, the
* width of the image is not known until the image has been loaded (i.e. load
* event has been fired for the image).
*
* @return the width of the image, or 0 if the width is unknown
*/
public int getWidth() {
return state.getWidth(this);
}
@Override
public void onBrowserEvent(Event event) {
switch (DOM.eventGetType(event)) {
case Event.ONCLICK: {
if (clickListeners != null) {
clickListeners.fireClick(this);
}
break;
}
case Event.ONMOUSEDOWN:
case Event.ONMOUSEUP:
case Event.ONMOUSEMOVE:
case Event.ONMOUSEOVER:
case Event.ONMOUSEOUT: {
if (mouseListeners != null) {
mouseListeners.fireMouseEvent(this, event);
}
break;
}
case Event.ONMOUSEWHEEL:
if (mouseWheelListeners != null) {
mouseWheelListeners.fireMouseWheelEvent(this, event);
}
break;
case Event.ONLOAD: {
if (loadListeners != null) {
loadListeners.fireLoad(this);
}
break;
}
case Event.ONERROR: {
if (loadListeners != null) {
loadListeners.fireError(this);
}
break;
}
}
}
public void removeClickListener(ClickListener listener) {
if (clickListeners != null) {
clickListeners.remove(listener);
}
}
public void removeLoadListener(LoadListener listener) {
if (loadListeners != null) {
loadListeners.remove(listener);
}
}
public void removeMouseListener(MouseListener listener) {
if (mouseListeners != null) {
mouseListeners.remove(listener);
}
}
public void removeMouseWheelListener(MouseWheelListener listener) {
if (mouseWheelListeners != null) {
mouseWheelListeners.remove(listener);
}
}
/**
* Sets the URL of the image to be displayed. If the image is in the clipped
* state, a call to this method will cause a transition of the image to the
* unclipped state. Regardless of whether or not the image is in the clipped
* or unclipped state, a load event will be fired.
*
* @param url the image URL
*/
public void setUrl(String url) {
state.setUrl(this, url);
}
/**
* Sets the url and the visibility rectangle for the image at the same time. A
* single load event will be fired if either the incoming url or visiblity
* rectangle co-ordinates differ from the image's current url or current
* visibility rectangle co-ordinates. If the image is currently in the
* unclipped state, a call to this method will cause a transition to the
* clipped state.
*
* @param url the image URL
* @param left the horizontal coordinate of the upper-left vertex of the
* visibility rectangle
* @param top the vertical coordinate of the upper-left vertex of the
* visibility rectangle
* @param width the width of the visibility rectangle
* @param height the height of the visibility rectangle
*/
public void setUrlAndVisibleRect(String url, int left, int top, int width,
int height) {
state.setUrlAndVisibleRect(this, url, left, top, width, height);
}
/**
* Sets the visibility rectangle of an image. The visibility rectangle is
* declared relative to the the rectangle which encompasses the entire image,
* which has an upper-left vertex of (0,0). Provided that any of the left,
* top, width, and height parameters are different than the those values that
* are currently set for the image, a load event will be fired. If the image
* is in the unclipped state, a call to this method will cause a transition of
* the image to the clipped state. This transition will cause a load event to
* fire.
*
* @param left the horizontal coordinate of the upper-left vertex of the
* visibility rectangle
* @param top the vertical coordinate of the upper-left vertex of the
* visibility rectangle
* @param width the width of the visibility rectangle
* @param height the height of the visibility rectangle
*/
public void setVisibleRect(int left, int top, int width, int height) {
state.setVisibleRect(this, left, top, width, height);
}
private void changeState(State newState) {
state = newState;
}
}