Package org.waveprotocol.wave.client.editor.content

Source Code of org.waveprotocol.wave.client.editor.content.AnnotationPainter$BoundaryFunction

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

import org.waveprotocol.wave.client.editor.Editor;
import org.waveprotocol.wave.client.scheduler.Scheduler;
import org.waveprotocol.wave.client.scheduler.TimerService;
import org.waveprotocol.wave.model.document.AnnotationCursor;
import org.waveprotocol.wave.model.document.MutableAnnotationSet;
import org.waveprotocol.wave.model.document.indexed.LocationMapper;
import org.waveprotocol.wave.model.document.raw.TextNodeOrganiser;
import org.waveprotocol.wave.model.document.util.Annotations;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.DocumentContext;
import org.waveprotocol.wave.model.document.util.ElementManager;
import org.waveprotocol.wave.model.document.util.LocalDocument;
import org.waveprotocol.wave.model.document.util.PersistentContent;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.document.util.Property;
import org.waveprotocol.wave.model.document.util.ReadableDocumentView;
import org.waveprotocol.wave.model.util.ConcurrentSet;
import org.waveprotocol.wave.model.util.ReadableStringSet;
import org.waveprotocol.wave.model.util.ReadableStringSet.Proc;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* A class for painting annotations that need simple stylistic renderings.
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class AnnotationPainter {

  /**
   * Property on the ContentDocument to indicate isEditing state;
   *
   * If non-null the editor is in editing mode, else it in non-editing mode.
   */
  public static final Property<Boolean> DOCUMENT_MODE =
      Property.immutable("doc_mode");

  public static <N, E extends N, T extends N> boolean isEditing(LocalDocument<N, E, T> doc) {
    return isInEditingDocument(doc, doc.getDocumentElement());
  }

  public static <E> boolean isInEditingDocument(ElementManager<E> mgr, E element) {
    return Boolean.TRUE.equals(mgr.getProperty(AnnotationPainter.DOCUMENT_MODE, element));
  }

  /**
   * Max "units of work" per render pass, before we defer. We want this to be
   * a fair bit bigger than 1, because of the startup cost before we actually
   * start doing said units of work.
   */
  private static final int MAX_RUN_ITERATIONS = 80;

  private static final int MANY_ITERATIONS = 2000;

  /**
   * Per-document paint worker
   *
   * Public API allows registering of per-document functions & keys, as opposed
   * to global ones on the annotation painter.
   */
  public static class DocPainter<N, E extends N, T extends N> {

    // protect the task from the public api as a member variable
    private final Scheduler.IncrementalTask task = new Scheduler.IncrementalTask() {
      public boolean execute() {
        return doRun(MAX_RUN_ITERATIONS);
      }
    };

    // Aliases for parts of the bundle we need.
    private final LocalDocument<N, E, T> localDoc;
    private final LocationMapper<N> mapper;
    private final TextNodeOrganiser<T> textNodeOrganiser;
    private final ReadableDocumentView<N, E, T> persistentView;
    private final ReadableDocumentView<N, E, T> hardView;
    private final MutableAnnotationSet.Local localAnnotations;

    private final PainterRegistry paintRegistry;

    // State vars. Reinitialised on each call to execute().
    private HashMap<String, Object> currentValues;
    private int startLocation, endLocation;
    private int chunkEnd;
    private AnnotationCursor cursor;
    private ReadableStringSet nextChangingKeys;
    private Map<String, String> renderAttrs;

    private boolean dead = false;

    Map<String, Object> boundaryBefore;
    Map<String, Object> boundaryAfter;

    private DocPainter(DocumentContext<N, E, T> bundle, PainterRegistry paintRegistry) {
      localDoc = bundle.annotatableContent();
      mapper = bundle.locationMapper();
      textNodeOrganiser = bundle.textNodeOrganiser();
      persistentView = bundle.persistentView();
      hardView = bundle.hardView();
      localAnnotations = bundle.localAnnotations();
      this.paintRegistry = paintRegistry;
    }

    private boolean doRun(final int maxIterations) {
      if (dead) {
        return false;
      }

      int docSize = mapper.size();
      int maybeLocation = localAnnotations.firstAnnotationChange(
          0, docSize, REPAINT_KEY, null);

      if (maybeLocation == -1) {
        maybeScheduledPainters.remove(this);
        return false;
      }

      Point<N> point = mapper.locate(maybeLocation);
      E containingAnnotator = getAnnotatingElement(point.getCanonicalNode());

      N startNode;

      if (containingAnnotator != null) {

        // TODO(danilatos): Optimise by using equality comparison with an end node, but
        // be careful because sometimes the code below skips several nodes at a time, so
        // maybe still do a location comparison in those cases.
        N first = persistentView.getFirstChild(containingAnnotator);
        N last = persistentView.getLastChild(containingAnnotator);

        assert first != null : "We're supposed to be in this node, so it has at least one child";

        startLocation = mapper.getLocation(first);
        // Mark the entire range covered by the current bit of paint, in case we want to change
        // it and its range exceeds the currently marked repaint range.
        localAnnotations.setAnnotation(startLocation, mapper.getLocation(
            Point.after(persistentView, last)), REPAINT_KEY, "y");

        startNode = containingAnnotator;
      } else {
        startNode = ensureNodeBoundary(point);
        startLocation = maybeLocation;
      }

      // Node we are up to, our "iterator" value.
      N currentNode = startNode;

      int remainingIterations = maxIterations;

      endLocation = getEnd(startLocation, docSize, REPAINT_KEY, "y");

      ReadableStringSet allKeys = getKeys();
      nextChangingKeys = allKeys;
      chunkEnd = startLocation;
      currentValues = new HashMap<String, Object>();
      cursor = localAnnotations.annotationCursor(startLocation, endLocation, allKeys);
      progress();

      N chunkEndNode = ensureNodeBoundary(mapper.locate(chunkEnd)); // exclusive

      E lastBoundaryElement = null;
      while (true) {

        int currentLocation = currentNode == null ? docSize
            : DocHelper.getFilteredLocation(mapper, persistentView,
                Point.before(localDoc, currentNode));

        while (chunkEnd <= currentLocation && chunkEnd < endLocation) {
          Point<N> boundaryPoint = mapper.locate(chunkEnd);
          progress();
          chunkEndNode = ensureNodeBoundary(mapper.locate(chunkEnd));

          // Do boundary rendering
          E boundaryParent;
          N boundaryNodeAfter;
          // Convert a text point to a parent/nodeAfter pair
          if (boundaryPoint.isInTextNode()) {
            T textNode = hardView.asText(boundaryPoint.getContainer());
            boolean isAtStartOfTextNode = boundaryPoint.getTextOffset() == 0;
            assert isAtStartOfTextNode ||
                boundaryPoint.getTextOffset() == localDoc.getLength(textNode)
                    : "Boundary point not at node boundary! "
                      + localDoc.getData(textNode) + ":" + boundaryPoint.getTextOffset();
            // (a) NOTE(danilatos): This slicing (and in the else block) is so that we
            // can make the assumption later on, see corresponding (a) below.
            boundaryNodeAfter = localDoc.transparentSlice(
                isAtStartOfTextNode ? textNode : hardView.getNextSibling(textNode));
            boundaryParent = boundaryNodeAfter != null
                ? localDoc.getParentElement(boundaryNodeAfter)
                : hardView.getParentElement(textNode);
          } else {
            boundaryNodeAfter = boundaryPoint.getNodeAfter();
            if (boundaryNodeAfter != null) {
              boundaryNodeAfter = localDoc.transparentSlice(boundaryNodeAfter);
            }
            boundaryParent = boundaryNodeAfter != null
                ? localDoc.getParentElement(boundaryNodeAfter)
                : localDoc.asElement(boundaryPoint.getContainer());
          }
          // maybe create a boundary element
          lastBoundaryElement = getBoundaryElement(boundaryParent, boundaryNodeAfter);

          if (chunkEnd == currentLocation) {
            break;
          }
        }

        if (currentLocation >= endLocation || remainingIterations <= 0) {
          localAnnotations.setAnnotation(startLocation, currentLocation, REPAINT_KEY, null);
          // TODO(danilatos): Conditionally break only if i >= maxIterations, otherwise
          // find next range to repaint.
          break;
        }

        N next;
        E element = localDoc.asElement(currentNode);
        if (element == null) {
          // Wrap adjacent text nodes up
          N fromIncl = currentNode;
          N toExcl = fromIncl;
          while (localDoc.asText(toExcl = localDoc.getNextSibling(toExcl)) != null) {
            if (localDoc.isSameNode(chunkEndNode, toExcl)) {
              break;
            }
          }
          if (renderAttrs.size() > 0) {
            next = getNextNode(wrap(renderAttrs, fromIncl, toExcl));
          } else {
            next = toExcl != null ? toExcl : getNextNode(localDoc.getParentElement(currentNode));
          }
        } else if (isPaintElement(element)) {
          // TODO(danilatos): passing a transparent element to a regular traversal method.
          // This will currently work, but we might get exceptions thrown if we add that
          // to the behaviour of filtered view. The intended behaviour here is that
          // we get the last visible node that is a child of element, or null if none.
          N firstPersistentChild = hardView.getFirstChild(element);
          if (firstPersistentChild == null) {
            next = getFirstNode(element);
            localDoc.transparentUnwrap(element);
          } else {
            // Again, same here
            N lastPersistentChild = hardView.getLastChild(element);
            int annotatorEnd = mapper.getLocation(Point.after(persistentView, lastPersistentChild));

            if (annotatorEnd > chunkEnd || !rendersSame(renderAttrs, element)) {
              // If this node is stale, or
              // if this node was annotating a range further than the next render change,
              // then it must be removed and be replaced by smaller bits. We also need to
              // mark its encompassing range as to-be-repainted.
              next = getFirstNode(element);
              localDoc.transparentUnwrap(element);
              localAnnotations.setAnnotation(currentLocation, annotatorEnd, REPAINT_KEY, "y");
            } else {
              // Otherwise, its range is done, so just skip it
              next = getNextNode(element);

              // (a) Assumption about boundary elements not being inside paint elements
              // allows us to safely skip over the current element, a nice optimisation.
              // This will not be as easy later when we have prioritised paint nodes.
              // See corresponding (a) above for why we can make this assumption.
            }
          }
        } else if (isBoundaryElement(element)) {
          next = getNextNode(element);

          // If it's a boundary element, we want to strip it out, unless it's one we just
          // made. Ones we made earlier are further back, so it shouldn't be possible that
          // we've come across one of those. It might not even be possible that we even
          // come across the one we just made...
          // TODO(danilatos): Test if this check is necessary (and/or sufficient...)
          if (element != lastBoundaryElement) {
            localDoc.transparentDeepRemove(element);
          }
        } else {
          next = DocHelper.getNextNodeDepthFirst(localDoc, element, null, true);
        }

        currentNode = next;
        remainingIterations--;
      }

      return true;
    }

    private void progress() {
      ReadableStringSet changingKeys;

      final int start = chunkEnd;

      if (!cursor.hasNext()) {
        chunkEnd = endLocation;
        changingKeys = null;
      } else {
        changingKeys = cursor.nextLocation();
        chunkEnd = cursor.currentLocation();
      }

      // TODO(danilatos): More efficient, too much hashmap munging.
      boundaryBefore = new HashMap<String, Object>();
      boundaryAfter = new HashMap<String, Object>();

      nextChangingKeys.each(new Proc() {
        @Override
        public void apply(String key) {
          Object newValue = localAnnotations.getAnnotation(start, key);

          boundaryBefore.put(key, currentValues.get(key));
          boundaryAfter.put(key, newValue);

          if (newValue == null) {
            currentValues.remove(key);
          } else {
            currentValues.put(key, newValue);
          }
        }
      });

      nextChangingKeys = changingKeys;
      computeRenderAttrs();
    }

    private int getEnd(int start, int end, String key, Object fromValue) {
      int ret = localAnnotations.firstAnnotationChange(start, end, key, fromValue);
      return ret == -1 ? end : ret;
    }

    private boolean rendersSame(Map<String, String> attrs, E annotatingElement) {
      return attrs != null && attrs.equals(localDoc.getAttributes(annotatingElement));
    }

    private N getNextNode(N node) {
      return DocHelper.getNextNodeDepthFirst(localDoc, node, null, false);
    }

    private N getFirstNode(N node) {
      return DocHelper.getNextNodeDepthFirst(localDoc, node, null, true);
    }

    /**
     * Ensures the given point is at a node boundary, possibly splitting a text
     * node in order to do so, in which case a new point is returned.
     *
     * @param point
     * @return a point at the same place as the input point, guaranteed to be at
     *         a node boundary.
     */
    private N ensureNodeBoundary(Point<N> point) {
      return DocHelper.ensureNodeBoundaryReturnNextNode(point, localDoc, textNodeOrganiser);
    }

    private E wrap(Map<String, String> attrs, N fromIncl, N toExcl)  {
      E el = localDoc.transparentCreate(paintRegistry.getPaintTagName(), attrs,
          localDoc.getParentElement(fromIncl), fromIncl);
      localDoc.transparentMove(el, fromIncl, toExcl, null);
      return el;
    }

    private boolean isPaintElement(E element) {
      return localDoc.getTagName(element).equals(paintRegistry.getPaintTagName());
    }

    private boolean isBoundaryElement(E element) {
      return localDoc.getTagName(element).equals(paintRegistry.getBoundaryTagName());
    }

    private E getAnnotatingElement(N node) {
      for (E parent = localDoc.getParentElement(node); parent != null;
          parent = localDoc.getParentElement(parent)) {
        if (isPaintElement(parent)) {
          return parent;
        }
      }

      return null;
    }

    private void computeRenderAttrs() {
      renderAttrs = new HashMap<String, String>();

      boolean isEditing = isEditing(localDoc);
      for (PaintFunction func : paintRegistry.getPaintFunctions()) {
        // TODO(danilatos): Make this better by hiding keys the function did not
        // register for, and making the input map unchangeable by the fucntion.
        renderAttrs.putAll(func.apply(currentValues, isEditing));
      }
    }

    /**
     * Maybe create a boundary element at the given point.
     *
     * @param parent
     * @param nodeAfter
     * @return a boundary rendering element, or null if none needed here
     */
    private E getBoundaryElement(E parent, N nodeAfter) {
      // The current parent our boundary functions are putting children in
      E currentParent = parent;
      // The boundary element. We lazily create it; the first time a function
      // returns non-null, we create the element, put it in place, put the
      // function's returned element as a child, and make the boundary element
      // the current parent, so subsequent elements go into this container.
      E boundaryContainerElement = null;

      boolean isEditing = isEditing(localDoc);
      for (BoundaryFunction func : paintRegistry.getBoundaryFunctions()) {
        E result = func.apply(localDoc, currentParent, nodeAfter, boundaryBefore, boundaryAfter,
            isEditing);
        if (result != null && boundaryContainerElement == null) {
          boundaryContainerElement = localDoc.transparentCreate(paintRegistry.getBoundaryTagName(),
              Collections.<String, String>emptyMap(), currentParent, nodeAfter);
          currentParent = boundaryContainerElement;
          nodeAfter = null;
          localDoc.transparentMove(currentParent, result, localDoc.getNextSibling(result), null);
          PersistentContent.makeDeepTransparent(localDoc, boundaryContainerElement);
        }
      }

      return boundaryContainerElement;
    }

    private ReadableStringSet getKeys() {
      return paintRegistry.getKeys();
    }
  }

  private static final Property<AnnotationPainter> PAINTER_PROP =
      Property.mutable("annotation-painter");

  private static final String REPAINT_KEY = Annotations.makeUniqueLocal("paint");

  private static final Property<DocPainter<?,?,?>> DOC_PAINTER_PROP =
      Property.mutable("doc-annotation-painter");

  /**
   * Function for mapping any annotation key-value pairs to paint render attributes
   */
  public static interface PaintFunction {
    Map<String, String> apply(Map<String, Object> from, boolean isEditing);
  }

  /**
   * Function for mapping any change in annotation key-value pairs to boundary
   * elements to be inserted in the html (tracked by wrapper nodse, of course)
   */
  // TODO(danilatos): Also handle things like specifying left/right border on an
  // adjacent paint element? Or is that more a paint style implemented cleverly?
  public static interface BoundaryFunction {
    /**
     * Callback for rendering boundary elements.
     *
     * Must only create an element at the given point. Must return the created element,
     * or null if none created.
     *
     * @param localDoc
     * @param parent parent of to-be-created element
     * @param nodeAfter next sibling of to-be-created-element
     * @param before map of annotation values before the boundary
     * @param after map of annotation values after the boundary
     * @return the created element, or null if nothing created
     */
    // TODO(danilatos): This is a potentially dangerous method if implemented incorrectly.
    // Find a way to make it safe without the API becoming too cumbersome.
    <N, E extends N, T extends N> E apply(LocalDocument<N, E, T> localDoc, E parent, N nodeAfter,
        Map<String, Object> before, Map<String, Object> after, boolean isEditing);
  }

  private static final ConcurrentSet<DocPainter<?, ?, ?>> maybeScheduledPainters
      = ConcurrentSet.create();

  private final TimerService scheduler;

  /**
   * @param scheduler Used for asynchronously repainting
   */
  public AnnotationPainter(TimerService scheduler) {
    this.scheduler = scheduler;
  }

  /**
   * Same as {@link #scheduleRepaint(DocumentContext, int, int)}, but attempts
   * to find a painter for the given document context, and will only schedule a
   * repaint if it finds one.
   */
  public static <N, E extends N, T extends N> void maybeScheduleRepaint(
      DocumentContext<N, E, T> bundle, int start, int end) {

    AnnotationPainter painter = bundle.elementManager().getProperty(
        PAINTER_PROP, bundle.document().getDocumentElement());

    if (painter != null) {
      painter.scheduleRepaint(bundle, start, end);
    }
  }

  private static <N, E extends N, T extends N> void setPainterProp(DocumentContext<N, E, T> bundle,
      AnnotationPainter painter) {
    E docElement = bundle.document().getDocumentElement();
    bundle.elementManager().setProperty(PAINTER_PROP, docElement, painter);
  }

  /**
   * Don't use this unless you are EditorImpl code. Using it indiscriminantly
   * can cause lots of problems and bugs.
   */
  public static <N, E extends N, T extends N> boolean repaintNow(DocumentContext<N, E, T> bundle) {
    E docElement = bundle.document().getDocumentElement();

    DocPainter<?, ?, ?> docPainter = bundle.elementManager().getProperty(
        DOC_PAINTER_PROP, docElement);

    if (docPainter != null) {
      return docPainter.doRun(MAX_RUN_ITERATIONS);
    } else {
      return false;
    }
  }

  /**
   * Don't use this unless you are playback code. Using it indiscriminantly
   * can cause lots of problems and bugs.
   */
  public static void hackFlush() {
    maybeScheduledPainters.lock();
    try {
      for (DocPainter<?, ?, ?> docPainter : maybeScheduledPainters) {
        flush(docPainter);
      }
    } finally {
      maybeScheduledPainters.unlock();
    }
  }

  /**
   * Flushes any painting scheduled for a document.
   *
   * @param context  document to paint
   */
  public static <N> void flush(DocumentContext<N, ?, ?> context) {
    flush(getDocPainter(context));
  }

  /**
   * Runs a painter until completion.
   *
   * @param painter painter to run
   */
  private static void flush(DocPainter<?, ?, ?> painter) {
    while (painter.doRun(MANY_ITERATIONS)) { }
  }

  private void schedule(DocPainter<?, ?, ?> docPainter) {
    scheduler.schedule(docPainter.task);
    maybeScheduledPainters.add(docPainter);
  }

  /**
   * Mark a region of the document as stale and in need of a repaint
   *
   * @param bundle everything we need to know about the current document
   * @param start as per defined annotation range semantics
   * @param end as per defined annotation range semantics
   */
  public <N, E extends N, T extends N> void scheduleRepaint(
      DocumentContext<N, E, T> bundle, int start, int end) {
    setPainterProp(bundle, this);

    // Expand re-render range by 3, so that boundary decorators will get rendered if at paint
    // range boundaries. (Maybe do a nicer way later).
    // HACK(user): Turns out 1 wasn't enough, not sure why but seems like the
    // boundary node is not directly next to the annotated region, work out
    // exactly what number to use here.
    int size = bundle.document().size();
    end = Math.min(size, end + 3);
    start = Math.max(0, start - 3);

    if (start == end) {
      if (start == 0) {
        if (end < size) {
          end++;
        }
      } else {
        start--;
      }
    }

    assert start >= 0 && end >= start && size >= end;

    bundle.localAnnotations().setAnnotation(start, end, REPAINT_KEY, "y");
    DocPainter<?, ?, ?> docPainter = getDocPainter(bundle);
    schedule(docPainter);
  }

  /**
   * Return the doc painter for the given document context, perhaps creating
   * it if one did not already exist
   *
   * @param bundle document context
   * @return doc painter for given context
   */
  public static <N, E extends N, T extends N> DocPainter<?, ?, ?> getDocPainter(
      DocumentContext<N, E, T> bundle) {
    E docElement = bundle.document().getDocumentElement();
    DocPainter<?, ?, ?> docPainter = bundle.elementManager().getProperty(
        DOC_PAINTER_PROP, docElement);
    // HACK(user): Initializing this is tricky. We set this property in
    // EditorImpl as soon as we have a document element. However, the document
    // element is only constructed when we consume the initial operations.
    // The initial operations trigger annotation handling which expect the
    // DocPainter to be set. Thus, we lazily create a temporary DocPainter,
    // until the real one is ready.
    if (docPainter == null) {
      docPainter = new DocPainter<N, E, T>(bundle, Editor.ROOT_PAINT_REGISTRY);
      bundle.elementManager().setProperty(DOC_PAINTER_PROP, docElement, docPainter);
    }
    return docPainter;
  }

  public static <N, E extends N, T extends N> void createAndSetDocPainter(
      DocumentContext<N, E, T> bundle, PainterRegistry painterRegistry) {
    E docElement = bundle.document().getDocumentElement();

    DocPainter<?, ?, ?> existing = bundle.elementManager().getProperty(
        DOC_PAINTER_PROP, docElement);

    // Cleanup existing if exists
    if (existing != null) {
      existing.dead = true;
    }

    DocPainter<?, ?, ?> docPainter = new DocPainter<N, E, T>(bundle, painterRegistry);
    bundle.elementManager().setProperty(DOC_PAINTER_PROP, docElement, docPainter);
  }

  public static <N, E extends N, T extends N> void clearDocPainter(
      DocumentContext<N, E, T> bundle) {

    DocPainter<?, ?, ?> existing = bundle.elementManager().getProperty(
        DOC_PAINTER_PROP, bundle.document().getDocumentElement());

    // Cleanup existing if exists
    if (existing != null) {
      existing.dead = true;
    }
  }

  /**
   * Register paint rendering behaviour
   *
   * @param dependentKeys
   * @param function
   * @deprecated
   */
  @Deprecated
  public void registerPaintFunctionz(ReadableStringSet dependentKeys, PaintFunction function) {
    Editor.ROOT_PAINT_REGISTRY.registerPaintFunction(dependentKeys, function);
  }

  /**
   * Register boundary rendering behaviour
   *
   * @param dependentKeys
   * @param function
   * @deprecated
   */
  @Deprecated
  public void registerBoundaryFunctionz(
      ReadableStringSet dependentKeys, BoundaryFunction function) {
    Editor.ROOT_PAINT_REGISTRY.registerBoundaryFunction(dependentKeys, function);
  }
}
TOP

Related Classes of org.waveprotocol.wave.client.editor.content.AnnotationPainter$BoundaryFunction

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.