Apple-like fading-out scrollbars


#1

I have made small changes to the current juce tip to implement scrollbars that automatically disappear after a time interval since the last moment they were moved/used (i.e. like they now behave on MacOS X Lion, Mountain Lion and iOS).

Basically I added a setFadeOut method to ScrollBar that takes the amount of time (in milliseconds) after which the scrollbar should disappear, then tweaked the existing code to nicely incorporate this feature.

In addition, since such scrollbars are useful in order not to take away room in a viewport, I have made the necessary changes in the Viewport class for them to be overlapped to the content when they have a fade out time.

Below is the code for the patch that does this all:

From a549951e976ccfd410fb843975bcb69f963c0410 Mon Sep 17 00:00:00 2001
From: fmarverti@almateq.com
Date: Wed, 17 Oct 2012 12:54:31 +0200
Subject: [PATCH] fading out scrollbars

---
 .../juce_gui_basics/layout/juce_ScrollBar.cpp      | 58 +++++++++++++++++-----
 .../juce_gui_basics/layout/juce_ScrollBar.h        | 10 +++-
 .../juce_gui_basics/layout/juce_Viewport.cpp       | 24 ++++++---
 3 files changed, 70 insertions(+), 22 deletions(-)

diff --git a/juce/modules/juce_gui_basics/layout/juce_ScrollBar.cpp b/juce/modules/juce_gui_basics/layout/juce_ScrollBar.cpp
index c8458e5..675b57d 100644
--- a/juce/modules/juce_gui_basics/layout/juce_ScrollBar.cpp
+++ b/juce/modules/juce_gui_basics/layout/juce_ScrollBar.cpp
@@ -23,6 +23,13 @@
   ==============================================================================
 */
 
+enum
+{
+    timer_autorepeat = 1,
+    timer_autohide,
+};
+
+
 class ScrollBar::ScrollbarButton  : public Button
 {
 public:
@@ -66,7 +73,8 @@ ScrollBar::ScrollBar (const bool vertical_)
       minimumDelayInMillisecs (10),
       vertical (vertical_),
       isDraggingThumb (false),
-      autohides (true)
+      autohides (true),
+      fadeOutMilliseconds (0)
 {
     setRepaintsOnMouseActivity (true);
     setFocusContainer (true);
@@ -99,6 +107,9 @@ bool ScrollBar::setCurrentRange (const Range<double>& newRange)
 {
     const Range<double> constrainedRange (totalRange.constrainRange (newRange));
 
+    if (fadeOutMilliseconds > 0)
+        startTimer (timer_autohide, fadeOutMilliseconds);
+
     if (visibleRange != constrainedRange)
     {
         visibleRange = constrainedRange;
@@ -243,6 +254,17 @@ bool ScrollBar::autoHides() const noexcept
     return autohides;
 }
 
+void ScrollBar::setFadesOut (int milliseconds)
+{
+    fadeOutMilliseconds = milliseconds;
+    updateThumbPosition();
+}
+
+bool ScrollBar::fadesOut () const noexcept
+{
+    return (fadeOutMilliseconds > 0);
+}
+
 //==============================================================================
 void ScrollBar::paint (Graphics& g)
 {
@@ -334,12 +356,12 @@ void ScrollBar::mouseDown (const MouseEvent& e)
     if (dragStartMousePos < thumbStart)
     {
         moveScrollbarInPages (-1);
-        startTimer (400);
+        startTimer (timer_autorepeat, 400);
     }
     else if (dragStartMousePos >= thumbStart + thumbSize)
     {
         moveScrollbarInPages (1);
-        startTimer (400);
+        startTimer (timer_autorepeat, 400);
     }
     else
     {
@@ -367,7 +389,7 @@ void ScrollBar::mouseDrag (const MouseEvent& e)
 void ScrollBar::mouseUp (const MouseEvent&)
 {
     isDraggingThumb = false;
-    stopTimer();
+    stopTimer(timer_autorepeat);
     repaint();
 }
 
@@ -383,20 +405,32 @@ void ScrollBar::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& whee
     setCurrentRange (visibleRange - singleStepSize * increment);
 }
 
-void ScrollBar::timerCallback()
+void ScrollBar::timerCallback(int timerId)
 {
-    if (isMouseButtonDown())
+    if (timerId == timer_autorepeat)
     {
-        startTimer (40);
+        if (isMouseButtonDown())
+        {
+            startTimer (timerId, 40);
 
-        if (lastMousePos < thumbStart)
-            setCurrentRange (visibleRange - visibleRange.getLength());
-        else if (lastMousePos > thumbStart + thumbSize)
-            setCurrentRangeStart (visibleRange.getEnd());
+            if (lastMousePos < thumbStart)
+                setCurrentRange (visibleRange - visibleRange.getLength());
+            else if (lastMousePos > thumbStart + thumbSize)
+                setCurrentRangeStart (visibleRange.getEnd());
+        }
+        else
+        {
+            stopTimer (timerId);
+        }
+    }
+    else if (timerId == timer_autohide)
+    {
+        setVisible (false);
+        stopTimer (timerId);
     }
     else
     {
-        stopTimer();
+        jassertfalse;
     }
 }
 
diff --git a/juce/modules/juce_gui_basics/layout/juce_ScrollBar.h b/juce/modules/juce_gui_basics/layout/juce_ScrollBar.h
index 9c791d9..9b16c0a 100644
--- a/juce/modules/juce_gui_basics/layout/juce_ScrollBar.h
+++ b/juce/modules/juce_gui_basics/layout/juce_ScrollBar.h
@@ -51,7 +51,7 @@ class Viewport;
 */
 class JUCE_API  ScrollBar  : public Component,
                              private AsyncUpdater,
-                             private Timer
+                             private MultiTimer
 {
 public:
     //==============================================================================
@@ -91,6 +91,11 @@ public:
     */
     bool autoHides() const noexcept;
 
+    void setFadesOut (int milliseconds);
+
+    bool fadesOut() const noexcept;
+
+
     //==============================================================================
     /** Sets the minimum and maximum values that the bar will move between.
 
@@ -317,10 +322,11 @@ private:
     friend class ScopedPointer<ScrollbarButton>;
     ScopedPointer<ScrollbarButton> upButton, downButton;
     ListenerList <Listener> listeners;
+    int fadeOutMilliseconds;
 
     void handleAsyncUpdate();
     void updateThumbPosition();
-    void timerCallback();
+    void timerCallback(int timerId);
 
     friend class Viewport;
 
diff --git a/juce/modules/juce_gui_basics/layout/juce_Viewport.cpp b/juce/modules/juce_gui_basics/layout/juce_Viewport.cpp
index 889f558..092b536 100644
--- a/juce/modules/juce_gui_basics/layout/juce_Viewport.cpp
+++ b/juce/modules/juce_gui_basics/layout/juce_Viewport.cpp
@@ -185,6 +185,9 @@ void Viewport::updateVisibleArea()
     const bool canShowHBar = showHScrollbar && canShowAnyBars;
     const bool canShowVBar = showVScrollbar && canShowAnyBars;
 
+    const bool overlapHBar = horizontalScrollBar.fadesOut();
+    const bool overlapVBar = verticalScrollBar.fadesOut();
+
     bool hBarVisible = false, vBarVisible = false;
     Rectangle<int> contentArea;
 
@@ -199,10 +202,10 @@ void Viewport::updateVisibleArea()
             hBarVisible = canShowHBar && (hBarVisible || contentComp->getX() < 0 || contentComp->getRight() > contentArea.getWidth());
             vBarVisible = canShowVBar && (vBarVisible || contentComp->getY() < 0 || contentComp->getBottom() > contentArea.getHeight());
 
-            if (vBarVisible)
+            if (vBarVisible && !overlapVBar)
                 contentArea.setWidth (getWidth() - scrollbarWidth);
 
-            if (hBarVisible)
+            if (hBarVisible && !overlapHBar)
                 contentArea.setHeight (getHeight() - scrollbarWidth);
 
             if (! contentArea.contains (contentComp->getBounds()))
@@ -212,8 +215,11 @@ void Viewport::updateVisibleArea()
             }
         }
 
-        if (vBarVisible)  contentArea.setWidth  (getWidth()  - scrollbarWidth);
-        if (hBarVisible)  contentArea.setHeight (getHeight() - scrollbarWidth);
+        if (vBarVisible && !overlapVBar)
+            contentArea.setWidth  (getWidth()  - scrollbarWidth);
+
+        if (hBarVisible && !overlapHBar)
+            contentArea.setHeight (getHeight() - scrollbarWidth);
 
         if (contentComp == nullptr)
         {
@@ -237,7 +243,8 @@ void Viewport::updateVisibleArea()
 
     if (hBarVisible)
     {
-        horizontalScrollBar.setBounds (0, contentArea.getHeight(), contentArea.getWidth(), scrollbarWidth);
+        horizontalScrollBar.toFront (false);
+        horizontalScrollBar.setBounds (0, overlapHBar ? (getHeight() - scrollbarWidth) : contentArea.getHeight(), contentArea.getWidth(), scrollbarWidth);
         horizontalScrollBar.setRangeLimits (0.0, contentBounds.getWidth());
         horizontalScrollBar.setCurrentRange (visibleOrigin.x, contentArea.getWidth());
         horizontalScrollBar.setSingleStepSize (singleStepX);
@@ -250,7 +257,8 @@ void Viewport::updateVisibleArea()
 
     if (vBarVisible)
     {
-        verticalScrollBar.setBounds (contentArea.getWidth(), 0, scrollbarWidth, contentArea.getHeight());
+        verticalScrollBar.toFront (false);
+        verticalScrollBar.setBounds (overlapVBar ? (getWidth() - scrollbarWidth) : contentArea.getWidth(), 0, scrollbarWidth, contentArea.getHeight());
         verticalScrollBar.setRangeLimits (0.0, contentBounds.getHeight());
         verticalScrollBar.setCurrentRange (visibleOrigin.y, contentArea.getHeight());
         verticalScrollBar.setSingleStepSize (singleStepY);
@@ -352,8 +360,8 @@ bool Viewport::useMouseWheelMoveIfNeeded (const MouseEvent& e, const MouseWheelD
 {
     if (! (e.mods.isAltDown() || e.mods.isCtrlDown()))
     {
-        const bool hasVertBar = verticalScrollBar.isVisible();
-        const bool hasHorzBar = horizontalScrollBar.isVisible();
+        const bool hasVertBar = verticalScrollBar.isVisible() || verticalScrollBar.fadesOut ();
+        const bool hasHorzBar = horizontalScrollBar.isVisible() || horizontalScrollBar.fadesOut();
 
         if (hasHorzBar || hasVertBar)
         {
-- 
1.7.12


#2

That’s cool (and I’d been meaning to do something similar myself) but for me to add it to the codebase, it’d really need to be done via the LookAndFeel - subtle appearance options like that don’t belong in the Scrollbar class itself…


#3

As a side note, as a user of a desktop, I hate that system. I want scrollbars, and I have the screen space for them. For anything with a direct touch surface (not a trackpad etc.) they make some sense.

So please - definitely not the default behavior.

Bruce


#4

Absolutely not meant as default behaviour!

And Jules, regarding implementing this in the LookAndFeel, that could be an idea but it seems like overcomplicating things… The simliar thing of having the scrollbars disappear when the whole range is visible is not in the look and feel either, and it feels right so: it’d be a pain to subclass the look and feel just to enable/disable both these features


#5

Oh, great. I made something like this a while ago, it would be really nice to see these changes in the LookAndFeel stuff though.