diff options
Diffstat (limited to 'layout/generic/ScrollbarActivity.cpp')
-rw-r--r-- | layout/generic/ScrollbarActivity.cpp | 469 |
1 files changed, 469 insertions, 0 deletions
diff --git a/layout/generic/ScrollbarActivity.cpp b/layout/generic/ScrollbarActivity.cpp new file mode 100644 index 000000000..78859bc2b --- /dev/null +++ b/layout/generic/ScrollbarActivity.cpp @@ -0,0 +1,469 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScrollbarActivity.h" +#include "nsIScrollbarMediator.h" +#include "nsIContent.h" +#include "nsICSSDeclaration.h" +#include "nsIDOMEvent.h" +#include "nsIDOMCSSStyleDeclaration.h" +#include "nsIFrame.h" +#include "nsContentUtils.h" +#include "nsAString.h" +#include "nsQueryFrame.h" +#include "nsComponentManagerUtils.h" +#include "nsStyledElement.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/Preferences.h" + +namespace mozilla { +namespace layout { + +NS_IMPL_ISUPPORTS(ScrollbarActivity, nsIDOMEventListener) + +static bool +GetForceAlwaysVisiblePref() +{ + static bool sForceAlwaysVisible; + static bool sForceAlwaysVisiblePrefCached = false; + if (!sForceAlwaysVisiblePrefCached) { + Preferences::AddBoolVarCache(&sForceAlwaysVisible, + "layout.testing.overlay-scrollbars.always-visible"); + sForceAlwaysVisiblePrefCached = true; + } + return sForceAlwaysVisible; +} + +void +ScrollbarActivity::QueryLookAndFeelVals() +{ + // Fade animation constants + mScrollbarFadeBeginDelay = + LookAndFeel::GetInt(LookAndFeel::eIntID_ScrollbarFadeBeginDelay); + mScrollbarFadeDuration = + LookAndFeel::GetInt(LookAndFeel::eIntID_ScrollbarFadeDuration); + // Controls whether we keep the mouse move listener so we can display the + // scrollbars whenever the user moves the mouse within the scroll area. + mDisplayOnMouseMove = + LookAndFeel::GetInt(LookAndFeel::eIntID_ScrollbarDisplayOnMouseMove); +} + +void +ScrollbarActivity::Destroy() +{ + StopListeningForScrollbarEvents(); + StopListeningForScrollAreaEvents(); + UnregisterFromRefreshDriver(); + CancelFadeBeginTimer(); +} + +void +ScrollbarActivity::ActivityOccurred() +{ + ActivityStarted(); + ActivityStopped(); +} + +void +ScrollbarActivity::ActivityStarted() +{ + mNestedActivityCounter++; + CancelFadeBeginTimer(); + if (!SetIsFading(false)) { + return; + } + UnregisterFromRefreshDriver(); + StartListeningForScrollbarEvents(); + StartListeningForScrollAreaEvents(); + SetIsActive(true); + + NS_ASSERTION(mIsActive, "need to be active during activity"); + NS_ASSERTION(!mIsFading, "must not be fading during activity"); +} + +void +ScrollbarActivity::ActivityStopped() +{ + if (!IsActivityOngoing()) { + // This can happen if there was a frame reconstruction while the activity + // was ongoing. In this case we just do nothing. We should probably handle + // this case better. + return; + } + NS_ASSERTION(mIsActive, "need to be active during activity"); + NS_ASSERTION(!mIsFading, "must not be fading during ongoing activity"); + + mNestedActivityCounter--; + + if (!IsActivityOngoing()) { + StartFadeBeginTimer(); + + NS_ASSERTION(mIsActive, "need to be active right after activity"); + NS_ASSERTION(!mIsFading, "must not be fading right after activity"); + } +} + +NS_IMETHODIMP +ScrollbarActivity::HandleEvent(nsIDOMEvent* aEvent) +{ + if (!mDisplayOnMouseMove && !mIsActive) + return NS_OK; + + nsAutoString type; + aEvent->GetType(type); + + if (type.EqualsLiteral("mousemove")) { + // Mouse motions anywhere in the scrollable frame should keep the + // scrollbars visible. + ActivityOccurred(); + return NS_OK; + } + + nsCOMPtr<nsIDOMEventTarget> target; + aEvent->GetOriginalTarget(getter_AddRefs(target)); + nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target); + + HandleEventForScrollbar(type, targetContent, GetHorizontalScrollbar(), + &mHScrollbarHovered); + HandleEventForScrollbar(type, targetContent, GetVerticalScrollbar(), + &mVScrollbarHovered); + + return NS_OK; +} + +void +ScrollbarActivity::WillRefresh(TimeStamp aTime) +{ + NS_ASSERTION(mIsActive, "should only fade while scrollbars are visible"); + NS_ASSERTION(!IsActivityOngoing(), "why weren't we unregistered from the refresh driver when scrollbar activity started?"); + NS_ASSERTION(mIsFading, "should only animate fading during fade"); + + if (!UpdateOpacity(aTime)) { + return; + } + + if (!IsStillFading(aTime)) { + EndFade(); + } +} + +bool +ScrollbarActivity::IsStillFading(TimeStamp aTime) +{ + return !mFadeBeginTime.IsNull() && (aTime - mFadeBeginTime < FadeDuration()); +} + +void +ScrollbarActivity::HandleEventForScrollbar(const nsAString& aType, + nsIContent* aTarget, + nsIContent* aScrollbar, + bool* aStoredHoverState) +{ + if (!aTarget || !aScrollbar || + !nsContentUtils::ContentIsDescendantOf(aTarget, aScrollbar)) + return; + + if (aType.EqualsLiteral("mousedown")) { + ActivityStarted(); + } else if (aType.EqualsLiteral("mouseup")) { + ActivityStopped(); + } else if (aType.EqualsLiteral("mouseover") || + aType.EqualsLiteral("mouseout")) { + bool newHoveredState = aType.EqualsLiteral("mouseover"); + if (newHoveredState && !*aStoredHoverState) { + ActivityStarted(); + HoveredScrollbar(aScrollbar); + } else if (*aStoredHoverState && !newHoveredState) { + ActivityStopped(); + // Don't call HoveredScrollbar(nullptr) here because we want the hover + // attribute to stick until the scrollbars are hidden. + } + *aStoredHoverState = newHoveredState; + } +} + +void +ScrollbarActivity::StartListeningForScrollbarEvents() +{ + if (mListeningForScrollbarEvents) + return; + + mHorizontalScrollbar = do_QueryInterface(GetHorizontalScrollbar()); + mVerticalScrollbar = do_QueryInterface(GetVerticalScrollbar()); + + AddScrollbarEventListeners(mHorizontalScrollbar); + AddScrollbarEventListeners(mVerticalScrollbar); + + mListeningForScrollbarEvents = true; +} + +void +ScrollbarActivity::StopListeningForScrollbarEvents() +{ + if (!mListeningForScrollbarEvents) + return; + + RemoveScrollbarEventListeners(mHorizontalScrollbar); + RemoveScrollbarEventListeners(mVerticalScrollbar); + + mHorizontalScrollbar = nullptr; + mVerticalScrollbar = nullptr; + mListeningForScrollbarEvents = false; +} + +void +ScrollbarActivity::StartListeningForScrollAreaEvents() +{ + if (mListeningForScrollAreaEvents) + return; + + nsIFrame* scrollArea = do_QueryFrame(mScrollableFrame); + nsCOMPtr<nsIDOMEventTarget> scrollAreaTarget + = do_QueryInterface(scrollArea->GetContent()); + if (scrollAreaTarget) { + scrollAreaTarget->AddEventListener(NS_LITERAL_STRING("mousemove"), this, + true); + } + mListeningForScrollAreaEvents = true; +} + +void +ScrollbarActivity::StopListeningForScrollAreaEvents() +{ + if (!mListeningForScrollAreaEvents) + return; + + nsIFrame* scrollArea = do_QueryFrame(mScrollableFrame); + nsCOMPtr<nsIDOMEventTarget> scrollAreaTarget = do_QueryInterface(scrollArea->GetContent()); + if (scrollAreaTarget) { + scrollAreaTarget->RemoveEventListener(NS_LITERAL_STRING("mousemove"), this, true); + } + mListeningForScrollAreaEvents = false; +} + +void +ScrollbarActivity::AddScrollbarEventListeners(nsIDOMEventTarget* aScrollbar) +{ + if (aScrollbar) { + aScrollbar->AddEventListener(NS_LITERAL_STRING("mousedown"), this, true); + aScrollbar->AddEventListener(NS_LITERAL_STRING("mouseup"), this, true); + aScrollbar->AddEventListener(NS_LITERAL_STRING("mouseover"), this, true); + aScrollbar->AddEventListener(NS_LITERAL_STRING("mouseout"), this, true); + } +} + +void +ScrollbarActivity::RemoveScrollbarEventListeners(nsIDOMEventTarget* aScrollbar) +{ + if (aScrollbar) { + aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mousedown"), this, true); + aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mouseup"), this, true); + aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mouseover"), this, true); + aScrollbar->RemoveEventListener(NS_LITERAL_STRING("mouseout"), this, true); + } +} + +void +ScrollbarActivity::BeginFade() +{ + NS_ASSERTION(mIsActive, "can't begin fade when we're already inactive"); + NS_ASSERTION(!IsActivityOngoing(), "why wasn't the fade begin timer cancelled when scrollbar activity started?"); + NS_ASSERTION(!mIsFading, "shouldn't be fading just yet"); + + CancelFadeBeginTimer(); + mFadeBeginTime = TimeStamp::Now(); + if (!SetIsFading(true)) { + return; + } + RegisterWithRefreshDriver(); + + NS_ASSERTION(mIsActive, "only fade while scrollbars are visible"); + NS_ASSERTION(mIsFading, "should be fading now"); +} + +void +ScrollbarActivity::EndFade() +{ + NS_ASSERTION(mIsActive, "still need to be active at this point"); + NS_ASSERTION(!IsActivityOngoing(), "why wasn't the fade end timer cancelled when scrollbar activity started?"); + + if (!SetIsFading(false)) { + return; + } + SetIsActive(false); + UnregisterFromRefreshDriver(); + StopListeningForScrollbarEvents(); + if (!mDisplayOnMouseMove) { + StopListeningForScrollAreaEvents(); + } + + NS_ASSERTION(!mIsActive, "should have gone inactive after fade end"); + NS_ASSERTION(!mIsFading, "shouldn't be fading anymore"); +} + +void +ScrollbarActivity::RegisterWithRefreshDriver() +{ + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (refreshDriver) { + refreshDriver->AddRefreshObserver(this, Flush_Style); + } +} + +void +ScrollbarActivity::UnregisterFromRefreshDriver() +{ + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (refreshDriver) { + refreshDriver->RemoveRefreshObserver(this, Flush_Style); + } +} + +static void +SetBooleanAttribute(nsIContent* aContent, nsIAtom* aAttribute, bool aValue) +{ + if (aContent) { + if (aValue) { + aContent->SetAttr(kNameSpaceID_None, aAttribute, + NS_LITERAL_STRING("true"), true); + } else { + aContent->UnsetAttr(kNameSpaceID_None, aAttribute, true); + } + } +} + +void +ScrollbarActivity::SetIsActive(bool aNewActive) +{ + if (mIsActive == aNewActive) + return; + + mIsActive = aNewActive; + if (!mIsActive) { + // Clear sticky scrollbar hover status. + HoveredScrollbar(nullptr); + } + + SetBooleanAttribute(GetHorizontalScrollbar(), nsGkAtoms::active, mIsActive); + SetBooleanAttribute(GetVerticalScrollbar(), nsGkAtoms::active, mIsActive); +} + +static void +SetOpacityOnElement(nsIContent* aContent, double aOpacity) +{ + nsCOMPtr<nsStyledElement> inlineStyleContent = + do_QueryInterface(aContent); + if (inlineStyleContent) { + nsICSSDeclaration* decl = inlineStyleContent->Style(); + nsAutoString str; + str.AppendFloat(aOpacity); + decl->SetProperty(NS_LITERAL_STRING("opacity"), str, EmptyString()); + } +} + +bool +ScrollbarActivity::UpdateOpacity(TimeStamp aTime) +{ + // Avoid division by zero if mScrollbarFadeDuration is zero, just jump + // to the end of the fade animation + double progress = mScrollbarFadeDuration + ? ((aTime - mFadeBeginTime) / FadeDuration()) + : 1.0; + double opacity = 1.0 - std::max(0.0, std::min(1.0, progress)); + + // 'this' may be getting destroyed during SetOpacityOnElement calls. + nsWeakFrame weakFrame((do_QueryFrame(mScrollableFrame))); + SetOpacityOnElement(GetHorizontalScrollbar(), opacity); + if (!weakFrame.IsAlive()) { + return false; + } + SetOpacityOnElement(GetVerticalScrollbar(), opacity); + if (!weakFrame.IsAlive()) { + return false; + } + return true; +} + +static void +UnsetOpacityOnElement(nsIContent* aContent) +{ + nsCOMPtr<nsStyledElement> inlineStyleContent = + do_QueryInterface(aContent); + if (inlineStyleContent) { + nsICSSDeclaration* decl = inlineStyleContent->Style(); + nsAutoString dummy; + decl->RemoveProperty(NS_LITERAL_STRING("opacity"), dummy); + } +} + +bool +ScrollbarActivity::SetIsFading(bool aNewFading) +{ + if (mIsFading == aNewFading) + return true; + + mIsFading = aNewFading; + if (!mIsFading) { + mFadeBeginTime = TimeStamp(); + // 'this' may be getting destroyed during UnsetOpacityOnElement calls. + nsWeakFrame weakFrame((do_QueryFrame(mScrollableFrame))); + UnsetOpacityOnElement(GetHorizontalScrollbar()); + if (!weakFrame.IsAlive()) { + return false; + } + UnsetOpacityOnElement(GetVerticalScrollbar()); + if (!weakFrame.IsAlive()) { + return false; + } + } + return true; +} + +void +ScrollbarActivity::StartFadeBeginTimer() +{ + if (GetForceAlwaysVisiblePref()) { + return; + } + if (!mFadeBeginTimer) { + mFadeBeginTimer = do_CreateInstance("@mozilla.org/timer;1"); + } + mFadeBeginTimer->InitWithNamedFuncCallback( + FadeBeginTimerFired, this, mScrollbarFadeBeginDelay, + nsITimer::TYPE_ONE_SHOT, "ScrollbarActivity::FadeBeginTimerFired"); +} + +void +ScrollbarActivity::CancelFadeBeginTimer() +{ + if (mFadeBeginTimer) { + mFadeBeginTimer->Cancel(); + } +} + +void +ScrollbarActivity::HoveredScrollbar(nsIContent* aScrollbar) +{ + SetBooleanAttribute(GetHorizontalScrollbar(), nsGkAtoms::hover, false); + SetBooleanAttribute(GetVerticalScrollbar(), nsGkAtoms::hover, false); + SetBooleanAttribute(aScrollbar, nsGkAtoms::hover, true); +} + +nsRefreshDriver* +ScrollbarActivity::GetRefreshDriver() +{ + nsIFrame* scrollableFrame = do_QueryFrame(mScrollableFrame); + return scrollableFrame->PresContext()->RefreshDriver(); +} + +nsIContent* +ScrollbarActivity::GetScrollbarContent(bool aVertical) +{ + nsIFrame* box = mScrollableFrame->GetScrollbarBox(aVertical); + return box ? box->GetContent() : nullptr; +} + +} // namespace layout +} // namespace mozilla |