Package org.waveprotocol.wave.client.wavepanel.render

Source Code of org.waveprotocol.wave.client.wavepanel.render.InlineAnchorLiveRenderer$CanonicalAnchorFinder

/**
* 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.render;

import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Display;

import org.waveprotocol.wave.client.common.util.DomHelper;
import org.waveprotocol.wave.client.common.util.LinkedPruningSequenceMap;
import org.waveprotocol.wave.client.common.util.SequenceElement;
import org.waveprotocol.wave.client.common.util.VolatileComparable;
import org.waveprotocol.wave.client.doodad.DoodadInstallers.BlipInstaller;
import org.waveprotocol.wave.client.editor.NodeEventHandler;
import org.waveprotocol.wave.client.editor.RenderingMutationHandler;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.content.Registries;
import org.waveprotocol.wave.client.editor.content.misc.ChunkyElementHandler;
import org.waveprotocol.wave.client.editor.event.EditorEvent;
import org.waveprotocol.wave.client.editor.selection.content.SelectionUtil;
import org.waveprotocol.wave.client.uibuilder.BuilderHelper;
import org.waveprotocol.wave.client.wavepanel.view.AnchorView;
import org.waveprotocol.wave.client.wavepanel.view.ViewIdMapper;
import org.waveprotocol.wave.client.wavepanel.view.View.Type;
import org.waveprotocol.wave.client.wavepanel.view.dom.DomAsViewProvider;
import org.waveprotocol.wave.client.wavepanel.view.dom.full.TypeCodes;
import org.waveprotocol.wave.model.conversation.Blips;
import org.waveprotocol.wave.model.conversation.Conversation;
import org.waveprotocol.wave.model.conversation.ConversationBlip;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.IdentitySet;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.util.ReadableIdentitySet.Proc;
import org.waveprotocol.wave.model.wave.Wavelet;

/**
* Renders a reply element as an anchor view.
*
*/
public final class InlineAnchorLiveRenderer extends RenderingMutationHandler {

  interface AnchorHandler {
    void onAnchorAddedBefore(ConversationBlip blip, ReplyDoodad ref, ReplyDoodad anchor);

    void onAnchorRemoved(ConversationBlip blip, ReplyDoodad anchor);
  }

  public final class ReplyDoodad implements VolatileComparable<ReplyDoodad> {
    private final ContentElement el;
    private final String threadId;
    private AnchorView anchor;

    public ReplyDoodad(ContentElement el, String id) {
      this.el = el;
      this.threadId = id;
    }

    public ConversationBlip getBlip() {
      return blip;
    }

    public String getId() {
      return threadId;
    }

    public AnchorView getAnchor() {
      if (anchor == null) {
        anchor = views.asAnchor(el.getImplNodelet());
      }
      return anchor;
    }

    public void setDomId(String id) {
      if (id != null) {
        el.getImplNodelet().setId(id);
        el.getImplNodelet().setAttribute(
            BuilderHelper.KIND_ATTRIBUTE, TypeCodes.kind(Type.ANCHOR));
      } else {
        el.getImplNodelet().setId(null);
        el.getImplNodelet().removeAttribute(BuilderHelper.KIND_ATTRIBUTE);
      }
    }

    @Override
    public int compareTo(ReplyDoodad o) {
      return el.compareTo(o.el);
    }

    @Override
    public boolean isComparable() {
      return el.isComparable();
    }

    @Override
    public boolean equals(Object obj) {
      return (obj == this || ((obj instanceof ReplyDoodad) && el.equals(((ReplyDoodad) obj).el)));
    }
  }

  public static BlipInstaller installer(final ViewIdMapper viewIdMapper,
      final AnchorHandler manager, final DomAsViewProvider views) {
    return new BlipInstaller() {
      @Override
      public void install(Wavelet w, Conversation c, ConversationBlip b, Registries r) {
        InlineAnchorLiveRenderer renderer =
            new InlineAnchorLiveRenderer(viewIdMapper, b, manager, views);

        r.getElementHandlerRegistry().registerRenderingMutationHandler(
            Blips.THREAD_INLINE_ANCHOR_TAGNAME, renderer);
        r.getElementHandlerRegistry().registerEventHandler(
            Blips.THREAD_INLINE_ANCHOR_TAGNAME, ANCHOR_HANDLER);
      }
    };
  }

  /**
   * Event handling logic for inline thread doodads
   */
  private final static NodeEventHandler ANCHOR_HANDLER = new ChunkyElementHandler() {
    @Override
    public boolean handleClick(ContentElement element, EditorEvent event) {
      SelectionUtil.placeCaretBeforeElement(element.getSelectionHelper(), element);
      return true;
    }

    @Override
    public boolean handleBackspaceAfterNode(ContentElement element, EditorEvent event) {
      // Do nothing, and report gesture as handled.
      return true;
    }

    @Override
    public boolean handleDeleteBeforeNode(ContentElement element, EditorEvent event) {
      // Do nothing, and report gesture as handled.
      return true;
    }
  };

  /** Reveals DOM elements as semantic views. */
  private final DomAsViewProvider views;

  /** Blip whose anchors this renderer renders. */
  private final ConversationBlip blip;

  /** View id mapper used to look up view id from model object. */
  private final ViewIdMapper viewIdMapper;

  //
  // Local state needed for providing AnchorHandler events through incremental
  // processing.
  //
  // Note that it might be possible to encode this state as extra attributes /
  // properties on the view itself, which may have benefits.
  //

  /** Anchors that have no predecessor with the same thread id. Lazily created. */
  private StringMap<ReplyDoodad> canonicals;

  /** Anchors that have a predecessor with the same thread id. Lazily created. */
  private IdentitySet<ReplyDoodad> duplicates;

  /** Ordering of canonical anchors in this document. Lazily created. */
  private LinkedPruningSequenceMap<ReplyDoodad, ReplyDoodad> canonicalOrder;

  /** Option listener to handle canonical anchor addition and removal. */
  private final AnchorHandler handler;

  static class CanonicalAnchorFinder implements Proc<ReplyDoodad> {
    /** Id of the anchor to find. */
    private final String id;
    /** Current minimum anchor with id {@code id}. */
    private ReplyDoodad min;

    CanonicalAnchorFinder(String id) {
      this.id = id;
    }

    static ReplyDoodad findMinimum(String id, IdentitySet<ReplyDoodad> anchors) {
      CanonicalAnchorFinder finder = new CanonicalAnchorFinder(id);
      anchors.each(finder);
      return finder.min;
    }

    @Override
    public void apply(ReplyDoodad duplicate) {
      if (duplicate.getId().equals(id)) {
        min = (min == null || duplicate.compareTo(min) < 0) ? duplicate : min;
      }
    }
  }

  private InlineAnchorLiveRenderer(ViewIdMapper viewIdMapper, ConversationBlip blip,
      AnchorHandler h, DomAsViewProvider views) {
    this.viewIdMapper = viewIdMapper;
    this.blip = blip;
    this.handler = h;
    this.views = views;
  }

  private IdentitySet<ReplyDoodad> getDuplicates() {
    if (duplicates == null) {
      duplicates = CollectionUtils.createIdentitySet();
    }
    return duplicates;
  }

  public StringMap<ReplyDoodad> getCanonicals() {
    if (canonicals == null) {
      canonicals = CollectionUtils.createStringMap();
    }
    return canonicals;
  }

  private LinkedPruningSequenceMap<ReplyDoodad, ReplyDoodad> getCanonicalOrder() {
    if (canonicalOrder == null) {
      canonicalOrder = LinkedPruningSequenceMap.create();
    }
    return canonicalOrder;
  }

  private void add(ReplyDoodad anchor) {
    ReplyDoodad existing = getCanonicals().get(anchor.getId());
    if (existing == null) {
      addCanonical(anchor);
    } else if (anchor.compareTo(existing) < 0) {
      // New anchor occurs earlier. Replace.
      removeCanonical(existing);
      getDuplicates().add(existing);
      addCanonical(anchor);
    } else {
      // New anchor occurs later. Ignore.
      getDuplicates().add(anchor);
    }
  }

  private void remove(ReplyDoodad anchor) {
    ReplyDoodad existing = getCanonicals().get(anchor.getId());
    if (!anchor.equals(existing)) {
      // Non-canonical removal.
      getDuplicates().remove(anchor);
    } else {
      removeCanonical(anchor);

      // Is there a duplicate to be promoted to a canonical?
      ReplyDoodad toPromote = CanonicalAnchorFinder.findMinimum(anchor.getId(), getDuplicates());
      if (toPromote != null) {
        getDuplicates().remove(toPromote);
        addCanonical(toPromote);
      }
    }
  }

  private void addCanonical(ReplyDoodad anchor) {
    getCanonicals().put(anchor.getId(), anchor);
    getCanonicalOrder().put(anchor, anchor);

    // Find next canonical, remembering to eliminate inconvenient circularity.
    SequenceElement<ReplyDoodad> nextNode = getCanonicalOrder().getElement(anchor).getNext();
    ReplyDoodad next = (nextNode != getCanonicalOrder().getFirst()) ? nextNode.value() : null;
    anchor.setDomId(viewIdMapper.inlineAnchorOf(blip, anchor.getId()));
    handler.onAnchorAddedBefore(blip, next, anchor);
  }

  private void removeCanonical(ReplyDoodad anchor) {
    getCanonicals().remove(anchor.getId());
    getCanonicalOrder().remove(anchor);
    handler.onAnchorRemoved(blip, anchor);
    // Clear DOM id only after handler processing.
    anchor.setDomId(null);
  }

  @Override
  public Element createDomImpl(Renderable element) {
    // HTML does not allow block elements inside inline elements, so it has to
    // be divs all the way, not spans.
    Element e = Document.get().createDivElement();
    e.getStyle().setDisplay(Display.INLINE);
    // Do the things that the doodad API should be doing by default.
    DomHelper.setContentEditable(e, false, false);
    DomHelper.makeUnselectable(e);
    // ContentElement attempts this, and fails, so we have to do this ourselves.
    e.getStyle().setProperty("whiteSpace", "normal");
    e.getStyle().setProperty("lineHeight", "normal");
    return e;
  }

  @Override
  public void onActivatedSubtree(ContentElement element) {
    String id = element.getAttribute(Blips.THREAD_INLINE_ANCHOR_ID_ATTR);
    if (id != null) {
      add(new ReplyDoodad(element, id));
    }
  }

  @Override
  public void onDeactivated(ContentElement element) {
    String id = element.getAttribute(Blips.THREAD_INLINE_ANCHOR_ID_ATTR);
    if (id != null) {
      remove(new ReplyDoodad(element, id));
    }
  }

  //
  // This renderer is only intended for a static rendering,
  // and is not intended for keeping that rendering live in response to events.
  // Since there is no API for static renderers (all renderers must be
  // implemented as mutation handlers), the only way to prevent misuse is to
  // detect it dynamically.
  //

  @Override
  public void onAttributeModified(ContentElement element, String name, String oldValue,
      String newValue) {
    if (Blips.THREAD_INLINE_ANCHOR_ID_ATTR.equals(name)) {
      if (oldValue != null) {
        remove(new ReplyDoodad(element, oldValue));
      }
      if (newValue != null) {
        add(new ReplyDoodad(element, newValue));
      }
    }
  }
}
TOP

Related Classes of org.waveprotocol.wave.client.wavepanel.render.InlineAnchorLiveRenderer$CanonicalAnchorFinder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.