blob: 2942bafcc7d5f89d70aacef7ffd9581f88361d92 [file] [log] [blame]
/*
* Copyright 2000-2014 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.
*/
/*
* Created by IntelliJ IDEA.
* User: max
* Date: Jun 10, 2002
* Time: 10:14:59 PM
* To change template for new class use
* Code Style | Class Templates options (Tools | IDE Options).
*/
package com.intellij.openapi.editor.impl;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.VisibleAreaEvent;
import com.intellij.openapi.editor.event.VisibleAreaListener;
import com.intellij.openapi.editor.ex.ScrollingModelEx;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.util.Disposer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.Animator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.util.*;
import java.util.List;
public class ScrollingModelImpl implements ScrollingModelEx {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.ScrollingModelImpl");
private final EditorImpl myEditor;
private final List<VisibleAreaListener> myVisibleAreaListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private AnimatedScrollingRunnable myCurrentAnimationRequest = null;
private boolean myAnimationDisabled = false;
private final DocumentAdapter myDocumentListener;
private int myAccumulatedXOffset = -1;
private int myAccumulatedYOffset = -1;
private boolean myAccumulateViewportChanges;
private boolean myViewportPositioned;
public ScrollingModelImpl(EditorImpl editor) {
myEditor = editor;
myEditor.getScrollPane().getViewport().addChangeListener(new ChangeListener() {
private Rectangle myLastViewRect;
@Override
public void stateChanged(ChangeEvent event) {
Rectangle viewRect = getVisibleArea();
VisibleAreaEvent visibleAreaEvent = new VisibleAreaEvent(myEditor, myLastViewRect, viewRect);
if (!myViewportPositioned && viewRect.height > 0) {
myViewportPositioned = true;
if (adjustVerticalOffsetIfNecessary()) {
return;
}
}
myLastViewRect = viewRect;
for (VisibleAreaListener listener : myVisibleAreaListeners) {
listener.visibleAreaChanged(visibleAreaEvent);
}
}
});
myDocumentListener = new DocumentAdapter() {
@Override
public void beforeDocumentChange(DocumentEvent e) {
cancelAnimatedScrolling(true);
}
};
myEditor.getDocument().addDocumentListener(myDocumentListener);
}
/**
* Corrects viewport position if necessary on initial editor showing.
*
* @return <code>true</code> if the vertical viewport position has been adjusted; <code>false</code> otherwise
*/
private boolean adjustVerticalOffsetIfNecessary() {
// There is a possible case that the editor is configured to show virtual space at file bottom and requested position is located
// somewhere around. We don't want to position viewport in a way that most of its area is used to represent that virtual empty space.
// So, we tweak vertical offset if necessary.
int maxY = Math.max(myEditor.getLineHeight(), myEditor.getDocument().getLineCount() * myEditor.getLineHeight());
int minPreferredY = maxY - getVisibleArea().height * 2 / 3;
final int currentOffset = getVerticalScrollOffset();
int offsetToUse = Math.min(minPreferredY, currentOffset);
if (offsetToUse != currentOffset) {
scrollToOffsets(getHorizontalScrollOffset(), offsetToUse);
return true;
}
return false;
}
@NotNull
@Override
public Rectangle getVisibleArea() {
assertIsDispatchThread();
if (myEditor.getScrollPane() == null) {
return new Rectangle(0, 0, 0, 0);
}
return myEditor.getScrollPane().getViewport().getViewRect();
}
@NotNull
@Override
public Rectangle getVisibleAreaOnScrollingFinished() {
assertIsDispatchThread();
if (myCurrentAnimationRequest != null) {
return myCurrentAnimationRequest.getTargetVisibleArea();
}
return getVisibleArea();
}
@Override
public void scrollToCaret(@NotNull ScrollType scrollType) {
assertIsDispatchThread();
LogicalPosition caretPosition = myEditor.getCaretModel().getLogicalPosition();
myEditor.validateSize();
scrollTo(caretPosition, scrollType);
}
@Override
public void scrollTo(@NotNull LogicalPosition pos, @NotNull ScrollType scrollType) {
assertIsDispatchThread();
if (myEditor.getScrollPane() == null) return;
AnimatedScrollingRunnable canceledThread = cancelAnimatedScrolling(false);
Rectangle viewRect = canceledThread != null ? canceledThread.getTargetVisibleArea() : getVisibleArea();
Point p = calcOffsetsToScroll(pos, scrollType, viewRect);
scrollToOffsets(p.x, p.y);
}
private static void assertIsDispatchThread() {
ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
}
@Override
public void runActionOnScrollingFinished(@NotNull Runnable action) {
assertIsDispatchThread();
if (myCurrentAnimationRequest != null) {
myCurrentAnimationRequest.addPostRunnable(action);
return;
}
action.run();
}
@Override
public void disableAnimation() {
myAnimationDisabled = true;
}
@Override
public void enableAnimation() {
myAnimationDisabled = false;
}
private Point calcOffsetsToScroll(LogicalPosition pos, ScrollType scrollType, Rectangle viewRect) {
Point targetLocation = myEditor.logicalPositionToXY(pos);
if (myEditor.getSettings().isRefrainFromScrolling() && viewRect.contains(targetLocation)) {
if (scrollType == ScrollType.CENTER ||
scrollType == ScrollType.CENTER_DOWN ||
scrollType == ScrollType.CENTER_UP) {
scrollType = ScrollType.RELATIVE;
}
}
int spaceWidth = EditorUtil.getSpaceWidth(Font.PLAIN, myEditor);
int xInsets = myEditor.getSettings().getAdditionalColumnsCount() * spaceWidth;
int hOffset = scrollType == ScrollType.CENTER ||
scrollType == ScrollType.CENTER_DOWN ||
scrollType == ScrollType.CENTER_UP ? 0 : viewRect.x;
if (targetLocation.x < hOffset) {
hOffset = targetLocation.x - 4 * spaceWidth;
hOffset = hOffset > 0 ? hOffset : 0;
}
else if (targetLocation.x >= hOffset + viewRect.width) {
hOffset = targetLocation.x - viewRect.width + xInsets;
}
// the following code tries to keeps 1 line above and 1 line below if available in viewRect
int lineHeight = myEditor.getLineHeight();
int scrollUpBy = viewRect.y - targetLocation.y + (viewRect.height > lineHeight ? lineHeight : 0);
int scrollDownBy = targetLocation.y - viewRect.y - Math.max(0, viewRect.height - 2 * lineHeight);
int centerPosition = targetLocation.y - viewRect.height / 3;
int vOffset = viewRect.y;
if (scrollType == ScrollType.CENTER) {
vOffset = centerPosition;
}
else if (scrollType == ScrollType.CENTER_UP) {
if (scrollUpBy > 0 || scrollDownBy > 0 || vOffset > centerPosition) {
vOffset = centerPosition;
}
}
else if (scrollType == ScrollType.CENTER_DOWN) {
if (scrollUpBy > 0 || scrollDownBy > 0 || vOffset < centerPosition) {
vOffset = centerPosition;
}
}
else if (scrollType == ScrollType.RELATIVE) {
if (scrollUpBy > 0) {
vOffset = viewRect.y - scrollUpBy;
}
else if (scrollDownBy > 0) {
vOffset = viewRect.y + scrollDownBy;
}
}
else if (scrollType == ScrollType.MAKE_VISIBLE) {
if (scrollUpBy > 0 || scrollDownBy > 0) {
vOffset = centerPosition;
}
}
JScrollPane scrollPane = myEditor.getScrollPane();
hOffset = Math.max(0, hOffset);
vOffset = Math.max(0, vOffset);
hOffset = Math.min(scrollPane.getHorizontalScrollBar().getMaximum() - getExtent(scrollPane.getHorizontalScrollBar()), hOffset);
vOffset = Math.min(scrollPane.getVerticalScrollBar().getMaximum() - getExtent(scrollPane.getVerticalScrollBar()), vOffset);
return new Point(hOffset, vOffset);
}
@Nullable
public JScrollBar getVerticalScrollBar() {
assertIsDispatchThread();
JScrollPane scrollPane = myEditor.getScrollPane();
return scrollPane.getVerticalScrollBar();
}
@Nullable
public JScrollBar getHorizontalScrollBar() {
assertIsDispatchThread();
if (myEditor.getScrollPane() == null) return null;
return myEditor.getScrollPane().getHorizontalScrollBar();
}
@Override
public int getVerticalScrollOffset() {
return getOffset(getVerticalScrollBar());
}
@Override
public int getHorizontalScrollOffset() {
return getOffset(getHorizontalScrollBar());
}
private static int getOffset(JScrollBar scrollBar) {
return scrollBar == null ? 0 : scrollBar.getValue();
}
private static int getExtent(JScrollBar scrollBar) {
return scrollBar == null ? 0 : scrollBar.getModel().getExtent();
}
@Override
public void scrollVertically(int scrollOffset) {
scrollToOffsets(getHorizontalScrollOffset(), scrollOffset);
}
private void _scrollVertically(int scrollOffset) {
assertIsDispatchThread();
if (myEditor.getScrollPane() == null) return;
myEditor.validateSize();
JScrollBar scrollbar = myEditor.getScrollPane().getVerticalScrollBar();
scrollbar.setValue(scrollOffset);
}
@Override
public void scrollHorizontally(int scrollOffset) {
scrollToOffsets(scrollOffset, getVerticalScrollOffset());
}
private void _scrollHorizontally(int scrollOffset) {
assertIsDispatchThread();
if (myEditor.getScrollPane() == null) return;
myEditor.validateSize();
JScrollBar scrollbar = myEditor.getScrollPane().getHorizontalScrollBar();
scrollbar.setValue(scrollOffset);
}
void scrollToOffsets(int hOffset, int vOffset) {
if (myAccumulateViewportChanges) {
myAccumulatedXOffset = hOffset;
myAccumulatedYOffset = vOffset;
return;
}
cancelAnimatedScrolling(false);
VisibleEditorsTracker editorsTracker = VisibleEditorsTracker.getInstance();
boolean useAnimation;
//System.out.println("myCurrentCommandStart - myLastCommandFinish = " + (myCurrentCommandStart - myLastCommandFinish));
if (!myEditor.getSettings().isAnimatedScrolling() || myAnimationDisabled || UISettings.isRemoteDesktopConnected()) {
useAnimation = false;
}
else if (CommandProcessor.getInstance().getCurrentCommand() == null) {
useAnimation = myEditor.getComponent().isShowing();
}
else if (editorsTracker.getCurrentCommandStart() - editorsTracker.getLastCommandFinish() <
AnimatedScrollingRunnable.SCROLL_DURATION) {
useAnimation = false;
}
else {
useAnimation = editorsTracker.wasEditorVisibleOnCommandStart(myEditor);
}
cancelAnimatedScrolling(false);
if (useAnimation) {
//System.out.println("scrollToAnimated: " + endVOffset);
int startHOffset = getHorizontalScrollOffset();
int startVOffset = getVerticalScrollOffset();
if (startHOffset == hOffset && startVOffset == vOffset) {
return;
}
//System.out.println("startVOffset = " + startVOffset);
try {
myCurrentAnimationRequest = new AnimatedScrollingRunnable(startHOffset, startVOffset, hOffset, vOffset);
}
catch (NoAnimationRequiredException e) {
_scrollHorizontally(hOffset);
_scrollVertically(vOffset);
}
}
else {
_scrollHorizontally(hOffset);
_scrollVertically(vOffset);
}
}
@Override
public void addVisibleAreaListener(@NotNull VisibleAreaListener listener) {
myVisibleAreaListeners.add(listener);
}
@Override
public void removeVisibleAreaListener(@NotNull VisibleAreaListener listener) {
boolean success = myVisibleAreaListeners.remove(listener);
LOG.assertTrue(success);
}
public void commandStarted() {
cancelAnimatedScrolling(true);
}
@Nullable
private AnimatedScrollingRunnable cancelAnimatedScrolling(boolean scrollToTarget) {
AnimatedScrollingRunnable request = myCurrentAnimationRequest;
myCurrentAnimationRequest = null;
if (request != null) {
request.cancel(scrollToTarget);
}
return request;
}
public void dispose() {
myEditor.getDocument().removeDocumentListener(myDocumentListener);
}
public void beforeModalityStateChanged() {
cancelAnimatedScrolling(true);
}
public boolean isScrollingNow() {
return myCurrentAnimationRequest != null;
}
@Override
public void accumulateViewportChanges() {
myAccumulateViewportChanges = true;
}
@Override
public void flushViewportChanges() {
myAccumulateViewportChanges = false;
if (myAccumulatedXOffset >= 0 && myAccumulatedYOffset >= 0) {
scrollToOffsets(myAccumulatedXOffset, myAccumulatedYOffset);
myAccumulatedXOffset = myAccumulatedYOffset = -1;
cancelAnimatedScrolling(true);
}
}
private class AnimatedScrollingRunnable {
private static final int SCROLL_DURATION = 100;
private static final int SCROLL_INTERVAL = 10;
private final int myStartHOffset;
private final int myStartVOffset;
private final int myEndHOffset;
private final int myEndVOffset;
private final int myAnimationDuration;
private final ArrayList<Runnable> myPostRunnables = new ArrayList<Runnable>();
private final int myHDist;
private final int myVDist;
private final int myMaxDistToScroll;
private final double myTotalDist;
private final double myScrollDist;
private final int myStepCount;
private final double myPow;
private final Animator myAnimator;
public AnimatedScrollingRunnable(int startHOffset,
int startVOffset,
int endHOffset,
int endVOffset) throws NoAnimationRequiredException {
myStartHOffset = startHOffset;
myStartVOffset = startVOffset;
myEndHOffset = endHOffset;
myEndVOffset = endVOffset;
myHDist = Math.abs(myEndHOffset - myStartHOffset);
myVDist = Math.abs(myEndVOffset - myStartVOffset);
myMaxDistToScroll = myEditor.getLineHeight() * 50;
myTotalDist = Math.sqrt((double)myHDist * myHDist + (double)myVDist * myVDist);
myScrollDist = Math.min(myTotalDist, myMaxDistToScroll);
myAnimationDuration = calcAnimationDuration();
if (myAnimationDuration < SCROLL_INTERVAL * 2) {
throw new NoAnimationRequiredException();
}
myStepCount = myAnimationDuration / SCROLL_INTERVAL - 1;
double firstStepTime = 1.0 / myStepCount;
double firstScrollDist = 5.0;
if (myTotalDist > myScrollDist) {
firstScrollDist *= myTotalDist / myScrollDist;
firstScrollDist = Math.min(firstScrollDist, myEditor.getLineHeight() * 5);
}
myPow = myScrollDist > 0 ? setupPow(firstStepTime, firstScrollDist / myScrollDist) : 1;
myAnimator = new Animator("Animated scroller", myStepCount, SCROLL_DURATION, false, true) {
@Override
public void paintNow(int frame, int totalFrames, int cycle) {
double time = ((double)(frame + 1)) / (double)totalFrames;
double fraction = timeToFraction(time);
final int hOffset = (int)(myStartHOffset + (myEndHOffset - myStartHOffset) * fraction + 0.5);
final int vOffset = (int)(myStartVOffset + (myEndVOffset - myStartVOffset) * fraction + 0.5);
_scrollHorizontally(hOffset);
_scrollVertically(vOffset);
}
@Override
protected void paintCycleEnd() {
finish(true);
}
};
myAnimator.resume();
}
@NotNull
public Rectangle getTargetVisibleArea() {
Rectangle viewRect = getVisibleArea();
return new Rectangle(myEndHOffset, myEndVOffset, viewRect.width, viewRect.height);
}
public void cancel(boolean scrollToTarget) {
assertIsDispatchThread();
finish(scrollToTarget);
}
public void addPostRunnable(Runnable runnable) {
myPostRunnables.add(runnable);
}
private void finish(boolean scrollToTarget) {
if (scrollToTarget || !myPostRunnables.isEmpty()) {
_scrollHorizontally(myEndHOffset);
_scrollVertically(myEndVOffset);
executePostRunnables();
}
Disposer.dispose(myAnimator);
if (myCurrentAnimationRequest == this) {
myCurrentAnimationRequest = null;
}
}
private void executePostRunnables() {
for (Runnable runnable : myPostRunnables) {
runnable.run();
}
}
private double timeToFraction(double time) {
if (time > 0.5) {
return 1 - timeToFraction(1 - time);
}
double fraction = Math.pow(time * 2, myPow) / 2;
if (myTotalDist > myMaxDistToScroll) {
fraction *= (double)myMaxDistToScroll / myTotalDist;
}
return fraction;
}
private double setupPow(double inTime, double moveBy) {
double pow = Math.log(2 * moveBy) / Math.log(2 * inTime);
if (pow < 1) pow = 1;
return pow;
}
private int calcAnimationDuration() {
int lineHeight = myEditor.getLineHeight();
double lineDist = myTotalDist / lineHeight;
double part = (lineDist - 1) / 10;
if (part > 1) part = 1;
//System.out.println("duration = " + duration);
return (int)(part * SCROLL_DURATION);
}
}
private static class NoAnimationRequiredException extends Exception {
}
}