2 * Created by IntelliJ IDEA.
6 * To change template for new class use
7 * Code Style | Class Templates options (Tools | IDE Options).
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
;
24 import javax
.swing
.event
.ChangeEvent
;
25 import javax
.swing
.event
.ChangeListener
;
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
) {
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
);
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();
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
);
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;
257 useAnimation
= editorsTracker
.wasEditorVisibleOnCommandStart(myEditor
);
260 cancelAnimatedScrolling(false);
263 //System.out.println("scrollToAnimated: " + endVOffset);
265 int startHOffset
= getHorizontalScrollOffset();
266 int startVOffset
= getVerticalScrollOffset();
268 if (startHOffset
== hOffset
&& startVOffset
== vOffset
) {
272 //System.out.println("startVOffset = " + startVOffset);
275 myCurrentAnimationRequest
= new AnimatedScrollingRunnable(startHOffset
, startVOffset
, hOffset
, vOffset
);
277 catch (NoAnimationRequiredException e
) {
278 _scrollHorizontally(hOffset
);
279 _scrollVertically(vOffset
);
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);
302 private AnimatedScrollingRunnable
cancelAnimatedScrolling(boolean scrollToTarget
) {
303 AnimatedScrollingRunnable request
= myCurrentAnimationRequest
;
304 myCurrentAnimationRequest
= null;
305 if (request
!= null) {
306 request
.cancel(scrollToTarget
);
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
,
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
) {
379 myTimer
.setRepeats(true);
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
) {
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();
424 if (myCurrentAnimationRequest
== this) {
425 myCurrentAnimationRequest
= null;
429 private void executePostRunnables() {
430 for (Runnable runnable
: myPostRunnables
) {
435 private double timeToFraction(double time
) {
437 return 1 - timeToFraction(1 - time
);
440 double fraction
= Math
.pow(time
* 2, myPow
) / 2;
442 if (myTotalDist
> myMaxDistToScroll
) {
443 fraction
*= (double)myMaxDistToScroll
/ myTotalDist
;
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;
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);
466 private static class NoAnimationRequiredException
extends Exception
{