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