/** * 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.util; import org.waveprotocol.wave.client.editor.EditorContext; import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations; import org.waveprotocol.wave.model.document.MutableAnnotationSet; import org.waveprotocol.wave.model.document.ReadableAnnotationSet; import org.waveprotocol.wave.model.document.util.Annotations; import org.waveprotocol.wave.model.document.util.Range; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.ReadableStringSet; import org.waveprotocol.wave.model.util.ReadableStringSet.Proc; import org.waveprotocol.wave.model.util.ValueUtils; public class EditorAnnotationUtil { /** Un-constructable utility class. */ private EditorAnnotationUtil() {} /** * Finds the first of the given keys that covers the entire selected range, and returns its value. * * @param editor Contains the non-null selection and document * @param keys Keys to look through * @return The first value annotation that covers the range at one of the given keys, else null. */ public static String getFirstAnnotationOverSelection(EditorContext editor, String... keys) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); return getFirstCoveringAnnotationOverRange(editor.getDocument(), editor.getCaretAnnotations(), keys, range.getStart(), range.getEnd()); } /** * Looks through a list of annotation keys, finding the first to cover the given range, * and return its annotation value. If the range is collapsed, it's assumed that the * desired annotation set is stored in the caret parameter. * * @param doc Document to check for annotations. * @param caret Annotations at the current selection. * @param keys Keys to look through * @param start Start of range to check * @param end End of range to check * @return The first value annotation that covers the range at one of the given keys, else null. */ public static String getFirstCoveringAnnotationOverRange(MutableAnnotationSet<String> doc, CaretAnnotations caret, String[] keys, int start, int end) { // iterate through each key: for (String key : keys) { String value = getAnnotationOverRangeIfFull(doc, caret, key, start, end); if (value != null) { return value; } } return null; // none found. } /** * Returns an annotation over the selected range only if the entire range has a single annotation. * If the annotation changes or if the range is not annotated (annotated with null), returns null. * * @param editor Editor whose annotations are to be checked, with non-null selection and doc. * @param key Key of annotation to retrieve */ public static String getAnnotationOverSelectionIfFull(EditorContext editor, String key) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); return getAnnotationOverRangeIfFull(editor.getDocument(), editor.getCaretAnnotations(), key, range.getStart(), range.getEnd()); } /** * Returns an annotation over a range only if the entire range has a single annotation. * If the annotation changes or if the range is not annotated (annotated with null), returns null. * If the range is collapsed it's assumed that the desired annotation set is in the caret param. * * @param doc Document to check for annotations. * @param caret Annotations at the current selection. * @param key Key of annotation to retrieve * @param start Start offset of range. * @param end End offset of range. */ public static String getAnnotationOverRangeIfFull(MutableAnnotationSet<String> doc, CaretAnnotations caret, String key, int start, int end) { if (start == end) { // Try to use the information about the cursor, even if it doesn't match // where the selection is. return caret.getAnnotation(key); } String currentValue = doc.getAnnotation(start, key); if (doc.firstAnnotationChange(start, end, key, currentValue) == -1) { // no change, fully annotated return currentValue; } // change is found, so return: return null; } /** * Sets the annotation key to a particular value over the entire selected range in an editor. * * @param editor Editor to set the annotation, with non-null selection and doc. * @param key Annotation key to set. * @param value Annotation value to set key to. */ public static void setAnnotationOverSelection(EditorContext editor, String key, String value) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); setAnnotationOverRange(editor.getDocument(), editor.getCaretAnnotations(), key, value, range.getStart(), range.getEnd()); } /** * Sets the annotation key to a particular value over an entire range. * If the range is collapsed it's assumed that the desired annotation set is in the caret param. * * @param doc Document to set the annotation in. * @param caret Collapsed-range annotations. * @param key key of annotation to set. * @param value value to set annotation to. * @param start start of range to set over. * @param end end of range to set over. */ public static void setAnnotationOverRange(MutableAnnotationSet<String> doc, CaretAnnotations caret, String key, String value, int start, int end) { // simple switch depending on whether the range is collapsed: if (start == end) { caret.setAnnotation(key, value); } else { doc.setAnnotation(start, end, key, value); } } /** * Clears all annotations for a set of keys over the current selected range. * * @param editor Editor whose annotations are to be cleared, with non-null selection and doc. * @param keys List of annotation keys to clear * @return true if annotations were actually changed */ public static boolean clearAnnotationsOverSelection(EditorContext editor, String... keys) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); return clearAnnotationsOverRange(editor.getDocument(), editor.getCaretAnnotations(), keys, range.getStart(), range.getEnd()); } /** * Clears all annotations over a particular range in the editor's document. * If the range is collapsed it's assumed that the desired annotation set is in the caret param. * * @param doc Document to check for annotations. * @param caret Annotations at the current collapsed range. * @param keys List of annotation keys to clear * @param start Start offset of range. * @param end End offset of range. * @return true if annotations were actually changed */ public static boolean clearAnnotationsOverRange(MutableAnnotationSet<String> doc, CaretAnnotations caret, String[] keys, int start, int end) { boolean wasRemoved = false; if (start == end) { // clear from caret annotation if collapsed range for (String key : keys) { if (caret.getAnnotation(key) != null) { caret.setAnnotation(key, null); // remove if present wasRemoved = true; } } } else { // clear from the entire range for (String key : keys) { if (doc.firstAnnotationChange(start, end, key, null) != -1) { doc.setAnnotation(start, end, key, null); // remove if present wasRemoved = true; } } } return wasRemoved; } /** * Finds the range of an adjacent or containing non-null range of contiguous * value for a given annotation key. * * If there are two ranges (the given location being at their boundary), then * prefer the one to the right. * * If there are no ranges (the key is null on either side of the location), * null is returned. * * @param doc * @param key * @param location * @return the range, or null if none found */ public static <V> Range getEncompassingAnnotationRange( final ReadableAnnotationSet<V> doc, String key, int location) { V value = doc.getAnnotation(location, key); if (value == null && location > 0) { value = doc.getAnnotation(location - 1, key); } if (value == null) { return null; } int start = doc.lastAnnotationChange(0, location, key, value); int end = doc.firstAnnotationChange(location, doc.size(), key, value); assert start < end : "Range should not be collapsed"; return new Range(start, end); } /** * Given the editor state, this examines the current caret annotations and adds any that * can be inferred from the position, given the alignment type. * * @param doc Document to check for annotations. * @param caret Current annotation styles at the caret. * @param keys Keys to supplement over the caret styles. * @param location Location of the caret in the document. * @param leftAlign Whether the annotations come from the left or right. */ public static void supplementAnnotations(final MutableAnnotationSet<String> doc, final CaretAnnotations caret, final ReadableStringSet keys, final int location, final boolean leftAlign) { // by default, everything inherits from the left, so for now, no need! if (leftAlign) { return; } // supplement anything that's missing and different: keys.each(new Proc() { @Override public void apply(String key) { if (!caret.hasAnnotation(key)) { String newValue = Annotations.getAlignedAnnotation(doc, location, key, leftAlign); String oldValue = doc.getAnnotation(location - 1, key); if (!ValueUtils.equal(newValue, oldValue)) { caret.setAnnotation(key, newValue); } } } }); } }