/**
* Copyright 2009 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 com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Text;
import org.waveprotocol.wave.client.common.util.QuirksConstants;
import org.waveprotocol.wave.client.editor.EditorStaticDeps;
import org.waveprotocol.wave.client.editor.content.ClientDocumentContext.EditingConcerns;
import org.waveprotocol.wave.client.editor.content.ExtendedClientDocumentContext.LowLevelEditingConcerns;
import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper;
import org.waveprotocol.wave.client.editor.selection.html.NativeSelectionUtil;
import org.waveprotocol.wave.model.document.indexed.SizedObject;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.util.FocusedPointRange;
import org.waveprotocol.wave.model.document.util.FocusedRange;
import org.waveprotocol.wave.model.document.util.Point;
/**
* Class to maintain the native selection across dom mutations, with minimal use
* of actually setting the selection to restore it (because this interferes with
* IMEs)
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class SelectionMaintainer {
private EditingConcerns editingConcerns;
private final SizedObject document;
SelectionMaintainer(SizedObject document) {
this.document = document;
}
public void attachEditor(EditingConcerns sadPackage) {
this.editingConcerns = sadPackage;
}
public void detachEditor() {
editingConcerns = null;
}
/**
* Describes a change to a text nodelet
*/
public enum TextNodeChangeType {
/** insert or delete data */
DATA,
/** split the node */
SPLIT,
/** move the node */
MOVE,
/** delete the node */
REMOVE,
/** call setData() */
REPLACE_DATA
}
// Saved selection information.
// There is some redundancy in the information here, the text node and offset might
// be updated, the point is only valid for element points.
// TODO(danilatos): Clean this up, without creating new point objects on every change.
// Use a mutable point class, when one exists.
private Point<Node> savedSelectionAnchor = null;
private Text savedSelectionAnchorTextNodelet = null;
private int savedSelectionAnchorOffset;
private Point<Node> savedSelectionFocus = null;
private Text savedSelectionFocusTextNodelet = null;
private int savedSelectionFocusOffset;
private boolean needToRestoreSelection = false;
private FocusedRange savedSelection;
/**
* Bad to actually save and restore selections in a nested fashion. Only do it
* for the outer most calls to matching save and restore.
*/
private int savedSelectionDepth = 0;
boolean isNested() {
return savedSelectionDepth > 0;
}
/**
* For recovery purposes only. Delete later, if no 3rd eye reports from errors
* logged at call sites.
*/
void hackForceClearDepth() {
savedSelectionDepth = 0;
}
void startDontSaveSelection() {
savedSelectionDepth++;
}
void endDontSaveSelection() {
savedSelectionDepth--;
}
/**
* Enter a block where the selection will be preserved against the adverse
* effects of dom mutation.
*
* Must be balanced with a call to either {@link #restoreSelection()} or
* {@link #restoreSelection(DocOp)}
*
* Code that mutates the HTML dom must be guarded by calls to save and
* restore.
*/
public void saveSelection() {
if (editingConcerns != null && savedSelectionDepth == 0) {
needToRestoreSelection = false;
SelectionHelper helper = editingConcerns.getSelectionHelper();
// Sometimes the document is not in an editing context, in which
// case there is no selection helper.
if (helper != null) {
FocusedPointRange<Node> htmlSelection = NativeSelectionUtil.get();
if (htmlSelection != null) {
savedSelectionAnchor = htmlSelection.getAnchor();
if (savedSelectionAnchor.isInTextNode()) {
savedSelectionAnchorTextNodelet = savedSelectionAnchor.getContainer().cast();
savedSelectionAnchorOffset = savedSelectionAnchor.getTextOffset();
}
savedSelectionFocus = htmlSelection.getFocus();
if (savedSelectionFocus.isInTextNode()) {
savedSelectionFocusTextNodelet = savedSelectionFocus.getContainer().cast();
savedSelectionFocusOffset = savedSelectionFocus.getTextOffset();
}
savedSelection = helper.getSelectionRange();
}
}
}
savedSelectionDepth++;
}
/**
* Restores the selection to the same document location from when it was
* saved, only if it is determined that the selection was inappropriately
* altered by a DOM mutation.
*/
public void restoreSelection() {
restoreSelection(null);
}
/**
* Same as {@link #restoreSelection()}, but if the selection is actually
* required to be explicitly set, transform its saved location with the given
* modifier.
*/
public void restoreSelection(DocOp modifier) {
savedSelectionDepth--;
if (editingConcerns != null && savedSelectionDepth == 0) {
try {
if (savedSelection != null) {
// selectionChangedInappropriately() only deals with selection boundaries. If we had
// a ranged selection, and we're in a browser where changing it internally matters,
// then just always restore the selection. This is safe to do w.r.t. IMEs because it's
// unlikely to have a ranged selection during an uncommitted IME state.
needToRestoreSelection |= QuirksConstants.RANGED_SELECTION_AFFECTED_BY_INTERNAL_CHANGED &&
!savedSelection.isCollapsed();
if (needToRestoreSelection || selectionChangedInappropriately()) {
if (modifier != null) {
savedSelection = RangeHelper.applyModifier(savedSelection, modifier);
}
EditorStaticDeps.logger.trace().log("Restoring selection");
if (document.size() >= 4) {
editingConcerns.getSelectionHelper().setSelectionRange(savedSelection);
}
} else {
EditorStaticDeps.logger.trace().log("Not restoring selection");
}
}
} finally {
savedSelection = null;
savedSelectionAnchor = null;
savedSelectionAnchorTextNodelet = null;
savedSelectionAnchorOffset = 0;
savedSelectionFocus = null;
savedSelectionFocusTextNodelet = null;
savedSelectionFocusOffset = 0;
}
}
}
/**
* Checks if the selection has been removed, or is somewhere where we are not
* expecting it to be.
*/
private boolean selectionChangedInappropriately() {
// The selection got lost. We need to restore it
if (!NativeSelectionUtil.selectionExists()) {
return true;
}
NativeSelectionUtil.cacheClear();
FocusedPointRange<Node> htmlSelection = NativeSelectionUtil.get();
Point<Node> newAnchor = htmlSelection.getAnchor();
if (savedSelectionAnchor.isInTextNode()) {
if (savedSelectionAnchorTextNodelet != newAnchor.getContainer() ||
savedSelectionAnchorOffset != newAnchor.getTextOffset()) {
return true;
}
} else {
if (savedSelectionAnchor.getContainer() != newAnchor.getContainer()) {
return true;
}
}
Point<Node> newFocus = htmlSelection.getFocus();
if (savedSelectionFocus.isInTextNode()) {
if (savedSelectionFocusTextNodelet != newFocus.getContainer() ||
savedSelectionFocusOffset != newFocus.getTextOffset()) {
return true;
}
} else {
if (savedSelectionFocus.getContainer() != newFocus.getContainer()) {
return true;
}
}
return false;
}
/**
* @see LowLevelEditingConcerns#textNodeletAffected(Text, int, int, TextNodeChangeType)
*/
void textNodeletAffected(Text nodelet, int affectedAfterOffset, int insertionAmount,
TextNodeChangeType changeType) {
if (needToRestoreSelection == true) {
return;
}
switch (changeType) {
case DATA:
if (!QuirksConstants.OK_SELECTION_ACROSS_TEXT_NODE_DATA_CHANGES &&
matchesSelectionTextNodes(nodelet, affectedAfterOffset)) {
needToRestoreSelection = true;
} else {
maybeUpdateNodeOffsets(nodelet, affectedAfterOffset, nodelet, insertionAmount);
}
return;
case SPLIT:
if (matchesSelectionTextNodes(nodelet, affectedAfterOffset)) {
if (!QuirksConstants.OK_SELECTION_ACROSS_TEXT_NODE_SPLITS) {
needToRestoreSelection = true;
} else {
maybeUpdateNodeOffsets(nodelet, affectedAfterOffset,
nodelet.getNextSibling().<Text>cast(), -affectedAfterOffset);
}
}
return;
case REMOVE:
if (!QuirksConstants.OK_SELECTION_ACROSS_NODE_REMOVALS &&
matchesSelectionTextNodes(nodelet)) {
needToRestoreSelection = true;
}
return;
case MOVE:
case REPLACE_DATA:
if (matchesSelectionTextNodes(nodelet)) {
needToRestoreSelection = true;
}
return;
}
}
private void maybeUpdateNodeOffsets(Text nodelet, int affectedAfterOffset,
Text newNodelet, int offsetDifference) {
if (nodelet == savedSelectionAnchorTextNodelet &&
savedSelectionAnchorOffset > affectedAfterOffset) {
savedSelectionAnchorOffset += offsetDifference;
savedSelectionAnchorTextNodelet = newNodelet;
}
if (nodelet == savedSelectionFocusTextNodelet &&
savedSelectionFocusOffset > affectedAfterOffset) {
savedSelectionFocusOffset += offsetDifference;
savedSelectionFocusTextNodelet = newNodelet;
}
}
private boolean matchesSelectionTextNodes(Text nodelet) {
return nodelet == savedSelectionAnchorTextNodelet || nodelet == savedSelectionFocusTextNodelet;
}
private boolean matchesSelectionTextNodes(Text nodelet, int affectedAfterOffset) {
if (savedSelection == null) {
return false;
}
if (savedSelection.isOrdered()) {
if (nodelet == savedSelectionAnchorTextNodelet) {
return savedSelectionAnchorOffset > affectedAfterOffset;
} else if (nodelet == savedSelectionFocusTextNodelet) {
return true;
}
} else {
// The inverse of the above
if (nodelet == savedSelectionFocusTextNodelet) {
return savedSelectionFocusOffset > affectedAfterOffset;
} else if (nodelet == savedSelectionAnchorTextNodelet) {
return true;
}
}
return false;
}
}