/** * Copyright 2010 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 org.waveprotocol.wave.client.doodad.attachment.render; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.dom.client.StyleInjector; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ErrorEvent; import com.google.gwt.event.dom.client.ErrorHandler; import com.google.gwt.event.dom.client.LoadEvent; import com.google.gwt.event.dom.client.LoadHandler; import com.google.gwt.event.dom.client.MouseOutEvent; import com.google.gwt.event.dom.client.MouseOutHandler; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.dom.client.MouseOverHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.CssResource.NotStrict; import com.google.gwt.resources.client.DataResource; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.resources.client.ImageResource.ImageOptions; import com.google.gwt.resources.client.ImageResource.RepeatStyle; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; import org.waveprotocol.wave.client.common.util.DomHelper; import org.waveprotocol.wave.client.common.util.UserAgent; import org.waveprotocol.wave.client.common.webdriver.DebugClassHelper; import org.waveprotocol.wave.client.scheduler.ScheduleCommand; import org.waveprotocol.wave.client.scheduler.Scheduler; import org.waveprotocol.wave.client.widget.button.ButtonFactory; import org.waveprotocol.wave.client.widget.button.ToggleButton.ToggleButtonListener; import org.waveprotocol.wave.client.widget.button.ToggleButtonWidget; import org.waveprotocol.wave.client.widget.button.icon.IconButtonTemplate.IconButtonStyle; import org.waveprotocol.wave.client.widget.progress.ProgressWidget; /** * Widget that implements a thumbnail structure. * Package-private as used only by ImageThumbnail. * */ // TODO(user): replace with no widgets, only lightweight elements class ImageThumbnailWidget extends Composite implements ImageThumbnailView { /** ClientBundle */ interface Resources extends ClientBundle { /** Css */ interface Css extends CssResource { /** Class for the thumbnail as a whole */ String imageThumbnail(); /** Class for the actual image */ String image(); /** Class applied to the progress widget */ String progress(); /** Class for the progress button */ String thumbSizeButton(); } /** Css resource */ @Source("Thumbnail.css") @NotStrict // TODO(user): make Strict by including all classes in the CssResource public Css css(); /** Thumbnail images */ @Source("thumb-n-2.png") @ImageOptions(repeatStyle = RepeatStyle.Horizontal) ImageResource chromeNorth(); @Source("thumb-ne-2.png") @ImageOptions(flipRtl = true) ImageResource chromeNorthEast(); @Source("thumb-e-2.png") @ImageOptions(repeatStyle = RepeatStyle.Vertical, flipRtl = true) ImageResource chromeEast(); @Source("thumb-se-2.png") @ImageOptions(flipRtl = true) ImageResource chromeSouthEast(); @Source("thumb-s-2.png") @ImageOptions(repeatStyle = RepeatStyle.Horizontal) ImageResource chromeSouth(); @Source("thumb-sw-2.png") @ImageOptions(flipRtl = true) ImageResource chromeSouthWest(); @Source("thumb-w-2.png") @ImageOptions(repeatStyle = RepeatStyle.Vertical, flipRtl = true) ImageResource chromeWest(); @Source("thumb-nw-2.png") @ImageOptions(flipRtl = true) ImageResource chromeNorthWest(); @Source("thumb-c-2.png") @ImageOptions(repeatStyle = RepeatStyle.Horizontal) ImageResource chromeCenter(); @Source("error.png") @ImageOptions(flipRtl = true) ImageResource errorAttachment(); /** loading images are animated GIF images. * DataResource is used instead of ImageResource to prevent conversion to PNG. */ @Source("slow_loading.gif") @ImageOptions(flipRtl = true) DataResource chromeLoadingSlow(); @Source("slow_loading_fast.gif") @ImageOptions(flipRtl = true) DataResource chromeLoadingFast(); @Source("att_loading.gif") @ImageOptions(flipRtl = true) DataResource chromeLoadingAttachment(); } /** UiBinder */ interface Binder extends UiBinder<HTMLPanel, ImageThumbnailWidget> {} private static final Binder BINDER = GWT.create(Binder.class); /** * Singleton instance of resource bundle */ static final Resources.Css css = GWT.<Resources>create(Resources.class).css(); static { StyleInjector.inject(css.getText()); } /** * Specifies whether or not to set the width of the image container element. */ private static final boolean DO_FRAME_WIDTH_UPDATE = UserAgent.isIE(); public ImageThumbnailWidget() { initWidget(BINDER.createAndBindUi(this)); // Restore the desired attributes of widget elements in the template (GWT rips them out) Element element = getElement(); addStyleName(css.imageThumbnail()); // Make the thumbnail as a whole uneditable and unselectable. element.setAttribute("contentEditable", "false"); DomHelper.makeUnselectable(element); errorLabel.setVisible(false); spin.setVisible(true); Element imageElement = image.getElement(); imageElement.addClassName(css.image()); imageElement.getStyle().setVisibility(Visibility.HIDDEN); button = ButtonFactory.createIconToggleButton( IconButtonStyle.PLUS_MINUS, "Options", new ToggleButtonListener() { public void onOff() { Event.getCurrentEvent().stopPropagation(); Event.getCurrentEvent().preventDefault(); if (listener != null) { listener.onRequestSetFullSizeMode(false); } } public void onOn() { Event.getCurrentEvent().stopPropagation(); Event.getCurrentEvent().preventDefault(); if (listener != null) { listener.onRequestSetFullSizeMode(true); } } }); button.addStyleName(css.thumbSizeButton()); DebugClassHelper.addDebugClass(button.getElement(), "image_toggle"); menuButtonContainer.add(button); final Element buttonContainerElement = menuButtonContainer.getElement(); buttonContainerElement.getStyle().setDisplay(Display.NONE); addDomHandler(new MouseOverHandler() { public void onMouseOver(MouseOverEvent event) { resizeButtonVisible = true; updateResizeButton(); } }, MouseOverEvent.getType()); addDomHandler(new MouseOutHandler() { public void onMouseOut(MouseOutEvent event) { resizeButtonVisible = false; updateResizeButton(); } }, MouseOutEvent.getType()); } @UiHandler("image") void onImageClicked(ClickEvent ignored) { handleImageRegionClicked(); } @UiHandler("spin") void onSpinClicked(ClickEvent ignored) { handleImageRegionClicked(); } @UiHandler("errorLabel") void onErrorClicked(ClickEvent ignored) { handleImageRegionClicked(); } private void handleImageRegionClicked() { if (listener != null) { listener.onClickImage(); } } /** * The double buffer loaded used to load the thumbnail. */ private DoubleBufferImage doubleBufferLoader; /** * Image widget representing the thumbnail image. */ @UiField Image image; /** * The container to which we will add our drop down menu button */ @UiField SimplePanel menuButtonContainer; /** * Caption panel to put the caption. */ @UiField SimplePanel captionPanel; /** * The Label that contains the spinning wheel */ @UiField Label spin; /** * The Label that contains the error image */ @UiField Label errorLabel; /** * The fancy chrome around the thumbnail */ @UiField HTMLPanel chromeContainer; /** * The progress widget. */ @UiField ProgressWidget progressWidget; private String attachmentUrl; private String thumbnailUrl; private ImageThumbnailViewListener listener; private final ToggleButtonWidget button; private boolean resizeButtonVisible = false; // logical state // TODO(danilatos): Move this state out of the display. Ideally displays should // be stateless. private boolean isFullSize = false; private int thumbnailWidth, thumbnailHeight; private int attachmentWidth, attachmentHeight; @Override public void displayDeadImage(String toolTip) { this.hideUploadProgress(); this.spin.setVisible(false); this.errorLabel.setTitle(toolTip); this.errorLabel.setVisible(true); } private final Scheduler.Task clearButtonTask = new Scheduler.Task() { public void execute() { Style style = menuButtonContainer.getElement().getStyle(); if (resizeButtonVisible) { style.setDisplay(Display.BLOCK); } else { style.setDisplay(Display.NONE); } } }; private void updateResizeButton() { ScheduleCommand.addCommand(clearButtonTask); } @Override public void setListener(ImageThumbnailViewListener listener) { this.listener = listener; } @Override public void setThumbnailSize(int width, int height) { this.thumbnailWidth = width; this.thumbnailHeight = height; if (isFullSize) { return; } image.setPixelSize(width, height); //TODO(user,danilatos): Whinge about how declarative UI doesn't let us avoid this hack: Style pstyle = image.getElement().getParentElement().getParentElement().getStyle(); if (width == 0) { image.setWidth(""); pstyle.clearWidth(); } else { pstyle.setWidth(width, Unit.PX); } if (height == 0) { image.setHeight(""); pstyle.clearHeight(); } else { pstyle.setHeight(height, Unit.PX); } // NOTE(user): IE requires that the imageCaptionContainer element has a width // in order to correctly center the caption. if (DO_FRAME_WIDTH_UPDATE) { captionPanel.getElement().getStyle().setWidth(width, Unit.PX); } } @Override public void setAttachmentSize(int width, int height) { this.attachmentWidth = width; this.attachmentHeight = height; } public void setFullSizeMode(boolean isOn) { isFullSize = isOn; button.setOn(isOn); Style pstyle = image.getElement().getParentElement().getParentElement().getStyle(); if (isOn) { chromeContainer.getElement().getStyle().setDisplay(Display.NONE); image.setPixelSize(attachmentWidth, attachmentHeight); if (attachmentWidth == 0) { image.setWidth(""); pstyle.clearWidth(); } else { pstyle.setWidth(attachmentWidth, Unit.PX); } if (attachmentHeight == 0) { image.setHeight(""); pstyle.clearHeight(); } else { pstyle.setHeight(attachmentHeight, Unit.PX); } setAttachmentUrl(attachmentUrl); } else { chromeContainer.getElement().getStyle().clearDisplay(); setThumbnailSize(thumbnailWidth, thumbnailHeight); setThumbnailUrl(thumbnailUrl); } } private static class DoubleBufferImage { /** * An image object used to preload the thumbnail before we display it */ private final Image doubleLoadedImage = new Image(); private final Image imageToLoad; private final Widget spinner; private final Widget error; /** * Create a double buffer loader for a given image widget * * @param spinner * @param imageToLoad */ public DoubleBufferImage(Widget spinner, Widget error, Image imageToLoad) { if (UserAgent.isIE()) { DomHelper.makeUnselectable(doubleLoadedImage.getElement()); } this.spinner = spinner; this.error = error; this.imageToLoad = imageToLoad; } /** * Registration of load handler to monitor the doubleLoadedImage to see if * it has finished. */ private HandlerRegistration onLoadHandlerRegistration; /** * Registration of error handler to monitor the doubleLoadedImage to see if * it has finished. */ private HandlerRegistration onErrorHandlerRegistration; /** * Handler for load + error events used by the double buffered image. */ private class DoubleLoadHandler implements LoadHandler, ErrorHandler { private final String url; private boolean completed = false; /***/ public DoubleLoadHandler(String url) { this.url = url; } /** {@inheritDoc}) */ public void onError(ErrorEvent e) { if (completed) { return; } cleanUp(); spinner.setVisible(false); error.setVisible(true); } /** {@inheritDoc}) */ public void onLoad(LoadEvent e) { if (completed) { return; } cleanUp(); spinner.setVisible(false); imageToLoad.getElement().getStyle().clearVisibility(); imageToLoad.setUrl(url); } private void cleanUp() { RootPanel.get().remove(doubleLoadedImage); final HandlerRegistration onLoadReg = onLoadHandlerRegistration; final HandlerRegistration onErrorReg = onErrorHandlerRegistration; // HACK(user): There is a bug in GWT which stops us from removing a listener in HOSTED // mode inside the invoke context. Put the remove in a deferred command to avoid this // error ScheduleCommand.addCommand(new Scheduler.Task() { public void execute() { onLoadReg.removeHandler(); onErrorReg.removeHandler(); } }); onLoadHandlerRegistration = null; onErrorHandlerRegistration = null; completed = true; } } /** * Get the thumbnail to load its image from the given url. * @param url */ public void loadImage(final String url) { // Remove the old double loader to stop the last double buffered load. if (onLoadHandlerRegistration != null) { onLoadHandlerRegistration.removeHandler(); onErrorHandlerRegistration.removeHandler(); // We used to set doubleLoadedImage's url to "" here. // It turns out to be a really bad thing to do. Setting an url to null // cause Wfe's bootstrap servelet to get called, which overload the server. RootPanel.get().remove(doubleLoadedImage); } // set up the handler to hide spinning wheel when loading has finished // We need to have the doubleLoadedImage created even if we are loading the image directly // in imageToLoad. This is done because we don't get a event otherwise. DoubleLoadHandler doubleLoadHandler = new DoubleLoadHandler(url); onLoadHandlerRegistration = doubleLoadedImage.addLoadHandler(doubleLoadHandler); onErrorHandlerRegistration = doubleLoadedImage.addErrorHandler(doubleLoadHandler); error.setVisible(false); doubleLoadedImage.setVisible(false); doubleLoadedImage.setUrl(url); RootPanel.get().add(doubleLoadedImage); imageToLoad.getElement().getStyle().setVisibility(Visibility.HIDDEN); // If image is empty, show the url directly. if (imageToLoad.getUrl().length() == 0) { imageToLoad.setUrl(url); } } } /** * Sets the {@code src} attribute of the image element, and performs any other browser-specific * actions to make the image display nicely. * * {@inheritDoc} */ public void setThumbnailUrl(String url) { // TODO(patcoleman): concurrency issue here, in the sense that there is nothing enforcing // setThumbnailUrl and setAttachmentUrl to be called in the same event loop. That currently // is happening, though should be enforced or this code should be changed, otherwise resizing // between these two calls will result in no images being loaded :( if (url.equals(thumbnailUrl)) { return; } this.thumbnailUrl = url; if (!isFullSize) { // load image into buffer if it is to be displayed if (doubleBufferLoader == null) { doubleBufferLoader = new DoubleBufferImage(spin, errorLabel, image); } doubleBufferLoader.loadImage(url); DOM.setStyleAttribute(image.getElement(), "visibility", ""); } else { Image.prefetch(url); } } /** * Set link to follow when the user click on the thumbnail * * {@inheritDoc} */ public void setAttachmentUrl(String url) { this.attachmentUrl = url; if (isFullSize) { // load image into buffer if it is to be displayed if (doubleBufferLoader == null) { doubleBufferLoader = new DoubleBufferImage(spin, errorLabel, image); } doubleBufferLoader.loadImage(url); DOM.setStyleAttribute(image.getElement(), "visibility", ""); } } /** {@inheritDoc} */ @Override public void showUploadProgress() { progressWidget.setVisible(true); } /** {@inheritDoc} */ @Override public void hideUploadProgress() { progressWidget.setVisible(false); } /** {@inheritDoc} */ @Override public void setUploadProgress(double progress) { progressWidget.setValue(progress); } /** * @return element the caption will be appended to */ public Element getCaptionContainer() { return captionPanel.getElement(); } }