// 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;
import com.google.collide.client.AppContext;
import com.google.collide.client.common.BaseResources;
import com.google.collide.client.common.Constants;
import com.google.collide.client.document.linedimensions.LineDimensionsCalculator;
import com.google.collide.client.document.linedimensions.LineDimensionsCalculator.RoundingStrategy;
import com.google.collide.client.editor.renderer.Renderer;
import com.google.collide.client.util.CssUtils;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.Executor;
import com.google.collide.client.util.dom.DomUtils;
import com.google.collide.client.util.dom.MouseGestureListener;
import com.google.collide.client.util.dom.DomUtils.Offset;
import com.google.collide.client.util.dom.FontDimensionsCalculator.FontDimensions;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
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.Document.LineCountListener;
import com.google.collide.shared.document.Document.LineListener;
import com.google.collide.shared.document.anchor.ReadOnlyAnchor;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.TextUtils;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
import com.google.collide.shared.util.ListenerRegistrar.RemoverManager;
import elemental.client.Browser;
import elemental.css.CSSStyleDeclaration;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventRemover;
import elemental.events.MouseEvent;
import elemental.html.ClientRect;
import elemental.html.DivElement;
import elemental.html.Element;
/*
* TODO: Buffer has turned into an EditorSurface, but is still
* called Buffer.
*/
/**
* The presenter for the text portion of the editor. This class is used to
* display text to the user, and to accept mouse input from the user.
*
* The lifecycle of this class is tied to the {@link Editor} that owns it.
*/
public class Buffer extends UiComponent<Buffer.View>
implements LineListener, LineCountListener, CoordinateMap.DocumentSizeProvider {
private static final int MARKER_COLUMN = 100;
/**
* Static factory method for obtaining an instance of Buffer.
*/
public static Buffer create(AppContext appContext, FontDimensions fontDimensions,
LineDimensionsCalculator lineDimensions, Executor renderTimeExecutor) {
View view = new View(appContext.getResources());
Buffer buffer = new Buffer(view, fontDimensions, lineDimensions, renderTimeExecutor);
MouseWheelRedirector.redirect(buffer, view.scrollableElement);
return buffer;
}
/**
* CssResource for the editor.
*/
public interface Css extends Editor.EditorSharedCss {
String editorLineHeight();
String line();
String scrollbar();
int scrollableLeftPadding();
String spacer();
String textLayer();
String root();
String columnMarkerLine();
}
/**
* Listener that is notified of multiple click and drag mouse actions in the
* text buffer area of the editor.
*/
public interface MouseDragListener {
void onMouseClick(Buffer buffer, int clickCount, int x, int y, boolean isShiftHeld);
void onMouseDrag(Buffer buffer, int x, int y);
void onMouseDragRelease(Buffer buffer, int x, int y);
}
/*
* TODO: listeners probably also want to be notified when the
* mouse leaves the buffer
*/
/**
* Listener that is notified of mouse movements in the buffer.
*
* <p>You probably want to use {@link MouseHoverManager} instead.
*
* TODO: Make it package-private.
*/
public interface MouseMoveListener {
void onMouseMove(int x, int y);
}
/**
* Listener that is notified of mouse movements out of the buffer.
*/
interface MouseOutListener {
void onMouseOut();
}
/**
* Listener that is called when there is a click anywhere in the editor.
*/
public interface MouseClickListener {
void onMouseClick(int x, int y);
}
/**
* Listener that is called when the buffer's height changes.
*/
public interface HeightListener {
void onHeightChanged(int height);
}
/**
* ClientBundle for the editor.
*/
public interface Resources extends BaseResources.Resources {
@Source({"Buffer.css", "constants.css", "com/google/collide/client/common/constants.css"})
Css workspaceEditorBufferCss();
}
/**
* Listener that is notified of scroll events.
*/
public interface ScrollListener {
void onScroll(Buffer buffer, int scrollTop);
}
/**
* Listener that is notified of window resize events.=
*/
public interface ResizeListener {
void onResize(Buffer buffer, int documentHeight, int viewportHeight, int scrollTop);
}
/**
* Listen for spacers being added or removed.
*/
public interface SpacerListener {
void onSpacerAdded(Spacer spacer);
void onSpacerHeightChanged(Spacer spacer, int oldHeight);
void onSpacerRemoved(Spacer spacer, Line oldLine, int oldLineNumber);
}
/**
* View for the buffer.
*/
public static class View extends CompositeView<ViewEvents> {
private final Css css;
private final EventListener mouseMoveListener = new EventListener() {
@Override
public void handleEvent(Event event) {
int eventOffsetX = DomUtils.getOffsetX((MouseEvent) event);
int eventOffsetY = DomUtils.getOffsetY((MouseEvent) event);
Offset targetOffsetInBuffer =
DomUtils.calculateElementOffset((Element) event.getTarget(), textLayerElement, true);
getDelegate().onMouseMove(
targetOffsetInBuffer.left + eventOffsetX, targetOffsetInBuffer.top + eventOffsetY);
}
};
private final EventListener mouseOutListener = new EventListener() {
@Override
public void handleEvent(Event evt) {
/*
* Check if we really should handle this event:
* For mouseout, there are two situations:
* 1. If relatedTarget is defined, then it (the DOM node to which the
* mouse moves) should NOT be inside the buffer element itself.
* 2. User leaves the window using a keyboard command or something else.
* In this case, relatedTarget is undefined.
*/
com.google.gwt.user.client.Event gwtEvent = (com.google.gwt.user.client.Event) evt;
Element relatedTarget = (Element) gwtEvent.getRelatedEventTarget();
if (relatedTarget == null || !scrollableElement.contains(relatedTarget)) {
getDelegate().onMouseOut();
}
}
};
private EventRemover mouseMoveListenerRemover;
private EventRemover mouseOutListenerRemover;
private final Element rootElement;
private final Element scrollbarElement;
private final Element scrollableElement;
private final Element textLayerElement;
private final Element columnMarkerElement;
private int scrollTopFromPreviousDispatch;
private View(Resources res) {
this.css = res.workspaceEditorBufferCss();
columnMarkerElement = Elements.createDivElement(css.columnMarkerLine());
textLayerElement = Elements.createDivElement(css.textLayer());
scrollableElement = createScrollableElement(res.baseCss());
if (false) {
/*
* TODO: Re-enable post-v1 when we have a settings page to configure the
* placement of this marker
*/
// Note: columnMarkerElement is lying under the textLayerElement,
// so spacers are not shadowed.
scrollableElement.appendChild(columnMarkerElement);
}
scrollableElement.appendChild(textLayerElement);
scrollbarElement = createScrollbarElement(res.baseCss());
rootElement = Elements.createDivElement(css.root());
rootElement.appendChild(scrollableElement);
rootElement.appendChild(scrollbarElement);
setElement(rootElement);
}
private Element createScrollbarElement(BaseResources.Css baseCss) {
final DivElement scrollbarElement = Elements.createDivElement(css.scrollbar());
scrollbarElement.addClassName(baseCss.documentScrollable());
scrollbarElement.addEventListener(Event.SCROLL, new EventListener() {
@Override
public void handleEvent(Event evt) {
setScrollTop(scrollbarElement.getScrollTop(), false);
}
}, false);
// Prevent stealing focus from scrollable.
scrollbarElement.addEventListener(Event.MOUSEDOWN, new EventListener() {
@Override
public void handleEvent(Event evt) {
evt.preventDefault();
}
}, false);
// Empty child will be set to the document height
scrollbarElement.appendChild(Elements.createDivElement());
return scrollbarElement;
}
private Element createScrollableElement(BaseResources.Css baseCss) {
final DivElement scrollableElement = Elements.createDivElement(css.scrollable());
scrollableElement.addClassName(baseCss.documentScrollable());
scrollableElement.addEventListener(Event.SCROLL, new EventListener() {
@Override
public void handleEvent(Event evt) {
setScrollTop(scrollableElement.getScrollTop(), false);
}
}, false);
scrollableElement.addEventListener(Event.CONTEXTMENU, new EventListener() {
@Override
public void handleEvent(Event evt) {
/*
* TODO: eventually have our context menu, but for now
* disallow browser's since it's confusing that it does not have copy
* nor paste options
*/
evt.stopPropagation();
evt.preventDefault();
}
}, false);
// TODO: Detach listener in appropriate moment.
MouseGestureListener.createAndAttach(scrollableElement, new MouseGestureListener.Callback() {
@Override
public boolean onClick(int clickCount, MouseEvent event) {
// The buffer area does not include the scrollable's padding
int bufferClientLeft = css.scrollableLeftPadding();
int bufferClientTop = 0;
for (Element element = scrollableElement; element.getOffsetParent() != null;
element = element.getOffsetParent()) {
bufferClientLeft += element.getOffsetLeft();
bufferClientTop += element.getOffsetTop();
}
/*
* This onClick method will get called for horizontal scrollbar interactions. We want to
* exit early for those. It will not get called for vertical scrollbar interactions since
* that is a separate element outside of the scrollable element.
*/
if (scrollableElement == event.getTarget()) {
// Test if the mouse event is on the horizontal scrollbar.
int relativeY = event.getClientY() - bufferClientTop;
if (relativeY > scrollableElement.getClientHeight()) {
// Prevent editor losing focus
event.preventDefault();
return false;
}
}
getDelegate().onMouseClick(clickCount,
event.getClientX(),
event.getClientY(),
bufferClientLeft,
bufferClientTop,
event.isShiftKey());
return true;
}
@Override
public void onDragRelease(MouseEvent event) {
getDelegate().onMouseDragRelease(event.getClientX(), event.getClientY());
}
@Override
public void onDrag(MouseEvent event) {
getDelegate().onMouseDrag(event.getClientX(), event.getClientY());
}
});
/*
* Don't allow tabbing to this -- the input element will be tabbable
* instead
*/
scrollableElement.setTabIndex(-1);
Browser.getWindow().addEventListener(Event.RESIZE, new EventListener() {
@Override
public void handleEvent(Event evt) {
// TODO: also listen for the navigation slider
// this event is being caught multiple times, and sometimes the
// calculated values are all zero. So only respond if we have positive
// values.
int height = (int) textLayerElement.getBoundingClientRect().getHeight();
int viewportHeight = getHeight();
if (height > 0 && viewportHeight > 0) {
getDelegate().onScrollableResize(
height, viewportHeight, scrollableElement.getScrollTop());
}
}
}, false);
return scrollableElement;
}
public int getScrollLeft() {
return scrollableElement.getScrollLeft();
}
private void addLine(Element lineElement) {
String className = lineElement.getClassName();
if (className == null || className.isEmpty()) {
lineElement.addClassName(css.line());
}
textLayerElement.appendChild(lineElement);
}
private Element getFirstLine() {
return textLayerElement.getFirstChildElement();
}
private int getHeight() {
return scrollableElement.getClientHeight();
}
private int getScrollTop() {
return scrollableElement.getScrollTop();
}
private int getScrollHeight() {
return scrollableElement.getScrollHeight();
}
private int getWidth() {
return scrollableElement.getClientWidth();
}
private void reset() {
scrollableElement.setScrollTop(0);
scrollTopFromPreviousDispatch = 0;
textLayerElement.setInnerHTML("");
}
private void setBufferHeight(int height) {
textLayerElement.getStyle().setHeight(height, CSSStyleDeclaration.Unit.PX);
columnMarkerElement.getStyle().setHeight(height, CSSStyleDeclaration.Unit.PX);
/*
* For expediency, we deviate from typical scrollbar behavior by having
* the vertical scrollbar span the entire height, even in the presence of
* a horizontal scrollbar in the scrollable element. The problem is that
* if the scrollable element is scrolled all the way to its bottom, its
* scrolltop will be SCROLLBAR_SIZE greater than what the
* scrollbarElement's scrolltop is when scrollbarElement's scrolled all
* the way to the bottom (because scrollTop + clientHeight cannot be
* greater than scrollHeight). We get around this problem by adding the
* SCROLLBAR_SIZE to the scrollbar element's height, which allows the
* scrollTops on both elements to be the same. (There's now a little room
* at the bottom of the vertical scrollbar that won't scroll the
* scrollable element, but that's OK.)
*/
scrollbarElement.getFirstChildElement().getStyle()
.setHeight(height + Constants.SCROLLBAR_SIZE, CSSStyleDeclaration.Unit.PX);
}
public void setWidth(int width) {
textLayerElement.getStyle().setWidth(width, CSSStyleDeclaration.Unit.PX);
}
private void setScrollTop(int scrollTop, boolean forceDispatch) {
if (scrollTop != scrollableElement.getScrollTop()) {
scrollableElement.setScrollTop(scrollTop);
}
if (scrollTop != scrollbarElement.getScrollTop()) {
scrollbarElement.setScrollTop(scrollTop);
}
if (scrollTop != scrollTopFromPreviousDispatch) {
// Use getScrollTop in case the desired scrollTop could not be set
int newScrollTop = scrollableElement.getScrollTop();
getDelegate().onScroll(newScrollTop);
scrollTopFromPreviousDispatch = newScrollTop;
}
}
public void setScrollLeft(int scrollLeft) {
scrollableElement.setScrollLeft(scrollLeft);
}
void registerMouseMoveListener() {
if (mouseMoveListenerRemover == null) {
mouseMoveListenerRemover =
scrollableElement.addEventListener(Event.MOUSEMOVE, mouseMoveListener, false);
}
}
void unregisterMouseMoveListener() {
if (mouseMoveListenerRemover != null) {
mouseMoveListenerRemover.remove();
mouseMoveListenerRemover = null;
}
}
void registerMouseOutListener() {
if (mouseOutListenerRemover == null) {
mouseOutListenerRemover =
scrollableElement.addEventListener(Event.MOUSEOUT, mouseOutListener, false);
}
}
void unregisterMouseOutListener() {
if (mouseOutListenerRemover != null) {
mouseOutListenerRemover.remove();
mouseOutListenerRemover = null;
}
}
}
private interface ViewEvents {
void onMouseClick(int clickCount,
int clientX,
int clientY,
int bufferClientLeft,
int bufferClientTop,
boolean isShiftHeld);
void onMouseDrag(int clientX, int clientY);
void onMouseDragRelease(int clientX, int clientY);
void onMouseMove(int bufferX, int bufferY);
void onMouseOut();
void onScroll(int scrollTop);
void onScrollableResize(int height, int viewportHeight, int scrollTop);
}
private final CoordinateMap coordinateMap;
private final ElementManager elementManager;
private final ListenerManager<HeightListener> heightListenerManager =
ListenerManager.create();
private int maxLineLength;
private final ListenerManager<MouseClickListener> mouseClickListenerManager =
ListenerManager.create();
private final ListenerManager<MouseDragListener> mouseDragListenerManager =
ListenerManager.create();
private final ListenerManager<MouseMoveListener> mouseMoveListenerManager =
ListenerManager.create(new ListenerManager.RegistrationListener<MouseMoveListener>() {
@Override
public void onListenerAdded(MouseMoveListener listener) {
if (mouseMoveListenerManager.getCount() == 1) {
getView().registerMouseMoveListener();
}
}
@Override
public void onListenerRemoved(MouseMoveListener listener) {
if (mouseMoveListenerManager.getCount() == 0) {
getView().unregisterMouseMoveListener();
}
}
});
private final ListenerManager<MouseOutListener> mouseOutListenerManager =
ListenerManager.create(new ListenerManager.RegistrationListener<MouseOutListener>() {
@Override
public void onListenerAdded(MouseOutListener listener) {
if (mouseOutListenerManager.getCount() == 1) {
getView().registerMouseOutListener();
}
}
@Override
public void onListenerRemoved(MouseOutListener listener) {
if (mouseOutListenerManager.getCount() == 0) {
getView().unregisterMouseOutListener();
}
}
});
private static final Dispatcher<MouseOutListener> mouseOutListenerDispatcher =
new Dispatcher<MouseOutListener>() {
@Override
public void dispatch(MouseOutListener listener) {
listener.onMouseOut();
}
};
private final ListenerManager<SpacerListener> spacerListenerManager = ListenerManager.create();
private Document document;
private final int editorLineHeight;
private final ListenerManager<ScrollListener> scrollListenerManager = ListenerManager.create();
private final ListenerManager<ResizeListener> resizeListenerManager = ListenerManager.create();
private final FontDimensions fontDimensions;
private final LineDimensionsCalculator lineDimensions;
private final RemoverManager documentChangedRemoverManager = new RemoverManager();
private final Executor renderTimeExecutor;
private Buffer(View view, FontDimensions fontDimensions, LineDimensionsCalculator lineDimensions,
Executor renderTimeExecutor) {
super(view);
this.fontDimensions = fontDimensions;
this.lineDimensions = lineDimensions;
this.renderTimeExecutor = renderTimeExecutor;
this.editorLineHeight = CssUtils.parsePixels(view.css.editorLineHeight());
coordinateMap = new CoordinateMap(this);
elementManager = new ElementManager(getView().textLayerElement, this);
updateColumnMarkerPosition();
view.setDelegate(new ViewEvents() {
private int bufferLeft;
private int bufferTop;
private int bufferRelativeX;
private int bufferRelativeY;
@Override
public void onMouseClick(final int clickCount,
int clientX,
int clientY,
int bufferClientLeft,
int bufferClientTop,
final boolean isShiftHeld) {
this.bufferLeft = bufferClientLeft;
this.bufferTop = bufferClientTop;
updateBufferRelativeXy(clientX, clientY);
if (clickCount == 1) {
// Dispatch to simple click listeners
mouseClickListenerManager.dispatch(new Dispatcher<MouseClickListener>() {
@Override
public void dispatch(MouseClickListener listener) {
listener.onMouseClick(bufferRelativeX, bufferRelativeY);
}
});
}
mouseDragListenerManager.dispatch(new Dispatcher<MouseDragListener>() {
@Override
public void dispatch(MouseDragListener listener) {
listener.onMouseClick(
Buffer.this, clickCount, bufferRelativeX, bufferRelativeY, isShiftHeld);
}
});
}
@Override
public void onMouseDrag(final int clientX, final int clientY) {
updateBufferRelativeXy(clientX, clientY);
mouseDragListenerManager.dispatch(new Dispatcher<MouseDragListener>() {
@Override
public void dispatch(MouseDragListener listener) {
listener.onMouseDrag(Buffer.this, bufferRelativeX, bufferRelativeY);
}
});
}
@Override
public void onMouseDragRelease(final int clientX, final int clientY) {
updateBufferRelativeXy(clientX, clientY);
mouseDragListenerManager.dispatch(new Dispatcher<MouseDragListener>() {
@Override
public void dispatch(MouseDragListener listener) {
listener.onMouseDragRelease(Buffer.this, bufferRelativeX, bufferRelativeY);
}
});
}
@Override
public void onMouseMove(final int bufferX, final int bufferY) {
mouseMoveListenerManager.dispatch(new Dispatcher<MouseMoveListener>() {
@Override
public void dispatch(MouseMoveListener listener) {
listener.onMouseMove(bufferX, bufferY);
}
});
}
@Override
public void onMouseOut() {
mouseOutListenerManager.dispatch(mouseOutListenerDispatcher);
}
private void updateBufferRelativeXy(int clientX, int clientY) {
/*
* TODO: consider moving this element top/left-relative
* code to MouseGestureListener
*/
bufferRelativeX = clientX - bufferLeft + getScrollLeft();
bufferRelativeY = clientY - bufferTop + getScrollTop();
}
@Override
public void onScroll(final int scrollTop) {
scrollListenerManager.dispatch(new Dispatcher<ScrollListener>() {
@Override
public void dispatch(ScrollListener listener) {
listener.onScroll(Buffer.this, scrollTop);
}
});
}
@Override
public void onScrollableResize(
final int height, final int viewportHeight, final int scrollTop) {
// TODO: Look into why this is necessary.
updateTextWidth();
updateVerticalScrollbarDisplayVisibility();
updateColumnMarkerHeight();
resizeListenerManager.dispatch(new Dispatcher<ResizeListener>() {
@Override
public void dispatch(ResizeListener listener) {
listener.onResize(Buffer.this, height, viewportHeight, scrollTop);
}
});
}
});
}
public void addLineElement(Element lineElement) {
getView().addLine(lineElement);
}
public boolean hasLineElement(Element lineElement) {
return lineElement.getParentElement() != null;
}
/*
* TODO: consider making ElementManager public, and a
* getElementManager() method instead
*/
public void addAnchoredElement(ReadOnlyAnchor anchor, Element element) {
elementManager.addAnchoredElement(anchor, element);
}
public void removeAnchoredElement(ReadOnlyAnchor anchor, Element element) {
elementManager.removeAnchoredElement(anchor, element);
}
public void addUnmanagedElement(Element element) {
elementManager.addUnmanagedElement(element);
}
public void removeUnmanagedElement(Element element) {
elementManager.removeUnmanagedElement(element);
}
/**
* Returns a newly added spacer above the given {@code lineInfo} with the
* given {@code height}.
*/
public Spacer addSpacer(LineInfo lineInfo, int height) {
final Spacer createdSpacer =
coordinateMap.createSpacer(lineInfo, height, this, getView().css.spacer());
updateBufferHeightAndMaybeScrollTop(calculateSpacerTop(createdSpacer), height);
spacerListenerManager.dispatch(new Dispatcher<Buffer.SpacerListener>() {
@Override
public void dispatch(SpacerListener listener) {
listener.onSpacerAdded(createdSpacer);
}
});
return createdSpacer;
}
public void removeSpacer(final Spacer spacer) {
final Line spacerLine = spacer.getLine();
final int spacerLineNumber = spacer.getLineNumber();
if (coordinateMap.removeSpacer(spacer)) {
updateBufferHeightAndMaybeScrollTop(convertLineNumberToY(spacerLineNumber),
-spacer.getHeight());
spacerListenerManager.dispatch(new Dispatcher<Buffer.SpacerListener>() {
@Override
public void dispatch(SpacerListener listener) {
listener.onSpacerRemoved(spacer, spacerLine, spacerLineNumber);
}
});
}
}
public boolean hasSpacers() {
return coordinateMap.getTotalSpacerHeight() != 0;
}
@Override
public void handleSpacerHeightChanged(final Spacer spacer, final int oldHeight) {
int deltaHeight = spacer.getHeight() - oldHeight;
updateBufferHeightAndMaybeScrollTop(calculateSpacerTop(spacer), deltaHeight);
spacerListenerManager.dispatch(new Dispatcher<Buffer.SpacerListener>() {
@Override
public void dispatch(SpacerListener listener) {
listener.onSpacerHeightChanged(spacer, oldHeight);
}
});
}
public int calculateSpacerTop(Spacer spacer) {
return coordinateMap.convertLineNumberToY(spacer.getLineNumber()) - spacer.getHeight();
}
/**
* @param inDocumentRange whether to do a validation check on the return line
* number to ensure it is in the document's range
*/
public int convertYToLineNumber(int y, boolean inDocumentRange) {
int lineNumber = coordinateMap.convertYToLineNumber(y);
return inDocumentRange ? LineUtils.getValidLineNumber(lineNumber, document) : lineNumber;
}
public int convertXToRoundedVisibleColumn(int x, Line line) {
int roundedColumn = convertXToColumn(x, line, RoundingStrategy.ROUND);
return TextUtils.findNextCharacterInclusive(line.getText(), roundedColumn);
}
public int convertXToColumn(int x, Line line, RoundingStrategy roundingStrategy) {
return LineUtils.rubberbandColumn(
line, lineDimensions.convertXToColumn(line, x, roundingStrategy));
}
public ListenerRegistrar<HeightListener> getHeightListenerRegistrar() {
return heightListenerManager;
}
/**
* Returns the top for a line, e.g. if {@code lineNumber} is 0 and it is a
* simple document, 0 will be returned.
*/
public int convertLineNumberToY(int lineNumber) {
return coordinateMap.convertLineNumberToY(lineNumber);
}
public int convertColumnToX(Line line, int column) {
return (int) Math.floor(lineDimensions.convertColumnToX(line, column));
}
public int calculateColumnLeftRelativeToScrollableIgnoringSpecialCharacters(int column) {
return calculateColumnLeftIgnoringSpecialCharacters(column)
+ getView().css.scrollableLeftPadding();
}
/**
* Converts a column to an x value assuming all characters in-between are
* number width.
* <p>
* DO NOT USE THIS UNLESS IT IS INTENTIONAL AND YOU UNDERSTAND THE
* CONSEQUENCES. DO NOT USE IT JUST BECAUSE IT DOES NOT REQUIRE A
* {@link Line}.
*/
public int calculateColumnLeftIgnoringSpecialCharacters(int column) {
return (int) Math.floor(fontDimensions.getCharacterWidth() * column);
}
public int calculateColumnLeft(Line line, int column) {
return Math.max(0, convertColumnToX(line, column));
}
public int calculateLineBottom(int lineNumber) {
return convertLineNumberToY(lineNumber) + editorLineHeight;
}
public int calculateLineTop(int lineNumber) {
return convertLineNumberToY(lineNumber);
}
public ListenerRegistrar<MouseClickListener> getMouseClickListenerRegistrar() {
return mouseClickListenerManager;
}
public ListenerRegistrar<SpacerListener> getSpacerListenerRegistrar() {
return spacerListenerManager;
}
public Document getDocument() {
return document;
}
@Override
public float getEditorCharacterWidth() {
return fontDimensions.getCharacterWidth();
}
/**
* TODO: So right now this uses a constant, unfortunately it's not
* accurate when zoomed in/out and sometimes leads to whitespace between
* selection lines. This should be converted to fontDimensions.getHeight().
*/
@Override
public int getEditorLineHeight() {
return editorLineHeight;
}
public Element getFirstLineElement() {
return getView().getFirstLine();
}
public int getFlooredHeightInLines() {
/*
* Imagine scrollTop = 0, lineHeight = 10, and height = 20. If we passed
* "true", convertYToLineNumber(0+20) would bound on the document size and
* return 1 instead of the 2 that we need.
*/
return convertYToLineNumber(getScrollTop() + getHeight(), false)
- convertYToLineNumber(getScrollTop(), false);
}
public int getHeight() {
return getView().getHeight();
}
public ListenerRegistrar<MouseDragListener> getMouseDragListenerRegistrar() {
return mouseDragListenerManager;
}
// TODO: Make it package-private.
public ListenerRegistrar<MouseMoveListener> getMouseMoveListenerRegistrar() {
return mouseMoveListenerManager;
}
ListenerRegistrar<MouseOutListener> getMouseOutListenerRegistrar() {
return mouseOutListenerManager;
}
public int getMaxLineLength() {
return maxLineLength;
}
public int getScrollLeft() {
return getView().getScrollLeft();
}
public ListenerRegistrar<ScrollListener> getScrollListenerRegistrar() {
return scrollListenerManager;
}
public ListenerRegistrar<ResizeListener> getResizeListenerRegistrar() {
return resizeListenerManager;
}
public int getScrollTop() {
return getView().getScrollTop();
}
public int getScrollHeight() {
return getView().getScrollHeight();
}
public int getWidth() {
return getView().getWidth();
}
public void handleDocumentChanged(Document newDocument) {
documentChangedRemoverManager.remove();
document = newDocument;
coordinateMap.handleDocumentChange(newDocument);
lineDimensions.handleDocumentChange(newDocument);
getView().reset();
documentChangedRemoverManager.track(newDocument.getLineListenerRegistrar().add(this));
updateBufferHeight();
}
@Override
public void onLineAdded(Document document, final int lineNumber,
final JsonArray<Line> addedLines) {
renderTimeExecutor.execute(new Runnable() {
@Override
public void run() {
updateBufferHeightAndMaybeScrollTop(convertLineNumberToY(lineNumber), addedLines.size()
* getEditorLineHeight());
}
});
}
@Override
public void onLineRemoved(final Document document, final int lineNumber,
final JsonArray<Line> removedLines) {
renderTimeExecutor.execute(new Runnable() {
@Override
public void run() {
/*
* Since the removed line(s) no longer exist, we need to make sure to
* clamp them
*/
int safeLineNumber =
Math.min(document.getLastLineNumber(), lineNumber);
updateBufferHeightAndMaybeScrollTop(convertLineNumberToY(safeLineNumber),
-removedLines.size() * getEditorLineHeight());
}
});
}
@Override
public void onLineCountChanged(Document document, int lineCount) {
updateBufferHeight();
}
public void setMaxLineLength(int maxLineLength) {
this.maxLineLength = maxLineLength;
updateTextWidth();
}
private void updateTextWidth() {
int longestLineWidth = (int) Math.floor(maxLineLength * getEditorCharacterWidth());
getView().setWidth(longestLineWidth);
}
/**
* Updates the buffer height and potentially the scroll top depending on
* whether the change is before the scroll top or not.
*
* @param changeTop the top (px) where the change occurred
* @param changeHeight the potentially negative change in height
*/
private void updateBufferHeightAndMaybeScrollTop(int changeTop, final int changeHeight) {
updateBufferHeight();
/*
* If the change is on or before the scrolled position and we don't update
* the scroll position, the content of the viewport will be different. To
* keep the content of the viewport the same, we adjust the scrolled
* position.
*/
final int scrollTop = getScrollTop();
if (changeTop <= scrollTop) {
setScrollTop(scrollTop + changeHeight);
}
}
/**
* Updates the buffer height to the calculated height. Most callers should use
* {@link #updateBufferHeightAndMaybeScrollTop(int, int)}.
*/
private void updateBufferHeight() {
final int totalBufferHeight =
coordinateMap.getTotalSpacerHeight() + document.getLineCount() * editorLineHeight;
getView().setBufferHeight(totalBufferHeight);
updateColumnMarkerHeight();
updateVerticalScrollbarDisplayVisibility();
heightListenerManager.dispatch(new Dispatcher<Buffer.HeightListener>() {
@Override
public void dispatch(HeightListener listener) {
listener.onHeightChanged(totalBufferHeight);
}
});
}
public void setScrollLeft(int scrollLeft) {
getView().setScrollLeft(scrollLeft);
}
/**
* Sets the scroll top of the buffer. Cannot set scroll top from a scroll
* listener it will be ignored.
*/
public void setScrollTop(int scrollTop) {
if (!scrollListenerManager.isDispatching()) {
getView().setScrollTop(scrollTop, false);
}
}
void handleComponentsInitialized(ViewportModel viewport, Renderer renderer) {
elementManager.handleDocumentChanged(viewport, renderer);
}
public void repositionAnchoredElementsWithColumn() {
updateColumnMarkerPosition();
elementManager.repositionAnchoredElementsWithColumn();
}
private void updateColumnMarkerPosition() {
getView().columnMarkerElement.getStyle().setLeft(
calculateColumnLeftRelativeToScrollableIgnoringSpecialCharacters(MARKER_COLUMN),
CSSStyleDeclaration.Unit.PX);
}
private void updateColumnMarkerHeight() {
int height = getView().textLayerElement.getClientHeight();
int limitHeight = Math.max(height, getHeight());
getView().columnMarkerElement.getStyle().setHeight(limitHeight, CSSStyleDeclaration.Unit.PX);
}
public ClientRect getBoundingClientRect() {
return getView().scrollableElement.getBoundingClientRect();
}
private void updateVerticalScrollbarDisplayVisibility() {
CssUtils.setDisplayVisibility2(getView().scrollbarElement,
getView().getScrollHeight() > getView().getHeight());
}
public void setColumnMarkerVisibility(boolean visible) {
CssUtils.setDisplayVisibility2(getView().columnMarkerElement, visible);
}
public void setVerticalScrollbarVisibility(boolean visible) {
if (visible) {
if (!getView().rootElement.contains(getView().scrollbarElement)) {
getView().rootElement.appendChild(getView().scrollbarElement);
}
getView().scrollableElement.getStyle().setOverflowY("auto");
} else {
getView().rootElement.removeChild(getView().scrollbarElement);
getView().scrollableElement.getStyle().setOverflowY("hidden");
}
}
/**
* Ensures that the scrollTop is synchronized across all editor components. For example, this
* should be called after the editor has been removed from the DOM and re-added.
*/
public void synchronizeScrollTop() {
getView().setScrollTop(getView().scrollTopFromPreviousDispatch, true);
}
}