/* * This is part of Geomajas, a GIS framework, http://www.geomajas.org/. * * Copyright 2008-2015 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.Dom; 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.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; 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.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.FocusWidget; import com.smartgwt.client.widgets.WidgetCanvas; 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 int previousWidth, previousHeight; private HandlerRegistration resizedHandlerRegistration; // ------------------------------------------------------------------------- // Constructors: // ------------------------------------------------------------------------- /** * Create and initialise a graphics widget. It will instantiate the correct delegate {@link GraphicsContext} * and build the initial DOM elements. * * @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 (Dom.isSvg()) { vectorContext = new SvgGraphicsContext(eventWidget.getWidget()); } else { vectorContext = new VmlGraphicsContext(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); // add a resize handler to connect the event widget and set the sizes resizedHandlerRegistration = addResizedHandler(new GwtResizedHandler()); } // ------------------------------------------------------------------------- // 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); } } } private void resizeIfReady() { // check whether the new size has been established if (hasStableSize()) { // attach now if (!hasMember(eventWidget)) { addMember(eventWidget); } // set the stable sizes on widget and context eventWidget.setWidth(getWidth()); eventWidget.setHeight(getHeight()); vectorContext.setSize(getWidth(), getHeight()); fireEvent(new GraphicsReadyEvent()); } else { // schedule a new call to ourselves to make sure we are called after the event loop with stable size ! Scheduler.get().scheduleDeferred(new ScheduledCommand() { public void execute() { resizeIfReady(); } }); } } /** * Is the size stable (browser resize causes many resize events) ? * @return true if the size has been established (2 times same size in succession) */ private boolean hasStableSize() { try { Integer.parseInt(getWidthAsString()); int width = getWidth(); int height = getHeight(); // compare with previous size and return true if same if (previousWidth != width || previousHeight != height) { // force small size (workaround for smartgwt problem where size can otherwise not shrink below the // previous size) vectorContext.setSize(EventWidget.INITIAL_SIZE, EventWidget.INITIAL_SIZE); eventWidget.setInitialSize(); previousWidth = width; previousHeight = height; return false; } else { return true; } } catch (NumberFormatException e) { return false; } } /** Fixes resize problem by manually re-adding this component */ private class GwtResizedHandler implements ResizedHandler { public void onResized(ResizedEvent event) { // try resize resizeIfReady(); } } /** 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; public static final int INITIAL_SIZE = 10; public EventWidget(FocusWidget widget) { super(widget); this.widget = widget; // this makes sure the auto-resizing works, see Overflow.VISIBLE javadoc setWidth(INITIAL_SIZE); setHeight(INITIAL_SIZE); } public void setInitialSize() { setWidth(INITIAL_SIZE); setHeight(INITIAL_SIZE); } protected void onDraw() { super.onDraw(); // must force size as in some cases smartgwt is not setting the size on our element ! String width = DOM.getStyleAttribute(getDOM(), "width"); String height = DOM.getStyleAttribute(getDOM(), "height"); // only set if not already defined, to avoid invisible images in IE (see GWT-432) if (width == null || width.isEmpty() || height == null || height.isEmpty()) { DOM.setStyleAttribute(getDOM(), "width", "100%"); DOM.setStyleAttribute(getDOM(), "height", "100%"); } } public EventWidget(String id) { this(new StyledFocusWidget(Document.get().createDivElement())); } 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(); } @Override protected void onDestroy() { resizedHandlerRegistration.removeHandler(); super.onDestroy(); } }