// 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.document.linedimensions.LineDimensionsUtils; import com.google.collide.client.editor.Buffer; import com.google.collide.client.util.Elements; import com.google.collide.client.util.logging.Log; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.document.Line; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.SortedList; import com.google.collide.shared.util.StringUtils; import com.google.common.base.Preconditions; import elemental.css.CSSStyleDeclaration; import elemental.html.Element; import elemental.html.SpanElement; /** * A class to maintain the list of {@link LineRenderer LineRenderers} and render * a line by delegating to each of the renderers. */ class LineRendererController { /* * TODO: consider recycling these if GC performance during * rendering is an issue */ private static class LineRendererTarget implements LineRenderer.Target { private static class Comparator implements SortedList.Comparator<LineRendererTarget> { @Override public int compare(LineRendererTarget a, LineRendererTarget b) { return a.remainingCount - b.remainingCount; } } /** The line renderer for which this is the target */ private final LineRenderer lineRenderer; /** * The remaining number of characters that should receive {@link #styleName} * . Once this is 0, the {@link #lineRenderer} will be asked to render its * next chunk */ private int remainingCount; /** The style to be applied to the {@link #remainingCount} */ private String styleName; public LineRendererTarget(LineRenderer lineRenderer) { this.lineRenderer = lineRenderer; } @Override public void render(int characterCount, String styleName) { remainingCount = characterCount; this.styleName = styleName; } } /** * A sorted list storing targets for the line renderers that are participating * in rendering the current line */ private final SortedList<LineRendererTarget> currentLineRendererTargets; /** * A list of all of the line renderers that are registered on the editor (Note * that some may not be participating in the current line) */ private final JsonArray<LineRenderer> lineRenderers; private final Buffer buffer; LineRendererController(Buffer buffer) { this.buffer = buffer; currentLineRendererTargets = new SortedList<LineRendererController.LineRendererTarget>( new LineRendererTarget.Comparator()); lineRenderers = JsonCollections.createArray(); } void addLineRenderer(LineRenderer lineRenderer) { if (!lineRenderers.contains(lineRenderer)) { /* * Prevent line renderer from appearing twice in the list if it is already * added. */ lineRenderers.add(lineRenderer); } } void removeLineRenderer(LineRenderer lineRenderer) { lineRenderers.remove(lineRenderer); } void renderLine(Line line, int lineNumber, Element targetElement, boolean isTargetElementEmpty) { currentLineRendererTargets.clear(); if (!resetLineRenderers(line, lineNumber)) { // No line renderers are participating, so exit early. setTextContentSafely(targetElement, line.getText()); return; } if (!isTargetElementEmpty) { targetElement.setInnerHTML(""); } Element contentElement = Elements.createSpanElement(); contentElement.getStyle().setDisplay(CSSStyleDeclaration.Display.INLINE_BLOCK); for (int indexInLine = 0, lineSize = line.getText().length(); indexInLine < lineSize && ensureAllRenderersHaveARenderedNextChunk();) { int chunkSize = currentLineRendererTargets.get(0).remainingCount; if (chunkSize == 0) { // Bad news, revert to naive rendering and log setTextContentSafely(targetElement, line.getText()); Log.error(getClass(), "Line renderers do not have remaining chunks"); return; } renderChunk(line.getText(), indexInLine, chunkSize, contentElement); markChunkRendered(chunkSize); indexInLine += chunkSize; } targetElement.appendChild(contentElement); if (line.getText().endsWith("\n")) { Element lastChunk = (Element) contentElement.getLastChild(); Preconditions.checkState(lastChunk != null, "This line has no chunks!"); if (!StringUtils.isNullOrWhitespace(lastChunk.getClassName())) { contentElement.getStyle().setProperty("float", "left"); Element newlineCharacterElement = createLastChunkElement(targetElement); // Created on demand only because it is rarely used. Element remainingSpaceElement = null; for (int i = 0, n = currentLineRendererTargets.size(); i < n; i++) { LineRendererTarget target = currentLineRendererTargets.get(i); if (target.styleName != null) { if (!target.lineRenderer.shouldLastChunkFillToRight()) { newlineCharacterElement.addClassName(target.styleName); } else { if (remainingSpaceElement == null) { newlineCharacterElement.getStyle().setProperty("float", "left"); remainingSpaceElement = createLastChunkElement(targetElement); remainingSpaceElement.getStyle().setWidth("100%"); } // Also apply to last chunk element so that there's no gap. newlineCharacterElement.addClassName(target.styleName); remainingSpaceElement.addClassName(target.styleName); } } } } } } private static Element createLastChunkElement(Element parent) { // we need to give them a whitespace element so that it can be styled. Element whitespaceElement = Elements.createSpanElement(); whitespaceElement.setTextContent("\u00A0"); whitespaceElement.getStyle().setDisplay("inline-block"); parent.appendChild(whitespaceElement); return whitespaceElement; } /** * Ensures all renderer targets (that want to render) have rendered each of * their next chunks. */ private boolean ensureAllRenderersHaveARenderedNextChunk() { while (currentLineRendererTargets.size() > 0 && currentLineRendererTargets.get(0).remainingCount == 0) { LineRendererTarget target = currentLineRendererTargets.get(0); try { target.lineRenderer.renderNextChunk(target); } catch (Throwable t) { // Cause naive rendering target.remainingCount = 0; Log.warn(getClass(), "An exception was thrown from renderNextChunk", t); } if (target.remainingCount > 0) { currentLineRendererTargets.repositionItem(0); } else { // Remove the line renderer because it has broken our contract currentLineRendererTargets.remove(0); Log.warn(getClass(), "The line renderer " + target.lineRenderer + " is lacking a next chunk, removing from rendering"); } } return currentLineRendererTargets.size() > 0; } /** * Marks the chunk rendered on all the renderers. */ private void markChunkRendered(int chunkSize) { for (int i = 0, n = currentLineRendererTargets.size(); i < n; i++) { LineRendererTarget target = currentLineRendererTargets.get(i); target.remainingCount -= chunkSize; } } /** * Renders the chunk by creating a span with all of the individual line * renderer's styles. */ private void renderChunk(String lineText, int lineIndex, int chunkLength, Element targetElement) { SpanElement element = Elements.createSpanElement(); // TODO: file a Chrome bug, place link here element.getStyle().setDisplay(CSSStyleDeclaration.Display.INLINE_BLOCK); setTextContentSafely(element, lineText.substring(lineIndex, lineIndex + chunkLength)); applyStyles(element); targetElement.appendChild(element); } private void applyStyles(Element element) { for (int i = 0, n = currentLineRendererTargets.size(); i < n; i++) { LineRendererTarget target = currentLineRendererTargets.get(i); if (target.styleName != null) { element.addClassName(target.styleName); } } } /** * Resets the line renderers, preparing for a new line to be rendered. * * This method fills the {@link #currentLineRendererTargets} with targets for * line renderers that will participate in rendering this line. * * @return true if there is at least one line renderer participating for the * given @{link Line} line. */ private boolean resetLineRenderers(Line line, int lineNumber) { boolean hasAtLeastOneParticipatingLineRenderer = false; for (int i = 0; i < lineRenderers.size(); i++) { LineRenderer lineRenderer = lineRenderers.get(i); boolean isParticipating = lineRenderer.resetToBeginningOfLine(line, lineNumber); if (isParticipating) { currentLineRendererTargets.add(new LineRendererTarget(lineRenderer)); hasAtLeastOneParticipatingLineRenderer = true; } } return hasAtLeastOneParticipatingLineRenderer; } private void setTextContentSafely(Element element, String text) { String cleansedText = text.replaceAll("\t", LineDimensionsUtils.getTabAsSpaces()); element.setTextContent(cleansedText); } }