optimization: replace all occurrence is dramatically faster
[fedora-idea.git] / platform-impl / src / com / intellij / openapi / editor / impl / ScrollingModelImpl.java
blob703a33f8e2880642bfd58f83778b6508ad3ca50f
1 /*
2 * Created by IntelliJ IDEA.
3 * User: max
4 * Date: Jun 10, 2002
5 * Time: 10:14:59 PM
6 * To change template for new class use
7 * Code Style | Class Templates options (Tools | IDE Options).
8 */
9 package com.intellij.openapi.editor.impl;
11 import com.intellij.openapi.application.ex.ApplicationManagerEx;
12 import com.intellij.openapi.command.CommandProcessor;
13 import com.intellij.openapi.diagnostic.Logger;
14 import com.intellij.openapi.editor.LogicalPosition;
15 import com.intellij.openapi.editor.ScrollType;
16 import com.intellij.openapi.editor.ScrollingModel;
17 import com.intellij.openapi.editor.event.DocumentAdapter;
18 import com.intellij.openapi.editor.event.DocumentEvent;
19 import com.intellij.openapi.editor.event.VisibleAreaEvent;
20 import com.intellij.openapi.editor.event.VisibleAreaListener;
21 import org.jetbrains.annotations.Nullable;
23 import javax.swing.*;
24 import javax.swing.event.ChangeEvent;
25 import javax.swing.event.ChangeListener;
26 import java.awt.*;
27 import java.awt.event.ActionEvent;
28 import java.awt.event.ActionListener;
29 import java.util.ArrayList;
30 import java.util.concurrent.CopyOnWriteArrayList;
32 public class ScrollingModelImpl implements ScrollingModel {
33 private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.ScrollingModelImpl");
35 private final EditorImpl myEditor;
36 private final CopyOnWriteArrayList<VisibleAreaListener> myVisibleAreaListeners = new CopyOnWriteArrayList<VisibleAreaListener>();
38 private AnimatedScrollingRunnable myCurrentAnimationRequest = null;
39 private boolean myAnimationDisabled = false;
40 private final DocumentAdapter myDocumentListener;
42 public ScrollingModelImpl(EditorImpl editor) {
43 myEditor = editor;
45 myEditor.getScrollPane().getViewport().addChangeListener(new ChangeListener() {
46 private Rectangle myLastViewRect;
48 public void stateChanged(ChangeEvent event) {
49 Rectangle viewRect = getVisibleArea();
50 VisibleAreaEvent visibleAreaEvent = new VisibleAreaEvent(myEditor, myLastViewRect, viewRect);
51 myLastViewRect = viewRect;
52 for (VisibleAreaListener listener : myVisibleAreaListeners) {
53 listener.visibleAreaChanged(visibleAreaEvent);
56 });
58 myDocumentListener = new DocumentAdapter() {
59 public void beforeDocumentChange(DocumentEvent e) {
60 cancelAnimatedScrolling(true);
63 myEditor.getDocument().addDocumentListener(myDocumentListener);
66 public Rectangle getVisibleArea() {
67 assertIsDispatchThread();
68 if (myEditor.getScrollPane() == null) {
69 return new Rectangle(0, 0, 0, 0);
71 return myEditor.getScrollPane().getViewport().getViewRect();
74 public Rectangle getVisibleAreaOnScrollingFinished() {
75 assertIsDispatchThread();
76 if (myCurrentAnimationRequest != null) {
77 return myCurrentAnimationRequest.getTargetVisibleArea();
79 else {
80 return getVisibleArea();
84 public void scrollToCaret(ScrollType scrollType) {
85 assertIsDispatchThread();
86 LogicalPosition caretPosition = myEditor.getCaretModel().getLogicalPosition();
87 myEditor.validateSize();
88 scrollTo(caretPosition, scrollType);
91 public void scrollTo(LogicalPosition pos, ScrollType scrollType) {
92 assertIsDispatchThread();
93 if (myEditor.getScrollPane() == null) return;
95 AnimatedScrollingRunnable canceledThread = cancelAnimatedScrolling(false);
96 Rectangle viewRect = canceledThread != null ? canceledThread.getTargetVisibleArea() : getVisibleArea();
98 Point p = calcOffsetsToScroll(pos, scrollType, viewRect);
99 scrollToOffsets(p.x, p.y);
102 private void assertIsDispatchThread() {
103 ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(myEditor.getComponent());
106 public void runActionOnScrollingFinished(Runnable action) {
107 assertIsDispatchThread();
109 if (myCurrentAnimationRequest != null) {
110 myCurrentAnimationRequest.addPostRunnable(action);
111 return;
114 action.run();
117 public void disableAnimation() {
118 myAnimationDisabled = true;
121 public void enableAnimation() {
122 myAnimationDisabled = false;
125 private Point calcOffsetsToScroll(LogicalPosition pos, ScrollType scrollType, Rectangle viewRect) {
126 Point targetLocation = myEditor.logicalPositionToXY(pos);
128 if (myEditor.getSettings().isRefrainFromScrolling() && viewRect.contains(targetLocation)) {
129 if (scrollType == ScrollType.CENTER ||
130 scrollType == ScrollType.CENTER_DOWN ||
131 scrollType == ScrollType.CENTER_UP) {
132 scrollType = ScrollType.RELATIVE;
136 int spaceWidth = myEditor.getSpaceWidth(Font.PLAIN);
137 int xInsets = myEditor.getSettings().getAdditionalColumnsCount() * spaceWidth;
139 int hOffset = scrollType == ScrollType.CENTER ||
140 scrollType == ScrollType.CENTER_DOWN ||
141 scrollType == ScrollType.CENTER_UP ? 0 : viewRect.x;
142 if (targetLocation.x < hOffset) {
143 hOffset = targetLocation.x - 4 * spaceWidth;
144 hOffset = hOffset > 0 ? hOffset : 0;
146 else if (targetLocation.x > viewRect.x + viewRect.width - xInsets) {
147 hOffset = targetLocation.x - viewRect.width + xInsets;
150 int scrollUpBy = viewRect.y + myEditor.getLineHeight() - targetLocation.y;
151 int scrollDownBy = targetLocation.y - (viewRect.y + viewRect.height - 2 * myEditor.getLineHeight());
152 int centerPosition = targetLocation.y - viewRect.height / 3;
154 int vOffset = viewRect.y;
155 if (scrollType == ScrollType.CENTER) {
156 vOffset = centerPosition;
158 else if (scrollType == ScrollType.CENTER_UP) {
159 if (scrollUpBy > 0 || scrollDownBy > 0 || vOffset > centerPosition) {
160 vOffset = centerPosition;
163 else if (scrollType == ScrollType.CENTER_DOWN) {
164 if (scrollUpBy > 0 || scrollDownBy > 0 || vOffset < centerPosition) {
165 vOffset = centerPosition;
168 else if (scrollType == ScrollType.RELATIVE) {
169 if (scrollUpBy > 0) {
170 vOffset = viewRect.y - scrollUpBy;
172 else if (scrollDownBy > 0) {
173 vOffset = viewRect.y + scrollDownBy;
176 else if (scrollType == ScrollType.MAKE_VISIBLE) {
177 if (scrollUpBy > 0 || scrollDownBy > 0) {
178 vOffset = centerPosition;
182 JScrollPane scrollPane = myEditor.getScrollPane();
183 hOffset = Math.max(0, hOffset);
184 vOffset = Math.max(0, vOffset);
185 hOffset = Math.min(scrollPane.getHorizontalScrollBar().getMaximum(), hOffset);
186 vOffset = Math.min(scrollPane.getVerticalScrollBar().getMaximum(), vOffset);
188 return new Point(hOffset, vOffset);
191 public int getVerticalScrollOffset() {
192 assertIsDispatchThread();
193 if (myEditor.getScrollPane() == null) return 0;
195 JScrollBar scrollbar = myEditor.getScrollPane().getVerticalScrollBar();
196 return scrollbar.getValue();
199 public int getHorizontalScrollOffset() {
200 assertIsDispatchThread();
201 if (myEditor.getScrollPane() == null) return 0;
203 JScrollBar scrollbar = myEditor.getScrollPane().getHorizontalScrollBar();
204 return scrollbar.getValue();
207 public void scrollVertically(int scrollOffset) {
208 scrollToOffsets(getHorizontalScrollOffset(), scrollOffset);
211 private void _scrollVertically(int scrollOffset) {
212 assertIsDispatchThread();
213 if (myEditor.getScrollPane() == null) return;
215 myEditor.validateSize();
216 JScrollBar scrollbar = myEditor.getScrollPane().getVerticalScrollBar();
218 if (scrollbar.getVisibleAmount() < Math.abs(scrollOffset - scrollbar.getValue()) + 50) {
219 myEditor.stopOptimizedScrolling();
222 scrollbar.setValue(scrollOffset);
224 //System.out.println("scrolled vertically to: " + scrollOffset);
227 public void scrollHorizontally(int scrollOffset) {
228 scrollToOffsets(scrollOffset, getVerticalScrollOffset());
231 private void _scrollHorizontally(int scrollOffset) {
232 assertIsDispatchThread();
233 if (myEditor.getScrollPane() == null) return;
235 myEditor.validateSize();
236 JScrollBar scrollbar = myEditor.getScrollPane().getHorizontalScrollBar();
237 scrollbar.setValue(scrollOffset);
240 private void scrollToOffsets(int hOffset, int vOffset) {
241 cancelAnimatedScrolling(false);
243 VisibleEditorsTracker editorsTracker = VisibleEditorsTracker.getInstance();
244 boolean useAnimation;
245 //System.out.println("myCurrentCommandStart - myLastCommandFinish = " + (myCurrentCommandStart - myLastCommandFinish));
246 if (!myEditor.getSettings().isAnimatedScrolling() || myAnimationDisabled) {
247 useAnimation = false;
249 else if (CommandProcessor.getInstance().getCurrentCommand() == null) {
250 useAnimation = myEditor.getComponent().isShowing();
252 else if (editorsTracker.getCurrentCommandStart() - editorsTracker.getLastCommandFinish() <
253 AnimatedScrollingRunnable.SCROLL_DURATION) {
254 useAnimation = false;
256 else {
257 useAnimation = editorsTracker.wasEditorVisibleOnCommandStart(myEditor);
260 cancelAnimatedScrolling(false);
262 if (useAnimation) {
263 //System.out.println("scrollToAnimated: " + endVOffset);
265 int startHOffset = getHorizontalScrollOffset();
266 int startVOffset = getVerticalScrollOffset();
268 if (startHOffset == hOffset && startVOffset == vOffset) {
269 return;
272 //System.out.println("startVOffset = " + startVOffset);
274 try {
275 myCurrentAnimationRequest = new AnimatedScrollingRunnable(startHOffset, startVOffset, hOffset, vOffset);
277 catch (NoAnimationRequiredException e) {
278 _scrollHorizontally(hOffset);
279 _scrollVertically(vOffset);
282 else {
283 _scrollHorizontally(hOffset);
284 _scrollVertically(vOffset);
288 public void addVisibleAreaListener(VisibleAreaListener listener) {
289 myVisibleAreaListeners.add(listener);
292 public void removeVisibleAreaListener(VisibleAreaListener listener) {
293 boolean success = myVisibleAreaListeners.remove(listener);
294 LOG.assertTrue(success);
297 public void commandStarted() {
298 cancelAnimatedScrolling(true);
301 @Nullable
302 private AnimatedScrollingRunnable cancelAnimatedScrolling(boolean scrollToTarget) {
303 AnimatedScrollingRunnable request = myCurrentAnimationRequest;
304 myCurrentAnimationRequest = null;
305 if (request != null) {
306 request.cancel(scrollToTarget);
308 return request;
311 public void dispose() {
312 myEditor.getDocument().removeDocumentListener(myDocumentListener);
315 public void beforeModalityStateChanged() {
316 cancelAnimatedScrolling(true);
319 private class AnimatedScrollingRunnable {
320 private static final int SCROLL_DURATION = 150;
321 private static final int SCROLL_INTERVAL = 10;
323 private final Timer myTimer;
324 private int myTicksCount = 0;
326 private final int myStartHOffset;
327 private final int myStartVOffset;
328 private final int myEndHOffset;
329 private final int myEndVOffset;
330 private final int myAnimationDuration;
332 private final ArrayList<Runnable> myPostRunnables = new ArrayList<Runnable>();
334 private final Runnable myStartCommand;
335 private final int myHDist;
336 private final int myVDist;
337 private final int myMaxDistToScroll;
338 private final double myTotalDist;
339 private final double myScrollDist;
341 private final int myStepCount;
342 private final double myPow;
344 public AnimatedScrollingRunnable(int startHOffset,
345 int startVOffset,
346 int endHOffset,
347 int endVOffset) throws NoAnimationRequiredException {
348 myStartHOffset = startHOffset;
349 myStartVOffset = startVOffset;
350 myEndHOffset = endHOffset;
351 myEndVOffset = endVOffset;
353 myHDist = Math.abs(myEndHOffset - myStartHOffset);
354 myVDist = Math.abs(myEndVOffset - myStartVOffset);
356 myMaxDistToScroll = myEditor.getLineHeight() * 50;
357 myTotalDist = Math.sqrt((double)myHDist * myHDist + (double)myVDist * myVDist);
358 myScrollDist = Math.min(myTotalDist, myMaxDistToScroll);
359 myAnimationDuration = calcAnimationDuration();
360 if (myAnimationDuration < SCROLL_INTERVAL * 2) {
361 throw new NoAnimationRequiredException();
363 myStepCount = myAnimationDuration / SCROLL_INTERVAL - 1;
364 double firstStepTime = 1.0 / myStepCount;
365 double firstScrollDist = 5.0;
366 if (myTotalDist > myScrollDist) {
367 firstScrollDist *= myTotalDist / myScrollDist;
368 firstScrollDist = Math.min(firstScrollDist, myEditor.getLineHeight() * 5);
370 myPow = myScrollDist > 0 ? setupPow(firstStepTime, firstScrollDist / myScrollDist) : 1;
372 myStartCommand = CommandProcessor.getInstance().getCurrentCommand();
374 myTimer = new Timer(SCROLL_INTERVAL, new ActionListener() {
375 public void actionPerformed(final ActionEvent e) {
376 tick();
379 myTimer.setRepeats(true);
380 myTimer.start();
383 public Rectangle getTargetVisibleArea() {
384 Rectangle viewRect = getVisibleArea();
385 return new Rectangle(myEndHOffset, myEndVOffset, viewRect.width, viewRect.height);
388 public Runnable getStartCommand() {
389 return myStartCommand;
392 private void tick() {
393 double time = (myTicksCount + 1) / (double)myStepCount;
394 double fraction = timeToFraction(time);
396 final int hOffset = (int)(myStartHOffset + (myEndHOffset - myStartHOffset) * fraction + 0.5);
397 final int vOffset = (int)(myStartVOffset + (myEndVOffset - myStartVOffset) * fraction + 0.5);
399 _scrollHorizontally(hOffset);
400 _scrollVertically(vOffset);
402 if (myTicksCount++ == myStepCount) {
403 finish(true);
407 public void cancel(boolean scrollToTarget) {
408 assertIsDispatchThread();
409 finish(scrollToTarget);
412 public void addPostRunnable(Runnable runnable) {
413 myPostRunnables.add(runnable);
416 private void finish(boolean scrollToTarget) {
417 if (scrollToTarget || !myPostRunnables.isEmpty()) {
418 _scrollHorizontally(myEndHOffset);
419 _scrollVertically(myEndVOffset);
420 executePostRunnables();
423 myTimer.stop();
424 if (myCurrentAnimationRequest == this) {
425 myCurrentAnimationRequest = null;
429 private void executePostRunnables() {
430 for (Runnable runnable : myPostRunnables) {
431 runnable.run();
435 private double timeToFraction(double time) {
436 if (time > 0.5) {
437 return 1 - timeToFraction(1 - time);
440 double fraction = Math.pow(time * 2, myPow) / 2;
442 if (myTotalDist > myMaxDistToScroll) {
443 fraction *= (double)myMaxDistToScroll / myTotalDist;
446 return fraction;
449 private double setupPow(double inTime, double moveBy) {
450 double pow = Math.log(2 * moveBy) / Math.log(2 * inTime);
451 if (pow < 1) pow = 1;
452 return pow;
455 private int calcAnimationDuration() {
456 int lineHeight = myEditor.getLineHeight();
457 double lineDist = myTotalDist / lineHeight;
458 double part = (lineDist - 1) / 10;
459 if (part > 1) part = 1;
460 int duration = (int)(part * SCROLL_DURATION);
461 //System.out.println("duration = " + duration);
462 return duration;
466 private static class NoAnimationRequiredException extends Exception {