/** * Copyright 2009 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.editor.gwt; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.user.client.ui.Widget; import org.waveprotocol.wave.client.common.util.DomHelper; import org.waveprotocol.wave.client.common.util.LogicalPanel; import org.waveprotocol.wave.client.editor.RenderingMutationHandler; import org.waveprotocol.wave.client.editor.content.ContentElement; import org.waveprotocol.wave.model.document.util.Property; import org.waveprotocol.wave.model.util.Preconditions; /** * TODO: this logic should be moved to an event handler instead of a renderer * * Renderers for doodads containing GWT widgets MUST extend or delegate to this * class, if they want to be logically attached (in order to receive dom * events). * * Subclasses must: - Either implement {@link * #createGwtWidget(org.waveprotocol.wave.client.editor.content.Renderer.Renderable)} * and always return a new widget - Or, if null is ever returned from that * method, for example to delay creating the widget, then when the widget is * finally created, {@link #receiveNewGwtWidget(ContentElement, Widget)} or its * variant must be called. * * The mutation handling implementation listens to element removal to perform * logical widget cleanup - this is also important, and they must be delegated * to if overridden. * * @author danilatos@google.com (Daniel Danilatos) */ public abstract class GwtRenderingMutationHandler extends RenderingMutationHandler { /** * GWT widget associated with element */ private static final Property<Widget> WIDGET = Property.immutable("widget"); /** * Logical GWT parent of the GWT widget */ private static final Property<LogicalPanel> LOGICAL_PANEL = Property.immutable("parent"); /** * The way doodads should be rendered in the document */ public enum Flow { /** css inline */ INLINE { @Override Element createContainer() { return Document.get().createSpanElement(); } }, /** css block */ BLOCK { @Override Element createContainer() { return Document.get().createDivElement(); } }, /** Use the widget directly */ USE_WIDGET { @Override Element createContainer() { throw new AssertionError("No container for Replace"); } }; abstract Element createContainer(); } /** * How the rendered doodads should flow */ private final Flow flow; /** * @param flow the GWT widget is by default wrapped by a single html node, and * this parameter indicates what type of flow behaviour the wrapper * html node should exhibit. */ public GwtRenderingMutationHandler(Flow flow) { this.flow = flow; } ///////////////////////////////////////////// // Public API /** * Associates a widget with an element, replacing the existing widget if any, * and performing necessary GWT book-keeping. * * Useful for delayed widget attachment (if the widget is added to an element * after some time), or to replace an existing widget. * * If null is passed, the widget is removed and cleanup is performed * * @param element * @param widget */ public final void receiveNewGwtWidget(ContentElement element, Widget widget) { Element nodelet = element.getImplNodelet(); Element parent = flow == Flow.USE_WIDGET ? null : nodelet; receiveNewGwtWidget(element, widget, parent); } /** * Same as {@link #receiveNewGwtWidget(ContentElement, Widget)}, but allows * specifying an arbitrary physical attach point, in case the default wrapper * span was replaced with something else and the widget belongs in a different * location within the HTML rendering of the ContentElement * * @param element * @param widget * @param physicalParent */ public final void receiveNewGwtWidget(ContentElement element, Widget widget, Element physicalParent) { maybeLogicalDetach(element); disassociateWidget(element); if (widget != null) { associateWidget(element, widget, physicalParent); maybeLogicalAttach(element); } } ///////////////////////////////////////////// // Subclassing API /** * Called to create the GWT Widget to be associated with the element, and * perform any other miscellaneous initialisation. * * If null is returned, then it is the responsibility of the subclass to * call {@link #associateWidget(Renderable, Widget, Element)} * * @param element element to associate the widget with * @return the newly created widget */ protected abstract Widget createGwtWidget(Renderable element); /** * Default behaviour is to not have a container nodelet, since child elements * are probably there for state, not for rendering. */ protected Element getContainerNodelet(Widget w) { return null; } /** * @param element * @return the GWT Widget for the given element */ @SuppressWarnings("unchecked") public static <T extends Widget> T getGwtWidget(ContentElement element) { return (T) element.getProperty(WIDGET); } ///////////////////////////////////////////// // Methods called by the core /** * Override {@link #createGwtWidget(Renderable)} to create your widget */ @Override public final Element createDomImpl(Renderable element) { Widget w = createGwtWidget(element); Element implNodelet; Element attachNodelet; if (flow == Flow.USE_WIDGET) { Preconditions.checkState(w != null, "Cannot have null widget with USE_WIDGET"); implNodelet = w.getElement(); attachNodelet = null; } else { implNodelet = flow.createContainer(); attachNodelet = implNodelet; } DomHelper.setContentEditable(implNodelet, false, false); DomHelper.makeUnselectable(implNodelet); if (w != null) { associateWidget(element, w, attachNodelet); } return implNodelet; } /** * Sets the logical panel in the GWT widget hierarchy that should be the * parent of the widget associated with the given element. * * A null parent may be given to indicate the element is no longer rendered * in a DOM with GWT widgets. * * @param element * @param parent */ public final void setLogicalPanel(ContentElement element, LogicalPanel parent) { LogicalPanel existingParent = element.getProperty(LOGICAL_PANEL); if (existingParent == null && parent == null) { return; // Nothing to do } Preconditions.checkState(existingParent == null || parent == null, "setLogicalPanel called for an element that already has it"); if (element.isContentAttached()) { if (parent == null) { assert existingParent != null && parent == null; maybeLogicalDetach(element); element.setProperty(LOGICAL_PANEL, null); } else { // The preconditions check should guard against this being // called for already-attached elements. assert existingParent == null && parent != null; element.setProperty(LOGICAL_PANEL, parent); maybeLogicalAttach(element); } } } @Override public void onActivationStart(ContentElement element) { maybeLogicalAttach(element); } /** * Cleans up for when the handler is unattached from an element. */ @Override public void onDeactivated(ContentElement element) { receiveNewGwtWidget(element, null); setLogicalPanel(element, null); } ///////////////////////////////////////////// private void maybeLogicalAttach(ContentElement element) { Widget w = getGwtWidget(element); LogicalPanel p = getLogicalPanel(element); if (p != null && w != null) { p.doAdopt(w); } } private void maybeLogicalDetach(ContentElement element) { Widget w = getGwtWidget(element); LogicalPanel p = getLogicalPanel(element); if (p != null && w != null && w.getParent() != null) { p.doOrphan(w); } } /** * Assigns a widget to an element and optionally attaches it to a parent html * node. Does not do any GWT logical attachment * * @param element the element the widget is associated with * @param w the widget to insert into the logical hierarchy * @param physicalParent the physical place in the dom to attach the widget. * May be null, in which case no physical attachment will take place. */ private void associateWidget(Renderable element, Widget w, Element physicalParent) { element.setProperty(WIDGET, w); if (physicalParent != null) { physicalParent.appendChild(w.getElement()); } element.setAutoAppendContainer(getContainerNodelet(w)); } /** * The opposite of {@link #associateWidget(Renderable, Widget, Element)} * * @param element */ private void disassociateWidget(ContentElement element) { Widget old = getGwtWidget(element); if (old != null) { old.getElement().removeFromParent(); element.setProperty(WIDGET, null); } assert element.getProperty(WIDGET) == null; } private LogicalPanel getLogicalPanel(ContentElement element) { return element.getProperty(LOGICAL_PANEL); } }