/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2011 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.gwt.client.widget;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.geomajas.geometry.Coordinate;
import org.geomajas.gwt.client.controller.GraphicsController;
import org.geomajas.gwt.client.controller.listener.Listener;
import org.geomajas.gwt.client.controller.listener.ListenerController;
import org.geomajas.gwt.client.gfx.GraphicsContext;
import org.geomajas.gwt.client.gfx.ImageContext;
import org.geomajas.gwt.client.gfx.MapContext;
import org.geomajas.gwt.client.gfx.MenuContext;
import org.geomajas.gwt.client.gfx.context.DefaultImageContext;
import org.geomajas.gwt.client.gfx.context.SvgGraphicsContext;
import org.geomajas.gwt.client.gfx.context.VmlGraphicsContext;
import org.geomajas.gwt.client.util.GwtEventUtil;
import org.geomajas.gwt.client.widget.event.GraphicsReadyEvent;
import org.geomajas.gwt.client.widget.event.GraphicsReadyHandler;
import org.geomajas.gwt.client.widget.event.HasGraphicsReadyHandlers;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.FocusWidget;
import com.smartgwt.client.util.SC;
import com.smartgwt.client.widgets.WidgetCanvas;
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.layout.VLayout;
/**
* <p>
* GWT widget implementation meant for actual drawing. To this end it implements the <code>GraphicsContext</code>
* interface. Actually it delegates the real work to either a
* {@link org.geomajas.gwt.client.gfx.context.VmlGraphicsContext} or a
* {@link org.geomajas.gwt.client.gfx.context.SvgGraphicsContext} object.
* </p>
* <p>
* By default this widget will draw in screen space. It will check the given ID's and add the screen space group in
* front of it if the ID is not compatible (meaning if an ID does not start with the graphicsId...).
* </p>
* <p>
* It is also responsible for handling {@link org.geomajas.gwt.client.controller.GraphicsController}s (only one at a
* time). The reason to place the controller handling here, is because we needed a default GWT widget to handle the
* events, not a SmartGWT widget. The SmartGWT events do not contain the actual DOM elements for MouseEvents, while the
* default GWT events do.
* </p>
* <p>
* One extra function this widget has, is to store the latest right mouse click. Usually the right mouse button is used
* for drawing context menus. But sometimes it is necessary to have the DOM element onto which the context menu was
* clicked, to influence this menu. That is why this widget always stores this latest event (or at least it's DOM
* element ID, and screen location).
* </p>
*
* @author Pieter De Graef
*/
public class GraphicsWidget extends VLayout implements MapContext, HasGraphicsReadyHandlers {
/** The ID from which to start building the rendering DOM tree. */
private String graphicsId;
/**
* The actual GraphicsContext implementation that does the drawing. Depending on the browser the user uses, this
* will be the {@link org.geomajas.gwt.client.gfx.context.VmlGraphicsContext} or the
* {@link org.geomajas.gwt.client.gfx.context.SvgGraphicsContext}.
*/
private GraphicsContext vectorContext;
/**
* The context for drawing raster images.
*/
private DefaultImageContext rasterContext;
/**
* The menu context.
*/
private MenuContext menuContext;
/** The current controller on the map. Can be only one at a time! */
private GraphicsController controller;
/**
* An optional fallbackController to return to, when no controller is explicitly set, or when null is set.
*/
private GraphicsController fallbackController;
/**
* A list of handler registrations that are needed to correctly clean up after a controller is deactivated.
*/
private List<HandlerRegistration> handlers;
/**
* A list of handler registrations that are needed to correctly clean up after a listener is deactivated.
*/
private Map<ListenerController, List<HandlerRegistration>> listeners;
/**
* Every time a right mouse button has been clicked, this widget will store the event's coordinates.
*/
private Coordinate rightButtonCoordinate;
/**
* Every time a right mouse button has been clicked, this widget will store the event's target DOM element.
*/
private String rightButtonTarget;
/** Focus widget with the real graphics, needed for native GWT events */
private EventWidget eventWidget;
private Timer resizeTimer;
// -------------------------------------------------------------------------
// Constructors:
// -------------------------------------------------------------------------
/**
* Create and initialise a graphics widget. It will instantiate the correct delegate <code>GraphicsContext</code>
* and build the initial DOM elements.
*
* @param parent
* The canvas to which this widget will be added as a child.
* @param graphicsId
* The ID from which to start building the rendering DOM tree.
*/
public GraphicsWidget(String graphicsId) {
eventWidget = new EventWidget(graphicsId);
setWidth100();
setHeight100();
setID(graphicsId);
this.graphicsId = graphicsId;
// append a raster context
rasterContext = new DefaultImageContext(eventWidget.getWidget());
// append a vector context
if (SC.isIE()) {
vectorContext = new VmlGraphicsContext(eventWidget.getWidget());
} else {
vectorContext = new SvgGraphicsContext(eventWidget.getWidget());
}
menuContext = new MapMenuContext();
handlers = new ArrayList<HandlerRegistration>();
listeners = new HashMap<ListenerController, List<HandlerRegistration>>();
// capture right mouse info (target id and coordinate)
RightMouseHandler rmh = new RightMouseHandler();
// we connect to both mouse events just to be sure (ubuntu/ff3.6 does
// not fire mouse up)
eventWidget.addMouseDownHandler(rmh);
eventWidget.addMouseUpHandler(rmh);
// raster at the back
// WARNING ! adding the child here was causing several problems:
// - GWT-153: embedding in plain html or other frameworks
// - GWT-145: autoscroll is no longer working (even after fixing height)
// base.addChild(this); // moved to GwtResizedHandler
// parent.addChild(base);
GwtResizedHandler h = new GwtResizedHandler();
super.addResizedHandler(h);
addDrawHandler(h);
}
// -------------------------------------------------------------------------
// Class specific methods:
// -------------------------------------------------------------------------
public HandlerRegistration addGraphicsReadyHandler(GraphicsReadyHandler handler) {
return doAddHandler(handler, GraphicsReadyEvent.TYPE);
}
/**
* Apply a new <code>GraphicsController</code> on the graphics. When an old controller is to be removed for this new
* controller, its <code>onDeactivate</code> method will be called. For the new controller, its
* <code>onActivate</code> method will be called.
*
* @param graphicsController
* The new <code>GraphicsController</code> to be applied on the graphics.
*/
public void setController(GraphicsController graphicsController) {
for (HandlerRegistration registration : handlers) {
registration.removeHandler();
}
if (controller != null) {
controller.onDeactivate();
controller = null;
}
handlers = new ArrayList<HandlerRegistration>();
if (null == graphicsController) {
graphicsController = fallbackController;
}
if (graphicsController != null) {
handlers.add(eventWidget.addMouseDownHandler(graphicsController));
handlers.add(eventWidget.addMouseMoveHandler(graphicsController));
handlers.add(eventWidget.addMouseOutHandler(graphicsController));
handlers.add(eventWidget.addMouseOverHandler(graphicsController));
handlers.add(eventWidget.addMouseUpHandler(graphicsController));
handlers.add(eventWidget.addMouseWheelHandler(graphicsController));
handlers.add(eventWidget.addDoubleClickHandler(graphicsController));
controller = graphicsController;
controller.onActivate();
}
}
public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
return eventWidget.addMouseWheelHandler(handler);
}
/**
* Get the currently active GraphicsController.
*
* @return current GraphicsController
*/
public GraphicsController getController() {
return controller;
}
/**
* An optional fallbackController to return to, when no controller is explicitly set, or when null is set. If no
* current controller is active when this setter is called, it is applied immediately.
*
* @param fallbackController
* The new fall-back controller.
*/
public void setFallbackController(GraphicsController fallbackController) {
boolean fallbackActive = (controller == this.fallbackController);
this.fallbackController = fallbackController;
if (controller == null || fallbackActive) {
setController(fallbackController);
}
}
/**
* Get the full set of listener controllers currently active on this widget.
*
* @return The list of listener controllers.
* @since 1.8.0
*/
public Set<ListenerController> getListeners() {
return listeners.keySet();
}
/**
* Add a new listener controller on this widget. These listeners passively listen to mouse events on the map. They
* do not interfere with these events.
*
* @param listenerController
* The new listener controller to add.
* @return Returns true if addition was successful, false otherwise.
* @since 1.8.0
*/
public boolean addListener(ListenerController listenerController) {
if (listenerController != null && !listeners.containsKey(listenerController)) {
List<HandlerRegistration> registrations = new ArrayList<HandlerRegistration>();
registrations.add(eventWidget.addMouseDownHandler(listenerController));
registrations.add(eventWidget.addMouseMoveHandler(listenerController));
registrations.add(eventWidget.addMouseOutHandler(listenerController));
registrations.add(eventWidget.addMouseOverHandler(listenerController));
registrations.add(eventWidget.addMouseUpHandler(listenerController));
registrations.add(eventWidget.addMouseWheelHandler(listenerController));
listeners.put(listenerController, registrations);
return true;
}
return false;
}
/**
* Remove an existing listener controller from this widget. These listeners passively listen to mouse events on the
* map. They do not interfere with these events.
*
* @param listenerController
* The existing listener controller to remove.
* @return Returns true if removal was successful, false otherwise (i.e. if it could not be found).
* @since 1.8.0
*/
public boolean removeListener(ListenerController listenerController) {
if (listenerController != null && listeners.containsKey(listenerController)) {
List<HandlerRegistration> registrations = listeners.get(listenerController);
for (HandlerRegistration registration : registrations) {
registration.removeHandler();
}
listeners.remove(listenerController);
return true;
}
return false;
}
/**
* Get the controller that belongs to the given listener. Protected method, used by the MapWidget.
*
* @param listener
* The listeners to search for.
* @return Return the controller, or null if it could not be found.
*/
protected ListenerController getController(Listener listener) {
for (ListenerController controller : listeners.keySet()) {
if (controller.getListener().equals(listener)) {
return controller;
}
}
return null;
}
// -------------------------------------------------------------------------
// Getters and setters:
// -------------------------------------------------------------------------
public String getGraphicsId() {
return graphicsId;
}
public MenuContext getMenuContext() {
return menuContext;
}
public ImageContext getRasterContext() {
return rasterContext;
}
public GraphicsContext getVectorContext() {
return vectorContext;
}
/**
* Menu context that captures raster and vector context events.
*
* @author Jan De Moerloose
*
*/
public class MapMenuContext implements MenuContext {
public Coordinate getRightButtonCoordinate() {
return rightButtonCoordinate;
}
public String getRightButtonName() {
String name = vectorContext.getNameById(rightButtonTarget);
if (name != null) {
return name;
} else {
return rasterContext.getNameById(rightButtonTarget);
}
}
public Object getRightButtonObject() {
Object object = vectorContext.getGroupById(rightButtonTarget);
if (object != null) {
return object;
} else {
return rasterContext.getGroupById(rightButtonTarget);
}
}
}
public void markForResize() {
// remove child while browser is resizing + schedule redraw in 0.1 sec
if (contains(eventWidget)) {
removeChild(eventWidget);
eventWidget.setVisible(false);
}
if (resizeTimer == null) {
resizeTimer = new Timer() {
@Override
public void run() {
resize();
}
};
}
resizeTimer.schedule(100);
}
public void resize() {
// child was removed in markForResize(), add it again
if (!contains(eventWidget)) {
addChild(eventWidget);
eventWidget.setVisible(true);
}
// Set size of the event widget:
eventWidget.setSize(getWidthAsString(), getHeightAsString());
// xhtml needs this, or <div> won't show !
eventWidget.setInnerSize(getWidth() + "px", getHeight() + "px");
// set the size and notify graphics users so they can redraw
vectorContext.setSize(getWidth(), getHeight());
if (isReady()) {
fireEvent(new GraphicsReadyEvent());
}
}
/** Fixes resize problem by manually re-adding this component */
private class GwtResizedHandler implements ResizedHandler, DrawHandler {
public void onResized(ResizedEvent event) {
// resize later
markForResize();
}
public void onDraw(DrawEvent event) {
// called on first draw
resize();
}
}
/** sets the right mouse coordinate and target */
private class RightMouseHandler implements MouseUpHandler, MouseDownHandler {
public void onMouseDown(MouseDownEvent event) {
process(event);
}
public void onMouseUp(MouseUpEvent event) {
process(event);
}
private void process(MouseEvent<?> event) {
if (event.getNativeButton() == Event.BUTTON_RIGHT) {
rightButtonCoordinate = GwtEventUtil.getPosition(event);
rightButtonTarget = GwtEventUtil.getTargetId(event);
}
}
}
/**
* Extension of WidgetCanvas that wraps a plain GWT FocusWidget. It allows for native GWT event registration and
* explicitly setting the size of the FocusWidget.
*
* @author Jan De Moerloose
*
*/
private static class EventWidget extends WidgetCanvas {
private FocusWidget widget;
private EventWidget(FocusWidget widget) {
super(widget);
this.widget = widget;
setWidth100();
setHeight100();
}
public EventWidget(String id) {
this(new StyledFocusWidget(Document.get().createDivElement()));
}
public void setInnerSize(String width, String height) {
// setSize(width, height);
widget.setSize(width, height);
}
public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
return widget.addMouseDownHandler(handler);
}
public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
return widget.addMouseMoveHandler(handler);
}
public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
return widget.addMouseOutHandler(handler);
}
public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
return widget.addMouseOverHandler(handler);
}
public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
return widget.addMouseUpHandler(handler);
}
public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
return widget.addMouseWheelHandler(handler);
}
public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler) {
return widget.addDoubleClickHandler(handler);
}
public FocusWidget getWidget() {
return widget;
}
/**
* Xhtml does not like divs without size.
*
* @author Jan De Moerloose
*
*/
private static final class StyledFocusWidget extends FocusWidget {
private StyledFocusWidget(Element elem) {
super(elem);
setSize("100%", "100%");
}
}
}
public boolean isReady() {
return contains(eventWidget) && eventWidget.getWidget().isAttached();
}
}