/*
* Copyright 2014 PRImA Research Lab, University of Salford, United Kingdom
*
* 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.primaresearch.web.gwt.client.ui.page;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.primaresearch.maths.geometry.Point;
import org.primaresearch.maths.geometry.Rect;
import org.primaresearch.web.gwt.client.page.PageLayoutC;
import org.primaresearch.web.gwt.client.page.PageSyncManager.PageSyncListener;
import org.primaresearch.web.gwt.client.ui.DocumentImageListener;
import org.primaresearch.web.gwt.client.ui.DocumentImageSource;
import org.primaresearch.web.gwt.client.ui.MouseScrollPanel;
import org.primaresearch.web.gwt.client.ui.MouseScrollPanel.MouseHandlerExtension;
import org.primaresearch.web.gwt.client.ui.page.SelectionManager.SelectionListener;
import org.primaresearch.web.gwt.client.ui.page.renderer.PageRenderer;
import org.primaresearch.web.gwt.client.ui.page.tool.controls.PageViewHoverWidget;
import org.primaresearch.web.gwt.client.ui.page.tool.drawing.PageViewTool;
import org.primaresearch.web.gwt.client.ui.page.tool.drawing.PageViewToolListener;
import org.primaresearch.web.gwt.shared.page.ContentObjectC;
import org.primaresearch.web.gwt.shared.page.ContentObjectSync;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseWheelEvent;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
/**
* View for displaying and interacting with a document page.
* Uses a MouseScrollPanel as panel.
*
* @author Christian Clausner
*
*/
public class PageScrollView implements DocumentImageListener, PageSyncListener,
SelectionListener, MouseHandlerExtension, IsWidget {
private MouseScrollPanel panel;
private PageRenderer renderer;
private PageLayoutC pageLayout;
private double zoomFactor = 1.0;
private double targetZoomFactor = 1.0;
private double minZoom = 0.2;
private double maxZoom = 2.0;
private final double zoomChangeFactor = 1.333333;
private final double zoomChangeFactorMouseWheel = 1.1666667;
private Timer zoomingTimer = null;
private Point zoomReferencePointInPanel = new Point();
private Point zoomReferencePointInDocument = new Point();
private boolean useSmoothZoomByDefault;
private boolean enableMouseWheelScrolling;
private DocumentImageSource imageSource;
private SelectionManager selectionManager;
private boolean multiSelectionEnabled = true;
private Set<ZoomChangeListener> zoomListeners = new HashSet<ZoomChangeListener>();
private AbsolutePanel viewPanel;
private PageViewTool activeTool;
private Set<PageViewHoverWidget> hoverWidgets = new HashSet<PageViewHoverWidget>();
/**
* Constructor
*
* @param pageLayout Page layout to display
* @param imageSource Document image source
* @param selectionManager Content object selection manager to listen to
* @param enableMouseWheelScrolling If set to <code>true</code> the mouse wheel is used for scrolling up and down, otherwise it is used for zooming
* @param useSmoothZoomByDefault If set to <code>true</code> the zoom is changed smoothly using a timer, otherwise the zoom is changed immediately.
*/
public PageScrollView(PageLayoutC pageLayout, DocumentImageSource imageSource, SelectionManager selectionManager,
boolean enableMouseWheelScrolling, boolean useSmoothZoomByDefault) {
panel = new MouseScrollPanel(true);
this.enableMouseWheelScrolling = enableMouseWheelScrolling;
this.pageLayout = pageLayout;
this.imageSource = imageSource;
this.selectionManager = selectionManager;
this.useSmoothZoomByDefault = useSmoothZoomByDefault;
selectionManager.addListener(this);
Canvas canvas = Canvas.createIfSupported();
renderer = new PageRenderer(canvas, pageLayout, selectionManager, imageSource);
canvas.addStyleName("pageViewCanvas");
//Initial refresh
renderer.refresh(zoomFactor);
viewPanel = new AbsolutePanel();
panel.add(viewPanel);
viewPanel.getElement().getStyle().setProperty("overflow", "visible");
viewPanel.addStyleName("pageViewPanel");
if (canvas != null) {
viewPanel.add(canvas, 0, 0);
}
panel.addMouseWheelHandler(panel);
panel.setMouseHandlerExtension(this);
}
/**
* Returns the page renderer.
*/
public PageRenderer getRenderer() {
return renderer;
}
/**
* Returns the panel that contains the document canvas and possibly other controls.
*/
public AbsolutePanel getViewPanel() {
return viewPanel;
}
/**
* Returns the document page layout that is currently displayed.
*/
public PageLayoutC getPageLayout() {
return pageLayout;
}
/**
* Adds a listener that will be notified if the zoom has been changed.
*/
public void addZoomListener(ZoomChangeListener listener) {
this.zoomListeners.add(listener);
}
/**
* Removes the given zoom listener.
*/
public void removeZoomListener(ZoomChangeListener listener) {
this.zoomListeners.remove(listener);
}
@Override
public void imageLoaded() {
pageLayout.setWidth(imageSource.getOriginalImageWidth());
pageLayout.setHeight(imageSource.getOriginalImageHeight());
updateSize();
//zoomToFitPage(); //Done externally now
renderer.refresh(zoomFactor);
//Zoom to 100%
//targetZoomFactor = 1.0;
//startSmoothZoomingTimer();
}
/**
* Updates view panel width and height (called after the after the zoom has been changed).
*/
private void updateSize() {
viewPanel.setWidth((int)(pageLayout.getWidth() * zoomFactor)+"px");
viewPanel.setHeight((int)(pageLayout.getHeight() * zoomFactor)+"px");
}
/**
* Calculates the zoom factor that is needed to fit the document page into the client window.
*/
private double getZoomFactorToFitPage() {
int clientWidth = panel.getElement().getClientWidth() - 20; //allow for some margin
int clientHeight = panel.getElement().getClientHeight() - 20;
int pageWidth = pageLayout.getWidth();
int pageHeight = pageLayout.getHeight();
return Math.min((double)clientWidth/(double)pageWidth, (double)clientHeight/(double)pageHeight);
}
/**
* Zooms and pans to fit the document page into the client window and centre it.
*/
public void zoomToFitPage() {
double oldZoom = zoomFactor;
zoomFactor = getZoomFactorToFitPage();
targetZoomFactor = zoomFactor;
updateSize();
renderer.refresh(zoomFactor);
//Center view
int clientWidth = panel.getElement().getClientWidth();
int clientHeight = panel.getElement().getClientHeight();
int pageWidth = (int)(pageLayout.getWidth() * zoomFactor);
int pageHeight = (int)(pageLayout.getHeight() * zoomFactor);
panel.scrollToPosition(pageWidth/2 - clientWidth/2, pageHeight/2 - clientHeight/2);
//panel.scrollToCenter();
centerZoomReferencePoints();
notifyZoomListeners(zoomFactor, oldZoom);
}
/**
* Changes the zoom factor to fit an object of the given size.
* @param width Width of the object to fit
* @param height Height of the object to fit
* @param zoomOutOnly If set to <code>true</code> the zoom factor will not be increased, only decreased (if needed).
*/
public void zoomToFit(int width, int height, boolean zoomOutOnly) {
int clientWidth = panel.getElement().getClientWidth();
int clientHeight = panel.getElement().getClientHeight();
double z = Math.min((double)clientWidth/(double)width, (double)clientHeight/(double)height);
if (z < zoomFactor || !zoomOutOnly) {
centerZoomReferencePoints();
double oldZoom = zoomFactor;
zoomFactor = z;
targetZoomFactor = zoomFactor;
updateSize();
renderer.refresh(zoomFactor);
alignZoomReferencePoints();
notifyZoomListeners(z, oldZoom);
}
}
@Override
public boolean onMouseWheel(MouseWheelEvent event) {
//Zoom
if (event.isControlKeyDown()
|| (enableMouseWheelScrolling && event.isShiftKeyDown())
|| (!enableMouseWheelScrolling && !event.isShiftKeyDown())) {
//This does not work for Chrome
Event e = DOM.eventGetCurrentEvent();
e.preventDefault();
event.stopPropagation();
setZoomReferencePoints(event.getRelativeX(panel.getElement()), event.getRelativeY(panel.getElement()));
int delta = event.getDeltaY();
if (delta == 0)
delta = workaroundEventGetMouseWheelVelocityY(event.getNativeEvent());
if (delta < 0) {
targetZoomFactor *= zoomChangeFactorMouseWheel;
if (targetZoomFactor > maxZoom)
targetZoomFactor = maxZoom;
} else if (delta > 0) {
targetZoomFactor /= zoomChangeFactorMouseWheel;
if (targetZoomFactor < minZoom)
targetZoomFactor = minZoom;
}
if (useSmoothZoomByDefault)
startSmoothZoomingTimer();
else
changeZoom(targetZoomFactor);
return true;
}
//Scroll
else {
return false;
}
}
private static native int workaroundEventGetMouseWheelVelocityY(NativeEvent evt) /*-{
if (typeof evt.wheelDelta == "undefined") {
return 0;
}
return Math.round(-evt.wheelDelta / 40) || 0;
}-*/;
@Override
public void postMouseWheel(MouseWheelEvent event) {
if (event.isControlKeyDown() || !enableMouseWheelScrolling) {
} else {
centerZoomReferencePoints();
}
}
@Override
public void postMouseOver(MouseOverEvent event){
}
/**
* Increases the zoom factor using the default zoom behaviour (smooth or immediate).
*/
public void zoomIn() {
zoomIn(useSmoothZoomByDefault);
}
/**
* Increases the zoom factor using custom zoom behaviour (smooth or immediate).
*/
public void zoomIn(boolean smooth) {
centerZoomReferencePoints();
targetZoomFactor *= zoomChangeFactor;
if (targetZoomFactor > maxZoom)
targetZoomFactor = maxZoom;
if (smooth)
startSmoothZoomingTimer();
else
changeZoom(targetZoomFactor);
}
/**
* Decreases the zoom factor using the default zoom behaviour (smooth or immediate).
*/
public void zoomOut() {
zoomOut(useSmoothZoomByDefault);
}
/**
* Decreases the zoom factor using custom zoom behaviour (smooth or immediate).
*/
public void zoomOut(boolean smooth) {
centerZoomReferencePoints();
targetZoomFactor /= zoomChangeFactor;
if (targetZoomFactor < minZoom)
targetZoomFactor = minZoom;
if (smooth)
startSmoothZoomingTimer();
else
changeZoom(targetZoomFactor);
}
/**
* Changes the zoom factor immediately.
* @param zoom New factor
*/
private void changeZoom(double zoom) {
double oldZoom = zoomFactor;
zoomFactor = targetZoomFactor = zoom;
updateSize();
renderer.refresh(zoomFactor);
alignZoomReferencePoints();
notifyZoomListeners(zoomFactor, oldZoom);
}
/**
* Sets the zoom factor to 1.0 using smooth zooming.
*/
public void zoomTo100Percent() {
centerZoomReferencePoints();
targetZoomFactor = 1.0;
startSmoothZoomingTimer();
}
/**
* Starts smooth zooming.
*/
private void startSmoothZoomingTimer() {
if (zoomingTimer == null) {
zoomingTimer = new Timer() {
@Override
public void run() {
double oldZoomFactor = zoomFactor;
if (zoomFactor < targetZoomFactor-0.01)
zoomFactor += (targetZoomFactor - zoomFactor)/3 ;
else if (zoomFactor > targetZoomFactor+0.01)
zoomFactor -= (zoomFactor - targetZoomFactor)/3;
else {
zoomFactor = targetZoomFactor;
this.cancel();
}
updateSize();
renderer.refresh(zoomFactor);
alignZoomReferencePoints();
if (zoomFactor != oldZoomFactor)
notifyZoomListeners(zoomFactor, oldZoomFactor);
}
};
}
zoomingTimer.scheduleRepeating(30);
}
/**
* Sets the zoom reference points to the centre of the client window.
*/
private void centerZoomReferencePoints() {
zoomReferencePointInPanel = new Point( panel.getElement().getClientWidth()/2,
panel.getElement().getClientHeight()/2);
zoomReferencePointInDocument.x = clientToDocumentCoordsX(zoomReferencePointInPanel.x);
zoomReferencePointInDocument.y = clientToDocumentCoordsY(zoomReferencePointInPanel.y);
}
/**
* Sets the zoom reference points to a specific position within the client window.
*/
private void setZoomReferencePoints(int clientX, int clientY) {
zoomReferencePointInPanel = new Point(clientX, clientY);
zoomReferencePointInDocument.x = clientToDocumentCoordsX(clientX);
zoomReferencePointInDocument.y = clientToDocumentCoordsY(clientY);
}
/**
* Converts the given screen x coordinate (relative to the panel) to a document page coordinate.
*/
public int clientToDocumentCoordsX(int clientX) {
return (int)(((double)(clientX + panel.getScrollPosition().x)) / zoomFactor);
}
/**
* Converts the given screen y coordinate (relative to the panel) to a document page coordinate.
*/
public int clientToDocumentCoordsY(int clientY) {
return (int)(((double)(clientY + panel.getScrollPosition().y)) / zoomFactor);
}
/**
* Converts the given document x coordinate to a screen coordinate (relative to the panel).
*/
public int documentToClientCoordsX(int docX) {
return (int)((docX * zoomFactor)-panel.getScrollPosition().x);
}
/**
* Converts the given document y coordinate to a screen coordinate (relative to the panel).
*/
public int documentToClientCoordsY(int docY) {
return (int)((docY * zoomFactor)-panel.getScrollPosition().y);
}
/**
* Scrolls the panel so that the zoom reference points in screen and document coordinates are aligned.
*/
private void alignZoomReferencePoints() {
//Calculate the offset of the reference points
int dx = (int)((clientToDocumentCoordsX(zoomReferencePointInPanel.x) - zoomReferencePointInDocument.x)*zoomFactor);
int dy = (int)((clientToDocumentCoordsY(zoomReferencePointInPanel.y) - zoomReferencePointInDocument.y)*zoomFactor);
panel.scroll(dx, dy);
}
@Override
public void contentLoaded(String contentType) {
renderer.setPageContentToRender(contentType);
renderer.refresh(zoomFactor);
}
@Override
public boolean onMouseMove(MouseMoveEvent event) {
if (activeTool != null && activeTool.onMouseMove(event))
return true;
//super.onMouseMove(event);
return false;
}
@Override
public void postMouseMove(MouseMoveEvent event){
if (activeTool != null && activeTool.onMouseMove(event))
return;
if (pageLayout != null && !panel.isAutoScrolling()) {
//Handle mouse hover
int x = clientToDocumentCoordsX(event.getRelativeX(panel.getElement()));
int y = clientToDocumentCoordsY(event.getRelativeY(panel.getElement()));
ContentObjectC obj = pageLayout.getObjectAt(x, y, renderer.getPageContentToRender());
renderer.highlightContentObject(obj != null ? obj.getId() : null);
}
}
@Override
public boolean onMouseOut(MouseOutEvent event) {
if (activeTool != null && activeTool.onMouseOut(event))
return true;
return false;
}
@Override
public void postMouseOut(MouseOutEvent event) {
if (activeTool != null && activeTool.onMouseOut(event))
return;
renderer.highlightContentObject(null);
}
@Override
public boolean onMouseDown(MouseDownEvent event) {
if (activeTool != null && activeTool.onMouseDown(event))
return true;
return false;
}
@Override
public void postMouseDown(MouseDownEvent event){
}
@Override
public boolean onMouseUp(MouseUpEvent event) {
if (activeTool != null && activeTool.onMouseUp(event))
return true;
if (!panel.isDragged()) {
//Selection handling
int x = clientToDocumentCoordsX(event.getRelativeX(panel.getElement()));
int y = clientToDocumentCoordsY(event.getRelativeY(panel.getElement()));
ContentObjectC obj = pageLayout.getObjectAt(x, y, renderer.getPageContentToRender());
if (multiSelectionEnabled && (event.isControlKeyDown() || event.isShiftKeyDown())) { //CTRL or SHIFT -> Toggle
if (obj != null) {
selectionManager.toggleSelection(obj);
}
}
else { //No CTRL or SHIFT or no multi-selection enabled -> Single selection
if (obj == null)
selectionManager.clearSelection();
else
selectionManager.setSelection(obj);
}
}
//Scrolling
//super.onMouseUp(event);
return false;
}
@Override
public void postMouseUp(MouseUpEvent event){
//if (activeTool != null && activeTool.onMouseUp(event, panel.isDragged()))
// return;
}
@Override
public void selectionChanged(SelectionManager manager) {
renderer.refresh(zoomFactor);
}
/**
* Returns <code>true</code> if the view allows the selection of multiple content objects.
*/
public boolean isMultiSelectionEnabled() {
return multiSelectionEnabled;
}
/**
* Enables or disables multiple selection.
* @param multiSelectionEnabled Set to <code>true</code> to allows the selection of multiple content objects.
*/
public void setMultiSelectionEnabled(boolean multiSelectionEnabled) {
this.multiSelectionEnabled = multiSelectionEnabled;
}
/**
* Centres the specified rectangle in the panel. Use document page coordinates.
*/
public void centerRectangle(int left, int top, int right, int bottom, boolean smoothScrolling) {
int rectWidthInClient = (int)((right-left+1) * zoomFactor);
int rectHeightInClient = (int)((bottom-top+1) * zoomFactor);
int clientWidth = panel.getElement().getClientWidth();
int clientHeight = panel.getElement().getClientHeight();
int posXInClient = 0;
int posYInClient = 0;
//Calculate centre
if (rectWidthInClient < clientWidth)
posXInClient = (clientWidth-rectWidthInClient)/2;
if (rectHeightInClient < clientHeight)
posYInClient = (clientHeight-rectHeightInClient)/2;
//Scroll
int dx = (int)((clientToDocumentCoordsX(posXInClient) - left)*zoomFactor);
int dy = (int)((clientToDocumentCoordsY(posYInClient) - top)*zoomFactor);
panel.scroll(dx, dy, smoothScrolling);
centerZoomReferencePoints();
}
/**
* Returns the given rectangle in client (panel) coordinates.
* @param rectInPageCoordinates The rectangle in document page coordiantes.
*/
public Rect translateToClientCoordinates(Rect rectInPageCoordinates) {
return new Rect( documentToClientCoordsX(rectInPageCoordinates.left),
documentToClientCoordsY(rectInPageCoordinates.top),
documentToClientCoordsX(rectInPageCoordinates.right),
documentToClientCoordsY(rectInPageCoordinates.bottom));
}
/**
* Returns the current zoom factor.
*/
public double getZoomFactor() {
return zoomFactor;
}
/**
* Notifies all zoom change listeners that the zoom has changed.
*/
public void notifyZoomListeners(double newZoom, double oldZoom) {
for (Iterator<ZoomChangeListener> it=zoomListeners.iterator(); it.hasNext(); ) {
it.next().zoomChanged(newZoom, oldZoom, Math.abs(newZoom - minZoom) < 0.001, Math.abs(newZoom - maxZoom) < 0.001);
}
refreshHoverWidgets();
}
//@Override
//public void metaDataLoaded() {
//}
@Override
public void pageIdLoaded(String id) {
}
public void setTool(PageViewTool tool) {
if (activeTool != null) {
activeTool.cancel();
}
activeTool = tool;
renderer.addPlugin(tool);
activeTool.addListener(new PageViewToolListener() {
@Override
public void onToolFinished(PageViewTool tool, boolean success) {
activeTool = null;
renderer.removePlugin(tool);
}
});
}
@Override
public boolean onMouseOver(MouseOverEvent event) {
return false;
}
@Override
public Widget asWidget() {
return panel;
}
@Override
public void contentObjectAdded(ContentObjectSync syncObj, ContentObjectC localObj) {
}
/**
* Adds a tool widget that hovers over the document page.
*/
public void addHoverWidget(PageViewHoverWidget icon) {
getViewPanel().add(icon, 0, 0);
hoverWidgets.add(icon);
icon.setPageView(this);
}
/**
* Removes a tool widget that hovers over the document page.
*/
public void removeHoverWidget(PageViewHoverWidget icon) {
getViewPanel().remove(icon);
hoverWidgets.remove(icon);
}
/**
* Refreshes all tool widgets that hovers over the document page.
*/
public void refreshHoverWidgets() {
for (Iterator<PageViewHoverWidget> it = hoverWidgets.iterator(); it.hasNext(); ) {
it.next().refresh();
}
}
/**
* Makes all tool widgets that hovers over the document page visible.
*/
public void showHoverWidgets() {
for (Iterator<PageViewHoverWidget> it = hoverWidgets.iterator(); it.hasNext(); ) {
PageViewHoverWidget w = it.next();
w.asWidget().setVisible(true);
w.refresh();
}
}
/**
* Hides all tool widgets that hovers over the document page.
*/
public void hideHoverWidgets() {
for (Iterator<PageViewHoverWidget> it = hoverWidgets.iterator(); it.hasNext(); ) {
it.next().asWidget().setVisible(false);
}
}
@Override
public void textContentSynchronized(ContentObjectC syncObj) {
}
@Override
public void objectOutlineSynchronized(ContentObjectC object) {
}
@Override
public void contentObjectDeleted(ContentObjectC object) {
}
public MouseScrollPanel getScrollPanel() {
return panel;
}
@Override
public void pageFileSaved() {
}
/**
* Interface for zoom change listeners.
*
* @author Christian Clausner
*
*/
public static interface ZoomChangeListener {
/** Called when the zoom of the page view has changed. */
public void zoomChanged(double newZoomFactor, double oldZoomFactor, boolean isMinZoom, boolean isMaxZoom);
}
@Override
public void regionTypeSynchronized(ContentObjectC object, ArrayList<String> toDelete) {
}
public double getMinZoom() {
return minZoom;
}
public void setMinZoom(double minZoom) {
this.minZoom = minZoom;
}
public double getMaxZoom() {
return maxZoom;
}
public void setMaxZoom(double maxZoom) {
this.maxZoom = maxZoom;
}
@Override
public void changesReverted() {
}
@Override
public void contentLoadingFailed(String contentType, Throwable caught) {
}
@Override
public void pageIdLoadingFailed(Throwable caught) {
}
@Override
public void contentObjectAddingFailed(ContentObjectC object,
Throwable caught) {
}
@Override
public void contentObjectDeletionFailed(ContentObjectC object,
Throwable caught) {
}
@Override
public void textContentSyncFailed(ContentObjectC object, Throwable caught) {
}
@Override
public void regionTypeSyncFailed(ContentObjectC object, Throwable caught) {
}
@Override
public void objectOutlineSyncFailed(ContentObjectC object, Throwable caught) {
}
@Override
public void pageFileSaveFailed(Throwable caught) {
}
@Override
public void revertChangesFailed(Throwable caught) {
}
}