// 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.client.editor.selection;
import static com.google.collide.shared.document.util.LineUtils.getLastCursorColumn;
import static com.google.collide.shared.document.util.LineUtils.rubberbandColumn;
import com.google.collide.client.document.linedimensions.LineDimensionsCalculator.RoundingStrategy;
import com.google.collide.client.editor.Buffer;
import com.google.collide.client.editor.ViewportModel;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.DocumentMutator;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorType;
import com.google.collide.shared.document.anchor.AnchorUtils;
import com.google.collide.shared.document.anchor.InsertionPlacementStrategy;
import com.google.collide.shared.document.anchor.ReadOnlyAnchor;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.document.util.PositionUtils;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.StringUtils;
import com.google.collide.shared.util.TextUtils;
import com.google.collide.shared.util.UnicodeUtils;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.regexp.shared.RegExp;
import org.waveprotocol.wave.client.common.util.UserAgent;
// TODO: this class is getting huge, time to split responsibilities
/**
* A class that models the user's selection. In addition to storing the
* selection and cursor positions, this class listens for mouse drags and other
* actions that affect the selection.
*
* The lifecycle of this class is tied to the current document. When the
* document is replaced, a new instance of this class is created for the new
* document.
*/
public class SelectionModel implements Buffer.MouseDragListener {
/**
* Enumeration of movement actions.
*/
public enum MoveAction {
LEFT,
RIGHT,
WORD_LEFT,
WORD_RIGHT,
UP,
DOWN,
PAGE_UP,
PAGE_DOWN,
LINE_START,
LINE_END,
TEXT_START,
TEXT_END
}
private static final AnchorType SELECTION_ANCHOR_TYPE = AnchorType.create(SelectionModel.class,
"selection");
/**
* Listener that is called when the user's cursor changes position.
*/
public interface CursorListener {
/**
* @param isExplicitChange true if this change was a result of either the
* user moving his cursor or through programatic setting, or false if
* it was caused by text mutations in the document
*/
void onCursorChange(LineInfo lineInfo, int column, boolean isExplicitChange);
}
/**
* Listener that is called when the user changes his selection. This will not
* be called if the selection's position in the document shifts
* because of edits elsewhere in the document.
*
* Note: The selection is different from the cursor. This will not be called
* if the user does not have a selection and his cursor moves.
*/
public interface SelectionListener {
/**
* @param oldSelectionRange the selection range before this selection, or
* null if there was not a selection
* @param newSelectionRange the new selection range, or null if there is not
* a selection
*/
void onSelectionChange(Position[] oldSelectionRange, Position[] newSelectionRange);
}
private class AnchorListener implements Anchor.ShiftListener {
@Override
public void onAnchorShifted(Anchor anchor) {
if (anchor == cursorAnchor) {
preferredCursorColumn = anchor.getColumn();
}
dispatchCursorChange(false);
}
}
/**
* A repeating command that continues a user's drag-based selection when the
* user's mouse pointer moves outside of the editor.
*/
// TODO: split out MouseDragRepeater into a smaller class
private class MouseDragRepeater implements RepeatingCommand {
private static final int REPEAT_PERIOD_MS = 100;
private int deltaX;
private int deltaY;
@Override
public boolean execute() {
// check for movement this frame
if (deltaY == 0 && deltaX == 0) {
return false;
}
LineInfo cursorLineInfo = cursorAnchor.getLineInfo();
int cursorColumn = cursorAnchor.getColumn();
int newScrollTop = buffer.getScrollTop() + deltaY;
if (deltaY != 0) {
int targetCursorY = deltaY < 0 ? newScrollTop : newScrollTop + buffer.getHeight();
int cursorLineNumber = buffer.convertYToLineNumber(targetCursorY, true);
int actualCursorTop = buffer.convertLineNumberToY(cursorLineNumber);
if (deltaY < 0 && actualCursorTop < newScrollTop && cursorLineNumber > 0) {
/*
* The current line is partially visible, increment so we get a fully
* visible line
*/
cursorLineNumber++;
} else if (deltaY > 0 && cursorLineNumber < document.getLastLineNumber()) {
// See above
cursorLineNumber--;
}
cursorLineInfo = document.getLineFinder().findLine(cursorLineNumber);
}
if (deltaX != 0) {
int targetCursorX =
buffer.calculateColumnLeft(cursorLineInfo.line(), cursorAnchor.getColumn()) + deltaX;
cursorColumn = buffer.convertXToRoundedVisibleColumn(targetCursorX, cursorLineInfo.line());
}
buffer.setScrollTop(newScrollTop);
if (viewport.isLineNumberFullyVisibleInViewport(cursorLineInfo.number())) {
// Only move cursor if the target line is visible inside of viewport
moveCursorUsingSelectionGranularity(
cursorLineInfo, buffer.convertColumnToX(cursorLineInfo.line(), cursorColumn), false);
}
return true;
}
private void schedule(int deltaX, int deltaY) {
if (this.deltaX == 0 && this.deltaY == 0) {
// The repeated command is not scheduled, so schedule it
Scheduler.get().scheduleFixedPeriod(this, REPEAT_PERIOD_MS);
}
this.deltaX = deltaX;
this.deltaY = deltaY;
}
private void cancel() {
deltaX = 0;
deltaY = 0;
}
}
private enum SelectionGranularity {
CHARACTER, WORD, LINE;
private static SelectionGranularity forClickCount(int clickCount) {
switch (clickCount) {
case 1:
return CHARACTER;
case 2:
return WORD;
case 3:
return LINE;
default:
return CHARACTER;
}
}
}
public static SelectionModel create(Document document, Buffer buffer) {
ListenerRegistrar.RemoverManager removalManager = new ListenerRegistrar.RemoverManager();
SelectionModel selection = new SelectionModel(document, buffer, removalManager);
removalManager.track(buffer.getMouseDragListenerRegistrar().add(selection));
return selection;
}
private Anchor createSelectionAnchor(Line line, int lineNumber, int column, Document document,
AnchorListener anchorListener) {
Anchor anchor =
document.getAnchorManager().createAnchor(SELECTION_ANCHOR_TYPE, line, lineNumber, column);
anchor.setRemovalStrategy(Anchor.RemovalStrategy.SHIFT);
anchor.getShiftListenerRegistrar().add(anchorListener);
return anchor;
}
private final AnchorListener anchorListener;
/**
* The anchor of the selection ("anchor" defined as "where the selection
* began", not "anchor" defined in terms of document anchors).
*/
private final Anchor baseAnchor;
private final Buffer buffer;
/** The cursor of the selection */
private final Anchor cursorAnchor;
private final ListenerManager<CursorListener> cursorListenerManager;
private final Document document;
/**
* While the user is dragging, this defines the lower bound for the minimum
* selection that must be selected regardless of where the user's mouse
* pointer is. This should be null outside of a drag.
*
* For example, if the user is in word-selection mode (by double-clicking to
* start the selection), the minimum selection will be the initial word that
* was double-clicked.
*/
private Anchor minimumDragSelectionLowerBound;
/** Like {@link #minimumDragSelectionLowerBound}, this defines the upper bound */
private Anchor minimumDragSelectionUpperBound;
private final MouseDragRepeater mouseDragRepeater = new MouseDragRepeater();
/**
* Tracks the column that the user explicitly moved to. For example, the user
* moves to line 2, column 80 and then presses the up arrow. Line 1 only has
* 30 columns, so it will move to column 30, but this will still be column 80
* so if the user presses the down arrow, it will take him back to column 80.
*/
private int preferredCursorColumn;
private SelectionGranularity selectionGranularity = SelectionGranularity.CHARACTER;
private final ListenerManager<SelectionListener> selectionListenerManager;
private final ListenerRegistrar.RemoverManager removerManager;
private ViewportModel viewport;
private SelectionModel(
Document document, Buffer buffer, ListenerRegistrar.RemoverManager removerManager) {
this.document = document;
this.buffer = buffer;
this.removerManager = removerManager;
anchorListener = new AnchorListener();
cursorAnchor = createSelectionAnchor(document.getFirstLine(), 0, 0, document, anchorListener);
baseAnchor = createSelectionAnchor(document.getFirstLine(), 0, 0, document, anchorListener);
cursorListenerManager = ListenerManager.create();
selectionListenerManager = ListenerManager.create();
}
public void deleteSelection(DocumentMutator documentMutator) {
Preconditions.checkState(hasSelection(), "can't delete selection when there is no selection");
Position[] selectionRange = getSelectionRange(true);
/*
* TODO: optimize. It's currently O(n) where n is the number of
* lines, but can be O(1) with an additional delete API
*/
int deleteCount =
LineUtils.getTextCount(selectionRange[0].getLine(), selectionRange[0].getColumn(),
selectionRange[1].getLine(), selectionRange[1].getColumn());
documentMutator.deleteText(selectionRange[0].getLine(), selectionRange[0].getLineNumber(),
selectionRange[0].getColumn(), deleteCount);
}
public void deselect() {
if (!hasSelection()) {
return;
}
Position[] oldSelectionRange = getSelectionRangeForCallback();
moveAnchor(baseAnchor, cursorAnchor.getLineInfo(), cursorAnchor.getColumn(), false);
dispatchSelectionChange(oldSelectionRange);
}
public int getBaseColumn() {
return baseAnchor.getColumn();
}
public Line getBaseLine() {
return baseAnchor.getLine();
}
public int getBaseLineNumber() {
return baseAnchor.getLineNumber();
}
public int getCursorColumn() {
return cursorAnchor.getColumn();
}
public Line getCursorLine() {
return cursorAnchor.getLine();
}
public int getCursorLineNumber() {
return cursorAnchor.getLineNumber();
}
public ListenerRegistrar<CursorListener> getCursorListenerRegistrar() {
return cursorListenerManager;
}
public ListenerRegistrar<SelectionListener> getSelectionListenerRegistrar() {
return selectionListenerManager;
}
// TODO: I think we should introduce SelectionRange bean.
/**
* Returns the selection range where position[0] is always the logical start
* of selection and position[1] is always the logical end.
*
* @param inclusiveEnd true for the returned position[1] to be the last
* character in the selection, false for position[1] to be the
* character after the last character in the selection. If true there
* must currently be a selection.
*/
public Position[] getSelectionRange(boolean inclusiveEnd) {
Preconditions.checkArgument(
hasSelection() || !inclusiveEnd, "There must be a selection if inclusiveEnd is requested.");
Position[] selection = new Position[2];
Anchor beginAnchor = getEarlierSelectionAnchor();
Anchor endAnchor = getLaterSelectionAnchor();
selection[0] = new Position(beginAnchor.getLineInfo(), beginAnchor.getColumn());
if (inclusiveEnd) {
Preconditions.checkState(hasSelection(),
"Can't get selection range inclusive end when nothing is selected");
selection[1] =
PositionUtils.getPosition(endAnchor.getLine(), endAnchor.getLineNumber(),
endAnchor.getColumn(), -1);
} else {
selection[1] = new Position(endAnchor.getLineInfo(), endAnchor.getColumn());
}
return selection;
}
public int getSelectionBeginLineNumber() {
return isCursorAtEndOfSelection() ? baseAnchor.getLineNumber() : cursorAnchor.getLineNumber();
}
public int getSelectionEndLineNumber() {
return isCursorAtEndOfSelection() ? cursorAnchor.getLineNumber()
: baseAnchor.getLineNumber();
}
public boolean hasSelection() {
return AnchorUtils.compare(cursorAnchor, baseAnchor) != 0;
}
public String getSelectedText() {
if (!hasSelection()) {
return "";
}
Position[] selectionRange = getSelectionRange(true);
return LineUtils.getText(selectionRange[0].getLine(), selectionRange[0].getColumn(),
selectionRange[1].getLine(), selectionRange[1].getColumn());
}
/**
* Returns true if the selection spans a newline character.
*/
public boolean hasMultilineSelection() {
return cursorAnchor.getLine() != baseAnchor.getLine();
}
public boolean isCursorAtEndOfSelection() {
return AnchorUtils.compare(cursorAnchor, baseAnchor) >= 0;
}
/**
* Performs specified movement action.
*/
public void move(MoveAction action, boolean isShiftHeld) {
boolean shouldUpdatePreferredColumn = true;
int column = cursorAnchor.getColumn();
LineInfo lineInfo = cursorAnchor.getLineInfo();
String lineText = lineInfo.line().getText();
switch (action) {
case LEFT:
column = TextUtils.findPreviousNonMarkNorOtherCharacter(lineText, column);
break;
case RIGHT:
column = TextUtils.findNonMarkNorOtherCharacter(lineText, column);
break;
case WORD_LEFT:
column = TextUtils.findPreviousWord(lineText, column, false);
/**
* {@link TextUtils#findNextWord} can return line length indicating it's
* at the end of a word on the line. If this line ends in a* {@code \n}
* that will cause us to move to the next line when we check
* {@link LineUtils#getLastCursorColumn} which isn't what we want. So
* fix it now in case the lines ends in {@code \n}.
*/
if (column == lineInfo.line().length()) {
column = rubberbandColumn(lineInfo.line(), column);
}
break;
case WORD_RIGHT:
column = TextUtils.findNextWord(lineText, column, true);
/**
* {@link TextUtils#findNextWord} can return line length indicating it's
* at the end of a word on the line. If this line ends in a* {@code \n}
* that will cause us to move to the next line when we check
* {@link LineUtils#getLastCursorColumn} which isn't what we want. So
* fix it now in case the lines ends in {@code \n}.
*/
if (column == lineInfo.line().length()) {
column = rubberbandColumn(lineInfo.line(), column);
}
break;
case UP:
column = preferredCursorColumn;
if (lineInfo.line() == document.getFirstLine() && (isShiftHeld || UserAgent.isMac())) {
/*
* Pressing up on the first line should:
* - On Mac, always go to first column, or
* - On all platforms, shift+up should select to first column
*/
column = 0;
} else {
lineInfo.moveToPrevious();
}
column = rubberbandColumn(lineInfo.line(), column);
shouldUpdatePreferredColumn = false;
break;
case DOWN:
column = preferredCursorColumn;
if (lineInfo.line() == document.getLastLine() && (isShiftHeld || UserAgent.isMac())) {
// Consistent with up-arrowing on first line
column = LineUtils.getLastCursorColumn(lineInfo.line());
} else {
lineInfo.moveToNext();
}
column = rubberbandColumn(lineInfo.line(), column);
shouldUpdatePreferredColumn = false;
break;
case PAGE_UP:
for (int i = buffer.getFlooredHeightInLines(); i > 0; i--) {
lineInfo.moveToPrevious();
}
column = rubberbandColumn(lineInfo.line(), preferredCursorColumn);
shouldUpdatePreferredColumn = false;
break;
case PAGE_DOWN:
for (int i = buffer.getFlooredHeightInLines(); i > 0; i--) {
lineInfo.moveToNext();
}
column = rubberbandColumn(lineInfo.line(), preferredCursorColumn);
shouldUpdatePreferredColumn = false;
break;
case LINE_START:
int firstNonWhitespaceColumn = TextUtils.countWhitespacesAtTheBeginningOfLine(
lineInfo.line().getText());
column = (column != firstNonWhitespaceColumn) ? firstNonWhitespaceColumn : 0;
break;
case LINE_END:
column = LineUtils.getLastCursorColumn(lineInfo.line());
break;
case TEXT_START:
lineInfo = new LineInfo(document.getFirstLine(), 0);
column = 0;
break;
case TEXT_END:
lineInfo = new LineInfo(document.getLastLine(), document.getLineCount() - 1);
column = LineUtils.getLastCursorColumn(lineInfo.line());
break;
}
if (column < 0) {
if (lineInfo.moveToPrevious()) {
column = getLastCursorColumn(lineInfo.line());
} else {
column = 0;
}
} else if (column > getLastCursorColumn(lineInfo.line())) {
if (lineInfo.moveToNext()) {
column = LineUtils.getFirstCursorColumn(lineInfo.line());
} else {
column = rubberbandColumn(lineInfo.line(), column);
}
}
moveCursor(lineInfo, column, shouldUpdatePreferredColumn, isShiftHeld,
getSelectionRangeForCallback());
}
@Override
public void onMouseClick(Buffer buffer, int clickCount, int x, int y, boolean isShiftHeld) {
int lineNumber = buffer.convertYToLineNumber(y, true);
LineInfo newLineInfo =
buffer.getDocument().getLineFinder().findLine(cursorAnchor.getLineInfo(), lineNumber);
int newColumn = buffer.convertXToRoundedVisibleColumn(x, newLineInfo.line());
// Allow the user to keep clicking to iterate through selection modes
clickCount = (clickCount - 1) % 3 + 1;
selectionGranularity = SelectionGranularity.forClickCount(clickCount);
if (clickCount == 1) {
moveCursor(newLineInfo, newColumn, true, isShiftHeld, getSelectionRangeForCallback());
} else {
setInitialSelectionForGranularity(newLineInfo, newColumn, x);
}
}
private void setInitialSelectionForGranularity(LineInfo lineInfo, int column, int x) {
/*
* If the given column is more the line's length (for example, when appending to the last line
* of the doc), then just assume no initial selection (since most of that calculation code
* relies on getting the out-of-bounds character).
*/
int lineTextLength = lineInfo.line().getText().length();
if (column >= lineTextLength) {
moveCursor(lineInfo, lineTextLength, true, false, getSelectionRangeForCallback());
} else if (selectionGranularity == SelectionGranularity.WORD) {
Line line = lineInfo.line();
String text = line.getText();
if (UnicodeUtils.isWhitespace(text.charAt(column))) {
moveCursor(lineInfo, column, true, false, getSelectionRangeForCallback());
} else {
// Start seeking from the next column so the character under cursor
// will belong to the "previous word".
int nextColumn = column + 1;
int wordStartColumn = TextUtils.findPreviousWord(text, nextColumn, false);
wordStartColumn = LineUtils.rubberbandColumn(line, wordStartColumn);
moveAnchor(baseAnchor, lineInfo, wordStartColumn, false);
moveCursorUsingSelectionGranularity(lineInfo, x, false);
}
} else if (selectionGranularity == SelectionGranularity.LINE) {
moveAnchor(baseAnchor, lineInfo, 0, false);
moveCursorUsingSelectionGranularity(lineInfo, x, false);
}
}
@Override
public void onMouseDrag(Buffer buffer, int x, int y) {
/*
* The click callback sets up the initial selection, this will become the
* minimum selection
*/
ensureMinimumDragSelectionFromCurrentSelection();
int lineNumber = buffer.convertYToLineNumber(y, true);
LineInfo newLineInfo =
document.getLineFinder().findLine(cursorAnchor.getLineInfo(), lineNumber);
// Only move the cursor
if (viewport.isLineNumberFullyVisibleInViewport(newLineInfo.number())) {
moveCursorUsingSelectionGranularity(newLineInfo, x, false);
}
manageRepeaterForDrag(x, y);
}
private void ensureMinimumDragSelectionFromCurrentSelection() {
if (minimumDragSelectionLowerBound != null) {
return;
}
Position[] selectionRange = getSelectionRange(false);
minimumDragSelectionLowerBound = createAnchorFromPosition(selectionRange[0]);
minimumDragSelectionUpperBound = createAnchorFromPosition(selectionRange[1]);
}
private Anchor createAnchorFromPosition(Position position) {
return document.getAnchorManager().createAnchor(SELECTION_ANCHOR_TYPE, position.getLine(),
position.getLineInfo().number(), position.getColumn());
}
private void removeMinimumDragSelection() {
if (minimumDragSelectionLowerBound == null) {
return;
}
document.getAnchorManager().removeAnchor(minimumDragSelectionLowerBound);
document.getAnchorManager().removeAnchor(minimumDragSelectionUpperBound);
minimumDragSelectionLowerBound = minimumDragSelectionUpperBound = null;
}
/**
* Moves the cursor in the general direction of the {@code targetColumn}, but
* since this takes into account the {@link #selectionGranularity}, the actual
* column may be different.
*
* @param targetLineInfo the cursor will (mostly) stay within this line. Most
* callers will give the line underneath the mouse pointer as this
* parameter. (The cursor may move to the next line if the selection
* granularity is line.)
*/
private void moveCursorUsingSelectionGranularity(LineInfo targetLineInfo, int x,
boolean updatePreferredColumn) {
Line targetLine = targetLineInfo.line();
int roundedTargetColumn = buffer.convertXToRoundedVisibleColumn(x, targetLine);
// Forward if the cursor anchor will be ahead of the base anchor
boolean growForward =
AnchorUtils.compare(baseAnchor, targetLineInfo.number(), roundedTargetColumn) <= 0;
LineInfo newLineInfo = targetLineInfo;
int newColumn = roundedTargetColumn;
switch (selectionGranularity) {
case WORD:
if (growForward) {
/*
* Floor the column so the last pixel of the last character of the
* current word does not trigger a finding of the next word
*/
newColumn =
TextUtils.findNextWord(
targetLine.getText(),
buffer.convertXToColumn(x, targetLine, RoundingStrategy.FLOOR), false);
} else {
// See note above about flooring, but we ceil here instead
newColumn =
TextUtils.findPreviousWord(
targetLine.getText(),
buffer.convertXToColumn(x, targetLine, RoundingStrategy.CEIL), false);
}
break;
case LINE:
// The cursor is on column 0 regardless
newColumn = 0;
if (growForward) {
// If growing forward, move to the next line, if possible
newLineInfo = targetLineInfo.copy();
if (!newLineInfo.moveToNext()) {
/*
* There isn't a next line, so just move the cursor to the end of
* line
*/
newColumn = LineUtils.getLastCursorColumn(newLineInfo.line());
}
}
break;
}
Position[] oldSelectionRange = getSelectionRangeForCallback();
newColumn = LineUtils.rubberbandColumn(newLineInfo.line(), newColumn);
ensureNewSelectionObeysMinimumDragSelection(newLineInfo, newColumn);
moveCursor(newLineInfo, newColumn, updatePreferredColumn, true, oldSelectionRange);
}
private void ensureNewSelectionObeysMinimumDragSelection(LineInfo newCursorLineInfo,
int newCursorColumn) {
if (minimumDragSelectionLowerBound == null
|| AnchorUtils.compare(minimumDragSelectionLowerBound,
minimumDragSelectionUpperBound) == 0) {
// There isn't a minimum drag selection set
return;
}
// Is the new selection growing forward?
boolean newGrowForward =
AnchorUtils.compare(baseAnchor, newCursorLineInfo.number(), newCursorColumn) <= 0;
boolean newSelectionIsAheadOfMinimum =
newGrowForward && AnchorUtils.compare(baseAnchor, minimumDragSelectionUpperBound) >= 0;
boolean newSelectionIsBehindMinimum =
!newGrowForward && AnchorUtils.compare(baseAnchor, minimumDragSelectionLowerBound) <= 0;
// Move base anchor to correct minimum selection bound
Anchor newBaseAnchorPosition = null;
if (newSelectionIsBehindMinimum) {
newBaseAnchorPosition = minimumDragSelectionUpperBound;
} else if (newSelectionIsAheadOfMinimum) {
newBaseAnchorPosition = minimumDragSelectionLowerBound;
}
if (newBaseAnchorPosition != null) {
moveAnchor(baseAnchor, newBaseAnchorPosition.getLineInfo(),
newBaseAnchorPosition.getColumn(), false);
}
}
private void manageRepeaterForDrag(int x, int y) {
int bufferScrollLeft = buffer.getScrollLeft();
int bufferScrollTop = buffer.getScrollTop();
int bufferHeight = buffer.getHeight();
int bufferWidth = buffer.getWidth();
int deltaX = 0;
int deltaY = 0;
if (y - bufferScrollTop < 0) {
deltaY = y - bufferScrollTop;
} else if (y >= bufferScrollTop + bufferHeight) {
deltaY = y - (bufferScrollTop + bufferHeight);
}
if (x - bufferScrollLeft < 0) {
deltaX = x - bufferScrollLeft;
} else if (x >= bufferScrollLeft + bufferWidth) {
deltaX = x - (bufferScrollLeft + bufferWidth);
}
if (deltaX == 0 && deltaY == 0) {
mouseDragRepeater.cancel();
} else {
mouseDragRepeater.schedule(deltaX, deltaY);
}
}
@Override
public void onMouseDragRelease(Buffer buffer, int x, int y) {
mouseDragRepeater.cancel();
removeMinimumDragSelection();
}
public void setSelection(LineInfo baseLineInfo, int baseColumn, LineInfo cursorLineInfo,
int cursorColumn) {
Preconditions.checkArgument(baseColumn <= LineUtils.getLastCursorColumn(baseLineInfo.line()),
"The base column is out-of-bounds");
int lastCursorColumn = LineUtils.getLastCursorColumn(cursorLineInfo.line());
Preconditions.checkArgument(cursorColumn <= lastCursorColumn,
"The cursor column is out-of-bounds. Expected <= " + lastCursorColumn
+ ", got " + cursorColumn + ", line " + cursorLineInfo.number());
baseColumn = LineUtils.rubberbandColumn(baseLineInfo.line(), baseColumn);
cursorColumn = LineUtils.rubberbandColumn(cursorLineInfo.line(), cursorColumn);
Position[] oldSelectionRange = getSelectionRangeForCallback();
moveAnchor(baseAnchor, baseLineInfo, baseColumn, false);
boolean hasSelection =
LineUtils.comparePositions(cursorLineInfo.number(), cursorColumn, baseLineInfo.number(),
baseColumn) != 0;
moveCursor(cursorLineInfo, cursorColumn, true, hasSelection, oldSelectionRange);
}
public void setCursorPosition(LineInfo lineInfo, int column) {
int lastCursorColumn = LineUtils.getLastCursorColumn(lineInfo.line());
Preconditions.checkArgument(column <= lastCursorColumn,
"The cursor column is out-of-bounds. Expected <= " + lastCursorColumn
+ ", got " + column + ", line " + lineInfo.number());
moveCursor(lineInfo, column, true, hasSelection(), getSelectionRangeForCallback());
}
public void selectAll() {
Position[] oldSelectionRange = getSelectionRangeForCallback();
moveAnchor(baseAnchor, new LineInfo(document.getFirstLine(), 0), 0, false);
moveCursor(new LineInfo(document.getLastLine(), document.getLastLineNumber()),
LineUtils.getLastCursorColumn(document.getLastLine()), true, true, oldSelectionRange);
}
public void teardown() {
removerManager.remove();
if (baseAnchor != cursorAnchor) {
document.getAnchorManager().removeAnchor(baseAnchor);
}
}
public ReadOnlyAnchor getCursorAnchor() {
return cursorAnchor;
}
public Position getCursorPosition() {
return new Position(cursorAnchor.getLineInfo(), cursorAnchor.getColumn());
}
private void dispatchCursorChange(final boolean isExplicitChange) {
cursorListenerManager.dispatch(new Dispatcher<SelectionModel.CursorListener>() {
@Override
public void dispatch(CursorListener listener) {
listener.onCursorChange(cursorAnchor.getLineInfo(), cursorAnchor.getColumn(),
isExplicitChange);
}
});
}
private void dispatchSelectionChange(final Position[] oldSelectionRange) {
selectionListenerManager.dispatch(new Dispatcher<SelectionModel.SelectionListener>() {
@Override
public void dispatch(SelectionListener listener) {
listener.onSelectionChange(oldSelectionRange, getSelectionRangeForCallback());
}
});
}
private Position[] getSelectionRangeForCallback() {
return hasSelection() ? getSelectionRange(true) : null;
}
/**
* Moves the cursor and potentially the base. This method will dispatch the
* appropriate callbacks.
*
* @param lineInfo the line where the cursor will be positioned
* @param column the column (on the given line) where the cursor will be
* positioned
* @param updatePreferredColumn see {@link #preferredCursorColumn}
* @param isSelecting false to ensure there is not a selection after the
* movement
* @param oldSelectionRange the selection range (via
* {@link #getSelectionRangeForCallback()}) before the caller modified
* the selection. This will be passed to the selection callback as the
* old selection range.
*/
private void moveCursor(LineInfo lineInfo, int column, boolean updatePreferredColumn,
boolean isSelecting, Position[] oldSelectionRange) {
boolean hadSelection = hasSelection();
// Check if base anchor should move
if (!isSelecting) {
moveAnchor(baseAnchor, lineInfo, column, false);
}
// Move cursor anchor
moveAnchor(cursorAnchor, lineInfo, column, updatePreferredColumn);
boolean willHaveSelection = hasSelection();
dispatchCursorChange(true);
if (isSelecting || willHaveSelection != hadSelection) {
dispatchSelectionChange(oldSelectionRange);
}
}
private void moveAnchor(Anchor anchor, LineInfo lineInfo, int column,
boolean updatePreferredColumn) {
if (anchor.getLine().equals(lineInfo.line()) && anchor.getColumn() == column) {
return;
}
if (updatePreferredColumn) {
preferredCursorColumn = column;
}
document.getAnchorManager().moveAnchor(anchor, lineInfo.line(), lineInfo.number(), column);
}
public void initialize(ViewportModel viewport) {
this.viewport = viewport;
}
/**
* Sets specified strategy to earlier selection anchor and runs routine;
* initial strategy is restored before return.
*/
private void runWithEarlierAnchorPlacementStrategy(InsertionPlacementStrategy strategy,
Runnable runnable) {
Anchor earlierSelectionAnchor = getEarlierSelectionAnchor();
InsertionPlacementStrategy existingInsertionPlacementStrategy =
earlierSelectionAnchor.getInsertionPlacementStrategy();
earlierSelectionAnchor.setInsertionPlacementStrategy(strategy);
try {
runnable.run();
} finally {
earlierSelectionAnchor.setInsertionPlacementStrategy(existingInsertionPlacementStrategy);
}
}
public void toggleComments(final DocumentMutator documentMutator,
final RegExp commentChecker, final String commentHead) {
if (hasSelection()) {
runWithEarlierAnchorPlacementStrategy(
InsertionPlacementStrategy.EARLIER, new Runnable() {
@Override
public void run() {
toggleCommentsAssumingEarlierSelectionAnchorWontShift(
documentMutator, commentChecker, commentHead);
}
});
} else {
toggleCommentsAssumingEarlierSelectionAnchorWontShift(
documentMutator, commentChecker, commentHead);
}
}
private void toggleCommentsAssumingEarlierSelectionAnchorWontShift(
DocumentMutator documentMutator, RegExp commentChecker, String commentHead) {
new ToggleCommentsController(commentChecker, commentHead).processLines(documentMutator, this);
}
/**
* Adjusts the indentation (either indents or dedents) of the line(s) in the
* selection.
*/
public void adjustSelectionIndentation(
final DocumentMutator documentMutator, final String tabString, final boolean indent) {
runWithEarlierAnchorPlacementStrategy(InsertionPlacementStrategy.EARLIER, new Runnable() {
@Override
public void run() {
adjustSelectionIndentationAssumingEarlierSelectionAnchorWontShift(
documentMutator, tabString, indent);
}
});
}
private void adjustSelectionIndentationAssumingEarlierSelectionAnchorWontShift(
final DocumentMutator documentMutator, final String tabString, final boolean indent) {
Position[] selectionRange = getSelectionRange(false);
Line terminator = selectionRange[1].getLine();
if (selectionRange[1].getColumn() != 0 || !hasSelection()) {
terminator = terminator.getNextLine();
}
int lineNumber = selectionRange[0].getLineNumber();
Line line = selectionRange[0].getLine();
while (line != terminator) {
if (indent) {
documentMutator.insertText(line, lineNumber, 0, tabString, false);
} else {
int toDelete = StringUtils.findCommonPrefixLength(tabString, line.getText());
if (toDelete > 0) {
documentMutator.deleteText(line, 0, toDelete);
}
}
lineNumber++;
line = line.getNextLine();
}
}
private Anchor getEarlierSelectionAnchor() {
return isCursorAtEndOfSelection() ? baseAnchor : cursorAnchor;
}
private Anchor getLaterSelectionAnchor() {
return isCursorAtEndOfSelection() ? cursorAnchor : baseAnchor;
}
}