// 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.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.anchor.Anchor; import com.google.collide.shared.document.anchor.AnchorManager; import com.google.collide.shared.document.anchor.AnchorType; import com.google.collide.shared.document.anchor.Anchor.RemovalStrategy; import com.google.collide.shared.util.SortedList; import com.google.collide.shared.util.ListenerRegistrar.Remover; import com.google.collide.shared.util.SortedList.OneWayIntComparator; /** * This class takes care of mapping between the different coordinates used by * the editor. The two supported systems are: * <ul> * <li>Offset (x,y) - in pixels, relative to the top left of line 0 in the * current document. * <li>Line (line, column) - the real line number and column, taking into * account spacer objects in between lines. Lines and columns are 0-indexed. * </ul> */ class CoordinateMap implements Document.LineListener { interface DocumentSizeProvider { float getEditorCharacterWidth(); int getEditorLineHeight(); void handleSpacerHeightChanged(Spacer spacer, int oldHeight); } private static class OffsetCache { private static final SortedList.Comparator<OffsetCache> COMPARATOR = new SortedList.Comparator<OffsetCache>() { @Override public int compare(OffsetCache a, OffsetCache b) { return a.offset - b.offset; } }; private static final SortedList.OneWayIntComparator<OffsetCache> Y_OFFSET_ONE_WAY_COMPARATOR = new SortedList.OneWayIntComparator<OffsetCache>() { @Override public int compareTo(OffsetCache s) { return value - s.offset; } }; private static final SortedList.OneWayIntComparator<OffsetCache> LINE_NUMBER_ONE_WAY_COMPARATOR = new SortedList.OneWayIntComparator<OffsetCache>() { @Override public int compareTo(OffsetCache s) { return value - s.lineNumber; } }; private final int offset; private final int height; private final int lineNumber; private OffsetCache(int offset, int lineNumber, int height) { this.offset = offset; this.height = height; this.lineNumber = lineNumber; } } private static final OffsetCache BEGINNING_EMPTY_OFFSET_CACHE = new OffsetCache(0, 0, 0); private static final AnchorType SPACER_ANCHOR_TYPE = AnchorType.create(CoordinateMap.class, "spacerAnchorType"); private static final Spacer.Comparator SPACER_COMPARATOR = new Spacer.Comparator(); private static final Spacer.OneWaySpacerComparator SPACER_ONE_WAY_COMPARATOR = new Spacer.OneWaySpacerComparator(); /** Used by {@link #getPrecedingOffsetCache(int, int)} */ private static final int IGNORE = Integer.MIN_VALUE; private Document document; private DocumentSizeProvider documentSizeProvider; /** List of offset cache items, sorted by the offset */ private SortedList<OffsetCache> offsetCache; /** * True if there is at least one spacer in the editor, false otherwise (false * means a simple height / line height calculation can be used) */ private boolean requiresMapping; /** Sorted by line number */ private SortedList<Spacer> spacers; /** Summation of all spacers' heights */ private int totalSpacerHeight; /** Remover for listener */ private Remover documentLineListenerRemover; CoordinateMap(DocumentSizeProvider documentSizeProvider) { this.documentSizeProvider = documentSizeProvider; requiresMapping = false; } int convertYToLineNumber(int y) { if (y < 0) { return 0; } int lineHeight = documentSizeProvider.getEditorLineHeight(); if (!requiresMapping) { return y / lineHeight; } OffsetCache precedingOffsetCache = getPrecedingOffsetCache(y, IGNORE); int precedingOffsetCacheBottom = precedingOffsetCache.offset + precedingOffsetCache.height; int lineNumberRelativeToOffsetCacheLine = (y - precedingOffsetCacheBottom) / lineHeight; if (y < precedingOffsetCacheBottom) { // y is inside the spacer return precedingOffsetCache.lineNumber; } else { return precedingOffsetCache.lineNumber + lineNumberRelativeToOffsetCacheLine; } } /** * Returns the top of the given line. */ int convertLineNumberToY(int lineNumber) { int lineHeight = documentSizeProvider.getEditorLineHeight(); if (!requiresMapping) { return lineNumber * lineHeight; } OffsetCache precedingOffsetCache = getPrecedingOffsetCache(IGNORE, lineNumber); int precedingOffsetCacheBottom = precedingOffsetCache.offset + precedingOffsetCache.height; int offsetRelativeToOffsetCacheBottom = (lineNumber - precedingOffsetCache.lineNumber) * lineHeight; return precedingOffsetCacheBottom + offsetRelativeToOffsetCacheBottom; } /** * Returns the first {@link OffsetCache} that is positioned less than or equal * to {@code y} or {@code lineNumber}. This methods fills the * {@link #offsetCache} if necessary ensuring the returned {@link OffsetCache} * is up-to-date. * * @param y the y, or {@link #IGNORE} if looking up by {@code lineNumber} * @param lineNumber the line number, or {@link #IGNORE} if looking up by * {@code y} */ private OffsetCache getPrecedingOffsetCache(int y, int lineNumber) { assert (y != IGNORE && lineNumber == IGNORE) || (lineNumber != IGNORE && y == IGNORE); final int lineHeight = documentSizeProvider.getEditorLineHeight(); OffsetCache previousOffsetCache; if (y != IGNORE) { previousOffsetCache = getCachedPrecedingOffsetCacheImpl(OffsetCache.Y_OFFSET_ONE_WAY_COMPARATOR, y); } else { previousOffsetCache = getCachedPrecedingOffsetCacheImpl(OffsetCache.LINE_NUMBER_ONE_WAY_COMPARATOR, lineNumber); } if (previousOffsetCache == null) { if (spacers.size() > 0 && spacers.get(0).getLineNumber() == 0) { previousOffsetCache = createOffsetCache(0, 0, spacers.get(0).getHeight()); } else { previousOffsetCache = BEGINNING_EMPTY_OFFSET_CACHE; } } /* * Optimization so the common case that the target has previously been * computed requires no more computation */ int offsetCacheSize = offsetCache.size(); if (offsetCacheSize > 0 && isTargetEarlierThanOffsetCache(y, lineNumber, offsetCache.get(offsetCacheSize - 1))) { return previousOffsetCache; } // This will return this offset cache's matching spacer int spacerPos = getPrecedingSpacerIndex(previousOffsetCache.lineNumber); /* * We want the spacer following this offset cache's spacer, or the first * spacer if none were found */ spacerPos++; for (int n = spacers.size(); spacerPos < n; spacerPos++) { Spacer curSpacer = spacers.get(spacerPos); int previousOffsetCacheBottom = previousOffsetCache.offset + previousOffsetCache.height; int simpleLinesHeight = (curSpacer.getLineNumber() - previousOffsetCache.lineNumber) * lineHeight; if (simpleLinesHeight == 0) { Log.warn(Spacer.class, "More than one spacer on line " + previousOffsetCache.lineNumber); } // Create an offset cache for this spacer OffsetCache curOffsetCache = createOffsetCache(previousOffsetCacheBottom + simpleLinesHeight, curSpacer.getLineNumber(), curSpacer.getHeight()); if (isTargetEarlierThanOffsetCache(y, lineNumber, curOffsetCache)) { return previousOffsetCache; } previousOffsetCache = curOffsetCache; } return previousOffsetCache; } /** * Returns the {@link OffsetCache} instance in list that has the greatest * value less than or equal to the given {@code value}. Returns null if there * isn't one. * * This should only be used by {@link #getPrecedingOffsetCache(int, int)}. */ private OffsetCache getCachedPrecedingOffsetCacheImpl( OneWayIntComparator<OffsetCache> comparator, int value) { comparator.setValue(value); int index = offsetCache.findInsertionIndex(comparator, false); return index >= 0 ? offsetCache.get(index) : null; } private boolean isTargetEarlierThanOffsetCache(int y, int lineNumber, OffsetCache offsetCache) { return ((y != IGNORE && y < offsetCache.offset) || (lineNumber != IGNORE && lineNumber < offsetCache.lineNumber)); } private OffsetCache createOffsetCache(int offset, int lineNumber, int height) { OffsetCache createdOffsetCache = new OffsetCache(offset, lineNumber, height); offsetCache.add(createdOffsetCache); return createdOffsetCache; } private int getPrecedingSpacerIndex(int lineNumber) { SPACER_ONE_WAY_COMPARATOR.setValue(lineNumber); return spacers.findInsertionIndex(SPACER_ONE_WAY_COMPARATOR, false); } /** * Adds a spacer above the given lineInfo line with height heightPx and * returns the created Spacer object. * * @param lineInfo the line before which the spacer will be inserted * @param height the height in pixels of the spacer */ Spacer createSpacer(LineInfo lineInfo, int height, Buffer buffer, String cssClass) { int lineNumber = lineInfo.number(); // create an anchor on the current line Anchor anchor = document.getAnchorManager().createAnchor(SPACER_ANCHOR_TYPE, lineInfo.line(), lineNumber, AnchorManager.IGNORE_COLUMN); anchor.setRemovalStrategy(RemovalStrategy.SHIFT); // account for the height of the line the spacer is on Spacer spacer = new Spacer(anchor, height, this, buffer, cssClass); spacers.add(spacer); totalSpacerHeight += height; invalidateLineNumberAndFollowing(lineNumber); requiresMapping = true; return spacer; } boolean removeSpacer(Spacer spacer) { int lineNumber = spacer.getLineNumber(); if (spacers.remove(spacer)) { document.getAnchorManager().removeAnchor(spacer.getAnchor()); totalSpacerHeight -= spacer.getHeight(); invalidateLineNumberAndFollowing(lineNumber - 1); updateRequiresMapping(); return true; } return false; } void handleDocumentChange(Document document) { if (documentLineListenerRemover != null) { documentLineListenerRemover.remove(); } this.document = document; spacers = new SortedList<Spacer>(SPACER_COMPARATOR); offsetCache = new SortedList<OffsetCache>(OffsetCache.COMPARATOR); documentLineListenerRemover = document.getLineListenerRegistrar().add(this); requiresMapping = false; // starts with no items in list totalSpacerHeight = 0; } @Override public void onLineAdded(Document document, int lineNumber, JsonArray<Line> addedLines) { invalidateLineNumberAndFollowing(lineNumber); } @Override public void onLineRemoved(Document document, int lineNumber, JsonArray<Line> removedLines) { invalidateLineNumberAndFollowing(lineNumber); } /** * Call this after any line changes (adding/deleting lines, changing line * heights). Only invalidate (delete) cache items >= lineNumber, don't * recalculate. */ void invalidateLineNumberAndFollowing(int lineNumber) { OffsetCache.LINE_NUMBER_ONE_WAY_COMPARATOR.setValue(lineNumber); int insertionIndex = offsetCache.findInsertionIndex(OffsetCache.LINE_NUMBER_ONE_WAY_COMPARATOR); offsetCache.removeThisAndFollowing(insertionIndex); } private void updateRequiresMapping() { // check to change active status requiresMapping = spacers.size() > 0; } int getTotalSpacerHeight() { return totalSpacerHeight; } void handleSpacerHeightChanged(Spacer spacer, int oldHeight) { totalSpacerHeight -= oldHeight; totalSpacerHeight += spacer.getHeight(); invalidateLineNumberAndFollowing(spacer.getLineNumber()); documentSizeProvider.handleSpacerHeightChanged(spacer, oldHeight); } }