/**
* 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.testing;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Text;
import org.waveprotocol.wave.client.common.util.DomHelper;
import org.waveprotocol.wave.client.editor.extract.InconsistencyException.HtmlInserted;
import org.waveprotocol.wave.client.editor.extract.InconsistencyException.HtmlMissing;
import org.waveprotocol.wave.client.editor.extract.TypingExtractor;
import org.waveprotocol.wave.client.editor.extract.TypingExtractor.SelectionSource;
import org.waveprotocol.wave.client.editor.impl.HtmlView;
import org.waveprotocol.wave.model.document.util.Point;
/**
* Simulates the behaviour of a browser updating the DOM due to a user typing
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class FakeUser implements SelectionSource{
public enum Action {
MOVE,
TYPE,
DELETE,
BACKSPACE,
SPLIT
}
private final HtmlView htmlView;
private Point<Node> caret;
public FakeUser(HtmlView htmlView) {
this.htmlView = htmlView;
}
@SuppressWarnings("unchecked") // NOTE(zdwang): This is for (Point<Node>) action[1]
public void run(TypingExtractor extractor, Object... actions) throws HtmlMissing, HtmlInserted {
for (Object a : actions) {
Object[] action = (Object[]) a;
Point<Node> sel = getSelectionStart();
Text txt = sel == null ? null : sel.getContainer().<Text>cast();
Action type = (Action) action[0];
switch (type) {
case MOVE:
//extractor.flush();
setCaret((Point<Node>) action[1]);
break;
case TYPE:
String typed = (String) action[1];
extractor.somethingHappened(getSelectionStart());
if (sel.isInTextNode()) {
txt.insertData(sel.getTextOffset(), (String) action[1]);
moveCaret(((String) action[1]).length());
} else {
txt = Document.get().createTextNode(typed);
sel.getContainer().insertBefore(txt, sel.getNodeAfter());
setCaret(Point.inText((Node)txt, typed.length()));
}
break;
case BACKSPACE:
case DELETE:
extractor.somethingHappened(getSelectionStart());
int amount = (Integer) action[1];
if (type == Action.BACKSPACE) {
moveCaret(-amount);
}
deleteText(amount);
break;
case SPLIT:
sel.getContainer().<Text>cast().splitText(sel.getTextOffset());
moveCaret(0);
break;
}
}
}
private void deleteText(int amount) {
Point<Node> sel = getSelectionStart();
Text txt = sel == null ? null : sel.getContainer().<Text>cast();
int startIndex = sel.getTextOffset(), len;
while (amount > 0) {
if (txt == null || !DomHelper.isTextNode(txt)) {
throw new RuntimeException("Action ran off end of text node");
}
String data = txt.getData();
int remainingInNode = data.length() - startIndex;
if (remainingInNode >= amount) {
len = amount;
} else {
len = remainingInNode;
}
txt.setData(data.substring(0, startIndex) + data.substring(startIndex + len));
amount -= len;
startIndex = 0;
txt = htmlView.getNextSibling(txt).cast();
}
moveCaret(0);
}
public void moveCaret(int distance) {
Point<Node> caret = getSelectionStart();
if (!caret.isInTextNode()) {
Node before = Point.nodeBefore(htmlView, caret.asElementPoint());
if (DomHelper.isTextNode(before)) {
caret = Point.inText(before, before.<Text>cast().getLength());
} else if (DomHelper.isTextNode(caret.getNodeAfter())) {
caret = Point.inText(caret.getNodeAfter(), 0);
} else {
throw new RuntimeException("Unimplemented/Invalid");
}
}
Text nodelet = caret.getContainer().cast();
int offset = caret.getTextOffset() + distance;
while (offset < 0) {
nodelet = htmlView.getPreviousSibling(nodelet).cast();
if (nodelet == null || !DomHelper.isTextNode(nodelet)) {
throw new RuntimeException("Action ran off end of text node");
}
offset += nodelet.getLength();
}
while (offset > nodelet.getLength()) {
offset -= nodelet.getLength();
nodelet = htmlView.getPreviousSibling(nodelet).cast();
if (nodelet == null || !DomHelper.isTextNode(nodelet)) {
throw new RuntimeException("Action ran off end of text node");
}
}
setCaret(Point.inText((Node)nodelet, offset));
}
private void setCaret(Point<Node> point) {
caret = point;
}
public static Object move(Point<Node> caret) {
return new Object[]{Action.MOVE, caret, caret};
}
//TODO(danilatos): Support non-collapsed selections
// public static Object move(Point<Node> start, Point<Node> end) {
// return new Object[]{Action.MOVE, start, end};
// }
public static Object type(String text) {
return new Object[]{Action.TYPE, text};
}
public static Object del() {
return del(1);
}
public static Object del(int len) {
return new Object[]{Action.DELETE, len};
}
public static Object bksp() {
return bksp(1);
}
public static Object bksp(int len) {
return new Object[]{Action.BACKSPACE, len};
}
public static Object split() {
return new Object[]{Action.SPLIT};
}
@Override
public Point<Node> getSelectionEnd() {
return caret;
}
@Override
public Point<Node> getSelectionStart() {
return caret;
}
}