1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "WheelHandlingHelper.h"
8
9 #include "mozilla/EventDispatcher.h"
10 #include "mozilla/EventStateManager.h"
11 #include "mozilla/MouseEvents.h"
12 #include "mozilla/Preferences.h"
13 #include "nsCOMPtr.h"
14 #include "nsContentUtils.h"
15 #include "nsIContent.h"
16 #include "nsIDocument.h"
17 #include "nsIPresShell.h"
18 #include "nsIScrollableFrame.h"
19 #include "nsITimer.h"
20 #include "nsPluginFrame.h"
21 #include "nsPresContext.h"
22 #include "prtime.h"
23 #include "Units.h"
24 #include "AsyncScrollBase.h"
25
26 namespace mozilla {
27
28 /******************************************************************/
29 /* mozilla::DeltaValues */
30 /******************************************************************/
31
DeltaValues(WidgetWheelEvent * aEvent)32 DeltaValues::DeltaValues(WidgetWheelEvent* aEvent)
33 : deltaX(aEvent->mDeltaX)
34 , deltaY(aEvent->mDeltaY)
35 {
36 }
37
38 /******************************************************************/
39 /* mozilla::WheelHandlingUtils */
40 /******************************************************************/
41
42 /* static */ bool
CanScrollInRange(nscoord aMin,nscoord aValue,nscoord aMax,double aDirection)43 WheelHandlingUtils::CanScrollInRange(nscoord aMin, nscoord aValue, nscoord aMax,
44 double aDirection)
45 {
46 return aDirection > 0.0 ? aValue < static_cast<double>(aMax) :
47 static_cast<double>(aMin) < aValue;
48 }
49
50 /* static */ bool
CanScrollOn(nsIFrame * aFrame,double aDirectionX,double aDirectionY)51 WheelHandlingUtils::CanScrollOn(nsIFrame* aFrame,
52 double aDirectionX, double aDirectionY)
53 {
54 nsIScrollableFrame* scrollableFrame = do_QueryFrame(aFrame);
55 if (scrollableFrame) {
56 return CanScrollOn(scrollableFrame, aDirectionX, aDirectionY);
57 }
58 nsPluginFrame* pluginFrame = do_QueryFrame(aFrame);
59 return pluginFrame && pluginFrame->WantsToHandleWheelEventAsDefaultAction();
60 }
61
62 /* static */ bool
CanScrollOn(nsIScrollableFrame * aScrollFrame,double aDirectionX,double aDirectionY)63 WheelHandlingUtils::CanScrollOn(nsIScrollableFrame* aScrollFrame,
64 double aDirectionX, double aDirectionY)
65 {
66 MOZ_ASSERT(aScrollFrame);
67 NS_ASSERTION(aDirectionX || aDirectionY,
68 "One of the delta values must be non-zero at least");
69
70 nsPoint scrollPt = aScrollFrame->GetScrollPosition();
71 nsRect scrollRange = aScrollFrame->GetScrollRange();
72 uint32_t directions = aScrollFrame->GetPerceivedScrollingDirections();
73
74 return (aDirectionX && (directions & nsIScrollableFrame::HORIZONTAL) &&
75 CanScrollInRange(scrollRange.x, scrollPt.x,
76 scrollRange.XMost(), aDirectionX)) ||
77 (aDirectionY && (directions & nsIScrollableFrame::VERTICAL) &&
78 CanScrollInRange(scrollRange.y, scrollPt.y,
79 scrollRange.YMost(), aDirectionY));
80 }
81
82 /******************************************************************/
83 /* mozilla::WheelTransaction */
84 /******************************************************************/
85
86 nsWeakFrame WheelTransaction::sTargetFrame(nullptr);
87 uint32_t WheelTransaction::sTime = 0;
88 uint32_t WheelTransaction::sMouseMoved = 0;
89 nsITimer* WheelTransaction::sTimer = nullptr;
90 int32_t WheelTransaction::sScrollSeriesCounter = 0;
91 bool WheelTransaction::sOwnScrollbars = false;
92
93 /* static */ bool
OutOfTime(uint32_t aBaseTime,uint32_t aThreshold)94 WheelTransaction::OutOfTime(uint32_t aBaseTime, uint32_t aThreshold)
95 {
96 uint32_t now = PR_IntervalToMilliseconds(PR_IntervalNow());
97 return (now - aBaseTime > aThreshold);
98 }
99
100 /* static */ void
OwnScrollbars(bool aOwn)101 WheelTransaction::OwnScrollbars(bool aOwn)
102 {
103 sOwnScrollbars = aOwn;
104 }
105
106 /* static */ void
BeginTransaction(nsIFrame * aTargetFrame,WidgetWheelEvent * aEvent)107 WheelTransaction::BeginTransaction(nsIFrame* aTargetFrame,
108 WidgetWheelEvent* aEvent)
109 {
110 NS_ASSERTION(!sTargetFrame, "previous transaction is not finished!");
111 MOZ_ASSERT(aEvent->mMessage == eWheel,
112 "Transaction must be started with a wheel event");
113 ScrollbarsForWheel::OwnWheelTransaction(false);
114 sTargetFrame = aTargetFrame;
115 sScrollSeriesCounter = 0;
116 if (!UpdateTransaction(aEvent)) {
117 NS_ERROR("BeginTransaction is called even cannot scroll the frame");
118 EndTransaction();
119 }
120 }
121
122 /* static */ bool
UpdateTransaction(WidgetWheelEvent * aEvent)123 WheelTransaction::UpdateTransaction(WidgetWheelEvent* aEvent)
124 {
125 nsIFrame* scrollToFrame = GetTargetFrame();
126 nsIScrollableFrame* scrollableFrame = scrollToFrame->GetScrollTargetFrame();
127 if (scrollableFrame) {
128 scrollToFrame = do_QueryFrame(scrollableFrame);
129 }
130
131 if (!WheelHandlingUtils::CanScrollOn(scrollToFrame,
132 aEvent->mDeltaX, aEvent->mDeltaY)) {
133 OnFailToScrollTarget();
134 // We should not modify the transaction state when the view will not be
135 // scrolled actually.
136 return false;
137 }
138
139 SetTimeout();
140
141 if (sScrollSeriesCounter != 0 && OutOfTime(sTime, kScrollSeriesTimeoutMs)) {
142 sScrollSeriesCounter = 0;
143 }
144 sScrollSeriesCounter++;
145
146 // We should use current time instead of WidgetEvent.time.
147 // 1. Some events doesn't have the correct creation time.
148 // 2. If the computer runs slowly by other processes eating the CPU resource,
149 // the event creation time doesn't keep real time.
150 sTime = PR_IntervalToMilliseconds(PR_IntervalNow());
151 sMouseMoved = 0;
152 return true;
153 }
154
155 /* static */ void
MayEndTransaction()156 WheelTransaction::MayEndTransaction()
157 {
158 if (!sOwnScrollbars && ScrollbarsForWheel::IsActive()) {
159 ScrollbarsForWheel::OwnWheelTransaction(true);
160 } else {
161 EndTransaction();
162 }
163 }
164
165 /* static */ void
EndTransaction()166 WheelTransaction::EndTransaction()
167 {
168 if (sTimer) {
169 sTimer->Cancel();
170 }
171 sTargetFrame = nullptr;
172 sScrollSeriesCounter = 0;
173 if (sOwnScrollbars) {
174 sOwnScrollbars = false;
175 ScrollbarsForWheel::OwnWheelTransaction(false);
176 ScrollbarsForWheel::Inactivate();
177 }
178 }
179
180 /* static */ bool
WillHandleDefaultAction(WidgetWheelEvent * aWheelEvent,nsWeakFrame & aTargetWeakFrame)181 WheelTransaction::WillHandleDefaultAction(WidgetWheelEvent* aWheelEvent,
182 nsWeakFrame& aTargetWeakFrame)
183 {
184 nsIFrame* lastTargetFrame = GetTargetFrame();
185 if (!lastTargetFrame) {
186 BeginTransaction(aTargetWeakFrame.GetFrame(), aWheelEvent);
187 } else if (lastTargetFrame != aTargetWeakFrame.GetFrame()) {
188 EndTransaction();
189 BeginTransaction(aTargetWeakFrame.GetFrame(), aWheelEvent);
190 } else {
191 UpdateTransaction(aWheelEvent);
192 }
193
194 // When the wheel event will not be handled with any frames,
195 // UpdateTransaction() fires MozMouseScrollFailed event which is for
196 // automated testing. In the event handler, the target frame might be
197 // destroyed. Then, the caller shouldn't try to handle the default action.
198 if (!aTargetWeakFrame.IsAlive()) {
199 EndTransaction();
200 return false;
201 }
202
203 return true;
204 }
205
206 /* static */ void
OnEvent(WidgetEvent * aEvent)207 WheelTransaction::OnEvent(WidgetEvent* aEvent)
208 {
209 if (!sTargetFrame) {
210 return;
211 }
212
213 if (OutOfTime(sTime, GetTimeoutTime())) {
214 // Even if the scroll event which is handled after timeout, but onTimeout
215 // was not fired by timer, then the scroll event will scroll old frame,
216 // therefore, we should call OnTimeout here and ensure to finish the old
217 // transaction.
218 OnTimeout(nullptr, nullptr);
219 return;
220 }
221
222 switch (aEvent->mMessage) {
223 case eWheel:
224 if (sMouseMoved != 0 &&
225 OutOfTime(sMouseMoved, GetIgnoreMoveDelayTime())) {
226 // Terminate the current mousewheel transaction if the mouse moved more
227 // than ignoremovedelay milliseconds ago
228 EndTransaction();
229 }
230 return;
231 case eMouseMove:
232 case eDragOver: {
233 WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
234 if (mouseEvent->IsReal()) {
235 // If the cursor is moving to be outside the frame,
236 // terminate the scrollwheel transaction.
237 nsIntPoint pt = GetScreenPoint(mouseEvent);
238 nsIntRect r = sTargetFrame->GetScreenRect();
239 if (!r.Contains(pt)) {
240 EndTransaction();
241 return;
242 }
243
244 // If the cursor is moving inside the frame, and it is less than
245 // ignoremovedelay milliseconds since the last scroll operation, ignore
246 // the mouse move; otherwise, record the current mouse move time to be
247 // checked later
248 if (!sMouseMoved && OutOfTime(sTime, GetIgnoreMoveDelayTime())) {
249 sMouseMoved = PR_IntervalToMilliseconds(PR_IntervalNow());
250 }
251 }
252 return;
253 }
254 case eKeyPress:
255 case eKeyUp:
256 case eKeyDown:
257 case eMouseUp:
258 case eMouseDown:
259 case eMouseDoubleClick:
260 case eMouseClick:
261 case eContextMenu:
262 case eDrop:
263 EndTransaction();
264 return;
265 default:
266 break;
267 }
268 }
269
270 /* static */ void
Shutdown()271 WheelTransaction::Shutdown()
272 {
273 NS_IF_RELEASE(sTimer);
274 }
275
276 /* static */ void
OnFailToScrollTarget()277 WheelTransaction::OnFailToScrollTarget()
278 {
279 NS_PRECONDITION(sTargetFrame, "We don't have mouse scrolling transaction");
280
281 if (Preferences::GetBool("test.mousescroll", false)) {
282 // This event is used for automated tests, see bug 442774.
283 nsContentUtils::DispatchTrustedEvent(
284 sTargetFrame->GetContent()->OwnerDoc(),
285 sTargetFrame->GetContent(),
286 NS_LITERAL_STRING("MozMouseScrollFailed"),
287 true, true);
288 }
289 // The target frame might be destroyed in the event handler, at that time,
290 // we need to finish the current transaction
291 if (!sTargetFrame) {
292 EndTransaction();
293 }
294 }
295
296 /* static */ void
OnTimeout(nsITimer * aTimer,void * aClosure)297 WheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure)
298 {
299 if (!sTargetFrame) {
300 // The transaction target was destroyed already
301 EndTransaction();
302 return;
303 }
304 // Store the sTargetFrame, the variable becomes null in EndTransaction.
305 nsIFrame* frame = sTargetFrame;
306 // We need to finish current transaction before DOM event firing. Because
307 // the next DOM event might create strange situation for us.
308 MayEndTransaction();
309
310 if (Preferences::GetBool("test.mousescroll", false)) {
311 // This event is used for automated tests, see bug 442774.
312 nsContentUtils::DispatchTrustedEvent(
313 frame->GetContent()->OwnerDoc(),
314 frame->GetContent(),
315 NS_LITERAL_STRING("MozMouseScrollTransactionTimeout"),
316 true, true);
317 }
318 }
319
320 /* static */ void
SetTimeout()321 WheelTransaction::SetTimeout()
322 {
323 if (!sTimer) {
324 nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID);
325 if (!timer) {
326 return;
327 }
328 timer.swap(sTimer);
329 }
330 sTimer->Cancel();
331 DebugOnly<nsresult> rv =
332 sTimer->InitWithFuncCallback(OnTimeout, nullptr, GetTimeoutTime(),
333 nsITimer::TYPE_ONE_SHOT);
334 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
335 "nsITimer::InitWithFuncCallback failed");
336 }
337
338 /* static */ nsIntPoint
GetScreenPoint(WidgetGUIEvent * aEvent)339 WheelTransaction::GetScreenPoint(WidgetGUIEvent* aEvent)
340 {
341 NS_ASSERTION(aEvent, "aEvent is null");
342 NS_ASSERTION(aEvent->mWidget, "aEvent-mWidget is null");
343 return (aEvent->mRefPoint + aEvent->mWidget->WidgetToScreenOffset())
344 .ToUnknownPoint();
345 }
346
347 /* static */ uint32_t
GetTimeoutTime()348 WheelTransaction::GetTimeoutTime()
349 {
350 return Preferences::GetUint("mousewheel.transaction.timeout", 1500);
351 }
352
353 /* static */ uint32_t
GetIgnoreMoveDelayTime()354 WheelTransaction::GetIgnoreMoveDelayTime()
355 {
356 return Preferences::GetUint("mousewheel.transaction.ignoremovedelay", 100);
357 }
358
359 /* static */ DeltaValues
AccelerateWheelDelta(WidgetWheelEvent * aEvent,bool aAllowScrollSpeedOverride)360 WheelTransaction::AccelerateWheelDelta(WidgetWheelEvent* aEvent,
361 bool aAllowScrollSpeedOverride)
362 {
363 DeltaValues result(aEvent);
364
365 // Don't accelerate the delta values if the event isn't line scrolling.
366 if (aEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_LINE) {
367 return result;
368 }
369
370 if (aAllowScrollSpeedOverride) {
371 result = OverrideSystemScrollSpeed(aEvent);
372 }
373
374 // Accelerate by the sScrollSeriesCounter
375 int32_t start = GetAccelerationStart();
376 if (start >= 0 && sScrollSeriesCounter >= start) {
377 int32_t factor = GetAccelerationFactor();
378 if (factor > 0) {
379 result.deltaX = ComputeAcceleratedWheelDelta(result.deltaX, factor);
380 result.deltaY = ComputeAcceleratedWheelDelta(result.deltaY, factor);
381 }
382 }
383
384 return result;
385 }
386
387 /* static */ double
ComputeAcceleratedWheelDelta(double aDelta,int32_t aFactor)388 WheelTransaction::ComputeAcceleratedWheelDelta(double aDelta, int32_t aFactor)
389 {
390 return mozilla::ComputeAcceleratedWheelDelta(aDelta, sScrollSeriesCounter, aFactor);
391 }
392
393 /* static */ int32_t
GetAccelerationStart()394 WheelTransaction::GetAccelerationStart()
395 {
396 return Preferences::GetInt("mousewheel.acceleration.start", -1);
397 }
398
399 /* static */ int32_t
GetAccelerationFactor()400 WheelTransaction::GetAccelerationFactor()
401 {
402 return Preferences::GetInt("mousewheel.acceleration.factor", -1);
403 }
404
405 /* static */ DeltaValues
OverrideSystemScrollSpeed(WidgetWheelEvent * aEvent)406 WheelTransaction::OverrideSystemScrollSpeed(WidgetWheelEvent* aEvent)
407 {
408 MOZ_ASSERT(sTargetFrame, "We don't have mouse scrolling transaction");
409 MOZ_ASSERT(aEvent->mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_LINE);
410
411 // If the event doesn't scroll to both X and Y, we don't need to do anything
412 // here.
413 if (!aEvent->mDeltaX && !aEvent->mDeltaY) {
414 return DeltaValues(aEvent);
415 }
416
417 return DeltaValues(aEvent->OverriddenDeltaX(),
418 aEvent->OverriddenDeltaY());
419 }
420
421 /******************************************************************/
422 /* mozilla::ScrollbarsForWheel */
423 /******************************************************************/
424
425 const DeltaValues ScrollbarsForWheel::directions[kNumberOfTargets] = {
426 DeltaValues(-1, 0), DeltaValues(+1, 0), DeltaValues(0, -1), DeltaValues(0, +1)
427 };
428
429 nsWeakFrame ScrollbarsForWheel::sActiveOwner = nullptr;
430 nsWeakFrame ScrollbarsForWheel::sActivatedScrollTargets[kNumberOfTargets] = {
431 nullptr, nullptr, nullptr, nullptr
432 };
433
434 bool ScrollbarsForWheel::sHadWheelStart = false;
435 bool ScrollbarsForWheel::sOwnWheelTransaction = false;
436
437 /* static */ void
PrepareToScrollText(EventStateManager * aESM,nsIFrame * aTargetFrame,WidgetWheelEvent * aEvent)438 ScrollbarsForWheel::PrepareToScrollText(EventStateManager* aESM,
439 nsIFrame* aTargetFrame,
440 WidgetWheelEvent* aEvent)
441 {
442 if (aEvent->mMessage == eWheelOperationStart) {
443 WheelTransaction::OwnScrollbars(false);
444 if (!IsActive()) {
445 TemporarilyActivateAllPossibleScrollTargets(aESM, aTargetFrame, aEvent);
446 sHadWheelStart = true;
447 }
448 } else {
449 DeactivateAllTemporarilyActivatedScrollTargets();
450 }
451 }
452
453 /* static */ void
SetActiveScrollTarget(nsIScrollableFrame * aScrollTarget)454 ScrollbarsForWheel::SetActiveScrollTarget(nsIScrollableFrame* aScrollTarget)
455 {
456 if (!sHadWheelStart) {
457 return;
458 }
459 nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(aScrollTarget);
460 if (!scrollbarMediator) {
461 return;
462 }
463 sHadWheelStart = false;
464 sActiveOwner = do_QueryFrame(aScrollTarget);
465 scrollbarMediator->ScrollbarActivityStarted();
466 }
467
468 /* static */ void
MayInactivate()469 ScrollbarsForWheel::MayInactivate()
470 {
471 if (!sOwnWheelTransaction && WheelTransaction::GetTargetFrame()) {
472 WheelTransaction::OwnScrollbars(true);
473 } else {
474 Inactivate();
475 }
476 }
477
478 /* static */ void
Inactivate()479 ScrollbarsForWheel::Inactivate()
480 {
481 nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sActiveOwner);
482 if (scrollbarMediator) {
483 scrollbarMediator->ScrollbarActivityStopped();
484 }
485 sActiveOwner = nullptr;
486 DeactivateAllTemporarilyActivatedScrollTargets();
487 if (sOwnWheelTransaction) {
488 sOwnWheelTransaction = false;
489 WheelTransaction::OwnScrollbars(false);
490 WheelTransaction::EndTransaction();
491 }
492 }
493
494 /* static */ bool
IsActive()495 ScrollbarsForWheel::IsActive()
496 {
497 if (sActiveOwner) {
498 return true;
499 }
500 for (size_t i = 0; i < kNumberOfTargets; ++i) {
501 if (sActivatedScrollTargets[i]) {
502 return true;
503 }
504 }
505 return false;
506 }
507
508 /* static */ void
OwnWheelTransaction(bool aOwn)509 ScrollbarsForWheel::OwnWheelTransaction(bool aOwn)
510 {
511 sOwnWheelTransaction = aOwn;
512 }
513
514 /* static */ void
TemporarilyActivateAllPossibleScrollTargets(EventStateManager * aESM,nsIFrame * aTargetFrame,WidgetWheelEvent * aEvent)515 ScrollbarsForWheel::TemporarilyActivateAllPossibleScrollTargets(
516 EventStateManager* aESM,
517 nsIFrame* aTargetFrame,
518 WidgetWheelEvent* aEvent)
519 {
520 for (size_t i = 0; i < kNumberOfTargets; i++) {
521 const DeltaValues *dir = &directions[i];
522 nsWeakFrame* scrollTarget = &sActivatedScrollTargets[i];
523 MOZ_ASSERT(!*scrollTarget, "scroll target still temporarily activated!");
524 nsIScrollableFrame* target = do_QueryFrame(
525 aESM->ComputeScrollTarget(aTargetFrame, dir->deltaX, dir->deltaY, aEvent,
526 EventStateManager::COMPUTE_DEFAULT_ACTION_TARGET));
527 nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(target);
528 if (scrollbarMediator) {
529 nsIFrame* targetFrame = do_QueryFrame(target);
530 *scrollTarget = targetFrame;
531 scrollbarMediator->ScrollbarActivityStarted();
532 }
533 }
534 }
535
536 /* static */ void
DeactivateAllTemporarilyActivatedScrollTargets()537 ScrollbarsForWheel::DeactivateAllTemporarilyActivatedScrollTargets()
538 {
539 for (size_t i = 0; i < kNumberOfTargets; i++) {
540 nsWeakFrame* scrollTarget = &sActivatedScrollTargets[i];
541 if (*scrollTarget) {
542 nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(*scrollTarget);
543 if (scrollbarMediator) {
544 scrollbarMediator->ScrollbarActivityStopped();
545 }
546 *scrollTarget = nullptr;
547 }
548 }
549 }
550
551 } // namespace mozilla
552