Skip to content

Instantly share code, notes, and snippets.

@dikadk
Created September 30, 2020 13:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dikadk/3e68cd7c7c024a38de54da524613a5e8 to your computer and use it in GitHub Desktop.
Save dikadk/3e68cd7c7c024a38de54da524613a5e8 to your computer and use it in GitHub Desktop.
juce_ios_viewport (native ios scrollview)
/*
This file is part of the JUCE library - "Jules’ Utility Class Extensions"
Copyright 2004-11 by Raw Material Software Ltd.
JUCE can be redistributed and/or modified under the terms of the GNU General
Public License (Version 2), as published by the Free Software Foundation.
A copy of the license is included in the JUCE distribution, or can be found
online at www.gnu.org/licenses.
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.rawmaterialsoftware.com/juce for more information.
==============================================================================
*/
#include "ios_viewport.h"
#if !JUCE_IOS
//==============================================================================
Viewport::Viewport (const juce::String& name) : juce::Component (name)
{
// content holder is used to clip the contents so they don't overlap the scrollbars
addAndMakeVisible (contentHolder);
contentHolder.setInterceptsMouseClicks (false, true);
scrollBarThickness = getLookAndFeel().getDefaultScrollbarWidth();
setInterceptsMouseClicks (false, true);
setWantsKeyboardFocus (true);
setScrollOnDragEnabled (juce::Desktop::getInstance().getMainMouseSource().isTouch());
recreateScrollbars();
}
Viewport::~Viewport()
{
setScrollOnDragEnabled (false);
deleteOrRemoveContentComp();
}
//==============================================================================
void Viewport::visibleAreaChanged (const juce::Rectangle<int>&) {}
void Viewport::viewedComponentChanged (juce::Component*) {}
//==============================================================================
void Viewport::deleteOrRemoveContentComp()
{
if (contentComp != nullptr)
{
contentComp->removeComponentListener (this);
if (deleteContent)
{
// This sets the content comp to a null pointer before deleting the old one, in case
// anything tries to use the old one while it's in mid-deletion..
std::unique_ptr<juce::Component> oldCompDeleter (contentComp.get());
contentComp = nullptr;
}
else
{
contentHolder.removeChildComponent (contentComp);
contentComp = nullptr;
}
}
}
void Viewport::setViewedComponent (juce::Component* const newViewedComponent, const bool deleteComponentWhenNoLongerNeeded)
{
if (contentComp.get() != newViewedComponent)
{
deleteOrRemoveContentComp();
contentComp = newViewedComponent;
deleteContent = deleteComponentWhenNoLongerNeeded;
if (contentComp != nullptr)
{
contentHolder.addAndMakeVisible (contentComp);
setViewPosition (juce::Point<int>());
contentComp->addComponentListener (this);
}
viewedComponentChanged (contentComp);
updateVisibleArea();
}
}
void Viewport::recreateScrollbars()
{
verticalScrollBar.reset();
horizontalScrollBar.reset();
verticalScrollBar .reset (createScrollBarComponent (true));
horizontalScrollBar.reset (createScrollBarComponent (false));
addChildComponent (verticalScrollBar.get());
addChildComponent (horizontalScrollBar.get());
getVerticalScrollBar().addListener (this);
getHorizontalScrollBar().addListener (this);
resized();
}
int Viewport::getMaximumVisibleWidth() const { return contentHolder.getWidth(); }
int Viewport::getMaximumVisibleHeight() const { return contentHolder.getHeight(); }
bool Viewport::canScrollVertically() const noexcept { return contentComp->getY() < 0 || contentComp->getBottom() > getHeight(); }
bool Viewport::canScrollHorizontally() const noexcept { return contentComp->getX() < 0 || contentComp->getRight() > getWidth(); }
juce::Point<int> Viewport::viewportPosToCompPos (juce::Point<int> pos) const
{
jassert (contentComp != nullptr);
auto contentBounds = contentHolder.getLocalArea (contentComp.get(), contentComp->getLocalBounds());
juce::Point<int> p (juce::jmax (juce::jmin (0, contentHolder.getWidth() - contentBounds.getWidth()), juce::jmin (0, -(pos.x))),
juce::jmax (juce::jmin (0, contentHolder.getHeight() - contentBounds.getHeight()), juce::jmin (0, -(pos.y))));
return p.transformedBy (contentComp->getTransform().inverted());
}
void Viewport::setViewPosition (const int xPixelsOffset, const int yPixelsOffset)
{
setViewPosition ({ xPixelsOffset, yPixelsOffset });
}
void Viewport::setViewPosition(const juce::Point<int> &newPosition) {
if (contentComp != nullptr)
contentComp->setTopLeftPosition (viewportPosToCompPos (newPosition));
}
void Viewport::setViewPositionProportionately (const double x, const double y)
{
if (contentComp != nullptr)
setViewPosition (juce::jmax (0, juce::roundToInt (x * (contentComp->getWidth() - getWidth()))),
juce::jmax (0, juce::roundToInt (y * (contentComp->getHeight() - getHeight()))));
}
bool Viewport::autoScroll (const int mouseX, const int mouseY, const int activeBorderThickness, const int maximumSpeed)
{
if (contentComp != nullptr)
{
int dx = 0, dy = 0;
if (getHorizontalScrollBar().isVisible() || canScrollHorizontally())
{
if (mouseX < activeBorderThickness)
dx = activeBorderThickness - mouseX;
else if (mouseX >= contentHolder.getWidth() - activeBorderThickness)
dx = (contentHolder.getWidth() - activeBorderThickness) - mouseX;
if (dx < 0)
dx = juce::jmax (dx, -maximumSpeed, contentHolder.getWidth() - contentComp->getRight());
else
dx = juce::jmin (dx, maximumSpeed, -contentComp->getX());
}
if (getVerticalScrollBar().isVisible() || canScrollVertically())
{
if (mouseY < activeBorderThickness)
dy = activeBorderThickness - mouseY;
else if (mouseY >= contentHolder.getHeight() - activeBorderThickness)
dy = (contentHolder.getHeight() - activeBorderThickness) - mouseY;
if (dy < 0)
dy = juce::jmax (dy, -maximumSpeed, contentHolder.getHeight() - contentComp->getBottom());
else
dy = juce::jmin (dy, maximumSpeed, -contentComp->getY());
}
if (dx != 0 || dy != 0)
{
contentComp->setTopLeftPosition (contentComp->getX() + dx,
contentComp->getY() + dy);
return true;
}
}
return false;
}
void Viewport::componentMovedOrResized (juce::Component&, bool, bool)
{
updateVisibleArea();
}
//==============================================================================
typedef juce::AnimatedPosition<juce::AnimatedPositionBehaviours::ContinuousWithMomentum> ViewportDragPosition;
struct Viewport::DragToScrollListener : private juce::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);
juce::Desktop::getInstance().removeGlobalMouseListener (this);
}
void positionChanged (ViewportDragPosition&, double) override
{
viewport.setViewPosition (originalViewPos - juce::Point<int> ((int) offsetX.getPosition(),
(int) offsetY.getPosition()));
}
void mouseDown (const juce::MouseEvent&) 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);
juce::Desktop::getInstance().addGlobalMouseListener (this);
isGlobalMouseListener = true;
}
}
void mouseDrag (const juce::MouseEvent& e) override
{
if (juce::Desktop::getInstance().getNumDraggingMouseSources() == 1 && ! 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 juce::MouseEvent&) override
{
if (isGlobalMouseListener && juce::Desktop::getInstance().getNumDraggingMouseSources() == 0)
endDragAndClearGlobalMouseListener();
}
void endDragAndClearGlobalMouseListener()
{
offsetX.endDrag();
offsetY.endDrag();
isDragging = false;
viewport.contentHolder.addMouseListener (this, true);
juce::Desktop::getInstance().removeGlobalMouseListener (this);
isGlobalMouseListener = false;
}
bool doesMouseEventComponentBlockViewportDrag (const juce::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;
juce::Point<int> originalViewPos;
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;
}
bool Viewport::isCurrentlyScrollingOnDrag() const noexcept
{
return dragToScrollListener != nullptr && dragToScrollListener->isDragging;
}
//==============================================================================
void Viewport::lookAndFeelChanged()
{
if (! customScrollBarThickness)
{
scrollBarThickness = getLookAndFeel().getDefaultScrollbarWidth();
resized();
}
}
void Viewport::resized()
{
updateVisibleArea();
}
//==============================================================================
void Viewport::updateVisibleArea()
{
auto scrollbarWidth = getScrollBarThickness();
const bool canShowAnyBars = getWidth() > scrollbarWidth && getHeight() > scrollbarWidth;
const bool canShowHBar = showHScrollbar && canShowAnyBars;
const bool canShowVBar = showVScrollbar && canShowAnyBars;
bool hBarVisible = false, vBarVisible = false;
juce::Rectangle<int> contentArea;
for (int i = 3; --i >= 0;)
{
hBarVisible = canShowHBar && ! getHorizontalScrollBar().autoHides();
vBarVisible = canShowVBar && ! getVerticalScrollBar().autoHides();
contentArea = getLocalBounds();
if (contentComp != nullptr && ! contentArea.contains (contentComp->getBounds()))
{
hBarVisible = canShowHBar && (hBarVisible || contentComp->getX() < 0 || contentComp->getRight() > contentArea.getWidth());
vBarVisible = canShowVBar && (vBarVisible || contentComp->getY() < 0 || contentComp->getBottom() > contentArea.getHeight());
if (vBarVisible)
contentArea.setWidth (getWidth() - scrollbarWidth);
if (hBarVisible)
contentArea.setHeight (getHeight() - scrollbarWidth);
if (! contentArea.contains (contentComp->getBounds()))
{
hBarVisible = canShowHBar && (hBarVisible || contentComp->getRight() > contentArea.getWidth());
vBarVisible = canShowVBar && (vBarVisible || contentComp->getBottom() > contentArea.getHeight());
}
}
if (vBarVisible) contentArea.setWidth (getWidth() - scrollbarWidth);
if (hBarVisible) contentArea.setHeight (getHeight() - scrollbarWidth);
if (! vScrollbarRight && vBarVisible)
contentArea.setX (scrollbarWidth);
if (! hScrollbarBottom && hBarVisible)
contentArea.setY (scrollbarWidth);
if (contentComp == nullptr)
{
contentHolder.setBounds (contentArea);
break;
}
auto oldContentBounds = contentComp->getBounds();
contentHolder.setBounds (contentArea);
// If the content has changed its size, that might affect our scrollbars, so go round again and re-calculate..
if (oldContentBounds == contentComp->getBounds())
break;
}
juce::Rectangle<int> contentBounds;
if (auto cc = contentComp.get())
contentBounds = contentHolder.getLocalArea (cc, cc->getLocalBounds());
auto visibleOrigin = -contentBounds.getPosition();
auto& hbar = getHorizontalScrollBar();
auto& vbar = getVerticalScrollBar();
hbar.setBounds (contentArea.getX(), hScrollbarBottom ? contentArea.getHeight() : 0, contentArea.getWidth(), scrollbarWidth);
hbar.setRangeLimits (0.0, contentBounds.getWidth());
hbar.setCurrentRange (visibleOrigin.x, contentArea.getWidth());
hbar.setSingleStepSize (singleStepX);
if (canShowHBar && ! hBarVisible)
visibleOrigin.setX (0);
vbar.setBounds (vScrollbarRight ? contentArea.getWidth() : 0, contentArea.getY(), scrollbarWidth, contentArea.getHeight());
vbar.setRangeLimits (0.0, contentBounds.getHeight());
vbar.setCurrentRange (visibleOrigin.y, contentArea.getHeight());
vbar.setSingleStepSize (singleStepY);
if (canShowVBar && ! vBarVisible)
visibleOrigin.setY (0);
// Force the visibility *after* setting the ranges to avoid flicker caused by edge conditions in the numbers.
hbar.setVisible (hBarVisible);
vbar.setVisible (vBarVisible);
if (contentComp != nullptr)
{
auto newContentCompPos = viewportPosToCompPos (visibleOrigin);
if (contentComp->getBounds().getPosition() != newContentCompPos)
{
contentComp->setTopLeftPosition (newContentCompPos); // (this will re-entrantly call updateVisibleArea again)
return;
}
}
const juce::Rectangle<int> visibleArea (visibleOrigin.x, visibleOrigin.y,
juce::jmin (contentBounds.getWidth() - visibleOrigin.x, contentArea.getWidth()),
juce::jmin (contentBounds.getHeight() - visibleOrigin.y, contentArea.getHeight()));
if (lastVisibleArea != visibleArea)
{
lastVisibleArea = visibleArea;
visibleAreaChanged (visibleArea);
}
hbar.handleUpdateNowIfNeeded();
vbar.handleUpdateNowIfNeeded();
}
//==============================================================================
void Viewport::setSingleStepSizes (const int stepX, const int stepY)
{
if (singleStepX != stepX || singleStepY != stepY)
{
singleStepX = stepX;
singleStepY = stepY;
updateVisibleArea();
}
}
void Viewport::setScrollBarsShown (const bool showVerticalScrollbarIfNeeded,
const bool showHorizontalScrollbarIfNeeded,
const bool allowVerticalScrollingWithoutScrollbar,
const bool allowHorizontalScrollingWithoutScrollbar)
{
allowScrollingWithoutScrollbarV = allowVerticalScrollingWithoutScrollbar;
allowScrollingWithoutScrollbarH = allowHorizontalScrollingWithoutScrollbar;
if (showVScrollbar != showVerticalScrollbarIfNeeded
|| showHScrollbar != showHorizontalScrollbarIfNeeded)
{
showVScrollbar = showVerticalScrollbarIfNeeded;
showHScrollbar = showHorizontalScrollbarIfNeeded;
updateVisibleArea();
}
}
void Viewport::setScrollBarThickness (const int thickness)
{
int newThickness;
// To stay compatible with the previous code: use the
// default thickness if thickness parameter is zero
// or negative
if (thickness <= 0)
{
customScrollBarThickness = false;
newThickness = getLookAndFeel().getDefaultScrollbarWidth();
}
else
{
customScrollBarThickness = true;
newThickness = thickness;
}
if (scrollBarThickness != newThickness)
{
scrollBarThickness = newThickness;
updateVisibleArea();
}
}
int Viewport::getScrollBarThickness() const
{
return scrollBarThickness;
}
void Viewport::scrollBarMoved (juce::ScrollBar* scrollBarThatHasMoved, double newRangeStart)
{
auto newRangeStartInt = juce::roundToInt (newRangeStart);
if (scrollBarThatHasMoved == horizontalScrollBar.get())
{
setViewPosition (newRangeStartInt, getViewPositionY());
}
else if (scrollBarThatHasMoved == verticalScrollBar.get())
{
setViewPosition (getViewPositionX(), newRangeStartInt);
}
}
void Viewport::mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)
{
if (! useMouseWheelMoveIfNeeded (e, wheel))
juce::Component::mouseWheelMove (e, wheel);
}
static int rescaleMouseWheelDistance (float distance, int singleStepSize) noexcept
{
if (distance == 0.0f)
return 0;
distance *= 14.0f * (float) singleStepSize;
return juce::roundToInt (distance < 0 ? juce::jmin (distance, -1.0f)
: juce::jmax (distance, 1.0f));
}
bool Viewport::useMouseWheelMoveIfNeeded (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)
{
if (! (e.mods.isAltDown() || e.mods.isCtrlDown() || e.mods.isCommandDown()))
{
const bool canScrollVert = (allowScrollingWithoutScrollbarV || getVerticalScrollBar().isVisible());
const bool canScrollHorz = (allowScrollingWithoutScrollbarH || getHorizontalScrollBar().isVisible());
if (canScrollHorz || canScrollVert)
{
auto deltaX = rescaleMouseWheelDistance (wheel.deltaX, singleStepX);
auto deltaY = rescaleMouseWheelDistance (wheel.deltaY, singleStepY);
auto pos = getViewPosition();
if (deltaX != 0 && deltaY != 0 && canScrollHorz && canScrollVert)
{
pos.x -= deltaX;
pos.y -= deltaY;
}
else if (canScrollHorz && (deltaX != 0 || e.mods.isShiftDown() || ! canScrollVert))
{
pos.x -= deltaX != 0 ? deltaX : deltaY;
}
else if (canScrollVert && deltaY != 0)
{
pos.y -= deltaY;
}
if (pos != getViewPosition())
{
setViewPosition (pos);
return true;
}
}
}
return false;
}
static bool isUpDownKeyPress (const juce::KeyPress& key)
{
return key == juce::KeyPress::upKey
|| key == juce::KeyPress::downKey
|| key == juce::KeyPress::pageUpKey
|| key == juce::KeyPress::pageDownKey
|| key == juce::KeyPress::homeKey
|| key == juce::KeyPress::endKey;
}
static bool isLeftRightKeyPress (const juce::KeyPress& key)
{
return key == juce::KeyPress::leftKey
|| key == juce::KeyPress::rightKey;
}
bool Viewport::keyPressed (const juce::KeyPress& key)
{
const bool isUpDownKey = isUpDownKeyPress (key);
if (getVerticalScrollBar().isVisible() && isUpDownKey)
return getVerticalScrollBar().keyPressed (key);
const bool isLeftRightKey = isLeftRightKeyPress (key);
if (getHorizontalScrollBar().isVisible() && (isUpDownKey || isLeftRightKey))
return getHorizontalScrollBar().keyPressed (key);
return false;
}
bool Viewport::respondsToKey (const juce::KeyPress& key)
{
return isUpDownKeyPress (key) || isLeftRightKeyPress (key);
}
juce::ScrollBar* Viewport::createScrollBarComponent (bool isVertical)
{
return new juce::ScrollBar (isVertical);
}
void Viewport::setScrollBarButtonVisibility(bool buttonsVisible) {
}
void Viewport::setScrollBarPosition (bool verticalScrollbarOnRight,
bool horizontalScrollbarAtBottom)
{
vScrollbarRight = verticalScrollbarOnRight;
hScrollbarBottom = horizontalScrollbarAtBottom;
resized();
}
#endif
#include <JuceHeader.h>
//==============================================================================
/*
A Viewport is used to contain a larger child component, and allows the child
to be automatically scrolled around.
To use a Viewport, just create one and set the component that goes inside it
using the setViewedComponent() method. When the child component changes size,
the Viewport will adjust its scrollbars accordingly.
A subclass of the viewport can be created which will receive calls to its
visibleAreaChanged() method when the subcomponent changes position or size.
*/
class Viewport : public juce::Component,
private juce::ComponentListener,
private juce::ScrollBar::Listener {
public:
//==============================================================================
/* Creates a Viewport.
The viewport is initially empty - use the setViewedComponent() method to
add a child component for it to manage.
*/
explicit Viewport(const juce::String &componentName = juce::String());
/** Destructor. */
~Viewport();
//==============================================================================
/** Sets the component that this viewport will contain and scroll around.
This will add the given component to this Viewport and position it at (0, 0).
(Don't add or remove any child components directly using the normal
Component::addChildComponent() methods).
@param newViewedComponent the component to add to this viewport, or null to remove
the current component.
@param deleteComponentWhenNoLongerNeeded if true, the component will be deleted
automatically when the viewport is deleted or when a different
component is added. If false, the caller must manage the lifetime
of the component
@see getViewedComponent
*/
void setViewedComponent(juce::Component *newViewedComponent,
bool deleteComponentWhenNoLongerNeeded = true);
/** Returns the component that's currently being used inside the Viewport.
@see setViewedComponent
*/
juce::Component *getViewedComponent() const noexcept { return contentComp.get(); }
//==============================================================================
/** Changes the position of the viewed component.
The inner component will be moved so that the pixel at the top left of
the viewport will be the pixel at position (xPixelsOffset, yPixelsOffset)
within the inner component.
This will update the scrollbars and might cause a call to visibleAreaChanged().
@see getViewPositionX, getViewPositionY, setViewPositionProportionately
*/
void setViewPosition(int xPixelsOffset, int yPixelsOffset);
/** Changes the position of the viewed component.
The inner component will be moved so that the pixel at the top left of
the viewport will be the pixel at the specified coordinates within the
inner component.
This will update the scrollbars and might cause a call to visibleAreaChanged().
@see getViewPositionX, getViewPositionY, setViewPositionProportionately
*/
void setViewPosition(const juce::Point<int> &newPosition);
/** Changes the view position as a proportion of the distance it can move.
The values here are from 0.0 to 1.0 - where (0, 0) would put the
visible area in the top-left, and (1, 1) would put it as far down and
to the right as it's possible to go whilst keeping the child component
on-screen.
*/
void setViewPositionProportionately(double proportionX, double proportionY);
/** If the specified position is at the edges of the viewport, this method scrolls
the viewport to bring that position nearer to the centre.
Call this if you're dragging an object inside a viewport and want to make it scroll
when the user approaches an edge. You might also find Component::beginDragAutoRepeat()
useful when auto-scrolling.
@param mouseX the x position, relative to the Viewport's top-left
@param mouseY the y position, relative to the Viewport's top-left
@param distanceFromEdge specifies how close to an edge the position needs to be
before the viewport should scroll in that direction
@param maximumSpeed the maximum number of pixels that the viewport is allowed
to scroll by.
@returns true if the viewport was scrolled
*/
bool autoScroll(int mouseX, int mouseY, int distanceFromEdge, int maximumSpeed);
/** Returns the position within the child component of the top-left of its visible area.
*/
const juce::Point<int> &getViewPosition() const noexcept { return lastVisibleArea.getPosition(); }
/** Returns the position within the child component of the top-left of its visible area.
@see getViewWidth, setViewPosition
*/
int getViewPositionX() const noexcept { return lastVisibleArea.getX(); }
/** Returns the position within the child component of the top-left of its visible area.
@see getViewHeight, setViewPosition
*/
int getViewPositionY() const noexcept { return lastVisibleArea.getY(); }
/** Returns the width of the visible area of the child component.
This may be less than the width of this Viewport if there's a vertical scrollbar
or if the child component is itself smaller.
*/
int getViewWidth() const noexcept { return lastVisibleArea.getWidth(); }
/** Returns the height of the visible area of the child component.
This may be less than the height of this Viewport if there's a horizontal scrollbar
or if the child component is itself smaller.
*/
int getViewHeight() const noexcept { return lastVisibleArea.getHeight(); }
/** Returns the width available within this component for the contents.
This will be the width of the viewport component minus the width of a
vertical scrollbar (if visible).
*/
int getMaximumVisibleWidth() const;
/** Returns the height available within this component for the contents.
This will be the height of the viewport component minus the space taken up
by a horizontal scrollbar (if visible).
*/
int getMaximumVisibleHeight() const;
//==============================================================================
/** Callback method that is called when the visible area changes.
This will be called when the visible area is moved either be scrolling or
by calls to setViewPosition(), etc.
*/
virtual void visibleAreaChanged(const juce::Rectangle<int> &newVisibleArea);
/** Callback method that is called when the viewed component is added, removed or swapped. */
virtual void viewedComponentChanged(Component *newComponent);
//==============================================================================
/** Turns scrollbars on or off.
If set to false, the scrollbars won't ever appear. When true (the default)
they will appear only when needed.
*/
void setScrollBarsShown(bool showVerticalScrollbarIfNeeded,
bool showHorizontalScrollbarIfNeeded,
bool allowVerticalScrollingWithoutScrollbar = false,
bool allowHorizontalScrollingWithoutScrollbar = false);
/** Changes where the scroll bars are positioned
If verticalScrollbarOnRight is set to true, then the vertical scrollbar will
appear on the right side of the view port's content (this is the default),
otherwise it will be on the left side of the content.
If horizontalScrollbarAtBottom is set to true, then the horizontal scrollbar
will appear at the bottom of the view port's content (this is the default),
otherwise it will be at the top.
*/
void setScrollBarPosition (bool verticalScrollbarOnRight,
bool horizontalScrollbarAtBottom);
/** True if the vertical scrollbar will appear on the right side of the content */
bool isVerticalScrollbarOnTheRight() const noexcept { return vScrollbarRight; }
/** True if the horizontal scrollbar will appear at the bottom of the content */
bool isHorizontalScrollbarAtBottom() const noexcept { return hScrollbarBottom; }
/** True if the vertical scrollbar is enabled.
@see setScrollBarsShown
*/
bool isVerticalScrollBarShown() const noexcept { return showVScrollbar; }
/** True if the horizontal scrollbar is enabled.
@see setScrollBarsShown
*/
bool isHorizontalScrollBarShown() const noexcept { return showHScrollbar; }
/** Changes the width of the scrollbars.
If this isn't specified, the default width from the LookAndFeel class will be used.
@see LookAndFeel::getDefaultScrollbarWidth
*/
void setScrollBarThickness(int thickness);
/** Returns the thickness of the scrollbars.
@see setScrollBarThickness
*/
int getScrollBarThickness() const;
/** Changes the distance that a single-step click on a scrollbar button
will move the viewport.
*/
void setSingleStepSizes(int stepX, int stepY);
/** Shows or hides the buttons on any scrollbars that are used.
@see ScrollBar::setButtonVisibility
*/
void setScrollBarButtonVisibility(bool buttonsVisible);
/** Returns a pointer to the scrollbar component being used.
Handy if you need to customise the bar somehow.
*/
juce::ScrollBar &getVerticalScrollBar() noexcept { return *verticalScrollBar; }
/** Returns a pointer to the scrollbar component being used.
Handy if you need to customise the bar somehow.
*/
juce::ScrollBar &getHorizontalScrollBar() noexcept { return *horizontalScrollBar; }
/** Re-instantiates the scrollbars, which is only really useful if you've overridden createScrollBarComponent(). */
void recreateScrollbars();
/** True if there's any off-screen content that could be scrolled vertically,
or false if everything is currently visible.
*/
bool canScrollVertically() const noexcept;
/** True if there's any off-screen content that could be scrolled horizontally,
or false if everything is currently visible.
*/
bool canScrollHorizontally() const noexcept;
/** Enables or disables drag-to-scroll functionality 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);
/** Returns true if drag-to-scroll functionality is enabled. */
bool isScrollOnDragEnabled() const noexcept;
/** Returns true if the user is currently dragging-to-scroll.
@see setScrollOnDragEnabled
*/
bool isCurrentlyScrollingOnDrag() const noexcept;
//==============================================================================
/** @internal */
void resized() override;
/** @internal */
void scrollBarMoved(juce::ScrollBar *scrollBarThatHasMoved, double newRangeStart) override;
/** @internal */
void mouseWheelMove(const juce::MouseEvent &e, const juce::MouseWheelDetails& wheel) override;
/** @internal */
bool keyPressed(const juce::KeyPress &key) override;
/** @internal */
void componentMovedOrResized(juce::Component &component, bool wasMoved, bool wasResized) override;
/** @internal */
void lookAndFeelChanged() override;
/** @internal */
bool useMouseWheelMoveIfNeeded(const juce::MouseEvent &e, const juce::MouseWheelDetails& d);
/** @internal */
static bool respondsToKey (const juce::KeyPress&);
juce::WeakReference<juce::Component> contentComp;
private:
//==============================================================================
std::unique_ptr<juce::ScrollBar> verticalScrollBar, horizontalScrollBar;
juce::Rectangle<int> lastVisibleArea;
int scrollBarThickness = 0;
int singleStepX = 16, singleStepY = 16;
bool showHScrollbar = true, showVScrollbar = true, deleteContent = true;
bool customScrollBarThickness = false;
bool allowScrollingWithoutScrollbarV = false, allowScrollingWithoutScrollbarH = false;
bool vScrollbarRight = true, hScrollbarBottom = true;
juce::Point<int> viewportPosToCompPos (juce::Point<int> pos) const;
//#if !JUCE_IOS
struct DragToScrollListener;
std::unique_ptr<DragToScrollListener> dragToScrollListener;
Component contentHolder;
//#endif
#if JUCE_IOS
public:
#endif
void updateVisibleArea();
#if JUCE_IOS
private:
juce::UIViewComponent embededView;
#endif
void deleteOrRemoveContentComp();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Viewport)
juce::ScrollBar *createScrollBarComponent(bool b);
};
#include "ios_viewport.h"
#if __OBJC__ && JUCE_IOS
#include <UIKit/UIKit.h>
@interface JuceUIScrollView : UIScrollView<UIScrollViewDelegate>
{
@private
Viewport* owner;
}
@end
@implementation JuceUIScrollView
- (id) initWithOwner: (Viewport*) owner_
{
if ((self = [super init]) != nil)
{
owner = owner_;
self.delegate = self;
}
return self;
}
- (void) dealloc
{
[super dealloc];
}
- (void) scrollViewDidScroll: (UIScrollView*) scrollView
{
const juce::Rectangle<int> visibleArea (scrollView.contentOffset.x, scrollView.contentOffset.y,
MIN (scrollView.contentSize.width - scrollView.contentOffset.x, owner->getWidth()),
MIN (scrollView.contentSize.height - scrollView.contentOffset.y, owner->getHeight()));
owner->visibleAreaChanged (visibleArea);
owner->updateVisibleArea();
}
@end
//==============================================================================
Viewport::Viewport (const juce::String& name)
{
JuceUIScrollView* scroll = [[JuceUIScrollView alloc] initWithOwner: this];
embededView.setView (scroll);
[scroll release];
setInterceptsMouseClicks (false, true);
setWantsKeyboardFocus (true);
addAndMakeVisible(&embededView);
}
Viewport::~Viewport()
{
deleteOrRemoveContentComp();
}
//==============================================================================
void Viewport::visibleAreaChanged (const juce::Rectangle<int>&) {}
void Viewport::viewedComponentChanged (Component*) {}
//==============================================================================
void Viewport::deleteOrRemoveContentComp()
{
if (contentComp != nullptr)
contentComp->removeFromDesktop();
if (deleteContent)
{
// This sets the content comp to a null pointer before deleting the old one, in case
// anything tries to use the old one while it's in mid-deletion..
juce::ScopedPointer<juce::Component> oldCompDeleter (contentComp);
}
else
{
contentComp = nullptr;
}
}
void Viewport::setViewedComponent (juce::Component* const newViewedComponent, const bool deleteComponentWhenNoLongerNeeded)
{
if (contentComp.get() != newViewedComponent)
{
deleteOrRemoveContentComp();
contentComp = newViewedComponent;
deleteContent = deleteComponentWhenNoLongerNeeded;
if (contentComp != nullptr)
{
contentComp->addToDesktop (0, (UIView*) embededView.getView());
contentComp->setVisible (true);
setViewPosition (juce::Point<int>());
contentComp->addComponentListener (this);
}
viewedComponentChanged (contentComp);
updateVisibleArea();
}
}
int Viewport::getMaximumVisibleWidth() const { return getWidth(); }
int Viewport::getMaximumVisibleHeight() const { return getHeight(); }
juce::Point<int> Viewport::viewportPosToCompPos (juce::Point<int> pos) const
{
using namespace juce;
jassert (contentComp != nullptr);
return juce::Point<int> (jmax (jmin (0, getWidth() - contentComp->getWidth()), jmin (0, -(pos.x))),
jmax (jmin (0, getHeight() - contentComp->getHeight()), jmin (0, -(pos.y))));
}
void Viewport::setViewPosition (const int xPixelsOffset, const int yPixelsOffset)
{
setViewPosition (juce::Point<int> (xPixelsOffset, yPixelsOffset));
}
void Viewport::setViewPosition (const juce::Point<int>& newPosition)
{
if (contentComp != nullptr)
[(UIScrollView*) embededView.getView() setContentOffset: CGPointMake (newPosition.getX(), newPosition.getY())];
}
void Viewport::setViewPositionProportionately (const double x, const double y)
{
using namespace juce;
if (contentComp != nullptr)
setViewPosition (jmax (0, roundToInt (x * (contentComp->getWidth() - getWidth()))),
jmax (0, roundToInt (y * (contentComp->getHeight() - getHeight()))));
}
bool Viewport::autoScroll (const int mouseX, const int mouseY, const int activeBorderThickness, const int maximumSpeed)
{
if (contentComp != nullptr)
{
int dx = 0, dy = 0;
if (horizontalScrollBar->isVisible() || contentComp->getX() < 0 || contentComp->getRight() > getWidth())
{
if (mouseX < activeBorderThickness)
dx = activeBorderThickness - mouseX;
else if (mouseX >= getWidth() - activeBorderThickness)
dx = (getWidth() - activeBorderThickness) - mouseX;
if (dx < 0)
dx = juce::jmax (dx, -maximumSpeed, getWidth() - contentComp->getRight());
else
dx = juce::jmin (dx, maximumSpeed, -contentComp->getX());
}
if (verticalScrollBar->isVisible() || contentComp->getY() < 0 || contentComp->getBottom() > getHeight())
{
if (mouseY < activeBorderThickness)
dy = activeBorderThickness - mouseY;
else if (mouseY >= getHeight() - activeBorderThickness)
dy = (getHeight() - activeBorderThickness) - mouseY;
if (dy < 0)
dy = juce::jmax (dy, -maximumSpeed, getHeight() - contentComp->getBottom());
else
dy = juce::jmin (dy, maximumSpeed, -contentComp->getY());
}
if (dx != 0 || dy != 0)
{
[(UIScrollView*) embededView.getView() setContentOffset: CGPointMake (contentComp->getX() + dx, contentComp->getY() + dy)];
return true;
}
}
return false;
}
void Viewport::componentMovedOrResized (Component&, bool, bool)
{
updateVisibleArea();
UIScrollView* scroll = (UIScrollView*) embededView.getView();
scroll.contentSize = CGSizeMake (contentComp->getWidth(), contentComp->getHeight());
if(canScrollVertically()){
scroll.alwaysBounceVertical = true;
}
if(canScrollHorizontally()){
scroll.alwaysBounceHorizontal = true;
}
}
void Viewport::resized()
{
embededView.setBounds(getLocalBounds());
updateVisibleArea();
}
//==============================================================================
void Viewport::updateVisibleArea()
{
auto contentArea = getLocalBounds();
juce::Rectangle<int> contentBounds;
if (contentComp != nullptr)
contentBounds = getLocalArea (contentComp, contentComp->getLocalBounds());
CGPoint contentOffset = ((UIScrollView*) embededView.getView()).contentOffset;
juce::Point<int> visibleOrigin (contentOffset.x, contentOffset.y);
juce::Rectangle<int> visibleArea (visibleOrigin.x, visibleOrigin.y,
juce::jmin (contentBounds.getWidth() - visibleOrigin.x, contentArea.getWidth()),
juce::jmin (contentBounds.getHeight() - visibleOrigin.y, contentArea.getHeight()));
if (lastVisibleArea != visibleArea)
{
lastVisibleArea = visibleArea;
visibleAreaChanged (visibleArea);
}
}
//==============================================================================
void Viewport::setSingleStepSizes (const int stepX, const int stepY)
{
// intentionally empty…
}
void Viewport::setScrollBarThickness (const int thickness)
{
// intentionally empty…
}
void Viewport::lookAndFeelChanged()
{
if (! customScrollBarThickness)
{
scrollBarThickness = getLookAndFeel().getDefaultScrollbarWidth();
resized();
}
}
int Viewport::getScrollBarThickness() const
{
return scrollBarThickness > 0 ? scrollBarThickness
: getLookAndFeel().getDefaultScrollbarWidth();
}
void Viewport::setScrollBarButtonVisibility (const bool buttonsVisible)
{
// intentionally empty…
}
void Viewport::scrollBarMoved (juce::ScrollBar* scrollBarThatHasMoved, double newRangeStart)
{
// intentionally empty…
}
void Viewport::mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)
{
// intentionally empty…
}
bool Viewport::useMouseWheelMoveIfNeeded (const juce::MouseEvent& e, const juce::MouseWheelDetails& d)
{
// Does not apply…
return false;
}
bool Viewport::keyPressed (const juce::KeyPress& key)
{
// Does not apply…
return false;
}
//==============================================================================
typedef juce::AnimatedPosition<juce::AnimatedPositionBehaviours::ContinuousWithMomentum> ViewportDragPosition;
struct Viewport::DragToScrollListener : private juce::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);
juce::Desktop::getInstance().removeGlobalMouseListener (this);
}
void positionChanged (ViewportDragPosition&, double) override
{
viewport.setViewPosition (originalViewPos - juce::Point<int> ((int) offsetX.getPosition(),
(int) offsetY.getPosition()));
}
void mouseDown (const juce::MouseEvent&) 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);
juce::Desktop::getInstance().addGlobalMouseListener (this);
isGlobalMouseListener = true;
}
}
void mouseDrag (const juce::MouseEvent& e) override
{
if (juce::Desktop::getInstance().getNumDraggingMouseSources() == 1 && ! 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 juce::MouseEvent&) override
{
if (isGlobalMouseListener && juce::Desktop::getInstance().getNumDraggingMouseSources() == 0)
endDragAndClearGlobalMouseListener();
}
void endDragAndClearGlobalMouseListener()
{
offsetX.endDrag();
offsetY.endDrag();
isDragging = false;
viewport.contentHolder.addMouseListener (this, true);
juce::Desktop::getInstance().removeGlobalMouseListener (this);
isGlobalMouseListener = false;
}
bool doesMouseEventComponentBlockViewportDrag (const juce::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;
juce::Point<int> originalViewPos;
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;
}
bool Viewport::canScrollVertically() const noexcept { return contentComp->getY() < 0 || contentComp->getBottom() > getHeight(); }
bool Viewport::canScrollHorizontally() const noexcept { return contentComp->getX() < 0 || contentComp->getRight() > getWidth(); }
bool Viewport::isCurrentlyScrollingOnDrag() const noexcept
{
return dragToScrollListener != nullptr && dragToScrollListener->isDragging;
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment