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