/**
* 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.wavepanel.event;
import static org.waveprotocol.wave.client.uibuilder.BuilderHelper.KIND_ATTRIBUTE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
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.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import org.waveprotocol.wave.client.common.util.LogicalPanel;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.StringMap;
/**
* A panel that enables arbitrary event handling using a single DOM event
* listener.
* <p>
* This panel acts as a sink for all DOM events in its subtree, with a single
* top-level DOM event listener. Application-level event handlers register
* themselves with this panel against elements of a particular "kind". When a
* browser-event occurs, this panel traces up the DOM hierarchy from the source
* of the event, locating the nearest ancestor with a kind for which an event
* handler is registered, and dispatches the event to that handler. Dispatching
* continues up the DOM tree to all such element/handler pairs, until this
* panel's element is reached, or until a handler declares that propagation
* should stop. This process is analogous to the native browser mechanism of
* event bubbling.
* <p>
* This dispatch mechanism has some specific advantages and disadvantages.<br/>
* Advantages:
* <ul>
* <li>since it uses only a single listener, with contextualization driven by
* data in the DOM, it is more appropriate for a page with a server-supplied
* rendering, since it avoids the cost of traversing the entire DOM in order to
* hook up individual DOM event listeners;</li>
* <li>it reduces memory overhead; and</li>
* <li>finally, it allows UI interaction to occur in a GWT application without
* using Widgets, which are relatively expensive and heavyweight.</li>
* </ul>
* <br/> Disadvantages:
* <ul>
* <li>runtime dispatch cost is slower;</li>
* <li>mixes state and control by injecting kind values into the DOM;</li>
* <li>event-handling setup requires global context (this object), in order to
* register event listeners, rather than being able to setup event-handling
* directly on a Widget.</li>
* </ul>
*
*/
//
// Example: (not in Javadoc, because Google's auto-formatter kills it)
//
// <div onclick="handle()"> <-- 2. Bubbling brings the event to this panel
// ..<div>
// ....<div kind="blip"> <-- 3. This panel dispatches to a "blip" handler
// ......<div></div> <-- 1. Click event occurs on this element
// ....</div>
// ..</div>
// </div>
//
public final class EventDispatcherPanel extends ComplexPanel
implements EventHandlerRegistry, LogicalPanel {
/**
* A collection of handlers for a particular event type. This collection
* registers itself for GWT events, and dispatches them to registered handlers
* based on kind.
*
* @param <E> event type
* @param <W> wave handler for that event type
*/
@VisibleForTesting
static abstract class HandlerCollection<E, W> {
/** Top element of the panel (where dispatch stops). */
private final Element top;
/** Name of the event type, for error reporting. */
private final String typeName;
/** Registered handlers, indexed by kind. */
private final StringMap<W> waveHandlers = CollectionUtils.createStringMap();
/** Optional global handler for this event type. */
private W globalHandler;
/** True iff this collection has registered itself for GWT events. */
private boolean registered;
HandlerCollection(Element top, String typeName) {
this.top = top;
this.typeName = typeName;
}
/**
* Installs the appropriate GWT event handlers for this event type,
* forwarding events to {@link #dispatch(Object, Element)}.
*/
abstract void registerGwtHandler();
/**
* Invokes a handler with a given event.
*
* @param event event that occurred
* @param context kind-annotated element associated with the event
* @param handler kind-registered handler
* @return true if the event should not propagate to other handlers.
*/
abstract boolean dispatch(E event, Element context, W handler);
/**
* Registers an event handler for elements of a particular kind.
*
* @param kind element kind for which events are to be handled, or {@code
* null} to handle global events
* @param handler handler for the events
*/
void register(String kind, W handler) {
if (kind == null) {
// Global handler.
if (globalHandler != null) {
throw new IllegalStateException(
"Feature conflict on UI: " + kind + " with event: " + typeName);
}
globalHandler = handler;
} else {
if (waveHandlers.containsKey(kind)) {
throw new IllegalStateException(
"Feature conflict on UI: " + kind + " with event: " + typeName);
}
waveHandlers.put(kind, handler);
}
if (!registered) {
registerGwtHandler();
registered = true;
}
}
/**
* Dispatches an event through this handler collection.
*
* @param event event to dispatch
* @param target target element of the event
* @return true if a handled, false otherwise.
*/
boolean dispatch(E event, Element target) {
while (target != null) {
if (target.hasAttribute(KIND_ATTRIBUTE)) {
W handler = waveHandlers.get(target.getAttribute(KIND_ATTRIBUTE));
if (handler != null) {
if (dispatch(event, target, handler)) {
return true;
}
}
}
target = !target.equals(top) ? target.getParentElement() : null;
}
return dispatchGlobal(event);
}
/**
* Dispatches an event to the global handler for this event type.
*
* @param event event to dispatch
* @return true if handled, false otherwise.
*/
boolean dispatchGlobal(E event) {
if (globalHandler != null) {
return dispatch(event, top, globalHandler);
} else {
return false;
}
}
}
/**
* Handler collection for click events.
*/
private final class ClickHandlers // \u2620
extends HandlerCollection<ClickEvent, WaveClickHandler> implements ClickHandler {
ClickHandlers() {
super(getElement(), "click");
}
@Override
void registerGwtHandler() {
addDomHandler(this, ClickEvent.getType());
}
@Override
boolean dispatch(ClickEvent event, Element context, WaveClickHandler handler) {
return handler.onClick(event, context);
}
@Override
public void onClick(ClickEvent event) {
if (dispatch(event, event.getNativeEvent().getEventTarget().<Element>cast())) {
event.stopPropagation();
}
}
}
/**
* Handler collection for double-click events.
*/
private final class DoubleClickHandlers // \u2620
extends HandlerCollection<DoubleClickEvent, WaveDoubleClickHandler>
implements DoubleClickHandler {
DoubleClickHandlers() {
super(getElement(), "double-click");
}
@Override
void registerGwtHandler() {
addDomHandler(this, DoubleClickEvent.getType());
}
@Override
boolean dispatch(DoubleClickEvent event, Element context, WaveDoubleClickHandler handler) {
return handler.onDoubleClick(event, context);
}
@Override
public void onDoubleClick(DoubleClickEvent event) {
if (dispatch(event, event.getNativeEvent().getEventTarget().<Element>cast())) {
event.stopPropagation();
}
}
}
/**
* Handler collection for mousedown events.
*/
private final class MouseDownHandlers // \u2620
extends HandlerCollection<MouseDownEvent, WaveMouseDownHandler> implements MouseDownHandler {
MouseDownHandlers() {
super(getElement(), "mousedown");
}
@Override
void registerGwtHandler() {
addDomHandler(this, MouseDownEvent.getType());
}
@Override
boolean dispatch(MouseDownEvent event, Element context, WaveMouseDownHandler handler) {
return handler.onMouseDown(event, context);
}
@Override
public void onMouseDown(MouseDownEvent event) {
if (dispatch(event, event.getNativeEvent().getEventTarget().<Element>cast())) {
event.stopPropagation();
}
}
}
private final DoubleClickHandlers doubleClickHandlers;
private final ClickHandlers clickHandlers;
private final MouseDownHandlers mouseDownHandlers;
EventDispatcherPanel(Element baseElement) {
setElement(baseElement);
// Must construct the handler collections after calling setElement().
doubleClickHandlers = new DoubleClickHandlers();
clickHandlers = new ClickHandlers();
mouseDownHandlers = new MouseDownHandlers();
}
/**
* Creates an EventDispatcherPanel.
*/
public static EventDispatcherPanel create() {
return new EventDispatcherPanel(Document.get().createDivElement());
}
/**
* Creates an EventDispatcherPanel on an existing element. If the element is
* part of a larger GWT widget structure, consider see
* {@link #inGwtContext(Element, LogicalPanel)}.
*
* @param element element to become the panel
*/
public static EventDispatcherPanel of(Element element) {
EventDispatcherPanel panel = new EventDispatcherPanel(element);
RootPanel.detachOnWindowClose(panel);
panel.onAttach();
return panel;
}
/**
* Creates an EventDispatcherPanel on an existing element in an existing GWT
* widget structure.
*
* @param element element to be wrapped
* @param container panel to adopt the widgetification of {@code element}
*/
public static EventDispatcherPanel inGwtContext(Element element, LogicalPanel container) {
Preconditions.checkArgument(container != null);
EventDispatcherPanel panel = new EventDispatcherPanel(element);
container.doAdopt(panel);
return panel;
}
@Override
public void registerDoubleClickHandler(String kind, WaveDoubleClickHandler handler) {
doubleClickHandlers.register(kind, handler);
}
@Override
public void registerClickHandler(String kind, WaveClickHandler handler) {
clickHandlers.register(kind, handler);
}
@Override
public void registerMouseDownHandler(String kind, WaveMouseDownHandler handler) {
mouseDownHandlers.register(kind, handler);
}
@Override
public void doAdopt(Widget child) {
getChildren().add(child);
adopt(child);
}
@Override
public void doOrphan(Widget child) {
orphan(child);
getChildren().remove(child);
}
}