/* * Copyright 2000-2015 JetBrains s.r.o. * * 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.intellij.diff.tools.util; import com.intellij.diff.util.Side; import com.intellij.diff.util.ThreeSide; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.editor.ScrollingModel; import com.intellij.openapi.editor.event.VisibleAreaEvent; import com.intellij.openapi.editor.event.VisibleAreaListener; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.impl.FoldingModelImpl; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.CalledInAwt; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.util.List; import static com.intellij.util.ArrayUtil.toObjectArray; public class SyncScrollSupport { public interface SyncScrollable { @CalledInAwt boolean isSyncScrollEnabled(); @CalledInAwt int transfer(@NotNull Side baseSide, int line); } public interface Support { void enterDisableScrollSection(); void exitDisableScrollSection(); } public static class TwosideSyncScrollSupport extends SyncScrollSupportBase { @NotNull private final List<? extends Editor> myEditors; @NotNull private final SyncScrollable myScrollable; @NotNull private final ScrollHelper myHelper1; @NotNull private final ScrollHelper myHelper2; public TwosideSyncScrollSupport(@NotNull List<? extends Editor> editors, @NotNull SyncScrollable scrollable) { myEditors = editors; myScrollable = scrollable; myHelper1 = create(Side.LEFT); myHelper2 = create(Side.RIGHT); } @Override @NotNull protected List<? extends Editor> getEditors() { return myEditors; } @Override @NotNull protected List<? extends ScrollHelper> getScrollHelpers() { return ContainerUtil.list(myHelper1, myHelper2); } @NotNull public SyncScrollable getScrollable() { return myScrollable; } public void visibleAreaChanged(VisibleAreaEvent e) { if (!myScrollable.isSyncScrollEnabled() || isDuringSyncScroll()) return; enterDisableScrollSection(); try { if (e.getEditor() == Side.LEFT.select(myEditors)) { myHelper1.visibleAreaChanged(e); } else if (e.getEditor() == Side.RIGHT.select(myEditors)) { myHelper2.visibleAreaChanged(e); } } finally { exitDisableScrollSection(); } } public void makeVisible(@NotNull Side masterSide, int startLine1, int endLine1, int startLine2, int endLine2, final boolean animate) { doMakeVisible(masterSide.getIndex(), new int[]{startLine1, startLine2}, new int[]{endLine1, endLine2}, animate); } @NotNull private ScrollHelper create(@NotNull Side side) { return ScrollHelper.create(myEditors, side.getIndex(), side.other().getIndex(), myScrollable, side); } } public static class ThreesideSyncScrollSupport extends SyncScrollSupportBase { @NotNull private final List<? extends Editor> myEditors; @NotNull private final SyncScrollable myScrollable12; @NotNull private final SyncScrollable myScrollable23; @NotNull private final ScrollHelper myHelper12; @NotNull private final ScrollHelper myHelper21; @NotNull private final ScrollHelper myHelper23; @NotNull private final ScrollHelper myHelper32; public ThreesideSyncScrollSupport(@NotNull List<? extends Editor> editors, @NotNull SyncScrollable scrollable12, @NotNull SyncScrollable scrollable23) { assert editors.size() == 3; myEditors = editors; myScrollable12 = scrollable12; myScrollable23 = scrollable23; myHelper12 = create(ThreeSide.LEFT, ThreeSide.BASE); myHelper21 = create(ThreeSide.BASE, ThreeSide.LEFT); myHelper23 = create(ThreeSide.BASE, ThreeSide.RIGHT); myHelper32 = create(ThreeSide.RIGHT, ThreeSide.BASE); } @Override @NotNull protected List<? extends Editor> getEditors() { return myEditors; } @Override @NotNull protected List<? extends ScrollHelper> getScrollHelpers() { return ContainerUtil.list(myHelper12, myHelper21, myHelper23, myHelper32); } @NotNull public SyncScrollable getScrollable12() { return myScrollable12; } @NotNull public SyncScrollable getScrollable23() { return myScrollable23; } public void visibleAreaChanged(VisibleAreaEvent e) { if (isDuringSyncScroll()) return; enterDisableScrollSection(); try { if (e.getEditor() == ThreeSide.LEFT.select(myEditors)) { if (myScrollable12.isSyncScrollEnabled()) { myHelper12.visibleAreaChanged(e); if (myScrollable23.isSyncScrollEnabled()) myHelper23.visibleAreaChanged(e); } } else if (e.getEditor() == ThreeSide.BASE.select(myEditors)) { if (myScrollable12.isSyncScrollEnabled()) myHelper21.visibleAreaChanged(e); if (myScrollable23.isSyncScrollEnabled()) myHelper23.visibleAreaChanged(e); } else if (e.getEditor() == ThreeSide.RIGHT.select(myEditors)) { if (myScrollable23.isSyncScrollEnabled()) { myHelper32.visibleAreaChanged(e); if (myScrollable12.isSyncScrollEnabled()) myHelper21.visibleAreaChanged(e); } } } finally { exitDisableScrollSection(); } } public void makeVisible(@NotNull ThreeSide masterSide, int[] startLines, int[] endLines, boolean animate) { doMakeVisible(masterSide.getIndex(), startLines, endLines, animate); } @NotNull private ScrollHelper create(@NotNull ThreeSide master, @NotNull ThreeSide slave) { assert master != slave; assert master == ThreeSide.BASE || slave == ThreeSide.BASE; boolean leftSide = master == ThreeSide.LEFT || slave == ThreeSide.LEFT; SyncScrollable scrollable = leftSide ? myScrollable12 : myScrollable23; Side side; if (leftSide) { // LEFT - BASE -> LEFT // BASE - LEFT -> RIGHT side = Side.fromLeft(master == ThreeSide.LEFT); } else { // BASE - RIGHT -> LEFT // RIGHT - BASE -> RIGHT side = Side.fromLeft(master == ThreeSide.BASE); } return ScrollHelper.create(myEditors, master.getIndex(), slave.getIndex(), scrollable, side); } } // // Impl // private abstract static class SyncScrollSupportBase implements Support { private int myDuringSyncScrollDepth = 0; public boolean isDuringSyncScroll() { return myDuringSyncScrollDepth > 0; } @Override public void enterDisableScrollSection() { myDuringSyncScrollDepth++; } @Override public void exitDisableScrollSection() { myDuringSyncScrollDepth--; assert myDuringSyncScrollDepth >= 0; } @NotNull protected abstract List<? extends Editor> getEditors(); @NotNull protected abstract List<? extends ScrollHelper> getScrollHelpers(); protected void doMakeVisible(final int masterIndex, int[] startLines, int[] endLines, final boolean animate) { final List<? extends Editor> editors = getEditors(); final List<? extends ScrollHelper> helpers = getScrollHelpers(); final int count = editors.size(); assert startLines.length == count; assert endLines.length == count; final int[] offsets = getTargetOffsets(toObjectArray(editors, Editor.class), startLines, endLines, -1); final int[] startOffsets = new int[count]; for (int i = 0; i < count; i++) { startOffsets[i] = editors.get(i).getScrollingModel().getVisibleArea().y; } final Editor masterEditor = editors.get(masterIndex); final int masterOffset = offsets[masterIndex]; final int masterStartOffset = startOffsets[masterIndex]; for (ScrollHelper helper : helpers) { helper.setAnchor(startOffsets[helper.getMasterIndex()], offsets[helper.getMasterIndex()], startOffsets[helper.getSlaveIndex()], offsets[helper.getSlaveIndex()]); } doScrollHorizontally(masterEditor, 0, false); // animation will be canceled by "scroll vertically" anyway doScrollVertically(masterEditor, masterOffset, animate); masterEditor.getScrollingModel().runActionOnScrollingFinished(() -> { for (ScrollHelper helper : helpers) { helper.removeAnchor(); } int masterFinalOffset = masterEditor.getScrollingModel().getVisibleArea().y; boolean animateSlaves = animate && masterFinalOffset == masterStartOffset; for (int i = 0; i < count; i++) { if (i == masterIndex) continue; Editor editor = editors.get(i); int finalOffset = editor.getScrollingModel().getVisibleArea().y; if (finalOffset != offsets[i]) { enterDisableScrollSection(); doScrollVertically(editor, offsets[i], animateSlaves); editor.getScrollingModel().runActionOnScrollingFinished(this::exitDisableScrollSection); } } }); } } private static abstract class ScrollHelper implements VisibleAreaListener { @NotNull private final List<? extends Editor> myEditors; private final int myMasterIndex; private final int mySlaveIndex; @Nullable private Anchor myAnchor; public ScrollHelper(@NotNull List<? extends Editor> editors, int masterIndex, int slaveIndex) { myEditors = editors; myMasterIndex = masterIndex; mySlaveIndex = slaveIndex; } @NotNull public static ScrollHelper create(@NotNull List<? extends Editor> editors, int masterIndex, int slaveIndex, @NotNull final SyncScrollable scrollable, @NotNull final Side side) { return new ScrollHelper(editors, masterIndex, slaveIndex) { @Override protected int convertLine(int value) { return scrollable.transfer(side, value); } }; } protected abstract int convertLine(int value); public void setAnchor(int masterStartOffset, int masterEndOffset, int slaveStartOffset, int slaveEndOffset) { myAnchor = new Anchor(masterStartOffset, masterEndOffset, slaveStartOffset, slaveEndOffset); } public void removeAnchor() { myAnchor = null; } @Override public void visibleAreaChanged(VisibleAreaEvent e) { if (((FoldingModelImpl)getSlave().getFoldingModel()).isInBatchFoldingOperation()) return; if (getMaster().isDisposed() || getSlave().isDisposed()) return; Rectangle newRectangle = e.getNewRectangle(); Rectangle oldRectangle = e.getOldRectangle(); if (oldRectangle == null) return; if (newRectangle.x != oldRectangle.x) syncHorizontalScroll(false); if (newRectangle.y != oldRectangle.y) syncVerticalScroll(false); } public int getMasterIndex() { return myMasterIndex; } public int getSlaveIndex() { return mySlaveIndex; } @NotNull public Editor getMaster() { return myEditors.get(myMasterIndex); } @NotNull public Editor getSlave() { return myEditors.get(mySlaveIndex); } private void syncVerticalScroll(boolean animated) { if (getMaster().getDocument().getTextLength() == 0) return; Rectangle viewRect = getMaster().getScrollingModel().getVisibleArea(); int middleY = viewRect.height / 3; int offset; if (myAnchor == null) { LogicalPosition masterPos = getMaster().xyToLogicalPosition(new Point(viewRect.x, viewRect.y + middleY)); int masterCenterLine = masterPos.line; int convertedCenterLine = convertLine(masterCenterLine); Point point = getSlave().logicalPositionToXY(new LogicalPosition(convertedCenterLine, masterPos.column)); int correction = (viewRect.y + middleY) % getMaster().getLineHeight(); offset = point.y - middleY + correction; } else { double progress = myAnchor.masterStartOffset == myAnchor.masterEndOffset || viewRect.y == myAnchor.masterEndOffset ? 1 : ((double)(viewRect.y - myAnchor.masterStartOffset)) / (myAnchor.masterEndOffset - myAnchor.masterStartOffset); offset = myAnchor.slaveStartOffset + (int)((myAnchor.slaveEndOffset - myAnchor.slaveStartOffset) * progress); } int deltaHeaderOffset = getHeaderOffset(getSlave()) - getHeaderOffset(getMaster()); doScrollVertically(getSlave(), offset + deltaHeaderOffset, animated); } private void syncHorizontalScroll(boolean animated) { int offset = getMaster().getScrollingModel().getVisibleArea().x; doScrollHorizontally(getSlave(), offset, animated); } } private static void doScrollVertically(@NotNull Editor editor, int offset, boolean animated) { ScrollingModel model = editor.getScrollingModel(); if (!animated) model.disableAnimation(); model.scrollVertically(offset); if (!animated) model.enableAnimation(); } private static void doScrollHorizontally(@NotNull Editor editor, int offset, boolean animated) { ScrollingModel model = editor.getScrollingModel(); if (!animated) model.disableAnimation(); model.scrollHorizontally(offset); if (!animated) model.enableAnimation(); } private static int getHeaderOffset(@NotNull final Editor editor) { final JComponent header = editor.getHeaderComponent(); return header == null ? 0 : header.getHeight(); } @NotNull public static int[] getTargetOffsets(@NotNull Editor editor1, @NotNull Editor editor2, int startLine1, int endLine1, int startLine2, int endLine2, int preferredTopShift) { return getTargetOffsets(new Editor[]{editor1, editor2}, new int[]{startLine1, startLine2}, new int[]{endLine1, endLine2}, preferredTopShift); } @NotNull private static int[] getTargetOffsets(@NotNull Editor[] editors, int[] startLines, int[] endLines, int preferredTopShift) { int count = editors.length; assert startLines.length == count; assert endLines.length == count; int[] topOffsets = new int[count]; int[] bottomOffsets = new int[count]; int[] rangeHeights = new int[count]; int[] gapLines = new int[count]; int[] editorHeights = new int[count]; int[] maximumOffsets = new int[count]; int[] topShifts = new int[count]; for (int i = 0; i < count; i++) { topOffsets[i] = editors[i].logicalPositionToXY(new LogicalPosition(startLines[i], 0)).y; bottomOffsets[i] = editors[i].logicalPositionToXY(new LogicalPosition(endLines[i] + 1, 0)).y; rangeHeights[i] = bottomOffsets[i] - topOffsets[i]; gapLines[i] = 2 * editors[i].getLineHeight(); editorHeights[i] = editors[i].getScrollingModel().getVisibleArea().height; maximumOffsets[i] = ((EditorEx)editors[i]).getScrollPane().getVerticalScrollBar().getMaximum() - editorHeights[i]; // 'shift' here - distance between editor's top and first line of range // make whole range visible. If possible, locate it at 'center' (1/3 of height) (or at 'preferredTopShift' if it was specified) // If can't show whole range - show as much as we can boolean canShow = 2 * gapLines[i] + rangeHeights[i] <= editorHeights[i]; int shift = preferredTopShift != -1 ? preferredTopShift : editorHeights[i] / 3; topShifts[i] = canShow ? Math.min(editorHeights[i] - gapLines[i] - rangeHeights[i], shift) : gapLines[i]; } int topShift = ArrayUtil.min(topShifts); // check if we're at the top of file topShift = Math.min(topShift, ArrayUtil.min(topOffsets)); int[] offsets = new int[count]; boolean haveEnoughSpace = true; for (int i = 0; i < count; i++) { offsets[i] = topOffsets[i] - topShift; haveEnoughSpace &= maximumOffsets[i] > offsets[i]; } if (haveEnoughSpace) return offsets; // One of the ranges is at end of file - we can't scroll where we want to. topShift = 0; for (int i = 0; i < count; i++) { topShift = Math.max(topOffsets[i] - maximumOffsets[i], topShift); } for (int i = 0; i < count; i++) { // Try to show as much of range as we can (even if it breaks alignment) offsets[i] = topOffsets[i] - topShift + Math.max(topShift + rangeHeights[i] + gapLines[i] - editorHeights[i], 0); // always show top of the range offsets[i] = Math.min(offsets[i], topOffsets[i] - gapLines[i]); } return offsets; } private static class Anchor { public final int masterStartOffset; public final int masterEndOffset; public final int slaveStartOffset; public final int slaveEndOffset; public Anchor(int masterStartOffset, int masterEndOffset, int slaveStartOffset, int slaveEndOffset) { this.masterStartOffset = masterStartOffset; this.masterEndOffset = masterEndOffset; this.slaveStartOffset = slaveStartOffset; this.slaveEndOffset = slaveEndOffset; } } }