org.waveprotocol.wave.client.editor.gwt.GwtRenderingMutationHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.waveprotocol.wave.client.editor.gwt.GwtRenderingMutationHandler.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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);
    }
}