// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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 com.google.collide.shared.document.anchor;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
// TODO: resolve the looseness in type safety
/*
* There is loosened type safety in this class for the ListenerManagers to allow
* for both the read-only interface (ReadOnlyAnchor) and this class to share the
* same underlying ListenerManager for each event.
*/
/**
* Model for an anchor in the document that maintains its up-to-date positioning
* in the document as text changes occur.
*
* An anchor should pass {@link AnchorManager#IGNORE_LINE_NUMBER} if its line
* number will not be used.
*
* Similarly, an anchor should pass {@link AnchorManager#IGNORE_COLUMN} if its
* column will not be used. Anchors with this flag will be called
* "line anchors".
*
* One caveat with line anchors that have the {@link RemovalStrategy#REMOVE}
* removal strategy is they will be removed when the line's preceding newline
* character is removed. For example, if the cursor is at (line 2, column 0) and
* the user presses backspace, line 2's contents will be appended to line 1. In
* this case, any line anchors on line 2 will be removed. One example where this
* might be confusing is if the user selects from (line 1, column 0) to (line 2,
* column 0) and presses delete. This would delete the line anchors on line 2
* with the {@link RemovalStrategy#REMOVE} strategy even though the user has
* logically deleted the text on line 1. There used to be a partial exception to
* this rule to account for this confusin case, but that led to subtle bugs.
*
* Anchors can be positioned through the {@link AnchorManager}.
*
*/
@SuppressWarnings("rawtypes")
public class Anchor implements ReadOnlyAnchor {
/**
* Listener that is notified when an anchor is shifted as a result of a text
* change that affects the line number or column of the anchor. If the anchor
* ignores the column, it will never receive a callback as a result of a
* column shift. The same is true for line numbers.
*
* <p>
* If the anchor moves because of
* {@link AnchorManager#moveAnchor(Anchor, Line, int, int)}, this will not be
* called (see {@link MoveListener}).
*/
public interface ShiftListener extends ShiftListenerImpl<Anchor> {
}
interface ShiftListenerImpl<A extends ReadOnlyAnchor> {
void onAnchorShifted(A anchor);
}
/**
* Listener that is notified when an anchor is moved via
* {@link AnchorManager#moveAnchor(Anchor, Line, int, int)}.
*
* <p>
* If the anchor shifts because of text changes in the document, this will not
* be called (see {@link ShiftListener}.
*/
public interface MoveListener extends MoveListenerImpl<Anchor> {
}
interface MoveListenerImpl<A extends ReadOnlyAnchor> {
void onAnchorMoved(A anchor);
}
/**
* Listener that is notified when an anchor is removed.
*/
public interface RemoveListener extends RemoveListenerImpl<Anchor> {
}
interface RemoveListenerImpl<A> {
void onAnchorRemoved(A anchor);
}
/**
* Defines the behavior for this anchor if the text where it is anchored gets
* deleted.
*/
public enum RemovalStrategy {
/**
* Removes the anchor if the text is deleted. Read the {@link Anchor}
* javadoc for caveats with line anchors.
*/
REMOVE,
/** Shifts the anchor's position if the text is deleted */
SHIFT
}
/**
* This id is guaranteed to have a lower insertion index than any other id
* in the same column.
*/
public static final int ID_FIRST_IN_COLUMN = -1;
/**
* Counter for {@link #id} generation.
*
* <p>
* <a href="http://code.google.com/webtoolkit/doc/latest/DevGuideCodingBasicsCompatibility.html">
* Actually</a> it is not a 32-bit integer value, but 64-bit double value.
* So it <a href="http://ecma262-5.com/ELS5_HTML.htm#Section_8.5">can</a>
* address 2^53 positive integer values.
*/
private static int nextId = 0;
private boolean attached = true;
private int column;
private InsertionPlacementStrategy insertionPlacementStrategy =
InsertionPlacementStrategy.DEFAULT;
/**
* The type of this anchor.
*/
private final AnchorType type;
private final int id;
/**
* The client may optionally stuff an opaque, dynamic value into the anchor
*/
private Object value;
private Line line;
private int lineNumber;
private ListenerManager<ShiftListenerImpl<? extends ReadOnlyAnchor>> shiftListenerManager;
private ListenerManager<MoveListenerImpl<? extends ReadOnlyAnchor>> moveListenerManager;
private ListenerManager<RemoveListenerImpl<? extends ReadOnlyAnchor>> removeListenerManager;
private RemovalStrategy removalStrategy = RemovalStrategy.REMOVE;
Anchor(AnchorType type, Line line, int lineNumber, int column) {
this.type = type;
this.id = nextId++;
this.line = line;
this.lineNumber = lineNumber;
this.column = column;
}
@Override
public int getColumn() {
return column;
}
/**
* @return a unique id that can serve as a stable identifier for this anchor.
*/
@Override
public int getId() {
return id;
}
@Override
public AnchorType getType() {
return type;
}
/**
* Update the value stored in this anchor
*
* @param value the opaque value to store
*/
public void setValue(Object value) {
this.value = value;
}
/**
* @param <T>
* @return the value stored in this anchor. Note that type safety is the
* responsibility of the caller.
*/
@Override
@SuppressWarnings("unchecked")
public <T> T getValue() {
return (T) value;
}
@Override
public Line getLine() {
return line;
}
public LineInfo getLineInfo() {
return new LineInfo(line, lineNumber);
}
@Override
public int getLineNumber() {
return lineNumber;
}
@Override
public boolean isLineAnchor() {
return column == AnchorManager.IGNORE_COLUMN;
}
@Override
public RemovalStrategy getRemovalStrategy() {
return removalStrategy;
}
@Override
public boolean hasLineNumber() {
return lineNumber != AnchorManager.IGNORE_LINE_NUMBER;
}
@Override
public boolean isAttached() {
return attached;
}
@Override
public InsertionPlacementStrategy getInsertionPlacementStrategy() {
return insertionPlacementStrategy;
}
public void setInsertionPlacementStrategy(InsertionPlacementStrategy insertionPlacementStrategy) {
this.insertionPlacementStrategy = insertionPlacementStrategy;
}
@SuppressWarnings("unchecked")
@Override
public ListenerRegistrar<ReadOnlyAnchor.ShiftListener> getReadOnlyShiftListenerRegistrar() {
return (ListenerRegistrar) getShiftListenerRegistrar();
}
@SuppressWarnings("unchecked")
public ListenerRegistrar<ShiftListener> getShiftListenerRegistrar() {
if (shiftListenerManager == null) {
shiftListenerManager = ListenerManager.create();
}
return (ListenerRegistrar) shiftListenerManager;
}
@SuppressWarnings("unchecked")
@Override
public ListenerRegistrar<ReadOnlyAnchor.MoveListener> getReadOnlyMoveListenerRegistrar() {
return (ListenerRegistrar) getMoveListenerRegistrar();
}
@SuppressWarnings("unchecked")
public ListenerRegistrar<MoveListener> getMoveListenerRegistrar() {
if (moveListenerManager == null) {
moveListenerManager = ListenerManager.create();
}
return (ListenerRegistrar) moveListenerManager;
}
@SuppressWarnings("unchecked")
@Override
public ListenerRegistrar<ReadOnlyAnchor.RemoveListener> getReadOnlyRemoveListenerRegistrar() {
return (ListenerRegistrar) getRemoveListenerRegistrar();
}
@SuppressWarnings("unchecked")
public ListenerRegistrar<RemoveListener> getRemoveListenerRegistrar() {
if (removeListenerManager == null) {
removeListenerManager = ListenerManager.create();
}
return (ListenerRegistrar) removeListenerManager;
}
public void setRemovalStrategy(RemovalStrategy removalStrategy) {
this.removalStrategy = removalStrategy;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(getType().toString());
sb.append(":").append(getId());
sb.append(" (").append(lineNumber).append(',').append(column).append(")");
sb.append("[").append(value).append("]");
sb.append(": ");
String lineStr = line.toString();
if (column <= lineStr.length()) {
int split = Math.max(0, Math.min(column, lineStr.length()));
sb.append(lineStr.subSequence(0, split));
sb.append("^");
sb.append(lineStr.substring(split));
} else {
sb.append(lineStr);
for (int i = 0, n = column - lineStr.length(); i < n; i++) {
sb.append(" ");
}
sb.append("^ (WARNING: the anchor is out-of-bounds)");
}
return sb.toString();
}
void dispatchShifted() {
if (shiftListenerManager != null) {
shiftListenerManager
.dispatch(new Dispatcher<Anchor.ShiftListenerImpl<? extends ReadOnlyAnchor>>() {
@SuppressWarnings("unchecked")
@Override
public void dispatch(ShiftListenerImpl listener) {
listener.onAnchorShifted(Anchor.this);
}
});
}
}
public void dispatchMoved() {
if (moveListenerManager != null) {
moveListenerManager
.dispatch(new Dispatcher<Anchor.MoveListenerImpl<? extends ReadOnlyAnchor>>() {
@SuppressWarnings("unchecked")
@Override
public void dispatch(MoveListenerImpl listener) {
listener.onAnchorMoved(Anchor.this);
}
});
}
}
void detach() {
this.attached = false;
}
void dispatchRemoved() {
if (removeListenerManager != null) {
removeListenerManager
.dispatch(new Dispatcher<Anchor.RemoveListenerImpl<? extends ReadOnlyAnchor>>() {
@SuppressWarnings("unchecked")
@Override
public void dispatch(RemoveListenerImpl listener) {
listener.onAnchorRemoved(Anchor.this);
}
});
}
}
/**
* Make sure all calls to this method are surrounded with removal and
* re-addition to the list(s) it belongs in!
*/
void setColumnWithoutDispatch(int column) {
this.column = column;
}
void setLineWithoutDispatch(Line line, int lineNumber) {
checkArgument(hasLineNumber() == (lineNumber != AnchorManager.IGNORE_LINE_NUMBER));
this.line = line;
this.lineNumber = lineNumber;
}
@Override
public boolean hasColumn() {
return column != AnchorManager.IGNORE_COLUMN;
}
}