// Copyright (C) 2013 The Android Open Source Project // // 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.gerrit.client.diff; import com.google.gwt.user.client.Timer; import net.codemirror.lib.CodeMirror; import net.codemirror.lib.ScrollInfo; class ScrollSynchronizer { private DiffTable diffTable; private LineMapper mapper; private OverviewBar overview; private ScrollCallback active; private ScrollCallback callbackA; private ScrollCallback callbackB; ScrollSynchronizer(DiffTable diffTable, CodeMirror cmA, CodeMirror cmB, LineMapper mapper) { this.diffTable = diffTable; this.mapper = mapper; this.overview = diffTable.overview; callbackA = new ScrollCallback(cmA, cmB, DisplaySide.A); callbackB = new ScrollCallback(cmB, cmA, DisplaySide.B); cmA.on("scroll", callbackA); cmB.on("scroll", callbackB); } void syncScroll(DisplaySide masterSide) { (masterSide == DisplaySide.A ? callbackA : callbackB).sync(); } private void updateScreenHeader(ScrollInfo si) { if (si.getTop() == 0 && !diffTable.isHeaderVisible()) { diffTable.setHeaderVisible(true); } else if (si.getTop() > 0.5 * si.getClientHeight() && diffTable.isHeaderVisible()) { diffTable.setHeaderVisible(false); } } class ScrollCallback implements Runnable { private final CodeMirror src; private final CodeMirror dst; private final DisplaySide srcSide; private final Timer fixup; private int state; ScrollCallback(CodeMirror src, CodeMirror dst, DisplaySide srcSide) { this.src = src; this.dst = dst; this.srcSide = srcSide; this.fixup = new Timer() { @Override public void run() { if (active == ScrollCallback.this) { fixup(); } } }; } void sync() { dst.scrollToY(align(src.getScrollInfo().getTop())); } @Override public void run() { if (active == null) { active = this; fixup.scheduleRepeating(20); } if (active == this) { ScrollInfo si = src.getScrollInfo(); updateScreenHeader(si); overview.update(si); dst.scrollTo(si.getLeft(), align(si.getTop())); state = 0; } } private void fixup() { switch (state) { case 0: state = 1; dst.scrollToY(align(src.getScrollInfo().getTop())); break; case 1: state = 2; break; case 2: active = null; fixup.cancel(); break; } } private double align(double srcTop) { // Since CM doesn't always take the height of line widgets into // account when calculating scrollInfo when scrolling too fast (e.g. // throw scrolling), simply setting scrollTop to be the same doesn't // guarantee alignment. int line = src.lineAtHeight(srcTop, "local"); if (line == 0) { // Padding for insert at start of file occurs above line 0, // and CM3 doesn't always compute heightAtLine correctly. return srcTop; } // Find a pair of lines that are aligned and near the top of // the viewport. Use that distance to correct the Y coordinate. LineMapper.AlignedPair p = mapper.align(srcSide, line); double sy = src.heightAtLine(p.src, "local"); double dy = dst.heightAtLine(p.dst, "local"); return Math.max(0, dy + (srcTop - sy)); } } }