/**
* Copyright 2011 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.doodad.selection;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.waveprotocol.wave.client.editor.EditorContext;
import org.waveprotocol.wave.client.editor.EditorUpdateEvent;
import org.waveprotocol.wave.client.editor.EditorUpdateEvent.EditorUpdateListener;
import org.waveprotocol.wave.client.scheduler.TimerService;
import org.waveprotocol.wave.model.document.MutableAnnotationSet;
import org.waveprotocol.wave.model.document.util.FocusedRange;
import org.waveprotocol.wave.model.document.util.Range;
/**
* Contains logic for reporting a selection as annotations.
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class SelectionExtractor implements EditorUpdateListener {
private final TimerService clock;
private final String address;
private final String sessionId;
public SelectionExtractor(TimerService clock, String address, String sessionId) {
Preconditions.checkNotNull(address, "Address must not be null");
Preconditions.checkNotNull(sessionId, "Session id must not be null");
this.clock = clock;
this.address = address;
this.sessionId = sessionId;
}
/**
* Starts writing the location of this session's browser selection into the
* edited document.
*/
public void start(EditorContext context) {
writeSelection(context);
context.addUpdateListener(this);
}
/**
* Stops writing the location of this session's browser selection into the
* edited document.
*/
public void stop(EditorContext context) {
context.removeUpdateListener(this);
MutableAnnotationSet.Persistent document = context.getDocument();
int size = document.size();
String rangeKey = SelectionAnnotationHandler.rangeKey(sessionId);
String endKey = SelectionAnnotationHandler.endKey(sessionId);
String dataKey = SelectionAnnotationHandler.dataKey(sessionId);
document.setAnnotation(0, size, dataKey, null);
document.setAnnotation(0, size, rangeKey, null);
document.setAnnotation(0, size, endKey, null);
}
@Override
public void onUpdate(EditorUpdateEvent event) {
if (event.selectionLocationChanged()) {
EditorContext context = event.context();
// Special case to exempt caret annotations from being undoable.
context.getResponsibilityManager().startIndirectSequence();
try {
writeSelection(context);
} finally {
context.getResponsibilityManager().endIndirectSequence();
}
}
}
private void writeSelection(EditorContext context) {
MutableAnnotationSet.Persistent document = context.getDocument();
FocusedRange selection = context.getSelectionHelper().getSelectionRange();
String compositionState = context.getImeCompositionState();
double currentTimeMillis = clock.currentTimeMillis();
writeSelection(document, selection, compositionState, currentTimeMillis);
}
@VisibleForTesting
void writeSelection(MutableAnnotationSet.Persistent document, FocusedRange selection,
String compositionState, double currentTimeMillis) {
// TODO(danilatos): Use focus and not end
Range range = selection == null ? null : selection.asRange();
String rangeKey = SelectionAnnotationHandler.rangeKey(sessionId);
String endKey = SelectionAnnotationHandler.endKey(sessionId);
String dataKey = SelectionAnnotationHandler.dataKey(sessionId);
String value = address;
int size = document.size();
// If we have a selection, then continually update regardless of old value,
// to refresh the timestamp.
if (range != null) {
document.setAnnotation(0, size, dataKey, address + "," + currentTimeMillis + ","
+ (compositionState != null ? compositionState : ""));
}
// TODO(danilatos): This fiddliness is necessary to avoid gratuitous
// re-rendering.
// Later the code can just use the resetAnnotation method, which will be
// MUCH
// simpler, once we have proper fine-granularity notifications.
int currentFocus = document.firstAnnotationChange(0, size, endKey, null);
int currentEnd = document.lastAnnotationChange(0, size, rangeKey, null);
if (currentEnd == -1) {
currentEnd = currentFocus;
}
if (currentEnd != -1) {
// if old selection is annotated
int currentStart = document.firstAnnotationChange(0, size, rangeKey, null);
if (currentStart == -1 || currentStart > currentEnd) {
currentStart = currentEnd;
}
if (range != null) {
// if new selection exists
int newStart = range.getStart();
int newEnd = range.getEnd();
int newFocus = selection.getFocus();
if (newFocus < currentFocus) {
document.setAnnotation(newFocus, currentFocus, endKey, value);
} else if (newFocus > currentFocus) {
document.setAnnotation(currentFocus, newFocus, endKey, null);
}
if (currentStart >= newEnd || newStart >= currentEnd) {
// If not overlapping
document.setAnnotation(currentStart, currentEnd, rangeKey, null);
document.setAnnotation(newStart, newEnd, rangeKey, value);
} else {
// If overlapping
if (currentStart < newStart) {
document.setAnnotation(currentStart, newStart, rangeKey, null);
} else if (currentStart > newStart) {
document.setAnnotation(newStart, currentStart, rangeKey, value);
}
if (currentEnd < newEnd) {
document.setAnnotation(currentEnd, newEnd, rangeKey, value);
} else if (currentEnd > newEnd) {
document.setAnnotation(newEnd, currentEnd, rangeKey, null);
}
}
} else {
// no new selection, clear old one
document.setAnnotation(currentFocus, size, endKey, null);
document.setAnnotation(currentStart, currentEnd, rangeKey, null);
document.setAnnotation(0, size, dataKey, null);
}
} else {
// no old selection
if (range != null) {
// new selection exists
document.setAnnotation(selection.getFocus(), size, endKey, value);
document.setAnnotation(range.getStart(), range.getEnd(), rangeKey, value);
}
}
}
}