/** * 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.editor.util; import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations; import org.waveprotocol.wave.model.document.AnnotationBehaviour; import org.waveprotocol.wave.model.document.AnnotationBehaviour.BiasDirection; import org.waveprotocol.wave.model.document.AnnotationBehaviour.ContentType; import org.waveprotocol.wave.model.document.AnnotationBehaviour.CursorDirection; import org.waveprotocol.wave.model.document.AnnotationBehaviour.InheritDirection; import org.waveprotocol.wave.model.document.MutableDocument; import org.waveprotocol.wave.model.document.util.AnnotationRegistry; import org.waveprotocol.wave.model.document.util.Annotations; import org.waveprotocol.wave.model.document.util.LineContainers; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.util.Box; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV; import org.waveprotocol.wave.model.util.ReadableStringSet.Proc; import org.waveprotocol.wave.model.util.StringMap; import org.waveprotocol.wave.model.util.StringSet; import org.waveprotocol.wave.model.util.ValueUtils; import java.util.Iterator; /** * Implementation of the logic for custom annotation behaviours. * See @link{AnnotationBehaviour} for description of the algorithm, this aggregates all the * registered behaviours and performs the bias/supplementing they define. * * @author patcoleman@google.com (Pat Coleman) */ public class AnnotationBehaviourLogic<N> { /** Registry storing all known annotation behaviour definitions. */ private final AnnotationRegistry registry; /// Mutable state that should not change inside init/reset /** Accessor to the extra annotations currently on the caret. */ private final CaretAnnotations caret; /** Current document being annotated. */ private final MutableDocument<N, ?, ?> doc; /// Mutable state used within behavioural logic /** maps for annotation changes */ StringMap<Object> leftSide = CollectionUtils.createStringMap(); StringMap<Object> rightSide = CollectionUtils.createStringMap(); /** Constructor that associates the behaviour with its registry. */ public AnnotationBehaviourLogic(AnnotationRegistry registry, MutableDocument<N, ?, ?> doc, CaretAnnotations caret) { this.registry = registry; this.doc = doc; this.caret = caret; } /** * Re-bias the cursor - based on the current selection positions, and the last known movement. * Checks to see whether any annotation or elements desire to update the bias, and if so, * obeys the one with the highest priority. * * Note that if the selection is ranged, we always bias to the right (over the first character) * * @param start Start of selection * @param end End of selection * @param lastMovement The previously last known movement of the cursor to get into this state. * @return The new bias type. */ public BiasDirection rebias(int start, int end, final CursorDirection lastMovement) { Preconditions.checkState(doc != null, "Cannot call out of init/reset cycle."); Preconditions.checkPositionIndexes(start, end, doc.size()); // ranged is special case, always bias to the right of the start if (start != end) { return BiasDirection.RIGHT; } // initialise with containers: final Box<Double> bestPriority = Box.create(0.0); final Box<BiasDirection> bias = Box.create(biasFromContainers(doc.locate(start))); if (bias.boxed == BiasDirection.NEITHER) { bias.boxed = CursorDirection.toBiasDirection(lastMovement); } else { bestPriority.boxed = AnnotationBehaviour.ELEMENT_PRIORITY; } // build maps buildBoundaryMaps(start); // iterate through and bias based on priority leftSide.each( new ProcV<Object>() { public void apply(String key, Object value) { Iterator<AnnotationBehaviour> behaviours = registry.getBehaviours(key); while (behaviours.hasNext()) { AnnotationBehaviour behaviour = behaviours.next(); double priority = behaviour.getPriority(); if (priority > bestPriority.boxed) { bestPriority.boxed = priority; bias.boxed = behaviour.getBias(leftSide, rightSide, lastMovement); } } } }); return bias.boxed; } /** * Alter the caret annotations to override the default inherit-from-left when registered * annotation behaviour dictates otherwise. * * @param location Position in the document that the caret resides. * @param bias The last direction of movement to get to here. * @param type The type of content that this replacement is for */ public void supplementAnnotations(final int location, final BiasDirection bias, final ContentType type) { Preconditions.checkState(doc != null, "Cannot call out of init/reset cycle."); // build context for replacements buildBoundaryMaps(location); Point<N> pointAt = doc.locate(location); // apply with bias leftSide.each(new ProcV<Object>() { @Override public void apply(String key, Object value) { if (caret.hasAnnotation(key)) { return; // skip, user already set one. } boolean inheritFromRight = false; InheritDirection direction = InheritDirection.INSIDE; Iterator<AnnotationBehaviour> behaviours = registry.getBehaviours(key); if (!behaviours.hasNext()) { // no behaviour defined, so inherit from right if explicitly set: inheritFromRight = (bias == BiasDirection.RIGHT); } else { // Check which side to inherit from direction = behaviours.next().replace(rightSide, leftSide, type); inheritFromRight = shouldInheritFromRight(bias, direction); } // Supplement annotations, either from right or with null: if (inheritFromRight) { String newValue = Annotations.getAlignedAnnotation(doc, location, key, false); String oldValue = doc.getAnnotation(location - 1, key); if (!ValueUtils.equal(newValue, oldValue)) { caret.setAnnotation(key, newValue); } } else if (direction == InheritDirection.NEITHER) { if (location > 0 && doc.getAnnotation(location - 1, key) != null) { caret.setAnnotation(key, null); } } } }); } /** Fills leftSide, rightSide with key+values for annotations that change over the boundary. */ private void buildBoundaryMaps(final int location) { // init to default state: leftSide.clear(); rightSide.clear(); final StringMap<String> leftValues = CollectionUtils.createStringMap(); final StringMap<String> rightValues = CollectionUtils.createStringMap(); final StringSet keysToCheck = CollectionUtils.createStringSet(); // collection up non-null annotations on both sides if (location > 0) { doc.forEachAnnotationAt(location - 1, new ProcV<String>() { public void apply(String key, String value) { if (value != null) { leftValues.put(key, value); keysToCheck.add(key); } } }); } if (location < doc.size()) { doc.forEachAnnotationAt(location, new ProcV<String>() { public void apply(String key, String value) { if (value != null) { rightValues.put(key, value); keysToCheck.add(key); } } }); } // fill in values that change keysToCheck.each(new Proc() { public void apply(String key) { String left = leftValues.get(key); String right = rightValues.get(key); if (ValueUtils.notEqual(left, right)) { leftSide.put(key, left); rightSide.put(key, right); } } }); } /** Calculates initial bias from container elements. */ private BiasDirection biasFromContainers(Point<N> at) { // TODO(patcoleman): allow for more container registry. boolean atContainerStart = LineContainers.isAtLineStart(doc, at); boolean atContainerEnd = LineContainers.isAtLineEnd(doc, at); // Logic: // if only at the end or only at the start, bias inwards. // otherwise, don't specialise any bias. if (atContainerStart && atContainerEnd) { return BiasDirection.NEITHER; } else if (atContainerStart) { return BiasDirection.RIGHT; } else if (atContainerEnd) { return BiasDirection.LEFT; } else { return BiasDirection.NEITHER; } } /** Utility that takes a bias and inheritence, then returns whether to inherit from the right. */ private boolean shouldInheritFromRight(BiasDirection bias, InheritDirection inherit) { switch (inherit) { case INSIDE: if (bias == BiasDirection.RIGHT) { // inherit from inside, which is the character to the right. return true; } break; case OUTSIDE: if (bias == BiasDirection.LEFT || bias == BiasDirection.NEITHER) { // inherit from outside, which is the character to the right return true; } break; } return false; // otherwise, the desired behaviour is what the model will do for us. } }