// 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.renderer;
import com.google.collide.client.editor.Buffer;
import com.google.collide.client.editor.FocusManager;
import com.google.collide.client.editor.Spacer;
import com.google.collide.client.editor.ViewportModel;
import com.google.collide.client.editor.ViewportModel.Edge;
import com.google.collide.client.editor.selection.SelectionModel;
import com.google.collide.client.util.ScheduledCommandExecutor;
import com.google.collide.client.util.logging.Log;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Document;
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.TextChange;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.document.util.PositionUtils;
import com.google.collide.shared.document.util.LineUtils.LineVisitor;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar.Remover;
import java.util.EnumSet;
/*-
* TODO:
* - store enough info so we can safely skip the render step if
* this was off-screen
*/
/**
* A class to track changes in the document or editor state that result in a
* render pass.
*/
class ChangeTracker
implements
Document.TextListener,
ViewportModel.Listener,
SelectionModel.SelectionListener,
Buffer.SpacerListener,
FocusManager.FocusListener {
enum ChangeType {
/** The viewport's top or bottom are now pointing to different lines */
VIEWPORT_SHIFT,
/** The viewport had a line added or removed */
VIEWPORT_CONTENT,
/** The contents of a line has changed */
DIRTY_LINE,
/** The selection has changed */
SELECTION,
/** The viewport's top or bottom line numbers have changed */
VIEWPORT_LINE_NUMBER
}
private class RenderCommand extends ScheduledCommandExecutor {
@Override
protected void execute() {
/*
* TODO: think about whether a render pass can cause a
* change, if so, need to fix some stuff here like clearing/cloning the
* changes BEFORE calling out to the renderer
*/
try {
renderer.renderChanges();
} catch (Throwable t) {
Log.error(getClass(), t);
}
clearChangeState();
}
}
/*
* TODO: More cleanly group the variables that track changed
* state
*/
/** Tracks the types of changes that occurred */
private final EnumSet<ChangeType> changes;
/**
* Tracks whether there was a content change that requires updating the top of
* existing following lines
*/
private boolean hadContentChangeThatRepositionsFollowingLines;
/** List of lines that need to be re-rendered */
private final JsonArray<Line> dirtyLines;
private final LineUtils.LineVisitor dirtyMarkingLineVisitor = new LineVisitor() {
@Override
public boolean accept(Line line, int lineNumber, int beginColumn, int endColumn) {
requestRenderLine(line);
return true;
}
};
private final Buffer buffer;
private final JsonArray<Remover> listenerRemovers;
/** Command that is scheduled-finally from any callback */
private final RenderCommand renderCommand = new RenderCommand();
private final Renderer renderer;
private final SelectionModel selection;
private final ViewportModel viewport;
/**
* List of lines that were removed. These were in the viewport at time of
* removal (and hence were most likely rendered)
*/
private final JsonArray<Line> viewportRemovedLines;
/**
* The line number of the topmost line that was added or removed, or
* {@value Integer#MAX_VALUE} if there weren't any of these changes
*/
private int topmostContentChangedLineNumber;
private final EnumSet<ViewportModel.Edge> viewportLineNumberChangedEdges = EnumSet
.noneOf(ViewportModel.Edge.class);
ChangeTracker(Renderer renderer, Buffer buffer, Document document, ViewportModel viewport,
SelectionModel selection, FocusManager focusManager) {
this.buffer = buffer;
this.renderer = renderer;
this.selection = selection;
this.listenerRemovers = JsonCollections.createArray();
this.changes = EnumSet.noneOf(ChangeType.class);
this.viewportRemovedLines = JsonCollections.createArray();
this.dirtyLines = JsonCollections.createArray();
this.viewport = viewport;
attach(buffer, document, viewport, selection, focusManager);
clearChangeState();
}
public EnumSet<ChangeType> getChanges() {
return changes;
}
public JsonArray<Line> getDirtyLines() {
return dirtyLines;
}
public JsonArray<Line> getViewportRemovedLines() {
return viewportRemovedLines;
}
public int getTopmostContentChangedLineNumber() {
return topmostContentChangedLineNumber;
}
/**
* Returns whether the {@link ChangeType#VIEWPORT_CONTENT} change type was one
* that requires updating the position of the
* {@link #getTopmostContentChangedLineNumber()} and all following
* lines.
*/
public boolean hadContentChangeThatUpdatesFollowingLines() {
return hadContentChangeThatRepositionsFollowingLines;
}
public EnumSet<ViewportModel.Edge> getViewportLineNumberChangedEdges() {
return viewportLineNumberChangedEdges;
}
@Override
public void onSelectionChange(Position[] oldSelectionRange, Position[] newSelectionRange) {
/*
* We only need to redraw those lines that either entered or left the
* selection
*/
Position[] viewportRange = getViewportRange();
if (oldSelectionRange != null && newSelectionRange != null) {
JsonArray<Position[]> differenceRanges =
PositionUtils.getDifference(oldSelectionRange, newSelectionRange);
for (int i = 0, n = differenceRanges.size(); i < n; i++) {
markVisibleLinesDirty(differenceRanges.get(i), viewportRange);
}
} else if (oldSelectionRange != null) {
markVisibleLinesDirty(oldSelectionRange, viewportRange);
} else if (newSelectionRange != null) {
markVisibleLinesDirty(newSelectionRange, viewportRange);
}
}
private void markVisibleLinesDirty(Position[] dirtyRange, Position[] viewportRange) {
/*
* We can safely intersect with the viewport. The typical danger in doing
* this right now is by the time things render, the viewport could have
* shifted. But, in that case, the new viewport will have to be rendered
* anyway, and thus the selection in that new viewport would be drawn
* regardless of any dirty-marking we do here.
*/
Position[] visibleRange = PositionUtils.getIntersection(dirtyRange, viewportRange);
if (visibleRange != null) {
PositionUtils.visit(dirtyMarkingLineVisitor, visibleRange[0], visibleRange[1]);
}
}
private Position[] getViewportRange() {
return new Position[] {new Position(viewport.getTop(), 0),
new Position(viewport.getBottom(), viewport.getBottomLine().getText().length() - 1)};
}
@Override
public void onTextChange(Document document, JsonArray<TextChange> textChanges) {
for (int i = 0, n = textChanges.size(); i < n; i++) {
/*
* For insertion, the second line through the second-to-last line can't
* have existed in the document, so no point in marking them dirty.
*/
TextChange textChange = textChanges.get(i);
Line line = textChange.getLine();
Line lastLine = textChange.getLastLine();
if (dirtyLines.indexOf(line) == -1) {
dirtyLines.add(line);
}
if (line != lastLine && dirtyLines.indexOf(lastLine) == -1) {
dirtyLines.add(lastLine);
}
}
scheduleRender(ChangeType.DIRTY_LINE);
}
@Override
public void onViewportContentChanged(ViewportModel viewport, int lineNumber,
boolean added, JsonArray<Line> lines) {
int relevantContentChangedLineNumber;
if (!added && viewport.getTopLineNumber() == lineNumber - 1) {
// TODO: rework this case is handled naturally
/*
* This handles the top viewport line being "removed" by backspacing on
* column 0. In this case, no one else draws the new top line of the
* viewport (the one that got the previous top line's contents appended to
* it).
*/
relevantContentChangedLineNumber = viewport.getTopLineNumber();
} else {
relevantContentChangedLineNumber = lineNumber;
}
if (!added) {
for (int i = 0, n = lines.size(); i < n; i++) {
Line curLine = lines.get(i);
if (ViewportRenderer.isRendered(curLine)) {
viewportRemovedLines.add(curLine);
}
}
}
/*
* If there is a spacer in the viewport below the line number change, line numbers shift
* non-uniformly around it.
*/
/*
* TODO: actually implement the check for spacers in the viewport, not just
* the document.
*/
handleContentChange(relevantContentChangedLineNumber, buffer.hasSpacers());
}
@Override
public void onViewportLineNumberChanged(ViewportModel viewport, Edge edge) {
viewportLineNumberChangedEdges.add(edge);
scheduleRender(ChangeType.VIEWPORT_LINE_NUMBER);
}
@Override
public void onViewportShifted(ViewportModel viewport, LineInfo top, LineInfo bottom,
LineInfo oldTop, LineInfo oldBottom) {
scheduleRender(ChangeType.VIEWPORT_SHIFT);
}
@Override
public void onFocusChange(boolean hasFocus) {
/*
* Schedule re-rendering of the lines in the selection so that we can update
* the selection color based on focused state. Note that by the time we are
* rendering, the selection could have changed. This is OK since in that
* case, the new selection has to be rendered anyway, and it will render in
* the correct color.
*/
if (selection.hasSelection()) {
markVisibleLinesDirty(selection.getSelectionRange(true), getViewportRange());
}
}
public void requestRenderLine(Line line) {
if (dirtyLines.indexOf(line) == -1) {
dirtyLines.add(line);
}
scheduleRender(ChangeType.DIRTY_LINE);
}
void teardown() {
for (int i = 0, n = listenerRemovers.size(); i < n; i++) {
listenerRemovers.get(i).remove();
}
renderCommand.cancel();
}
private void attach(Buffer buffer, Document document, ViewportModel viewport,
SelectionModel selection, FocusManager focusManager) {
listenerRemovers.add(focusManager.getFocusListenerRegistrar().add(this));
listenerRemovers.add(document.getTextListenerRegistrar().add(this));
listenerRemovers.add(viewport.getListenerRegistrar().add(this));
listenerRemovers.add(selection.getSelectionListenerRegistrar().add(this));
listenerRemovers.add(buffer.getSpacerListenerRegistrar().add(this));
}
private void clearChangeState() {
changes.clear();
viewportRemovedLines.clear();
dirtyLines.clear();
topmostContentChangedLineNumber = Integer.MAX_VALUE;
hadContentChangeThatRepositionsFollowingLines = false;
viewportLineNumberChangedEdges.clear();
}
@Override
public void onSpacerAdded(Spacer spacer) {
handleContentChange(spacer.getLineNumber(), true);
}
@Override
public void onSpacerRemoved(Spacer spacer, Line oldLine, int oldLineNumber) {
handleContentChange(oldLineNumber, true);
}
@Override
public void onSpacerHeightChanged(Spacer spacer, int oldHeight) {
handleContentChange(spacer.getLineNumber(), true);
}
private void handleContentChange(int lineNumber, boolean requiresRepositioningFollowingLines) {
hadContentChangeThatRepositionsFollowingLines |= requiresRepositioningFollowingLines;
if (topmostContentChangedLineNumber > lineNumber) {
topmostContentChangedLineNumber = lineNumber;
}
scheduleRender(ChangeType.VIEWPORT_CONTENT);
}
private void scheduleRender(ChangeType change) {
changes.add(change);
renderCommand.scheduleFinally();
}
}