// 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.editor.renderer.Renderer;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonIntegerMap;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorManager;
import com.google.collide.shared.document.anchor.AnchorUtils;
import com.google.collide.shared.document.anchor.ReadOnlyAnchor;
import com.google.collide.shared.document.anchor.AnchorManager.AnchorVisitor;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar;
import elemental.css.CSSStyleDeclaration;
import elemental.html.Element;
// TODO: support RangeAnchoredElements
/**
* A manager that allows for adding and removing elements to some given
* container element. This manager is capable of adding elements that are
* anchored to a point or to a range.
*
* Some restrictions of anchored elements:
* <ul>
* <li>An anchor cannot be used to anchor multiple elements</li>
* <li>Anchors must be assigned a line number (though this can be loosened if a
* use case arises)</li>
* </ul>
*/
public class ElementManager {
private final ReadOnlyAnchor.ShiftListener anchorShiftedListener =
new ReadOnlyAnchor.ShiftListener() {
@Override
public void onAnchorShifted(ReadOnlyAnchor anchor) {
updateAnchoredElements(anchor);
}
};
private final ReadOnlyAnchor.MoveListener anchorMovedListener =
new ReadOnlyAnchor.MoveListener() {
@Override
public void onAnchorMoved(ReadOnlyAnchor anchor) {
updateAnchoredElements(anchor);
}
};
private final ReadOnlyAnchor.RemoveListener anchorRemovalListener =
new ReadOnlyAnchor.RemoveListener() {
@Override
public void onAnchorRemoved(ReadOnlyAnchor anchor) {
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
for (int i = 0, n = elements.size(); i < n; i++) {
removeAnchoredElement(anchor, elements.get(i));
}
}
};
private final Buffer buffer;
private final Element container;
private final JsonIntegerMap<JsonArray<Element>> anchoredElements =
JsonCollections.createIntegerMap();
private final JsonArray<ReadOnlyAnchor> anchoredElementAnchors = JsonCollections.createArray();
private final Renderer.LineLifecycleListener renderedLineLifecycleListener =
new Renderer.LineLifecycleListener() {
private final AnchorVisitor lineCreatedAnchorVisitor = new AnchorVisitor() {
@Override
public void visitAnchor(Anchor anchor) {
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
if (elements != null) {
for (int i = 0, n = elements.size(); i < n; i++) {
Element element = elements.get(i);
attachElement(element);
positionElementToAnchorTopLeft(anchor, element);
}
}
}
};
private final AnchorVisitor lineShiftedAnchorVisitor = new AnchorVisitor() {
@Override
public void visitAnchor(Anchor anchor) {
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
if (elements != null) {
for (int i = 0, n = elements.size(); i < n; i++) {
updateAnchoredElement(anchor, elements.get(i));
}
}
}
};
private final AnchorVisitor lineGarbageCollectedAnchorVisitor = new AnchorVisitor() {
@Override
public void visitAnchor(Anchor anchor) {
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
if (elements != null) {
for (int i = 0, n = elements.size(); i < n; i++) {
detachElement(elements.get(i));
}
}
}
};
@Override
public void onRenderedLineGarbageCollected(Line line) {
AnchorUtils.visitAnchorsOnLine(line, lineGarbageCollectedAnchorVisitor);
}
@Override
public void onRenderedLineCreated(Line line, int lineNumber) {
AnchorUtils.visitAnchorsOnLine(line, lineCreatedAnchorVisitor);
}
@Override
public void onRenderedLineShifted(Line line, int lineNumber) {
/*
* TODO: Given this callback exists now, do we really
* need to require anchors with line numbers?
*/
AnchorUtils.visitAnchorsOnLine(line, lineShiftedAnchorVisitor);
}
};
private ListenerRegistrar.Remover rendererListenerRemover;
private final JsonArray<Element> unmanagedElements = JsonCollections.createArray();
private ViewportModel viewport;
public ElementManager(Element container, Buffer buffer) {
this.container = container;
this.buffer = buffer;
}
public void handleDocumentChanged(ViewportModel viewport, Renderer renderer) {
if (rendererListenerRemover != null) {
rendererListenerRemover.remove();
}
removeAnchoredElements();
detachElements(unmanagedElements);
unmanagedElements.clear();
this.viewport = viewport;
rendererListenerRemover =
renderer.getLineLifecycleListenerRegistrar().add(renderedLineLifecycleListener);
}
public void addAnchoredElement(ReadOnlyAnchor anchor, Element element) {
if (!anchor.hasLineNumber()) {
throw new IllegalArgumentException(
"The given anchor does not have a line number; create it with line numbers");
}
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
if (elements == null) {
elements = JsonCollections.createArray();
anchoredElements.put(anchor.getId(), elements);
anchoredElementAnchors.add(anchor);
anchor.getReadOnlyShiftListenerRegistrar().add(anchorShiftedListener);
anchor.getReadOnlyMoveListenerRegistrar().add(anchorMovedListener);
anchor.getReadOnlyRemoveListenerRegistrar().add(anchorRemovalListener);
} else if (elements.contains(element)) {
// Already anchored, do nothing
return;
}
elements.add(element);
initializeElementForBeingManaged(element);
updateAnchoredElement(anchor, element);
}
public void removeAnchoredElement(ReadOnlyAnchor anchor, Element element) {
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
if (elements == null || !elements.remove(element)) {
return;
}
if (elements.size() == 0) {
anchor.getReadOnlyShiftListenerRegistrar().remove(anchorShiftedListener);
anchor.getReadOnlyMoveListenerRegistrar().remove(anchorMovedListener);
anchor.getReadOnlyRemoveListenerRegistrar().remove(anchorRemovalListener);
anchoredElements.erase(anchor.getId());
anchoredElementAnchors.remove(anchor);
}
detachElement(element);
}
private void removeAnchoredElements() {
while (anchoredElementAnchors.size() > 0) {
ReadOnlyAnchor anchor = anchoredElementAnchors.get(0);
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
for (int i = 0, n = elements.size(); i < n; i++) {
removeAnchoredElement(anchor, elements.get(i));
}
}
}
public void addUnmanagedElement(Element element) {
unmanagedElements.add(element);
attachElement(element);
}
public void removeUnmanagedElement(Element element) {
unmanagedElements.remove(element);
detachElement(element);
}
private void initializeElementForBeingManaged(Element element) {
element.getStyle().setPosition(CSSStyleDeclaration.Position.ABSOLUTE);
}
private void updateAnchoredElements(ReadOnlyAnchor anchor) {
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
if (elements != null) {
for (int i = 0, n = elements.size(); i < n; i++) {
updateAnchoredElement(anchor, elements.get(i));
}
}
}
/**
* Renders an anchored element intelligently; adds it to the DOM when it is in
* the viewport and removes it once it leaves the viewport.
*/
private void updateAnchoredElement(ReadOnlyAnchor anchor, Element element) {
/*
* We only want the line number if the anchor is in the viewport, and this
* is a quick way of achieving that
*/
int lineNumberGuess = LineUtils.getCachedLineNumber(anchor.getLine());
boolean isInViewport =
lineNumberGuess != -1 && lineNumberGuess >= viewport.getTopLineNumber()
&& lineNumberGuess <= viewport.getBottomLineNumber();
boolean isRendered = element.getParentElement() != null;
if (isInViewport && !isRendered) {
// Anchor moved into the viewport
attachElement(element);
positionElementToAnchorTopLeft(anchor, element);
} else if (isRendered && !isInViewport) {
// Anchor moved out of the viewport
detachElement(element);
} else if (isInViewport) {
// Anchor was and is in viewport, reposition
positionElementToAnchorTopLeft(anchor, element);
}
}
private void positionElementToAnchorTopLeft(ReadOnlyAnchor anchor, Element element) {
CSSStyleDeclaration style = element.getStyle();
style.setTop(buffer.convertLineNumberToY(anchor.getLineNumber()), CSSStyleDeclaration.Unit.PX);
int column = anchor.getColumn();
if (column != AnchorManager.IGNORE_COLUMN) {
style.setLeft(buffer.convertColumnToX(anchor.getLine(), column), CSSStyleDeclaration.Unit.PX);
}
}
private void attachElement(Element element) {
container.appendChild(element);
}
private void detachElements(JsonArray<Element> elements) {
for (int i = 0, n = elements.size(); i < n; i++) {
detachElement(elements.get(i));
}
}
private void detachElement(Element element) {
if (container.contains(element)) {
container.removeChild(element);
}
}
public void repositionAnchoredElementsWithColumn() {
for (int i = 0, n = anchoredElementAnchors.size(); i < n; i++) {
ReadOnlyAnchor anchor = anchoredElementAnchors.get(i);
if (!anchor.hasColumn()) {
continue;
}
JsonArray<Element> elements = anchoredElements.get(anchor.getId());
for (int elementsPos = 0, elementsSize = elements.size(); elementsPos < elementsSize;
elementsPos++) {
positionElementToAnchorTopLeft(anchor, elements.get(elementsPos));
}
}
}
}