/**
* 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.content.paragraph;
import static org.waveprotocol.wave.model.document.util.LineContainers.PARAGRAPH_FULL_TAGNAME;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.content.ContentNode;
import org.waveprotocol.wave.client.editor.content.ContentTextNode;
import org.waveprotocol.wave.client.editor.content.FullContentView;
import org.waveprotocol.wave.model.document.MutableDocument;
import org.waveprotocol.wave.model.document.util.DocumentContext;
import org.waveprotocol.wave.model.document.util.PersistentContent;
import org.waveprotocol.wave.model.document.util.Property;
import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
import org.waveprotocol.wave.model.util.Preconditions;
import java.util.Map;
/**
* Metadata useful for managing line rendering and behaviour within documents.
* Additionally contains useful methods to manipulate this information.
*
* @author danilatos@google.com (Daniel Danilatos)
* @author patcoleman@google.com (Pat Coleman)
*/
public class Line {
/** Property used to attack line metadata to the elements. */
private static final Property<Line> LINE = Property.immutable("line");
/** Property used to indicate which line a linecontainer is rendering. */
private static final Property<Line> FIRST_LINE = Property.mutable("fline");
// Utilities (line element methods, then local paragraph ones, then line container ones).
/** Retrieve the metadata from a line element. */
public static Line fromLineElement(ContentElement line) {
assert LineRendering.isLineElement(line)
: "Not line element: " + line.getTagName();
return line.getProperty(LINE);
}
/**
* Retrieve the metadata from a paragraph element.
* NOTE(danilatos): Might be something other than a paragraph or line.
*/
public static Line fromParagraph(ContentElement paragraph) {
assert !LineRendering.isLineElement(paragraph)
: "Is but shouldn't be line element: " + paragraph.getTagName();
return paragraph.getProperty(LINE);
}
/** Retrieve the first semantic line from a container. */
public static Line getFirstLineOfContainer(ContentElement container) {
assert LineRendering.isLineContainerElement(container)
: "Not container: " + container.getTagName();
return container.getProperty(FIRST_LINE);
}
/** Assign a first line to a line container. */
public static void setFirstLineOfContainer(ContentElement container, Line first) {
assert LineRendering.isLineContainerElement(container)
: "Not container: " + container.getTagName();
container.setProperty(FIRST_LINE, first);
}
// Metadata information
public static final int DIRTY = -1;
private final DocumentContext<ContentNode, ContentElement, ContentTextNode> cxt;
private final ContentElement lineElement;
private final ContentElement paragraph;
// Linked list of adjacent lines
private Line previous;
private Line next;
private int cachedNumberValue = DIRTY;
/**
* Given a document and a line element, this creates a local paragraph to hold the line's
* content, and wraps them up in a Line bundle to allow easier document traversal.
*/
Line(DocumentContext<ContentNode, ContentElement, ContentTextNode> cxt,
ContentElement lineElement) {
assert LineRendering.isLineElement(lineElement);
this.cxt = cxt;
this.lineElement = lineElement;
this.paragraph = cxt.annotatableContent().transparentCreate(PARAGRAPH_FULL_TAGNAME,
cxt.document().getAttributes(lineElement),
lineElement.getParentElement(), lineElement.getNextSibling());
PersistentContent.makeHard(ContentElement.ELEMENT_MANAGER, paragraph);
lineElement.setProperty(LINE, this);
paragraph.setProperty(LINE, this);
}
// Linked list
public Line next() {
return next;
}
public Line previous() {
return previous;
}
public void insertAfter(Line previousLine) {
Preconditions.checkNotNull(previousLine, "Previous line must not be null");
previous = previousLine;
next = previous.next;
previous.next = this;
if (next != null) {
next.previous = this;
}
}
public void insertBefore(Line nextLine) {
Preconditions.checkNotNull(nextLine, "Next line must not be null");
next = nextLine;
previous = next.previous;
next.previous = this;
if (previous != null) {
previous.next = this;
}
}
public void remove() {
// delete if in tree:
// NOTE(danilatos): This must come first so the rendering logic has
// a chance to take into account the surrounding structure, before
// the line is removed from the linked list.
if (paragraph.getParentElement() != null) {
cxt.annotatableContent().transparentDeepRemove(paragraph);
}
if (next != null) {
next.previous = previous;
}
if (previous != null) {
previous.next = next;
}
previous = next = null;
}
// Bundle exposure
public MutableDocument<ContentNode, ContentElement, ContentTextNode> getMutableDoc() {
return cxt.document();
}
public ContentElement getParagraph() {
return paragraph;
}
public ContentElement getLineElement() {
return lineElement;
}
// Replacement for element methods
public String getAttribute(String name) {
return lineElement.getAttribute(name);
}
public Map<String, String> getAttributes() {
return getMutableDoc().getAttributes(lineElement);
}
// Behavioural modifiers
public ParagraphBehaviour getBehaviour() {
return ParagraphBehaviour.of(getAttribute(Paragraph.SUBTYPE_ATTR));
}
public boolean isDecimalListItem() {
return Paragraph.isDecimalListItem(paragraph);
}
public int getIndent() {
return lineElement == null ? 0 :
Paragraph.getIndent(lineElement.getAttribute(Paragraph.INDENT_ATTR));
}
/**
* Cache the number value for a bullet point. Use {@value #DIRTY} to clear
* the cache.
*
* The value must only be set if it is accurate. It must be cleared when
* inaccurate.
*/
public void setCachedNumberValue(int value) {
this.cachedNumberValue = value;
}
/**
* Returns the current cached number value for a bullet point, or
* {@value #DIRTY} if no value is cached. If there is a value, it should
* always be usable without additional verification (see
* {@link #setCachedNumberValue(int)})
*/
public int getCachedNumberValue() {
return cachedNumberValue;
}
@Override
public String toString() {
return "Line("
+ XmlStringBuilder.createNode(FullContentView.INSTANCE, lineElement).getXmlString() + ", "
+ cachedNumberValue + ")";
}
}