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 "ScrollbarActivity.h"
8 #include "nsIScrollbarMediator.h"
9 #include "nsIContent.h"
10 #include "nsICSSDeclaration.h"
11 #include "nsIDOMEvent.h"
12 #include "nsIFrame.h"
13 #include "nsContentUtils.h"
14 #include "nsAString.h"
15 #include "nsQueryFrame.h"
16 #include "nsComponentManagerUtils.h"
17 #include "nsStyledElement.h"
18 #include "mozilla/dom/Element.h"
19 #include "mozilla/LookAndFeel.h"
20 #include "mozilla/Preferences.h"
21
22 namespace mozilla {
23 namespace layout {
24
NS_IMPL_ISUPPORTS(ScrollbarActivity,nsIDOMEventListener)25 NS_IMPL_ISUPPORTS(ScrollbarActivity, nsIDOMEventListener)
26
27 static bool GetForceAlwaysVisiblePref() {
28 static bool sForceAlwaysVisible;
29 static bool sForceAlwaysVisiblePrefCached = false;
30 if (!sForceAlwaysVisiblePrefCached) {
31 Preferences::AddBoolVarCache(
32 &sForceAlwaysVisible,
33 "layout.testing.overlay-scrollbars.always-visible");
34 sForceAlwaysVisiblePrefCached = true;
35 }
36 return sForceAlwaysVisible;
37 }
38
QueryLookAndFeelVals()39 void ScrollbarActivity::QueryLookAndFeelVals() {
40 // Fade animation constants
41 mScrollbarFadeBeginDelay =
42 LookAndFeel::GetInt(LookAndFeel::eIntID_ScrollbarFadeBeginDelay);
43 mScrollbarFadeDuration =
44 LookAndFeel::GetInt(LookAndFeel::eIntID_ScrollbarFadeDuration);
45 // Controls whether we keep the mouse move listener so we can display the
46 // scrollbars whenever the user moves the mouse within the scroll area.
47 mDisplayOnMouseMove =
48 LookAndFeel::GetInt(LookAndFeel::eIntID_ScrollbarDisplayOnMouseMove);
49 }
50
Destroy()51 void ScrollbarActivity::Destroy() {
52 StopListeningForScrollbarEvents();
53 StopListeningForScrollAreaEvents();
54 UnregisterFromRefreshDriver();
55 CancelFadeBeginTimer();
56 }
57
ActivityOccurred()58 void ScrollbarActivity::ActivityOccurred() {
59 ActivityStarted();
60 ActivityStopped();
61 }
62
ActivityStarted()63 void ScrollbarActivity::ActivityStarted() {
64 mNestedActivityCounter++;
65 CancelFadeBeginTimer();
66 if (!SetIsFading(false)) {
67 return;
68 }
69 UnregisterFromRefreshDriver();
70 StartListeningForScrollbarEvents();
71 StartListeningForScrollAreaEvents();
72 SetIsActive(true);
73
74 NS_ASSERTION(mIsActive, "need to be active during activity");
75 NS_ASSERTION(!mIsFading, "must not be fading during activity");
76 }
77
ActivityStopped()78 void ScrollbarActivity::ActivityStopped() {
79 if (!IsActivityOngoing()) {
80 // This can happen if there was a frame reconstruction while the activity
81 // was ongoing. In this case we just do nothing. We should probably handle
82 // this case better.
83 return;
84 }
85 NS_ASSERTION(mIsActive, "need to be active during activity");
86 NS_ASSERTION(!mIsFading, "must not be fading during ongoing activity");
87
88 mNestedActivityCounter--;
89
90 if (!IsActivityOngoing()) {
91 StartFadeBeginTimer();
92
93 NS_ASSERTION(mIsActive, "need to be active right after activity");
94 NS_ASSERTION(!mIsFading, "must not be fading right after activity");
95 }
96 }
97
98 NS_IMETHODIMP
HandleEvent(nsIDOMEvent * aEvent)99 ScrollbarActivity::HandleEvent(nsIDOMEvent* aEvent) {
100 if (!mDisplayOnMouseMove && !mIsActive) return NS_OK;
101
102 nsAutoString type;
103 aEvent->GetType(type);
104
105 if (type.EqualsLiteral("mousemove")) {
106 // Mouse motions anywhere in the scrollable frame should keep the
107 // scrollbars visible.
108 ActivityOccurred();
109 return NS_OK;
110 }
111
112 nsCOMPtr<nsIDOMEventTarget> target;
113 aEvent->GetOriginalTarget(getter_AddRefs(target));
114 nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
115
116 HandleEventForScrollbar(type, targetContent, GetHorizontalScrollbar(),
117 &mHScrollbarHovered);
118 HandleEventForScrollbar(type, targetContent, GetVerticalScrollbar(),
119 &mVScrollbarHovered);
120
121 return NS_OK;
122 }
123
WillRefresh(TimeStamp aTime)124 void ScrollbarActivity::WillRefresh(TimeStamp aTime) {
125 NS_ASSERTION(mIsActive, "should only fade while scrollbars are visible");
126 NS_ASSERTION(!IsActivityOngoing(),
127 "why weren't we unregistered from the refresh driver when "
128 "scrollbar activity started?");
129 NS_ASSERTION(mIsFading, "should only animate fading during fade");
130
131 if (!UpdateOpacity(aTime)) {
132 return;
133 }
134
135 if (!IsStillFading(aTime)) {
136 EndFade();
137 }
138 }
139
IsStillFading(TimeStamp aTime)140 bool ScrollbarActivity::IsStillFading(TimeStamp aTime) {
141 return !mFadeBeginTime.IsNull() && (aTime - mFadeBeginTime < FadeDuration());
142 }
143
HandleEventForScrollbar(const nsAString & aType,nsIContent * aTarget,Element * aScrollbar,bool * aStoredHoverState)144 void ScrollbarActivity::HandleEventForScrollbar(const nsAString& aType,
145 nsIContent* aTarget,
146 Element* aScrollbar,
147 bool* aStoredHoverState) {
148 if (!aTarget || !aScrollbar ||
149 !nsContentUtils::ContentIsDescendantOf(aTarget, aScrollbar))
150 return;
151
152 if (aType.EqualsLiteral("mousedown")) {
153 ActivityStarted();
154 } else if (aType.EqualsLiteral("mouseup")) {
155 ActivityStopped();
156 } else if (aType.EqualsLiteral("mouseover") ||
157 aType.EqualsLiteral("mouseout")) {
158 bool newHoveredState = aType.EqualsLiteral("mouseover");
159 if (newHoveredState && !*aStoredHoverState) {
160 ActivityStarted();
161 HoveredScrollbar(aScrollbar);
162 } else if (*aStoredHoverState && !newHoveredState) {
163 ActivityStopped();
164 // Don't call HoveredScrollbar(nullptr) here because we want the hover
165 // attribute to stick until the scrollbars are hidden.
166 }
167 *aStoredHoverState = newHoveredState;
168 }
169 }
170
StartListeningForScrollbarEvents()171 void ScrollbarActivity::StartListeningForScrollbarEvents() {
172 if (mListeningForScrollbarEvents) return;
173
174 mHorizontalScrollbar = do_QueryInterface(GetHorizontalScrollbar());
175 mVerticalScrollbar = do_QueryInterface(GetVerticalScrollbar());
176
177 AddScrollbarEventListeners(mHorizontalScrollbar);
178 AddScrollbarEventListeners(mVerticalScrollbar);
179
180 mListeningForScrollbarEvents = true;
181 }
182
StopListeningForScrollbarEvents()183 void ScrollbarActivity::StopListeningForScrollbarEvents() {
184 if (!mListeningForScrollbarEvents) return;
185
186 RemoveScrollbarEventListeners(mHorizontalScrollbar);
187 RemoveScrollbarEventListeners(mVerticalScrollbar);
188
189 mHorizontalScrollbar = nullptr;
190 mVerticalScrollbar = nullptr;
191 mListeningForScrollbarEvents = false;
192 }
193
StartListeningForScrollAreaEvents()194 void ScrollbarActivity::StartListeningForScrollAreaEvents() {
195 if (mListeningForScrollAreaEvents) return;
196
197 nsIFrame* scrollArea = do_QueryFrame(mScrollableFrame);
198 nsCOMPtr<nsIDOMEventTarget> scrollAreaTarget =
199 do_QueryInterface(scrollArea->GetContent());
200 if (scrollAreaTarget) {
201 scrollAreaTarget->AddEventListener(NS_LITERAL_STRING("mousemove"), this,
202 true);
203 }
204 mListeningForScrollAreaEvents = true;
205 }
206
StopListeningForScrollAreaEvents()207 void ScrollbarActivity::StopListeningForScrollAreaEvents() {
208 if (!mListeningForScrollAreaEvents) return;
209
210 nsIFrame* scrollArea = do_QueryFrame(mScrollableFrame);
211 nsCOMPtr<nsIDOMEventTarget> scrollAreaTarget =
212 do_QueryInterface(scrollArea->GetContent());
213 if (scrollAreaTarget) {
214 scrollAreaTarget->RemoveEventListener(NS_LITERAL_STRING("mousemove"), this,
215 true);
216 }
217 mListeningForScrollAreaEvents = false;
218 }
219
AddScrollbarEventListeners(nsIDOMEventTarget * aScrollbar)220 void ScrollbarActivity::AddScrollbarEventListeners(
221 nsIDOMEventTarget* aScrollbar) {
222 if (aScrollbar) {
223 aScrollbar->AddEventListener(NS_LITERAL_STRING("mousedown"), this, true);
224 aScrollbar->AddEventListener(NS_LITERAL_STRING("mouseup"), this, true);
225 aScrollbar->AddEventListener(NS_LITERAL_STRING("mouseover"), this, true);
226 aScrollbar->AddEventListener(NS_LITERAL_STRING("mouseout"), this, true);
227 }
228 }
229
RemoveScrollbarEventListeners(nsIDOMEventTarget * aScrollbar)230 void ScrollbarActivity::RemoveScrollbarEventListeners(
231 nsIDOMEventTarget* aScrollbar) {
232 if (aScrollbar) {
233 aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mousedown"), this, true);
234 aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mouseup"), this, true);
235 aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mouseover"), this, true);
236 aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mouseout"), this, true);
237 }
238 }
239
BeginFade()240 void ScrollbarActivity::BeginFade() {
241 NS_ASSERTION(mIsActive, "can't begin fade when we're already inactive");
242 NS_ASSERTION(!IsActivityOngoing(),
243 "why wasn't the fade begin timer cancelled when scrollbar "
244 "activity started?");
245 NS_ASSERTION(!mIsFading, "shouldn't be fading just yet");
246
247 CancelFadeBeginTimer();
248 mFadeBeginTime = TimeStamp::Now();
249 if (!SetIsFading(true)) {
250 return;
251 }
252 RegisterWithRefreshDriver();
253
254 NS_ASSERTION(mIsActive, "only fade while scrollbars are visible");
255 NS_ASSERTION(mIsFading, "should be fading now");
256 }
257
EndFade()258 void ScrollbarActivity::EndFade() {
259 NS_ASSERTION(mIsActive, "still need to be active at this point");
260 NS_ASSERTION(!IsActivityOngoing(),
261 "why wasn't the fade end timer cancelled when scrollbar "
262 "activity started?");
263
264 if (!SetIsFading(false)) {
265 return;
266 }
267 SetIsActive(false);
268 UnregisterFromRefreshDriver();
269 StopListeningForScrollbarEvents();
270 if (!mDisplayOnMouseMove) {
271 StopListeningForScrollAreaEvents();
272 }
273
274 NS_ASSERTION(!mIsActive, "should have gone inactive after fade end");
275 NS_ASSERTION(!mIsFading, "shouldn't be fading anymore");
276 }
277
RegisterWithRefreshDriver()278 void ScrollbarActivity::RegisterWithRefreshDriver() {
279 nsRefreshDriver* refreshDriver = GetRefreshDriver();
280 if (refreshDriver) {
281 refreshDriver->AddRefreshObserver(this, FlushType::Style);
282 }
283 }
284
UnregisterFromRefreshDriver()285 void ScrollbarActivity::UnregisterFromRefreshDriver() {
286 nsRefreshDriver* refreshDriver = GetRefreshDriver();
287 if (refreshDriver) {
288 refreshDriver->RemoveRefreshObserver(this, FlushType::Style);
289 }
290 }
291
SetBooleanAttribute(Element * aElement,nsAtom * aAttribute,bool aValue)292 static void SetBooleanAttribute(Element* aElement, nsAtom* aAttribute,
293 bool aValue) {
294 if (aElement) {
295 if (aValue) {
296 aElement->SetAttr(kNameSpaceID_None, aAttribute,
297 NS_LITERAL_STRING("true"), true);
298 } else {
299 aElement->UnsetAttr(kNameSpaceID_None, aAttribute, true);
300 }
301 }
302 }
303
SetIsActive(bool aNewActive)304 void ScrollbarActivity::SetIsActive(bool aNewActive) {
305 if (mIsActive == aNewActive) return;
306
307 mIsActive = aNewActive;
308 if (!mIsActive) {
309 // Clear sticky scrollbar hover status.
310 HoveredScrollbar(nullptr);
311 }
312
313 SetBooleanAttribute(GetHorizontalScrollbar(), nsGkAtoms::active, mIsActive);
314 SetBooleanAttribute(GetVerticalScrollbar(), nsGkAtoms::active, mIsActive);
315 }
316
SetOpacityOnElement(nsIContent * aContent,double aOpacity)317 static void SetOpacityOnElement(nsIContent* aContent, double aOpacity) {
318 nsCOMPtr<nsStyledElement> inlineStyleContent = do_QueryInterface(aContent);
319 if (inlineStyleContent) {
320 nsICSSDeclaration* decl = inlineStyleContent->Style();
321 nsAutoString str;
322 str.AppendFloat(aOpacity);
323 decl->SetProperty(NS_LITERAL_STRING("opacity"), str, EmptyString());
324 }
325 }
326
UpdateOpacity(TimeStamp aTime)327 bool ScrollbarActivity::UpdateOpacity(TimeStamp aTime) {
328 // Avoid division by zero if mScrollbarFadeDuration is zero, just jump
329 // to the end of the fade animation
330 double progress = mScrollbarFadeDuration
331 ? ((aTime - mFadeBeginTime) / FadeDuration())
332 : 1.0;
333 double opacity = 1.0 - std::max(0.0, std::min(1.0, progress));
334
335 // 'this' may be getting destroyed during SetOpacityOnElement calls.
336 AutoWeakFrame weakFrame((do_QueryFrame(mScrollableFrame)));
337 SetOpacityOnElement(GetHorizontalScrollbar(), opacity);
338 if (!weakFrame.IsAlive()) {
339 return false;
340 }
341 SetOpacityOnElement(GetVerticalScrollbar(), opacity);
342 if (!weakFrame.IsAlive()) {
343 return false;
344 }
345 return true;
346 }
347
UnsetOpacityOnElement(nsIContent * aContent)348 static void UnsetOpacityOnElement(nsIContent* aContent) {
349 nsCOMPtr<nsStyledElement> inlineStyleContent = do_QueryInterface(aContent);
350 if (inlineStyleContent) {
351 nsICSSDeclaration* decl = inlineStyleContent->Style();
352 nsAutoString dummy;
353 decl->RemoveProperty(NS_LITERAL_STRING("opacity"), dummy);
354 }
355 }
356
SetIsFading(bool aNewFading)357 bool ScrollbarActivity::SetIsFading(bool aNewFading) {
358 if (mIsFading == aNewFading) return true;
359
360 mIsFading = aNewFading;
361 if (!mIsFading) {
362 mFadeBeginTime = TimeStamp();
363 // 'this' may be getting destroyed during UnsetOpacityOnElement calls.
364 AutoWeakFrame weakFrame((do_QueryFrame(mScrollableFrame)));
365 UnsetOpacityOnElement(GetHorizontalScrollbar());
366 if (!weakFrame.IsAlive()) {
367 return false;
368 }
369 UnsetOpacityOnElement(GetVerticalScrollbar());
370 if (!weakFrame.IsAlive()) {
371 return false;
372 }
373 }
374 return true;
375 }
376
StartFadeBeginTimer()377 void ScrollbarActivity::StartFadeBeginTimer() {
378 if (GetForceAlwaysVisiblePref()) {
379 return;
380 }
381 if (!mFadeBeginTimer) {
382 mFadeBeginTimer = NS_NewTimer();
383 }
384 mFadeBeginTimer->InitWithNamedFuncCallback(
385 FadeBeginTimerFired, this, mScrollbarFadeBeginDelay,
386 nsITimer::TYPE_ONE_SHOT, "ScrollbarActivity::FadeBeginTimerFired");
387 }
388
CancelFadeBeginTimer()389 void ScrollbarActivity::CancelFadeBeginTimer() {
390 if (mFadeBeginTimer) {
391 mFadeBeginTimer->Cancel();
392 }
393 }
394
HoveredScrollbar(Element * aScrollbar)395 void ScrollbarActivity::HoveredScrollbar(Element* aScrollbar) {
396 SetBooleanAttribute(GetHorizontalScrollbar(), nsGkAtoms::hover, false);
397 SetBooleanAttribute(GetVerticalScrollbar(), nsGkAtoms::hover, false);
398 SetBooleanAttribute(aScrollbar, nsGkAtoms::hover, true);
399 }
400
GetRefreshDriver()401 nsRefreshDriver* ScrollbarActivity::GetRefreshDriver() {
402 nsIFrame* scrollableFrame = do_QueryFrame(mScrollableFrame);
403 return scrollableFrame->PresContext()->RefreshDriver();
404 }
405
GetScrollbarContent(bool aVertical)406 Element* ScrollbarActivity::GetScrollbarContent(bool aVertical) {
407 nsIFrame* box = mScrollableFrame->GetScrollbarBox(aVertical);
408 return box ? box->GetContent()->AsElement() : nullptr;
409 }
410
411 } // namespace layout
412 } // namespace mozilla
413