/*
* Copyright (C) 2012 Jan Pokorsky
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.cas.lib.proarc.webapp.client.widget;
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.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Image;
import com.smartgwt.client.types.Alignment;
import com.smartgwt.client.types.Cursor;
import com.smartgwt.client.types.DragAppearance;
import com.smartgwt.client.types.HeaderControls;
import com.smartgwt.client.types.ImageStyle;
import com.smartgwt.client.types.Overflow;
import com.smartgwt.client.util.Page;
import com.smartgwt.client.widgets.Canvas;
import com.smartgwt.client.widgets.Img;
import com.smartgwt.client.widgets.WidgetCanvas;
import com.smartgwt.client.widgets.Window;
import com.smartgwt.client.widgets.events.CloseClickEvent;
import com.smartgwt.client.widgets.events.CloseClickHandler;
import com.smartgwt.client.widgets.events.DragMoveEvent;
import com.smartgwt.client.widgets.events.DragMoveHandler;
import com.smartgwt.client.widgets.events.DragStartEvent;
import com.smartgwt.client.widgets.events.DragStartHandler;
import com.smartgwt.client.widgets.events.DragStopEvent;
import com.smartgwt.client.widgets.events.DragStopHandler;
import com.smartgwt.client.widgets.events.DrawEvent;
import com.smartgwt.client.widgets.events.DrawHandler;
import com.smartgwt.client.widgets.events.ResizedEvent;
import com.smartgwt.client.widgets.events.ResizedHandler;
import com.smartgwt.client.widgets.form.DynamicForm;
import com.smartgwt.client.widgets.form.ValuesManager;
import com.smartgwt.client.widgets.form.fields.SelectItem;
import com.smartgwt.client.widgets.form.fields.events.ChangedEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangedHandler;
import com.smartgwt.client.widgets.layout.Layout;
import com.smartgwt.client.widgets.layout.VLayout;
import cz.cas.lib.proarc.webapp.client.ClientMessages;
import cz.cas.lib.proarc.webapp.client.ClientUtils;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Shows preview of digital objects.
*
* @author Jan Pokorsky
*/
public final class DigitalObjectPreview {
// darker variant #E6E6F5
public static final String BACKGROUND_COLOR = "#F5F5FB";
private static final String FIELD_ZOOM = "zoom";
private static final Logger LOG = Logger.getLogger(DigitalObjectPreview.class.getName());
/** MIMEs displayable by img element. */
private static final HashSet<String> SUPPORTED_IMAGES = new HashSet<String>();
static {
SUPPORTED_IMAGES.add("image/jpeg");
SUPPORTED_IMAGES.add("image/png");
SUPPORTED_IMAGES.add("image/gif");
}
private final ClientMessages i18n;
private final VLayout previewLayout;
private final VLayout windowContainer;
private Window imageWindow;
private final ImageLoadTask previewLoadTask;
private final ValuesManager zoomValues;
public DigitalObjectPreview(ClientMessages i18n) {
this.i18n = i18n;
zoomValues = new ValuesManager();
zoomValues.setValue(FIELD_ZOOM, Zoom.FIT_PANEL);
VLayout imgContainer = new VLayout();
imgContainer.setAlign(Alignment.CENTER);
windowContainer = new VLayout();
windowContainer.setAlign(Alignment.CENTER);
windowContainer.setWidth100();
windowContainer.setHeight100();
imageWindow = createFullImageWindow(windowContainer);
previewLayout = new VLayout();
previewLayout.setBackgroundColor(BACKGROUND_COLOR);
previewLayout.addMember(imgContainer);
previewLoadTask = new ImageLoadTask(imgContainer,
new Zoom(zoomValues.getValueAsString(FIELD_ZOOM)), false, i18n);
}
public Canvas asCanvas() {
return previewLayout;
}
public void show(String previewUrl) {
show(previewUrl, "image/jpeg");
}
public void show(String previewUrl, String mime) {
if (previewUrl == null) {
Layout container = previewLoadTask.getImgContainer();
container.removeMembers(container.getMembers());
previewLoadTask.stop();
} else {
previewLoadTask.load(previewUrl, mime);
}
}
/**
* Gets widget to zoom image in the preview panel.
* @return
*/
public Canvas getPreviewZoomer() {
SelectItem zoomItem = createZoomForm();
zoomItem.addChangedHandler(new ChangedHandler() {
@Override
public void onChanged(ChangedEvent event) {
onZoomChange(event);
}
});
DynamicForm form = new DynamicForm();
form.setValuesManager(zoomValues);
form.setFields(zoomItem);
form.setLayoutAlign(Alignment.CENTER);
return form;
}
/**
* Gets widget to zoom image in the window.
* @return
*/
public Canvas getWindowZoomer() {
SelectItem zoomItem = createZoomForm();
zoomItem.setHeight(15);
zoomItem.setPickerIconSrc("[SKIN]/headerIcons/zoom.png");
zoomItem.setPickerIconHeight(15);
zoomItem.setPickerIconWidth(15);
zoomItem.addChangedHandler(new ChangedHandler() {
@Override
public void onChanged(ChangedEvent event) {
onZoomChange(event);
}
});
DynamicForm form = new DynamicForm();
form.setValuesManager(zoomValues);
form.setFields(zoomItem);
form.setLayoutAlign(Alignment.CENTER);
return form;
}
private SelectItem createZoomForm() {
SelectItem selectItem = new SelectItem(FIELD_ZOOM);
selectItem.setShowTitle(Boolean.FALSE);
selectItem.setValueMap(Zoom.getValueMap(i18n));
return selectItem;
}
private Window createFullImageWindow(Canvas content) {
final Window window = new Window();
window.setWidth(Page.getWidth() - 200);
window.setHeight(Page.getHeight() - 40);
window.setAutoCenter(true);
window.setMaximized(true);
window.setCanDragResize(true);
window.setCanDragReposition(true);
window.setIsModal(true);
window.setDismissOnEscape(true);
window.setDismissOnOutsideClick(true);
window.setKeepInParentRect(true);
window.setShowMaximizeButton(true);
window.setShowMinimizeButton(false);
window.setModalMaskOpacity(10);
window.setShowModalMask(true);
window.setShowResizer(true);
window.setTitle(i18n.DigitalObjectPreview_Window_Title());
window.setBodyColor(BACKGROUND_COLOR);
window.setCanFocus(true);
window.addItem(content);
window.setHeaderControls(HeaderControls.HEADER_ICON, HeaderControls.HEADER_LABEL,
getWindowZoomer(), HeaderControls.MAXIMIZE_BUTTON, HeaderControls.CLOSE_BUTTON);
window.addCloseClickHandler(new CloseClickHandler() {
@Override
public void onCloseClick(CloseClickEvent event) {
event.cancel();
window.close();
onWindowClose();
}
});
return window;
}
public void setBackgroundColor(String color) {
previewLayout.setBackgroundColor(color);
imageWindow.setBodyColor(color);
}
public void showInNewWindow(String url) {
com.google.gwt.user.client.Window.open(url, "_blank", "");
}
/**
* Gets preview container and put it to window. On close it returns the container back.
*/
public void showInWindow(String title) {
// put focus inside window to enable Window.setDismissOnEscape
Canvas[] members = previewLayout.getMembers();
windowContainer.setMembers(members);
imageWindow.setTitle(title);
imageWindow.show();
imageWindow.focus();
}
private void onWindowClose() {
Canvas[] members = windowContainer.getMembers();
previewLayout.setMembers(members);
}
private void onZoomChange(ChangedEvent event) {
Zoom zoom = new Zoom(String.valueOf(event.getValue()));
previewLoadTask.resize(zoom);
zoomValues.synchronizeMembers();
}
/**
* Scrolls container by dragging the image.
* @param container image container
* @param image image
*/
private static void addContainerMoveListener(final Layout container, final Img image) {
final int[] lastX = { 0 };
final int[] lastY = { 0 };
final Cursor[] cursor = new Cursor[1];
image.setCanDrag(Boolean.TRUE);
image.setDragAppearance(DragAppearance.NONE);
image.addDragStartHandler(new DragStartHandler() {
@Override
public void onDragStart(DragStartEvent event) {
lastX[0] = event.getX();
lastY[0] = event.getY();
cursor[0] = image.getCursor();
image.setCursor(Cursor.MOVE);
ClientUtils.fine(LOG, "dragStart [%s, %s]", lastX[0], lastY[0]);
}
});
image.addDragMoveHandler(new DragMoveHandler() {
@Override
public void onDragMove(DragMoveEvent event) {
int dx = lastX[0] - event.getX();
int dy = lastY[0] - event.getY();
lastX[0] = event.getX();
lastY[0] = event.getY();
container.scrollBy(-2 * dx, -2 * dy);
ClientUtils.fine(LOG, "dragMove: delta[%s, %s], new position[%s, %s]", dx, dy, lastX[0], lastY[0]);
}
});
image.addDragStopHandler(new DragStopHandler() {
@Override
public void onDragStop(DragStopEvent event) {
image.setCursor(cursor[0]);
ClientUtils.fine(LOG, "dragStop");
}
});
}
private static final class Zoom {
public static final String FIT_PANEL = "FitToPanel";
public static final String FIT_WIDTH = "FitToWidth";
public static final String FIT_HEIGHT = "FitToHeight";
private static final String ZOOM_PREFIX = "Zoom-";
private static LinkedHashMap<String, String> values;
public static LinkedHashMap<String, String> getValueMap(ClientMessages i18n) {
if (values == null) {
values = new LinkedHashMap<String, String>();
values.put(FIT_PANEL, i18n.DigitalObjectPreview_ZoomFitPanel_Title());
values.put(FIT_WIDTH, i18n.DigitalObjectPreview_ZoomFitWidth_Title());
values.put(FIT_HEIGHT, i18n.DigitalObjectPreview_ZoomFitHeight_Title());
for (int i = 25; i <= 200; i += 25) {
values.put(ZOOM_PREFIX + i, i18n.DigitalObjectPreview_ZoomPrefix_Title(String.valueOf(i)));
}
}
return values;
}
private final String zoomValue;
public Zoom() {
this(FIT_PANEL);
}
public Zoom(String zoomValue) {
this.zoomValue = zoomValue;
}
public String getValue() {
return zoomValue;
}
public double ratio(double containerWidth, double containerHeight, double imageWidth, double imageHeight) {
double ratio;
double hRatio = containerHeight / imageHeight;
double wRatio = containerWidth / imageWidth;
if (FIT_PANEL.equals(zoomValue)) {
ratio = Math.min(hRatio, wRatio);
} else if (FIT_WIDTH.equals(zoomValue)) {
ratio = wRatio;
} else if (FIT_HEIGHT.equals(zoomValue)) {
ratio = hRatio;
} else if (zoomValue != null && zoomValue.startsWith(ZOOM_PREFIX)) {
String zoom = zoomValue.substring(ZOOM_PREFIX.length(), zoomValue.length());
int parsedZoom = Integer.parseInt(zoom);
ratio = (double) parsedZoom / 100.0;
} else {
throw new IllegalStateException(zoomValue);
}
ClientUtils.fine(LOG,
"wRatio: %s, hRatio: %s, ratio: %s, width: %s, height:%s, iwidth: %s, iheight:%s",
wRatio, hRatio, ratio, containerWidth, containerHeight, imageWidth, imageHeight);
return ratio;
}
}
/**
* Loads the image to get its parameters in order to zoom and layout it properly.
*/
private static final class ImageLoadTask extends Timer implements
LoadHandler, ErrorHandler, DrawHandler, ResizedHandler {
private final Layout imgContainer;
private final Layout display;
private Image image;
private HandlerRegistration drawHandler;
private HandlerRegistration resizedHandler;
private Zoom zoom;
private final boolean focus;
private final ClientMessages i18n;
private boolean loadFailed;
/**
* Last ratio of horizontal scrollbar and image width to keep
* scrollbar position between image reloads. It should help read e.g. page numbers.
*/
private double scrollHorizontal;
/**
* Last ratio of vertical scrollbar and image height to keep
* scrollbar position between image reloads. It should help read e.g. page numbers.
*/
private double scrollVertical;
public ImageLoadTask(Layout display, Zoom zoom, boolean focus, ClientMessages i18n) {
this.imgContainer = new VLayout();
this.imgContainer.setLayoutMargin(4);
// center vertically
this.imgContainer.setAlign(Alignment.CENTER);
// center horizontally
// #461: do not try to center horizontally as browsers crop large images in small containers
// this.imgContainer.setDefaultLayoutAlign(Alignment.CENTER);
this.imgContainer.setOverflow(Overflow.AUTO);
this.display = display;
this.i18n = i18n;
this.zoom = zoom;
this.focus = focus;
}
public Layout getImgContainer() {
return display;
}
public Zoom getZoom() {
return zoom;
}
/**
* Loads a resource in proper HTML element.
*/
public void load(String url, String mime) {
stop();
loadFailed = false;
if (SUPPORTED_IMAGES.contains(mime)) {
loadImage(url);
} else {
loadObject(url, mime);
}
}
/**
* Shows a resource as the {@code <object>} element.
*/
public void loadObject(String url, String mime) {
String click = ClientUtils.format("<a href='%s' target='_blank'>%s</a>",
url, i18n.DigitalObjectPreview_UnkownContent_Param0());
String msg = i18n.DigitalObjectPreview_UnkownContent_Msg(mime, click);
// use the height in pixels to strech the object to full height
// HTMLFlow or Canvas creates div that does not respect height='100%' of the object element
Canvas objectWidget = new Canvas() {
@Override
public String getInnerHTML() {
String html = ClientUtils.format(
// object@type is necessary for Chrome not to auto-download content
"<object data='%s' type='%s' style='height:%spx; width:100%;'>"
+ "<div style='margin-left: 10px; margin-top: 20px;'>"
+ "%s"
+ "</div>"
+ "</object>",
// url, mime, display.getVisibleHeight() - 6, msg
url, mime, getInnerHeight() - 4, msg
);
return html;
}
};
objectWidget.setHeight100();
objectWidget.setRedrawOnResize(true);
ClientUtils.setMembers(display, objectWidget);
}
/**
* Shows a resource as the {@code <img>} element.
*/
public void loadImage(String url) {
ClientUtils.setMembers(display, imgContainer);
image = new Image();
image.addLoadHandler(this);
image.addErrorHandler(this);
image.setUrl(url);
drawHandler = imgContainer.addDrawHandler(this);
resizedHandler = imgContainer.addResizedHandler(this);
ClientUtils.fine(LOG, "loadImage url: %s, width: %s", url, image.getWidth());
if (image.getWidth() == 0) {
WidgetCanvas widgetCanvas = new WidgetCanvas(image);
widgetCanvas.setVisible(false);
widgetCanvas.setWidth(1);
widgetCanvas.setHeight(1);
widgetCanvas.draw();
Img loadingImg = new Img("[SKIN]/loadingSmall.gif", 16, 16);
// Img loadingImg = new Img("[SKIN]/shared/progressCursorTracker.gif", 16, 16);
loadingImg.setAltText(i18n.ImportBatchDataSource_State_LOADING());
loadingImg.setPrompt(i18n.ImportBatchDataSource_State_LOADING());
loadingImg.setLayoutAlign(Alignment.CENTER);
imgContainer.setMembers(loadingImg, widgetCanvas);
}
scheduleForRender();
}
public void stop() {
if (drawHandler != null) {
drawHandler.removeHandler();
resizedHandler.removeHandler();
drawHandler = null;
resizedHandler = null;
}
cancel();
scrollHorizontal = (double) imgContainer.getScrollLeft() / (double) imgContainer.getWidth();
scrollVertical = (double) imgContainer.getScrollTop() / (double) imgContainer.getHeight();
ClientUtils.fine(LOG, "stop: [%s, %s]", scrollHorizontal, scrollVertical);
}
private void scheduleForRender() {
if (image.getWidth() == 0) {
return ;
} else if (imgContainer.isDirty() || !imgContainer.isDrawn()) {
return ;
}
schedule(100);
}
public void render() {
if (loadFailed) {
return ;
}
double ratio = zoom.ratio(
imgContainer.getInnerWidth(), imgContainer.getInnerHeight(),
image.getWidth(), image.getHeight());
double width = (double) image.getWidth() * ratio;
double height = (double) image.getHeight() * ratio;
log("render", width, height);
// do not try to center horizontally as browsers crop large images in small containers
Img img = new Img(image.getUrl(),
(int) width - imgContainer.getScrollbarSize() - 4,
(int) height - imgContainer.getScrollbarSize() - 4);
img.setCanFocus(Boolean.TRUE);
img.setImageType(ImageStyle.STRETCH);
imgContainer.setMembers(img);
imgContainer.adjustForContent(true);
int scrollLeft = (int) (imgContainer.getWidth() * scrollHorizontal);
int scrollTop = (int) (imgContainer.getHeight() * scrollVertical);
imgContainer.scrollTo(scrollLeft, scrollTop);
addContainerMoveListener(imgContainer, img);
if (focus) {
img.focus();
}
}
public void resize(Zoom zoom) {
this.zoom = zoom;
if (loadFailed) {
return ;
}
if (image.getWidth() != 0) {
final Canvas img = imgContainer.getMember(0);
scrollHorizontal = (double) imgContainer.getScrollLeft() / (double) imgContainer.getWidth();
scrollVertical = (double) imgContainer.getScrollTop() / (double) imgContainer.getHeight();
double ratio = zoom.ratio(
imgContainer.getInnerWidth(), imgContainer.getInnerHeight(),
image.getWidth(), image.getHeight());
double width = (double) image.getWidth() * ratio;
double height = (double) image.getHeight() * ratio;
log("resize", width, height);
img.animateResize((int) width - imgContainer.getScrollbarSize() - 4,
(int) height - imgContainer.getScrollbarSize() - 4,
(boolean earlyFinish) -> {
img.focus();
log("after resize.earlyFinish: " + earlyFinish, 0, 0);
});
}
}
private void log(String msg, double width, double height) {
if (LOG.isLoggable(Level.FINE)) {
final Canvas img = imgContainer.getMember(0);
String imgDebug = "-";
if (img != null) {
imgDebug = ClientUtils.format("%s, %s, %s, %s",
img.getLeft(), img.getTop(), img.getWidth(), img.getHeight()
);
}
ClientUtils.fine(LOG, "%s: %s,"
+ "\nscrollbar: %s"
+ "\nimage[%s, %s] => [%s, %s],"
+ "\nimageContainer.visible: %s, drawn: %s, attached: %s, dirty: %s,"
+ "\nImg[%s]"
+ "\nsize[%s, %s], innerSize[%s, %s], innerContentSize[%s, %s], viewport[%s, %s], visible[%s, %s]"
+ "\nscrollCurrent[%s, %s], scrollSize[%s, %s]",
msg,
image.getUrl(),
imgContainer.getScrollbarSize(),
image.getWidth(), image.getHeight(), (int) width, (int) height,
imgContainer.isVisible(), imgContainer.isDrawn(), imgContainer.isAttached(), imgContainer.isDirty(),
imgDebug,
imgContainer.getWidth(), imgContainer.getHeight(),
imgContainer.getInnerWidth(), imgContainer.getInnerHeight(),
imgContainer.getInnerContentWidth(), imgContainer.getInnerContentHeight(),
imgContainer.getViewportWidth(), imgContainer.getViewportHeight(),
imgContainer.getVisibleWidth(), imgContainer.getVisibleHeight(),
imgContainer.getScrollLeft(), imgContainer.getScrollTop(),
imgContainer.getScrollWidth(), imgContainer.getScrollHeight()
);
}
}
@Override
public void onLoad(LoadEvent event) {
ClientUtils.fine(LOG, "image onLoad: %s", image.getUrl());
scheduleForRender();
}
@Override
public void onError(ErrorEvent event) {
loadFailed = true;
ClientUtils.warning(LOG, "image onError: %s", image.getUrl());
Img img = new Img("[SKIN]/Dialog/warn.png", 2 * 16, 2 * 16);
img.setLayoutAlign(Alignment.CENTER);
img.setAltText(i18n.DigitalObjectPreview_NoContent_Msg());
img.setPrompt(i18n.DigitalObjectPreview_NoContent_Msg());
imgContainer.setMembers(img);
}
@Override
public void onDraw(DrawEvent event) {
ClientUtils.fine(LOG, "image onDraw: %s", image.getUrl());
scheduleForRender();
}
@Override
public void onResized(ResizedEvent event) {
ClientUtils.fine(LOG, "image onResized: %s", image.getUrl());
scheduleForRender();
}
@Override
public void run() {
render();
}
}
}