From 4cf74dfff6822b6c2aebc9c5ad02faebcb383705 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 27 Jan 2022 17:52:46 +0000 Subject: [PATCH] Viewport: Improve drag-to-scroll on devices that can accept simultaneous mouse and touch input Some Windows 11 devices have both touch screens and mouse inputs, and these can be used simultaneously. The Viewport (and ListBox) now check the input source of each mouse down. If the source is not a mouse, the viewport will always enter drag-to-scroll mode, regardless of the result of isScrollOnDragEnabled. --- examples/Utilities/InAppPurchasesDemo.h | 1 - .../juce_gui_basics/layout/juce_Viewport.cpp | 253 +++++++++--------- .../juce_gui_basics/layout/juce_Viewport.h | 32 ++- .../juce_gui_basics/widgets/juce_ListBox.cpp | 10 +- 4 files changed, 158 insertions(+), 138 deletions(-) diff --git a/examples/Utilities/InAppPurchasesDemo.h b/examples/Utilities/InAppPurchasesDemo.h index b9157457fc..9e1e1db5e9 100644 --- a/examples/Utilities/InAppPurchasesDemo.h +++ b/examples/Utilities/InAppPurchasesDemo.h @@ -499,7 +499,6 @@ public: voiceListBox.setRowHeight (66); voiceListBox.selectRow (0); voiceListBox.updateContent(); - voiceListBox.getViewport()->setScrollOnDragEnabled (true); addAndMakeVisible (phraseLabel); addAndMakeVisible (phraseListBox); diff --git a/modules/juce_gui_basics/layout/juce_Viewport.cpp b/modules/juce_gui_basics/layout/juce_Viewport.cpp index dfccd68ac9..b59de1164e 100644 --- a/modules/juce_gui_basics/layout/juce_Viewport.cpp +++ b/modules/juce_gui_basics/layout/juce_Viewport.cpp @@ -26,7 +26,132 @@ namespace juce { -Viewport::Viewport (const String& name) : Component (name) +static bool viewportWouldScrollOnEvent (const Viewport* vp, const MouseInputSource& src) noexcept +{ + if (vp != nullptr) + { + switch (vp->getScrollOnDragMode()) + { + case Viewport::ScrollOnDragMode::all: return true; + case Viewport::ScrollOnDragMode::nonHover: return ! src.canHover(); + case Viewport::ScrollOnDragMode::never: return false; + } + } + + return false; +} + +using ViewportDragPosition = AnimatedPosition; + +struct Viewport::DragToScrollListener : private MouseListener, + private ViewportDragPosition::Listener +{ + DragToScrollListener (Viewport& v) : viewport (v) + { + viewport.contentHolder.addMouseListener (this, true); + offsetX.addListener (this); + offsetY.addListener (this); + offsetX.behaviour.setMinimumVelocity (60); + offsetY.behaviour.setMinimumVelocity (60); + } + + ~DragToScrollListener() override + { + viewport.contentHolder.removeMouseListener (this); + Desktop::getInstance().removeGlobalMouseListener (this); + } + + void positionChanged (ViewportDragPosition&, double) override + { + viewport.setViewPosition (originalViewPos - Point ((int) offsetX.getPosition(), + (int) offsetY.getPosition())); + } + + void mouseDown (const MouseEvent& e) override + { + if (! isGlobalMouseListener && viewportWouldScrollOnEvent (&viewport, e.source)) + { + offsetX.setPosition (offsetX.getPosition()); + offsetY.setPosition (offsetY.getPosition()); + + // switch to a global mouse listener so we still receive mouseUp events + // if the original event component is deleted + viewport.contentHolder.removeMouseListener (this); + Desktop::getInstance().addGlobalMouseListener (this); + + isGlobalMouseListener = true; + + scrollSource = e.source; + } + } + + void mouseDrag (const MouseEvent& e) override + { + if (e.source == scrollSource + && ! doesMouseEventComponentBlockViewportDrag (e.eventComponent)) + { + auto totalOffset = e.getOffsetFromDragStart().toFloat(); + + if (! isDragging && totalOffset.getDistanceFromOrigin() > 8.0f && viewportWouldScrollOnEvent (&viewport, e.source)) + { + isDragging = true; + + originalViewPos = viewport.getViewPosition(); + offsetX.setPosition (0.0); + offsetX.beginDrag(); + offsetY.setPosition (0.0); + offsetY.beginDrag(); + } + + if (isDragging) + { + offsetX.drag (totalOffset.x); + offsetY.drag (totalOffset.y); + } + } + } + + void mouseUp (const MouseEvent& e) override + { + if (isGlobalMouseListener && e.source == scrollSource) + endDragAndClearGlobalMouseListener(); + } + + void endDragAndClearGlobalMouseListener() + { + offsetX.endDrag(); + offsetY.endDrag(); + isDragging = false; + + viewport.contentHolder.addMouseListener (this, true); + Desktop::getInstance().removeGlobalMouseListener (this); + + isGlobalMouseListener = false; + } + + bool doesMouseEventComponentBlockViewportDrag (const Component* eventComp) + { + for (auto c = eventComp; c != nullptr && c != &viewport; c = c->getParentComponent()) + if (c->getViewportIgnoreDragFlag()) + return true; + + return false; + } + + Viewport& viewport; + ViewportDragPosition offsetX, offsetY; + Point originalViewPos; + MouseInputSource scrollSource = Desktop::getInstance().getMainMouseSource(); + bool isDragging = false; + bool isGlobalMouseListener = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DragToScrollListener) +}; + +//============================================================================== +Viewport::Viewport (const String& name) + : Component (name), + dragToScrollListener (std::make_unique (*this)) { // content holder is used to clip the contents so they don't overlap the scrollbars addAndMakeVisible (contentHolder); @@ -36,14 +161,12 @@ Viewport::Viewport (const String& name) : Component (name) setInterceptsMouseClicks (false, true); setWantsKeyboardFocus (true); - setScrollOnDragEnabled (Desktop::getInstance().getMainMouseSource().isTouch()); recreateScrollbars(); } Viewport::~Viewport() { - setScrollOnDragEnabled (false); deleteOrRemoveContentComp(); } @@ -196,132 +319,14 @@ void Viewport::componentMovedOrResized (Component&, bool, bool) } //============================================================================== -typedef AnimatedPosition ViewportDragPosition; - -struct Viewport::DragToScrollListener : private MouseListener, - private ViewportDragPosition::Listener +void Viewport::setScrollOnDragMode (const ScrollOnDragMode mode) { - DragToScrollListener (Viewport& v) : viewport (v) - { - viewport.contentHolder.addMouseListener (this, true); - offsetX.addListener (this); - offsetY.addListener (this); - offsetX.behaviour.setMinimumVelocity (60); - offsetY.behaviour.setMinimumVelocity (60); - } - - ~DragToScrollListener() override - { - viewport.contentHolder.removeMouseListener (this); - Desktop::getInstance().removeGlobalMouseListener (this); - } - - void positionChanged (ViewportDragPosition&, double) override - { - viewport.setViewPosition (originalViewPos - Point ((int) offsetX.getPosition(), - (int) offsetY.getPosition())); - } - - void mouseDown (const MouseEvent& e) override - { - if (! isGlobalMouseListener) - { - offsetX.setPosition (offsetX.getPosition()); - offsetY.setPosition (offsetY.getPosition()); - - // switch to a global mouse listener so we still receive mouseUp events - // if the original event component is deleted - viewport.contentHolder.removeMouseListener (this); - Desktop::getInstance().addGlobalMouseListener (this); - - isGlobalMouseListener = true; - - scrollSource = e.source; - } - } - - void mouseDrag (const MouseEvent& e) override - { - if (e.source == scrollSource - && ! doesMouseEventComponentBlockViewportDrag (e.eventComponent)) - { - auto totalOffset = e.getOffsetFromDragStart().toFloat(); - - if (! isDragging && totalOffset.getDistanceFromOrigin() > 8.0f) - { - isDragging = true; - - originalViewPos = viewport.getViewPosition(); - offsetX.setPosition (0.0); - offsetX.beginDrag(); - offsetY.setPosition (0.0); - offsetY.beginDrag(); - } - - if (isDragging) - { - offsetX.drag (totalOffset.x); - offsetY.drag (totalOffset.y); - } - } - } - - void mouseUp (const MouseEvent& e) override - { - if (isGlobalMouseListener && e.source == scrollSource) - endDragAndClearGlobalMouseListener(); - } - - void endDragAndClearGlobalMouseListener() - { - offsetX.endDrag(); - offsetY.endDrag(); - isDragging = false; - - viewport.contentHolder.addMouseListener (this, true); - Desktop::getInstance().removeGlobalMouseListener (this); - - isGlobalMouseListener = false; - } - - bool doesMouseEventComponentBlockViewportDrag (const Component* eventComp) - { - for (auto c = eventComp; c != nullptr && c != &viewport; c = c->getParentComponent()) - if (c->getViewportIgnoreDragFlag()) - return true; - - return false; - } - - Viewport& viewport; - ViewportDragPosition offsetX, offsetY; - Point originalViewPos; - MouseInputSource scrollSource = Desktop::getInstance().getMainMouseSource(); - bool isDragging = false; - bool isGlobalMouseListener = false; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DragToScrollListener) -}; - -void Viewport::setScrollOnDragEnabled (bool shouldScrollOnDrag) -{ - if (isScrollOnDragEnabled() != shouldScrollOnDrag) - { - if (shouldScrollOnDrag) - dragToScrollListener.reset (new DragToScrollListener (*this)); - else - dragToScrollListener.reset(); - } -} - -bool Viewport::isScrollOnDragEnabled() const noexcept -{ - return dragToScrollListener != nullptr; + scrollOnDragMode = mode; } bool Viewport::isCurrentlyScrollingOnDrag() const noexcept { - return dragToScrollListener != nullptr && dragToScrollListener->isDragging; + return dragToScrollListener->isDragging; } //============================================================================== diff --git a/modules/juce_gui_basics/layout/juce_Viewport.h b/modules/juce_gui_basics/layout/juce_Viewport.h index 9738517f33..77e6700b7c 100644 --- a/modules/juce_gui_basics/layout/juce_Viewport.h +++ b/modules/juce_gui_basics/layout/juce_Viewport.h @@ -271,16 +271,39 @@ public: */ bool canScrollHorizontally() const noexcept; - /** Enables or disables drag-to-scroll functionality in the viewport. + /** Enables or disables drag-to-scroll functionality for mouse sources in the viewport. If your viewport contains a Component that you don't want to receive mouse events when the user is drag-scrolling, you can disable this with the Component::setViewportIgnoreDragFlag() method. */ - void setScrollOnDragEnabled (bool shouldScrollOnDrag); + [[deprecated ("Use setScrollOnDragMode instead.")]] + void setScrollOnDragEnabled (bool shouldScrollOnDrag) + { + setScrollOnDragMode (shouldScrollOnDrag ? ScrollOnDragMode::all : ScrollOnDragMode::never); + } - /** Returns true if drag-to-scroll functionality is enabled. */ - bool isScrollOnDragEnabled() const noexcept; + /** Returns true if drag-to-scroll functionality is enabled for mouse input sources. */ + [[deprecated ("Use getScrollOnDragMode instead.")]] + bool isScrollOnDragEnabled() const noexcept { return getScrollOnDragMode() == ScrollOnDragMode::all; } + + enum class ScrollOnDragMode + { + never, /**< Dragging will never scroll the viewport. */ + nonHover, /**< Dragging will only scroll the viewport if the input source cannot hover. */ + all /**< Dragging will always scroll the viewport. */ + }; + + /** Sets the current scroll-on-drag mode. The default is ScrollOnDragMode::nonHover. + + If your viewport contains a Component that you don't want to receive mouse events when the + user is drag-scrolling, you can disable this with the Component::setViewportIgnoreDragFlag() + method. + */ + void setScrollOnDragMode (ScrollOnDragMode scrollOnDragMode); + + /** Returns the current scroll-on-drag mode. */ + ScrollOnDragMode getScrollOnDragMode() const { return scrollOnDragMode; } /** Returns true if the user is currently dragging-to-scroll. @see setScrollOnDragEnabled @@ -320,6 +343,7 @@ private: Rectangle lastVisibleArea; int scrollBarThickness = 0; int singleStepX = 16, singleStepY = 16; + ScrollOnDragMode scrollOnDragMode = ScrollOnDragMode::nonHover; bool showHScrollbar = true, showVScrollbar = true, deleteContent = true; bool customScrollBarThickness = false; bool allowScrollingWithoutScrollbarV = false, allowScrollingWithoutScrollbarH = false; diff --git a/modules/juce_gui_basics/widgets/juce_ListBox.cpp b/modules/juce_gui_basics/widgets/juce_ListBox.cpp index 5c98a310e1..0d488e725a 100644 --- a/modules/juce_gui_basics/widgets/juce_ListBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ListBox.cpp @@ -107,14 +107,6 @@ public: m->listBoxItemClicked (row, e); } - bool isInDragToScrollViewport() const noexcept - { - if (auto* vp = owner.getViewport()) - return vp->isScrollOnDragEnabled() && (vp->canScrollVertically() || vp->canScrollHorizontally()); - - return false; - } - void mouseDown (const MouseEvent& e) override { isDragging = false; @@ -123,7 +115,7 @@ public: if (isEnabled()) { - if (owner.selectOnMouseDown && ! (isSelected || isInDragToScrollViewport())) + if (owner.selectOnMouseDown && ! isSelected && ! viewportWouldScrollOnEvent (owner.getViewport(), e.source)) performSelection (e, false); else selectRowOnMouseUp = true;