/**
* Copyright 2008 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;
import org.waveprotocol.wave.model.document.util.Point;
/**
* Content points
*
* NOTE(danilatos): This is a MUTABLE object
* TODO(danilatos): Deprecate this in favour of Point<ContentNode>
*
*/
public final class ContentPoint {
/**
* The node
*/
private ContentNode container;
private ContentNode nodeAfter;
/**
* The offset
*/
private int offset;
/** Copy constructor */
public ContentPoint(ContentPoint other) {
container = other.container;
nodeAfter = other.nodeAfter;
offset = other.offset;
}
/**
* Element-Node point
* @see Point#inElement(Object, Object)
*/
public ContentPoint(ContentElement container, ContentNode nodeAfter) {
this.container = container;
this.nodeAfter = nodeAfter;
this.offset = -1;
}
/**
* Text-Offset point
* @see Point#inText(Object, int)
*/
public ContentPoint(ContentTextNode textNode, int offset) {
this.container = textNode;
this.nodeAfter = null;
this.offset = offset;
}
/**
* @return The content point as a {@link Point}
*/
public Point<ContentNode> asPoint() {
return isInTextNode()
? Point.inText(container, offset)
: Point.inElement(container, nodeAfter);
}
/**
* Construct from a Point
*/
public static ContentPoint fromPoint(Point<ContentNode> point) {
return point.isInTextNode()
? new ContentPoint((ContentTextNode)point.getContainer(), point.getTextOffset())
: new ContentPoint((ContentElement)point.getContainer(), point.getNodeAfter());
}
/**
* @return The container node
*/
public ContentNode getContainer() {
return container;
}
/**
* @return The node after the point. Only valid for element-node points
*/
public ContentNode getNodeAfter() {
assert !isInTextNode();
return nodeAfter;
}
/**
* @return The text offset. Only valid for text-offset points
*/
public int getTextOffset() {
assert isInTextNode();
return offset;
}
/**
* @return true if this is a text-offset point.
*/
public boolean isInTextNode() {
return offset >= 0;
}
/**
* Mutator
* @see #ContentPoint(ContentElement, ContentNode)
*/
public ContentPoint set(ContentElement container, ContentNode nodeAfter) {
this.container = container;
this.nodeAfter = nodeAfter;
this.offset = -1;
return this;
}
/**
* Mutator
* @see #ContentPoint(ContentTextNode, int)
*/
public ContentPoint set(ContentTextNode container, int offset) {
this.container = container;
this.offset = offset;
this.nodeAfter = null;
return this;
}
/**
* Set the text offset. Only valid for text-offset points
* @param offset
*/
public void setTextOffset(int offset) {
if (!isInTextNode()) {
throw new RuntimeException("Can't set text offset of a point not in a text node");
}
this.offset = offset;
}
/**
* Sets point at beginning of input node
*
* @param node
* @return this for convenience
*/
public ContentPoint setToBeginning(ContentNode node) {
if (node instanceof ContentElement) {
set((ContentElement)node, node.getFirstChild());
} else {
set((ContentTextNode)node, 0);
}
return this;
}
/**
* Sets point at end of input node
*
* @param node
* @return this for convenience
*/
public ContentPoint setToEnd(ContentNode node) {
if (node instanceof ContentElement) {
set((ContentElement)node, null);
} else {
ContentTextNode textNode = (ContentTextNode) node;
set(textNode, textNode.getLength());
}
return this;
}
/**
* Sets point outside + before input node
*
* NOTE(user): potentially costly to call findChildIndex!
*
* @param node
* @return this for convenience
*/
public ContentPoint setToBefore(ContentNode node) {
set(node.getParentElement(), node);
return this;
}
/**
* Sets point outside + after input node
*
* NOTE(user): potentially costly to call findChildIndex!
*
* @param node
* @return this for convenience
*/
public ContentPoint setToAfter(ContentNode node) {
set(node.getParentElement(), node.getNextSibling());
return this;
}
/**
* @return the node before the current point in the given view, or null
* if isInTextNode
*/
public ContentNode getNodeBefore(ContentView view) {
return isInTextNode() ? null : (
nodeAfter == null ? view.getLastChild(container) : view.getPreviousSibling(nodeAfter));
}
/**
* @param node
* @return true if point is directly in node
*/
public boolean isIn(ContentNode node) {
return container.equals(node);
}
/**
* @return true if point is a beginning of its node
*/
public boolean isAtBeginning() {
return isInTextNode() ? offset == 0 : nodeAfter == container.getFirstChild();
}
/**
* @return true if point is an end of its node
*/
public boolean isAtEnd() {
return isInTextNode()
? offset == getTextNodeLength()
: nodeAfter == null;
}
private int getTextNodeLength() {
return ((ContentTextNode)container).getLength();
}
/**
* Moves the point outside its current element if the
* offset places the point at the beginning or end of that
* element. Biased towards a left move.
*
* @return True iff point was moved
*/
public boolean maybeMoveOut() {
int dir = 0;
if (isInTextNode()) {
if (offset == 0) {
dir = -1;
} else if (offset >= getTextNodeLength()) {
dir = 1;
}
} else {
if (nodeAfter == container.getFirstChild()) {
dir = -1;
} else if (nodeAfter == null) {
dir = 1;
}
}
if (dir != 0) {
nodeAfter = container;
container = nodeAfter.getParentElement();
if (dir == 1) {
nodeAfter = nodeAfter.getNextSibling();
}
offset = -1;
return true;
} else {
return false;
}
}
/**
* {@inheritDoc}
*
* Equality if node and offset are the same
*/
@Override
public final boolean equals(Object o) {
if (o == this) return true;
if (o instanceof ContentPoint) {
ContentPoint point = (ContentPoint) o;
return container.equals(point.getContainer()) && offset == point.getTextOffset()
&& (nodeAfter == point.getNodeAfter() || nodeAfter.equals(point.getNodeAfter()));
}
return false;
}
/** {@inheritDoc} */
@Override
public final int hashCode() {
return container.hashCode() + 37 * offset + 1009 * nodeAfter.hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
// TODO(user): can we find way to use ContentNode.getPrettyHtml to print
// the point more nicely?
return "(" + container.toString() + ":" + (isInTextNode() ? offset : nodeAfter) + ")";
}
}