fix macOS build (following Projucer changes made in Windows, which removed /Applications/JUCE/modules from its headers). move JUCE headers under source control, so that Windows and macOS can both build against same version of JUCE. remove AUv3 target (I think it's an iOS thing, so it will never work with this macOS fluidsynth dylib).
This commit is contained in:
209
modules/juce_gui_basics/layout/juce_AnimatedPosition.h
Normal file
209
modules/juce_gui_basics/layout/juce_AnimatedPosition.h
Normal file
@ -0,0 +1,209 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Models a 1-dimensional position that can be dragged around by the user, and which
|
||||
will then continue moving with a customisable physics behaviour when released.
|
||||
|
||||
This is useful for things like scrollable views or objects that can be dragged and
|
||||
thrown around with the mouse/touch, and by writing your own behaviour class, you can
|
||||
customise the trajectory that it follows when released.
|
||||
|
||||
The class uses its own Timer to continuously change its value when a drag ends, and
|
||||
Listener objects can be registered to receive callbacks whenever the value changes.
|
||||
|
||||
The value is stored as a double, and can be used to represent whatever units you need.
|
||||
|
||||
The template parameter Behaviour must be a class that implements various methods to
|
||||
return the physics of the value's movement - you can use the classes provided for this
|
||||
in the AnimatedPositionBehaviours namespace, or write your own custom behaviour.
|
||||
|
||||
@see AnimatedPositionBehaviours::ContinuousWithMomentum,
|
||||
AnimatedPositionBehaviours::SnapToPageBoundaries
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
template <typename Behaviour>
|
||||
class AnimatedPosition : private Timer
|
||||
{
|
||||
public:
|
||||
AnimatedPosition()
|
||||
: position(), grabbedPos(), releaseVelocity(),
|
||||
range (-std::numeric_limits<double>::max(),
|
||||
std::numeric_limits<double>::max())
|
||||
{
|
||||
}
|
||||
|
||||
/** Sets a range within which the value will be constrained. */
|
||||
void setLimits (Range<double> newRange) noexcept
|
||||
{
|
||||
range = newRange;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
/** Called to indicate that the object is now being controlled by a
|
||||
mouse-drag or similar operation.
|
||||
|
||||
After calling this method, you should make calls to the drag() method
|
||||
each time the mouse drags the position around, and always be sure to
|
||||
finish with a call to endDrag() when the mouse is released, which allows
|
||||
the position to continue moving freely according to the specified behaviour.
|
||||
*/
|
||||
void beginDrag()
|
||||
{
|
||||
grabbedPos = position;
|
||||
releaseVelocity = 0;
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
/** Called during a mouse-drag operation, to indicate that the mouse has moved.
|
||||
The delta is the difference between the position when beginDrag() was called
|
||||
and the new position that's required.
|
||||
*/
|
||||
void drag (double deltaFromStartOfDrag)
|
||||
{
|
||||
moveTo (grabbedPos + deltaFromStartOfDrag);
|
||||
}
|
||||
|
||||
/** Called after beginDrag() and drag() to indicate that the drag operation has
|
||||
now finished.
|
||||
*/
|
||||
void endDrag()
|
||||
{
|
||||
startTimerHz (60);
|
||||
}
|
||||
|
||||
/** Called outside of a drag operation to cause a nudge in the specified direction.
|
||||
This is intended for use by e.g. mouse-wheel events.
|
||||
*/
|
||||
void nudge (double deltaFromCurrentPosition)
|
||||
{
|
||||
startTimerHz (10);
|
||||
moveTo (position + deltaFromCurrentPosition);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the current position. */
|
||||
double getPosition() const noexcept
|
||||
{
|
||||
return position;
|
||||
}
|
||||
|
||||
/** Explicitly sets the position and stops any further movement.
|
||||
This will cause a synchronous call to any listeners if the position actually
|
||||
changes.
|
||||
*/
|
||||
void setPosition (double newPosition)
|
||||
{
|
||||
stopTimer();
|
||||
setPositionAndSendChange (newPosition);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
/** Implement this class if you need to receive callbacks when the value of
|
||||
an AnimatedPosition changes.
|
||||
@see AnimatedPosition::addListener, AnimatedPosition::removeListener
|
||||
*/
|
||||
class Listener
|
||||
{
|
||||
public:
|
||||
virtual ~Listener() {}
|
||||
|
||||
/** Called synchronously when an AnimatedPosition changes. */
|
||||
virtual void positionChanged (AnimatedPosition&, double newPosition) = 0;
|
||||
};
|
||||
|
||||
/** Adds a listener to be called when the value changes. */
|
||||
void addListener (Listener* listener) { listeners.add (listener); }
|
||||
|
||||
/** Removes a previously-registered listener. */
|
||||
void removeListener (Listener* listener) { listeners.remove (listener); }
|
||||
|
||||
//==============================================================================
|
||||
/** The behaviour object.
|
||||
This is public to let you tweak any parameters that it provides.
|
||||
*/
|
||||
Behaviour behaviour;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
double position, grabbedPos, releaseVelocity;
|
||||
Range<double> range;
|
||||
Time lastUpdate, lastDrag;
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
static double getSpeed (const Time last, double lastPos,
|
||||
const Time now, double newPos)
|
||||
{
|
||||
auto elapsedSecs = jmax (0.005, (now - last).inSeconds());
|
||||
auto v = (newPos - lastPos) / elapsedSecs;
|
||||
return std::abs (v) > 0.2 ? v : 0.0;
|
||||
}
|
||||
|
||||
void moveTo (double newPos)
|
||||
{
|
||||
auto now = Time::getCurrentTime();
|
||||
releaseVelocity = getSpeed (lastDrag, position, now, newPos);
|
||||
behaviour.releasedWithVelocity (newPos, releaseVelocity);
|
||||
lastDrag = now;
|
||||
|
||||
setPositionAndSendChange (newPos);
|
||||
}
|
||||
|
||||
void setPositionAndSendChange (double newPosition)
|
||||
{
|
||||
newPosition = range.clipValue (newPosition);
|
||||
|
||||
if (position != newPosition)
|
||||
{
|
||||
position = newPosition;
|
||||
listeners.call ([this, newPosition] (Listener& l) { l.positionChanged (*this, newPosition); });
|
||||
}
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
auto now = Time::getCurrentTime();
|
||||
auto elapsed = jlimit (0.001, 0.020, (now - lastUpdate).inSeconds());
|
||||
lastUpdate = now;
|
||||
auto newPos = behaviour.getNextPosition (position, elapsed);
|
||||
|
||||
if (behaviour.isStopped (newPos))
|
||||
stopTimer();
|
||||
else
|
||||
startTimerHz (60);
|
||||
|
||||
setPositionAndSendChange (newPos);
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnimatedPosition)
|
||||
};
|
||||
|
||||
} // namespace juce
|
161
modules/juce_gui_basics/layout/juce_AnimatedPositionBehaviours.h
Normal file
161
modules/juce_gui_basics/layout/juce_AnimatedPositionBehaviours.h
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/** Contains classes for different types of physics behaviours - these classes
|
||||
are used as template parameters for the AnimatedPosition class.
|
||||
*/
|
||||
namespace AnimatedPositionBehaviours
|
||||
{
|
||||
/** A non-snapping behaviour that allows the content to be freely flicked in
|
||||
either direction, with momentum based on the velocity at which it was
|
||||
released, and variable friction to make it come to a halt.
|
||||
|
||||
This class is intended to be used as a template parameter to the
|
||||
AnimatedPosition class.
|
||||
|
||||
@see AnimatedPosition
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
struct ContinuousWithMomentum
|
||||
{
|
||||
ContinuousWithMomentum() noexcept
|
||||
{
|
||||
}
|
||||
|
||||
/** Sets the friction that damps the movement of the value.
|
||||
A typical value is 0.08; higher values indicate more friction.
|
||||
*/
|
||||
void setFriction (double newFriction) noexcept
|
||||
{
|
||||
damping = 1.0 - newFriction;
|
||||
}
|
||||
|
||||
/** Sets the minimum velocity of the movement. Any velocity that's slower than
|
||||
this will stop the animation. The default is 0.05. */
|
||||
void setMinimumVelocity (double newMinimumVelocityToUse) noexcept
|
||||
{
|
||||
minimumVelocity = newMinimumVelocityToUse;
|
||||
}
|
||||
|
||||
/** Called by the AnimatedPosition class. This tells us the position and
|
||||
velocity at which the user is about to release the object.
|
||||
The velocity is measured in units/second.
|
||||
*/
|
||||
void releasedWithVelocity (double /*position*/, double releaseVelocity) noexcept
|
||||
{
|
||||
velocity = releaseVelocity;
|
||||
}
|
||||
|
||||
/** Called by the AnimatedPosition class to get the new position, after
|
||||
the given time has elapsed.
|
||||
*/
|
||||
double getNextPosition (double oldPos, double elapsedSeconds) noexcept
|
||||
{
|
||||
velocity *= damping;
|
||||
|
||||
if (std::abs (velocity) < minimumVelocity)
|
||||
velocity = 0;
|
||||
|
||||
return oldPos + velocity * elapsedSeconds;
|
||||
}
|
||||
|
||||
/** Called by the AnimatedPosition class to check whether the object
|
||||
is now stationary.
|
||||
*/
|
||||
bool isStopped (double /*position*/) const noexcept
|
||||
{
|
||||
return velocity == 0.0;
|
||||
}
|
||||
|
||||
private:
|
||||
double velocity = 0, damping = 0.92, minimumVelocity = 0.05;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** A behaviour that gravitates an AnimatedPosition object towards the nearest
|
||||
integer position when released.
|
||||
|
||||
This class is intended to be used as a template parameter to the
|
||||
AnimatedPosition class. It's handy when using an AnimatedPosition to show a
|
||||
series of pages, because it allows the pages can be scrolled smoothly, but when
|
||||
released, snaps back to show a whole page.
|
||||
|
||||
@see AnimatedPosition
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
struct SnapToPageBoundaries
|
||||
{
|
||||
SnapToPageBoundaries() noexcept : targetSnapPosition()
|
||||
{
|
||||
}
|
||||
|
||||
/** Called by the AnimatedPosition class. This tells us the position and
|
||||
velocity at which the user is about to release the object.
|
||||
The velocity is measured in units/second.
|
||||
*/
|
||||
void releasedWithVelocity (double position, double releaseVelocity) noexcept
|
||||
{
|
||||
targetSnapPosition = std::floor (position + 0.5);
|
||||
|
||||
if (releaseVelocity > 1.0 && targetSnapPosition < position) ++targetSnapPosition;
|
||||
if (releaseVelocity < -1.0 && targetSnapPosition > position) --targetSnapPosition;
|
||||
}
|
||||
|
||||
/** Called by the AnimatedPosition class to get the new position, after
|
||||
the given time has elapsed.
|
||||
*/
|
||||
double getNextPosition (double oldPos, double elapsedSeconds) const noexcept
|
||||
{
|
||||
if (isStopped (oldPos))
|
||||
return targetSnapPosition;
|
||||
|
||||
const double snapSpeed = 10.0;
|
||||
const double velocity = (targetSnapPosition - oldPos) * snapSpeed;
|
||||
const double newPos = oldPos + velocity * elapsedSeconds;
|
||||
|
||||
return isStopped (newPos) ? targetSnapPosition : newPos;
|
||||
}
|
||||
|
||||
/** Called by the AnimatedPosition class to check whether the object
|
||||
is now stationary.
|
||||
*/
|
||||
bool isStopped (double position) const noexcept
|
||||
{
|
||||
return std::abs (targetSnapPosition - position) < 0.001;
|
||||
}
|
||||
|
||||
private:
|
||||
double targetSnapPosition;
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace juce
|
345
modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp
Normal file
345
modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp
Normal file
@ -0,0 +1,345 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class ComponentAnimator::AnimationTask
|
||||
{
|
||||
public:
|
||||
AnimationTask (Component* c) noexcept : component (c) {}
|
||||
|
||||
void reset (const Rectangle<int>& finalBounds,
|
||||
float finalAlpha,
|
||||
int millisecondsToSpendMoving,
|
||||
bool useProxyComponent,
|
||||
double startSpd, double endSpd)
|
||||
{
|
||||
msElapsed = 0;
|
||||
msTotal = jmax (1, millisecondsToSpendMoving);
|
||||
lastProgress = 0;
|
||||
destination = finalBounds;
|
||||
destAlpha = finalAlpha;
|
||||
|
||||
isMoving = (finalBounds != component->getBounds());
|
||||
isChangingAlpha = (finalAlpha != component->getAlpha());
|
||||
|
||||
left = component->getX();
|
||||
top = component->getY();
|
||||
right = component->getRight();
|
||||
bottom = component->getBottom();
|
||||
alpha = component->getAlpha();
|
||||
|
||||
const double invTotalDistance = 4.0 / (startSpd + endSpd + 2.0);
|
||||
startSpeed = jmax (0.0, startSpd * invTotalDistance);
|
||||
midSpeed = invTotalDistance;
|
||||
endSpeed = jmax (0.0, endSpd * invTotalDistance);
|
||||
|
||||
if (useProxyComponent)
|
||||
proxy.reset (new ProxyComponent (*component));
|
||||
else
|
||||
proxy.reset();
|
||||
|
||||
component->setVisible (! useProxyComponent);
|
||||
}
|
||||
|
||||
bool useTimeslice (const int elapsed)
|
||||
{
|
||||
if (auto* c = proxy != nullptr ? proxy.get()
|
||||
: component.get())
|
||||
{
|
||||
msElapsed += elapsed;
|
||||
double newProgress = msElapsed / (double) msTotal;
|
||||
|
||||
if (newProgress >= 0 && newProgress < 1.0)
|
||||
{
|
||||
const WeakReference<AnimationTask> weakRef (this);
|
||||
newProgress = timeToDistance (newProgress);
|
||||
const double delta = (newProgress - lastProgress) / (1.0 - lastProgress);
|
||||
jassert (newProgress >= lastProgress);
|
||||
lastProgress = newProgress;
|
||||
|
||||
if (delta < 1.0)
|
||||
{
|
||||
bool stillBusy = false;
|
||||
|
||||
if (isMoving)
|
||||
{
|
||||
left += (destination.getX() - left) * delta;
|
||||
top += (destination.getY() - top) * delta;
|
||||
right += (destination.getRight() - right) * delta;
|
||||
bottom += (destination.getBottom() - bottom) * delta;
|
||||
|
||||
const Rectangle<int> newBounds (roundToInt (left),
|
||||
roundToInt (top),
|
||||
roundToInt (right - left),
|
||||
roundToInt (bottom - top));
|
||||
|
||||
if (newBounds != destination)
|
||||
{
|
||||
c->setBounds (newBounds);
|
||||
stillBusy = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the animation was cancelled/deleted during
|
||||
// a callback during the setBounds method
|
||||
if (weakRef.wasObjectDeleted())
|
||||
return false;
|
||||
|
||||
if (isChangingAlpha)
|
||||
{
|
||||
alpha += (destAlpha - alpha) * delta;
|
||||
c->setAlpha ((float) alpha);
|
||||
stillBusy = true;
|
||||
}
|
||||
|
||||
if (stillBusy)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveToFinalDestination();
|
||||
return false;
|
||||
}
|
||||
|
||||
void moveToFinalDestination()
|
||||
{
|
||||
if (component != nullptr)
|
||||
{
|
||||
const WeakReference<AnimationTask> weakRef (this);
|
||||
component->setAlpha ((float) destAlpha);
|
||||
component->setBounds (destination);
|
||||
|
||||
if (! weakRef.wasObjectDeleted())
|
||||
if (proxy != nullptr)
|
||||
component->setVisible (destAlpha > 0);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct ProxyComponent : public Component
|
||||
{
|
||||
ProxyComponent (Component& c)
|
||||
{
|
||||
setWantsKeyboardFocus (false);
|
||||
setBounds (c.getBounds());
|
||||
setTransform (c.getTransform());
|
||||
setAlpha (c.getAlpha());
|
||||
setInterceptsMouseClicks (false, false);
|
||||
|
||||
if (auto* parent = c.getParentComponent())
|
||||
parent->addAndMakeVisible (this);
|
||||
else if (c.isOnDesktop() && c.getPeer() != nullptr)
|
||||
addToDesktop (c.getPeer()->getStyleFlags() | ComponentPeer::windowIgnoresKeyPresses);
|
||||
else
|
||||
jassertfalse; // seem to be trying to animate a component that's not visible..
|
||||
|
||||
auto scale = (float) Desktop::getInstance().getDisplays()
|
||||
.getDisplayContaining (getScreenBounds().getCentre()).scale;
|
||||
|
||||
image = c.createComponentSnapshot (c.getLocalBounds(), false, scale);
|
||||
|
||||
setVisible (true);
|
||||
toBehind (&c);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.setOpacity (1.0f);
|
||||
g.drawImageTransformed (image, AffineTransform::scale (getWidth() / (float) image.getWidth(),
|
||||
getHeight() / (float) image.getHeight()), false);
|
||||
}
|
||||
|
||||
private:
|
||||
Image image;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProxyComponent)
|
||||
};
|
||||
|
||||
WeakReference<Component> component;
|
||||
std::unique_ptr<Component> proxy;
|
||||
|
||||
Rectangle<int> destination;
|
||||
double destAlpha;
|
||||
|
||||
int msElapsed, msTotal;
|
||||
double startSpeed, midSpeed, endSpeed, lastProgress;
|
||||
double left, top, right, bottom, alpha;
|
||||
bool isMoving, isChangingAlpha;
|
||||
|
||||
private:
|
||||
double timeToDistance (const double time) const noexcept
|
||||
{
|
||||
return (time < 0.5) ? time * (startSpeed + time * (midSpeed - startSpeed))
|
||||
: 0.5 * (startSpeed + 0.5 * (midSpeed - startSpeed))
|
||||
+ (time - 0.5) * (midSpeed + (time - 0.5) * (endSpeed - midSpeed));
|
||||
}
|
||||
|
||||
JUCE_DECLARE_WEAK_REFERENCEABLE (AnimationTask)
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnimationTask)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
ComponentAnimator::ComponentAnimator() : lastTime (0) {}
|
||||
ComponentAnimator::~ComponentAnimator() {}
|
||||
|
||||
//==============================================================================
|
||||
ComponentAnimator::AnimationTask* ComponentAnimator::findTaskFor (Component* const component) const noexcept
|
||||
{
|
||||
for (int i = tasks.size(); --i >= 0;)
|
||||
if (component == tasks.getUnchecked(i)->component.get())
|
||||
return tasks.getUnchecked(i);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ComponentAnimator::animateComponent (Component* const component,
|
||||
const Rectangle<int>& finalBounds,
|
||||
const float finalAlpha,
|
||||
const int millisecondsToSpendMoving,
|
||||
const bool useProxyComponent,
|
||||
const double startSpeed,
|
||||
const double endSpeed)
|
||||
{
|
||||
// the speeds must be 0 or greater!
|
||||
jassert (startSpeed >= 0 && endSpeed >= 0);
|
||||
|
||||
if (component != nullptr)
|
||||
{
|
||||
auto* at = findTaskFor (component);
|
||||
|
||||
if (at == nullptr)
|
||||
{
|
||||
at = new AnimationTask (component);
|
||||
tasks.add (at);
|
||||
sendChangeMessage();
|
||||
}
|
||||
|
||||
at->reset (finalBounds, finalAlpha, millisecondsToSpendMoving,
|
||||
useProxyComponent, startSpeed, endSpeed);
|
||||
|
||||
if (! isTimerRunning())
|
||||
{
|
||||
lastTime = Time::getMillisecondCounter();
|
||||
startTimerHz (50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentAnimator::fadeOut (Component* component, int millisecondsToTake)
|
||||
{
|
||||
if (component != nullptr)
|
||||
{
|
||||
if (component->isShowing() && millisecondsToTake > 0)
|
||||
animateComponent (component, component->getBounds(), 0.0f, millisecondsToTake, true, 1.0, 1.0);
|
||||
|
||||
component->setVisible (false);
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentAnimator::fadeIn (Component* component, int millisecondsToTake)
|
||||
{
|
||||
if (component != nullptr && ! (component->isVisible() && component->getAlpha() == 1.0f))
|
||||
{
|
||||
component->setAlpha (0.0f);
|
||||
component->setVisible (true);
|
||||
animateComponent (component, component->getBounds(), 1.0f, millisecondsToTake, false, 1.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentAnimator::cancelAllAnimations (const bool moveComponentsToTheirFinalPositions)
|
||||
{
|
||||
if (tasks.size() > 0)
|
||||
{
|
||||
if (moveComponentsToTheirFinalPositions)
|
||||
for (int i = tasks.size(); --i >= 0;)
|
||||
tasks.getUnchecked(i)->moveToFinalDestination();
|
||||
|
||||
tasks.clear();
|
||||
sendChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentAnimator::cancelAnimation (Component* const component,
|
||||
const bool moveComponentToItsFinalPosition)
|
||||
{
|
||||
if (auto* at = findTaskFor (component))
|
||||
{
|
||||
if (moveComponentToItsFinalPosition)
|
||||
at->moveToFinalDestination();
|
||||
|
||||
tasks.removeObject (at);
|
||||
sendChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle<int> ComponentAnimator::getComponentDestination (Component* const component)
|
||||
{
|
||||
jassert (component != nullptr);
|
||||
|
||||
if (auto* at = findTaskFor (component))
|
||||
return at->destination;
|
||||
|
||||
return component->getBounds();
|
||||
}
|
||||
|
||||
bool ComponentAnimator::isAnimating (Component* component) const noexcept
|
||||
{
|
||||
return findTaskFor (component) != nullptr;
|
||||
}
|
||||
|
||||
bool ComponentAnimator::isAnimating() const noexcept
|
||||
{
|
||||
return tasks.size() != 0;
|
||||
}
|
||||
|
||||
void ComponentAnimator::timerCallback()
|
||||
{
|
||||
auto timeNow = Time::getMillisecondCounter();
|
||||
|
||||
if (lastTime == 0)
|
||||
lastTime = timeNow;
|
||||
|
||||
auto elapsed = (int) (timeNow - lastTime);
|
||||
|
||||
for (auto* task : Array<AnimationTask*> (tasks.begin(), tasks.size()))
|
||||
{
|
||||
if (tasks.contains (task) && ! task->useTimeslice (elapsed))
|
||||
{
|
||||
tasks.removeObject (task);
|
||||
sendChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = timeNow;
|
||||
|
||||
if (tasks.size() == 0)
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
} // namespace juce
|
163
modules/juce_gui_basics/layout/juce_ComponentAnimator.h
Normal file
163
modules/juce_gui_basics/layout/juce_ComponentAnimator.h
Normal file
@ -0,0 +1,163 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Animates a set of components, moving them to a new position and/or fading their
|
||||
alpha levels.
|
||||
|
||||
To animate a component, create a ComponentAnimator instance or (preferably) use the
|
||||
global animator object provided by Desktop::getAnimator(), and call its animateComponent()
|
||||
method to commence the movement.
|
||||
|
||||
If you're using your own ComponentAnimator instance, you'll need to make sure it isn't
|
||||
deleted before it finishes moving the components, or they'll be abandoned before reaching their
|
||||
destinations.
|
||||
|
||||
It's ok to delete components while they're being animated - the animator will detect this
|
||||
and safely stop using them.
|
||||
|
||||
The class is a ChangeBroadcaster and sends a notification when any components
|
||||
start or finish being animated.
|
||||
|
||||
@see Desktop::getAnimator
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ComponentAnimator : public ChangeBroadcaster,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a ComponentAnimator. */
|
||||
ComponentAnimator();
|
||||
|
||||
/** Destructor. */
|
||||
~ComponentAnimator();
|
||||
|
||||
//==============================================================================
|
||||
/** Starts a component moving from its current position to a specified position.
|
||||
|
||||
If the component is already in the middle of an animation, that will be abandoned,
|
||||
and a new animation will begin, moving the component from its current location.
|
||||
|
||||
The start and end speed parameters let you apply some acceleration to the component's
|
||||
movement.
|
||||
|
||||
@param component the component to move
|
||||
@param finalBounds the destination bounds to which the component should move. To leave the
|
||||
component in the same place, just pass component->getBounds() for this value
|
||||
@param finalAlpha the alpha value that the component should have at the end of the animation
|
||||
@param animationDurationMilliseconds how long the animation should last, in milliseconds
|
||||
@param useProxyComponent if true, this means the component should be replaced by an internally
|
||||
managed temporary component which is a snapshot of the original component.
|
||||
This avoids the component having to paint itself as it moves, so may
|
||||
be more efficient. This option also allows you to delete the original
|
||||
component immediately after starting the animation, because the animation
|
||||
can proceed without it. If you use a proxy, the original component will be
|
||||
made invisible by this call, and then will become visible again at the end
|
||||
of the animation. It'll also mean that the proxy component will be temporarily
|
||||
added to the component's parent, so avoid it if this might confuse the parent
|
||||
component, or if there's a chance the parent might decide to delete its children.
|
||||
@param startSpeed a value to indicate the relative start speed of the animation. If this is 0,
|
||||
the component will start by accelerating from rest; higher values mean that it
|
||||
will have an initial speed greater than zero. If the value if greater than 1, it
|
||||
will decelerate towards the middle of its journey. To move the component at a
|
||||
constant rate for its entire animation, set both the start and end speeds to 1.0
|
||||
@param endSpeed a relative speed at which the component should be moving when the animation finishes.
|
||||
If this is 0, the component will decelerate to a standstill at its final position;
|
||||
higher values mean the component will still be moving when it stops. To move the component
|
||||
at a constant rate for its entire animation, set both the start and end speeds to 1.0
|
||||
*/
|
||||
void animateComponent (Component* component,
|
||||
const Rectangle<int>& finalBounds,
|
||||
float finalAlpha,
|
||||
int animationDurationMilliseconds,
|
||||
bool useProxyComponent,
|
||||
double startSpeed,
|
||||
double endSpeed);
|
||||
|
||||
/** Begins a fade-out of this components alpha level.
|
||||
This is a quick way of invoking animateComponent() with a target alpha value of 0.0f, using
|
||||
a proxy. You're safe to delete the component after calling this method, and this won't
|
||||
interfere with the animation's progress.
|
||||
*/
|
||||
void fadeOut (Component* component, int millisecondsToTake);
|
||||
|
||||
/** Begins a fade-in of a component.
|
||||
This is a quick way of invoking animateComponent() with a target alpha value of 1.0f.
|
||||
*/
|
||||
void fadeIn (Component* component, int millisecondsToTake);
|
||||
|
||||
/** Stops a component if it's currently being animated.
|
||||
|
||||
If moveComponentToItsFinalPosition is true, then the component will
|
||||
be immediately moved to its destination position and size. If false, it will be
|
||||
left in whatever location it currently occupies.
|
||||
*/
|
||||
void cancelAnimation (Component* component,
|
||||
bool moveComponentToItsFinalPosition);
|
||||
|
||||
/** Clears all of the active animations.
|
||||
|
||||
If moveComponentsToTheirFinalPositions is true, all the components will
|
||||
be immediately set to their final positions. If false, they will be
|
||||
left in whatever locations they currently occupy.
|
||||
*/
|
||||
void cancelAllAnimations (bool moveComponentsToTheirFinalPositions);
|
||||
|
||||
/** Returns the destination position for a component.
|
||||
|
||||
If the component is being animated, this will return the target position that
|
||||
was specified when animateComponent() was called.
|
||||
|
||||
If the specified component isn't currently being animated, this method will just
|
||||
return its current position.
|
||||
*/
|
||||
Rectangle<int> getComponentDestination (Component* component);
|
||||
|
||||
/** Returns true if the specified component is currently being animated. */
|
||||
bool isAnimating (Component* component) const noexcept;
|
||||
|
||||
/** Returns true if any components are currently being animated. */
|
||||
bool isAnimating() const noexcept;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
class AnimationTask;
|
||||
OwnedArray<AnimationTask> tasks;
|
||||
uint32 lastTime;
|
||||
|
||||
AnimationTask* findTaskFor (Component*) const noexcept;
|
||||
void timerCallback() override;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentAnimator)
|
||||
};
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,297 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
ComponentBoundsConstrainer::ComponentBoundsConstrainer() noexcept {}
|
||||
ComponentBoundsConstrainer::~ComponentBoundsConstrainer() {}
|
||||
|
||||
//==============================================================================
|
||||
void ComponentBoundsConstrainer::setMinimumWidth (int minimumWidth) noexcept { minW = minimumWidth; }
|
||||
void ComponentBoundsConstrainer::setMaximumWidth (int maximumWidth) noexcept { maxW = maximumWidth; }
|
||||
void ComponentBoundsConstrainer::setMinimumHeight (int minimumHeight) noexcept { minH = minimumHeight; }
|
||||
void ComponentBoundsConstrainer::setMaximumHeight (int maximumHeight) noexcept { maxH = maximumHeight; }
|
||||
|
||||
void ComponentBoundsConstrainer::setMinimumSize (int minimumWidth, int minimumHeight) noexcept
|
||||
{
|
||||
jassert (maxW >= minimumWidth);
|
||||
jassert (maxH >= minimumHeight);
|
||||
jassert (minimumWidth > 0 && minimumHeight > 0);
|
||||
|
||||
minW = minimumWidth;
|
||||
minH = minimumHeight;
|
||||
|
||||
if (minW > maxW) maxW = minW;
|
||||
if (minH > maxH) maxH = minH;
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::setMaximumSize (int maximumWidth, int maximumHeight) noexcept
|
||||
{
|
||||
jassert (maximumWidth >= minW);
|
||||
jassert (maximumHeight >= minH);
|
||||
jassert (maximumWidth > 0 && maximumHeight > 0);
|
||||
|
||||
maxW = jmax (minW, maximumWidth);
|
||||
maxH = jmax (minH, maximumHeight);
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::setSizeLimits (int minimumWidth,
|
||||
int minimumHeight,
|
||||
int maximumWidth,
|
||||
int maximumHeight) noexcept
|
||||
{
|
||||
jassert (maximumWidth >= minimumWidth);
|
||||
jassert (maximumHeight >= minimumHeight);
|
||||
jassert (maximumWidth > 0 && maximumHeight > 0);
|
||||
jassert (minimumWidth > 0 && minimumHeight > 0);
|
||||
|
||||
minW = jmax (0, minimumWidth);
|
||||
minH = jmax (0, minimumHeight);
|
||||
maxW = jmax (minW, maximumWidth);
|
||||
maxH = jmax (minH, maximumHeight);
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::setMinimumOnscreenAmounts (int minimumWhenOffTheTop,
|
||||
int minimumWhenOffTheLeft,
|
||||
int minimumWhenOffTheBottom,
|
||||
int minimumWhenOffTheRight) noexcept
|
||||
{
|
||||
minOffTop = minimumWhenOffTheTop;
|
||||
minOffLeft = minimumWhenOffTheLeft;
|
||||
minOffBottom = minimumWhenOffTheBottom;
|
||||
minOffRight = minimumWhenOffTheRight;
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::setFixedAspectRatio (double widthOverHeight) noexcept
|
||||
{
|
||||
aspectRatio = jmax (0.0, widthOverHeight);
|
||||
}
|
||||
|
||||
double ComponentBoundsConstrainer::getFixedAspectRatio() const noexcept
|
||||
{
|
||||
return aspectRatio;
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::setBoundsForComponent (Component* component,
|
||||
Rectangle<int> targetBounds,
|
||||
bool isStretchingTop,
|
||||
bool isStretchingLeft,
|
||||
bool isStretchingBottom,
|
||||
bool isStretchingRight)
|
||||
{
|
||||
jassert (component != nullptr);
|
||||
|
||||
Rectangle<int> limits, bounds (targetBounds);
|
||||
BorderSize<int> border;
|
||||
|
||||
if (auto* parent = component->getParentComponent())
|
||||
{
|
||||
limits.setSize (parent->getWidth(), parent->getHeight());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto* peer = component->getPeer())
|
||||
border = peer->getFrameSize();
|
||||
|
||||
auto screenBounds = Desktop::getInstance().getDisplays().getDisplayContaining (targetBounds.getCentre()).userArea;
|
||||
|
||||
limits = component->getLocalArea (nullptr, screenBounds) + component->getPosition();
|
||||
}
|
||||
|
||||
border.addTo (bounds);
|
||||
|
||||
checkBounds (bounds,
|
||||
border.addedTo (component->getBounds()), limits,
|
||||
isStretchingTop, isStretchingLeft,
|
||||
isStretchingBottom, isStretchingRight);
|
||||
|
||||
border.subtractFrom (bounds);
|
||||
|
||||
applyBoundsToComponent (*component, bounds);
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::checkComponentBounds (Component* component)
|
||||
{
|
||||
setBoundsForComponent (component, component->getBounds(),
|
||||
false, false, false, false);
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::applyBoundsToComponent (Component& component, Rectangle<int> bounds)
|
||||
{
|
||||
if (auto* positioner = component.getPositioner())
|
||||
positioner->applyNewBounds (bounds);
|
||||
else
|
||||
component.setBounds (bounds);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ComponentBoundsConstrainer::resizeStart()
|
||||
{
|
||||
}
|
||||
|
||||
void ComponentBoundsConstrainer::resizeEnd()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ComponentBoundsConstrainer::checkBounds (Rectangle<int>& bounds,
|
||||
const Rectangle<int>& old,
|
||||
const Rectangle<int>& limits,
|
||||
bool isStretchingTop,
|
||||
bool isStretchingLeft,
|
||||
bool isStretchingBottom,
|
||||
bool isStretchingRight)
|
||||
{
|
||||
if (isStretchingLeft)
|
||||
bounds.setLeft (jlimit (old.getRight() - maxW, old.getRight() - minW, bounds.getX()));
|
||||
else
|
||||
bounds.setWidth (jlimit (minW, maxW, bounds.getWidth()));
|
||||
|
||||
if (isStretchingTop)
|
||||
bounds.setTop (jlimit (old.getBottom() - maxH, old.getBottom() - minH, bounds.getY()));
|
||||
else
|
||||
bounds.setHeight (jlimit (minH, maxH, bounds.getHeight()));
|
||||
|
||||
if (bounds.isEmpty())
|
||||
return;
|
||||
|
||||
if (minOffTop > 0)
|
||||
{
|
||||
const int limit = limits.getY() + jmin (minOffTop - bounds.getHeight(), 0);
|
||||
|
||||
if (bounds.getY() < limit)
|
||||
{
|
||||
if (isStretchingTop)
|
||||
bounds.setTop (limits.getY());
|
||||
else
|
||||
bounds.setY (limit);
|
||||
}
|
||||
}
|
||||
|
||||
if (minOffLeft > 0)
|
||||
{
|
||||
const int limit = limits.getX() + jmin (minOffLeft - bounds.getWidth(), 0);
|
||||
|
||||
if (bounds.getX() < limit)
|
||||
{
|
||||
if (isStretchingLeft)
|
||||
bounds.setLeft (limits.getX());
|
||||
else
|
||||
bounds.setX (limit);
|
||||
}
|
||||
}
|
||||
|
||||
if (minOffBottom > 0)
|
||||
{
|
||||
const int limit = limits.getBottom() - jmin (minOffBottom, bounds.getHeight());
|
||||
|
||||
if (bounds.getY() > limit)
|
||||
{
|
||||
if (isStretchingBottom)
|
||||
bounds.setBottom (limits.getBottom());
|
||||
else
|
||||
bounds.setY (limit);
|
||||
}
|
||||
}
|
||||
|
||||
if (minOffRight > 0)
|
||||
{
|
||||
const int limit = limits.getRight() - jmin (minOffRight, bounds.getWidth());
|
||||
|
||||
if (bounds.getX() > limit)
|
||||
{
|
||||
if (isStretchingRight)
|
||||
bounds.setRight (limits.getRight());
|
||||
else
|
||||
bounds.setX (limit);
|
||||
}
|
||||
}
|
||||
|
||||
// constrain the aspect ratio if one has been specified..
|
||||
if (aspectRatio > 0.0)
|
||||
{
|
||||
bool adjustWidth;
|
||||
|
||||
if ((isStretchingTop || isStretchingBottom) && ! (isStretchingLeft || isStretchingRight))
|
||||
{
|
||||
adjustWidth = true;
|
||||
}
|
||||
else if ((isStretchingLeft || isStretchingRight) && ! (isStretchingTop || isStretchingBottom))
|
||||
{
|
||||
adjustWidth = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
const double oldRatio = (old.getHeight() > 0) ? std::abs (old.getWidth() / (double) old.getHeight()) : 0.0;
|
||||
const double newRatio = std::abs (bounds.getWidth() / (double) bounds.getHeight());
|
||||
|
||||
adjustWidth = (oldRatio > newRatio);
|
||||
}
|
||||
|
||||
if (adjustWidth)
|
||||
{
|
||||
bounds.setWidth (roundToInt (bounds.getHeight() * aspectRatio));
|
||||
|
||||
if (bounds.getWidth() > maxW || bounds.getWidth() < minW)
|
||||
{
|
||||
bounds.setWidth (jlimit (minW, maxW, bounds.getWidth()));
|
||||
bounds.setHeight (roundToInt (bounds.getWidth() / aspectRatio));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds.setHeight (roundToInt (bounds.getWidth() / aspectRatio));
|
||||
|
||||
if (bounds.getHeight() > maxH || bounds.getHeight() < minH)
|
||||
{
|
||||
bounds.setHeight (jlimit (minH, maxH, bounds.getHeight()));
|
||||
bounds.setWidth (roundToInt (bounds.getHeight() * aspectRatio));
|
||||
}
|
||||
}
|
||||
|
||||
if ((isStretchingTop || isStretchingBottom) && ! (isStretchingLeft || isStretchingRight))
|
||||
{
|
||||
bounds.setX (old.getX() + (old.getWidth() - bounds.getWidth()) / 2);
|
||||
}
|
||||
else if ((isStretchingLeft || isStretchingRight) && ! (isStretchingTop || isStretchingBottom))
|
||||
{
|
||||
bounds.setY (old.getY() + (old.getHeight() - bounds.getHeight()) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isStretchingLeft)
|
||||
bounds.setX (old.getRight() - bounds.getWidth());
|
||||
|
||||
if (isStretchingTop)
|
||||
bounds.setY (old.getBottom() - bounds.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
jassert (! bounds.isEmpty());
|
||||
}
|
||||
|
||||
} // namespace juce
|
198
modules/juce_gui_basics/layout/juce_ComponentBoundsConstrainer.h
Normal file
198
modules/juce_gui_basics/layout/juce_ComponentBoundsConstrainer.h
Normal file
@ -0,0 +1,198 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A class that imposes restrictions on a Component's size or position.
|
||||
|
||||
This is used by classes such as ResizableCornerComponent,
|
||||
ResizableBorderComponent and ResizableWindow.
|
||||
|
||||
The base class can impose some basic size and position limits, but you can
|
||||
also subclass this for custom uses.
|
||||
|
||||
@see ResizableCornerComponent, ResizableBorderComponent, ResizableWindow
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ComponentBoundsConstrainer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** When first created, the object will not impose any restrictions on the components. */
|
||||
ComponentBoundsConstrainer() noexcept;
|
||||
|
||||
/** Destructor. */
|
||||
virtual ~ComponentBoundsConstrainer();
|
||||
|
||||
//==============================================================================
|
||||
/** Imposes a minimum width limit. */
|
||||
void setMinimumWidth (int minimumWidth) noexcept;
|
||||
|
||||
/** Returns the current minimum width. */
|
||||
int getMinimumWidth() const noexcept { return minW; }
|
||||
|
||||
/** Imposes a maximum width limit. */
|
||||
void setMaximumWidth (int maximumWidth) noexcept;
|
||||
|
||||
/** Returns the current maximum width. */
|
||||
int getMaximumWidth() const noexcept { return maxW; }
|
||||
|
||||
/** Imposes a minimum height limit. */
|
||||
void setMinimumHeight (int minimumHeight) noexcept;
|
||||
|
||||
/** Returns the current minimum height. */
|
||||
int getMinimumHeight() const noexcept { return minH; }
|
||||
|
||||
/** Imposes a maximum height limit. */
|
||||
void setMaximumHeight (int maximumHeight) noexcept;
|
||||
|
||||
/** Returns the current maximum height. */
|
||||
int getMaximumHeight() const noexcept { return maxH; }
|
||||
|
||||
/** Imposes a minimum width and height limit. */
|
||||
void setMinimumSize (int minimumWidth,
|
||||
int minimumHeight) noexcept;
|
||||
|
||||
/** Imposes a maximum width and height limit. */
|
||||
void setMaximumSize (int maximumWidth,
|
||||
int maximumHeight) noexcept;
|
||||
|
||||
/** Set all the maximum and minimum dimensions. */
|
||||
void setSizeLimits (int minimumWidth,
|
||||
int minimumHeight,
|
||||
int maximumWidth,
|
||||
int maximumHeight) noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Sets the amount by which the component is allowed to go off-screen.
|
||||
|
||||
The values indicate how many pixels must remain on-screen when dragged off
|
||||
one of its parent's edges, so e.g. if minimumWhenOffTheTop is set to 10, then
|
||||
when the component goes off the top of the screen, its y-position will be
|
||||
clipped so that there are always at least 10 pixels on-screen. In other words,
|
||||
the lowest y-position it can take would be (10 - the component's height).
|
||||
|
||||
If you pass 0 or less for one of these amounts, the component is allowed
|
||||
to move beyond that edge completely, with no restrictions at all.
|
||||
|
||||
If you pass a very large number (i.e. larger that the dimensions of the
|
||||
component itself), then the component won't be allowed to overlap that
|
||||
edge at all. So e.g. setting minimumWhenOffTheLeft to 0xffffff will mean that
|
||||
the component will bump into the left side of the screen and go no further.
|
||||
*/
|
||||
void setMinimumOnscreenAmounts (int minimumWhenOffTheTop,
|
||||
int minimumWhenOffTheLeft,
|
||||
int minimumWhenOffTheBottom,
|
||||
int minimumWhenOffTheRight) noexcept;
|
||||
|
||||
|
||||
/** Returns the minimum distance the bounds can be off-screen. @see setMinimumOnscreenAmounts */
|
||||
int getMinimumWhenOffTheTop() const noexcept { return minOffTop; }
|
||||
/** Returns the minimum distance the bounds can be off-screen. @see setMinimumOnscreenAmounts */
|
||||
int getMinimumWhenOffTheLeft() const noexcept { return minOffLeft; }
|
||||
/** Returns the minimum distance the bounds can be off-screen. @see setMinimumOnscreenAmounts */
|
||||
int getMinimumWhenOffTheBottom() const noexcept { return minOffBottom; }
|
||||
/** Returns the minimum distance the bounds can be off-screen. @see setMinimumOnscreenAmounts */
|
||||
int getMinimumWhenOffTheRight() const noexcept { return minOffRight; }
|
||||
|
||||
//==============================================================================
|
||||
/** Specifies a width-to-height ratio that the resizer should always maintain.
|
||||
|
||||
If the value is 0, no aspect ratio is enforced. If it's non-zero, the width
|
||||
will always be maintained as this multiple of the height.
|
||||
|
||||
@see setResizeLimits
|
||||
*/
|
||||
void setFixedAspectRatio (double widthOverHeight) noexcept;
|
||||
|
||||
/** Returns the aspect ratio that was set with setFixedAspectRatio().
|
||||
|
||||
If no aspect ratio is being enforced, this will return 0.
|
||||
*/
|
||||
double getFixedAspectRatio() const noexcept;
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** This callback changes the given coordinates to impose whatever the current
|
||||
constraints are set to be.
|
||||
|
||||
@param bounds the target position that should be examined and adjusted
|
||||
@param previousBounds the component's current size
|
||||
@param limits the region in which the component can be positioned
|
||||
@param isStretchingTop whether the top edge of the component is being resized
|
||||
@param isStretchingLeft whether the left edge of the component is being resized
|
||||
@param isStretchingBottom whether the bottom edge of the component is being resized
|
||||
@param isStretchingRight whether the right edge of the component is being resized
|
||||
*/
|
||||
virtual void checkBounds (Rectangle<int>& bounds,
|
||||
const Rectangle<int>& previousBounds,
|
||||
const Rectangle<int>& limits,
|
||||
bool isStretchingTop,
|
||||
bool isStretchingLeft,
|
||||
bool isStretchingBottom,
|
||||
bool isStretchingRight);
|
||||
|
||||
/** This callback happens when the resizer is about to start dragging. */
|
||||
virtual void resizeStart();
|
||||
|
||||
/** This callback happens when the resizer has finished dragging. */
|
||||
virtual void resizeEnd();
|
||||
|
||||
/** Checks the given bounds, and then sets the component to the corrected size. */
|
||||
void setBoundsForComponent (Component* component,
|
||||
Rectangle<int> bounds,
|
||||
bool isStretchingTop,
|
||||
bool isStretchingLeft,
|
||||
bool isStretchingBottom,
|
||||
bool isStretchingRight);
|
||||
|
||||
/** Performs a check on the current size of a component, and moves or resizes
|
||||
it if it fails the constraints.
|
||||
*/
|
||||
void checkComponentBounds (Component* component);
|
||||
|
||||
/** Called by setBoundsForComponent() to apply a new constrained size to a
|
||||
component.
|
||||
|
||||
By default this just calls setBounds(), but is virtual in case it's needed for
|
||||
extremely cunning purposes.
|
||||
*/
|
||||
virtual void applyBoundsToComponent (Component&, Rectangle<int> bounds);
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
int minW = 0, maxW = 0x3fffffff, minH = 0, maxH = 0x3fffffff;
|
||||
int minOffTop = 0, minOffLeft = 0, minOffBottom = 0, minOffRight = 0;
|
||||
double aspectRatio = 0;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentBoundsConstrainer)
|
||||
};
|
||||
|
||||
} // namespace juce
|
287
modules/juce_gui_basics/layout/juce_ComponentBuilder.cpp
Normal file
287
modules/juce_gui_basics/layout/juce_ComponentBuilder.cpp
Normal file
@ -0,0 +1,287 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
namespace ComponentBuilderHelpers
|
||||
{
|
||||
static String getStateId (const ValueTree& state)
|
||||
{
|
||||
return state [ComponentBuilder::idProperty].toString();
|
||||
}
|
||||
|
||||
static Component* removeComponentWithID (OwnedArray<Component>& components, const String& compId)
|
||||
{
|
||||
jassert (compId.isNotEmpty());
|
||||
|
||||
for (int i = components.size(); --i >= 0;)
|
||||
{
|
||||
Component* const c = components.getUnchecked (i);
|
||||
|
||||
if (c->getComponentID() == compId)
|
||||
return components.removeAndReturn (i);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static Component* findComponentWithID (Component& c, const String& compId)
|
||||
{
|
||||
jassert (compId.isNotEmpty());
|
||||
if (c.getComponentID() == compId)
|
||||
return &c;
|
||||
|
||||
for (auto* child : c.getChildren())
|
||||
if (auto* found = findComponentWithID (*child, compId))
|
||||
return found;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static Component* createNewComponent (ComponentBuilder::TypeHandler& type,
|
||||
const ValueTree& state, Component* parent)
|
||||
{
|
||||
Component* const c = type.addNewComponentFromState (state, parent);
|
||||
jassert (c != nullptr && c->getParentComponent() == parent);
|
||||
c->setComponentID (getStateId (state));
|
||||
return c;
|
||||
}
|
||||
|
||||
static void updateComponent (ComponentBuilder& builder, const ValueTree& state)
|
||||
{
|
||||
if (Component* topLevelComp = builder.getManagedComponent())
|
||||
{
|
||||
ComponentBuilder::TypeHandler* const type = builder.getHandlerForState (state);
|
||||
const String uid (getStateId (state));
|
||||
|
||||
if (type == nullptr || uid.isEmpty())
|
||||
{
|
||||
// ..handle the case where a child of the actual state node has changed.
|
||||
if (state.getParent().isValid())
|
||||
updateComponent (builder, state.getParent());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Component* const changedComp = findComponentWithID (*topLevelComp, uid))
|
||||
type->updateComponentFromState (changedComp, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
const Identifier ComponentBuilder::idProperty ("id");
|
||||
|
||||
ComponentBuilder::ComponentBuilder()
|
||||
: imageProvider (nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
ComponentBuilder::ComponentBuilder (const ValueTree& state_)
|
||||
: state (state_), imageProvider (nullptr)
|
||||
{
|
||||
state.addListener (this);
|
||||
}
|
||||
|
||||
ComponentBuilder::~ComponentBuilder()
|
||||
{
|
||||
state.removeListener (this);
|
||||
|
||||
#if JUCE_DEBUG
|
||||
// Don't delete the managed component!! The builder owns that component, and will delete
|
||||
// it automatically when it gets deleted.
|
||||
jassert (componentRef.get() == component.get());
|
||||
#endif
|
||||
}
|
||||
|
||||
Component* ComponentBuilder::getManagedComponent()
|
||||
{
|
||||
if (component == nullptr)
|
||||
{
|
||||
component.reset (createComponent());
|
||||
|
||||
#if JUCE_DEBUG
|
||||
componentRef = component.get();
|
||||
#endif
|
||||
}
|
||||
|
||||
return component.get();
|
||||
}
|
||||
|
||||
Component* ComponentBuilder::createComponent()
|
||||
{
|
||||
jassert (types.size() > 0); // You need to register all the necessary types before you can load a component!
|
||||
|
||||
if (TypeHandler* const type = getHandlerForState (state))
|
||||
return ComponentBuilderHelpers::createNewComponent (*type, state, nullptr);
|
||||
|
||||
jassertfalse; // trying to create a component from an unknown type of ValueTree
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ComponentBuilder::registerTypeHandler (ComponentBuilder::TypeHandler* const type)
|
||||
{
|
||||
jassert (type != nullptr);
|
||||
|
||||
// Don't try to move your types around! Once a type has been added to a builder, the
|
||||
// builder owns it, and you should leave it alone!
|
||||
jassert (type->builder == nullptr);
|
||||
|
||||
types.add (type);
|
||||
type->builder = this;
|
||||
}
|
||||
|
||||
ComponentBuilder::TypeHandler* ComponentBuilder::getHandlerForState (const ValueTree& s) const
|
||||
{
|
||||
const Identifier targetType (s.getType());
|
||||
|
||||
for (int i = 0; i < types.size(); ++i)
|
||||
{
|
||||
TypeHandler* const t = types.getUnchecked(i);
|
||||
|
||||
if (t->type == targetType)
|
||||
return t;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int ComponentBuilder::getNumHandlers() const noexcept
|
||||
{
|
||||
return types.size();
|
||||
}
|
||||
|
||||
ComponentBuilder::TypeHandler* ComponentBuilder::getHandler (const int index) const noexcept
|
||||
{
|
||||
return types [index];
|
||||
}
|
||||
|
||||
void ComponentBuilder::registerStandardComponentTypes()
|
||||
{
|
||||
}
|
||||
|
||||
void ComponentBuilder::setImageProvider (ImageProvider* newImageProvider) noexcept
|
||||
{
|
||||
imageProvider = newImageProvider;
|
||||
}
|
||||
|
||||
ComponentBuilder::ImageProvider* ComponentBuilder::getImageProvider() const noexcept
|
||||
{
|
||||
return imageProvider;
|
||||
}
|
||||
|
||||
void ComponentBuilder::valueTreePropertyChanged (ValueTree& tree, const Identifier&)
|
||||
{
|
||||
ComponentBuilderHelpers::updateComponent (*this, tree);
|
||||
}
|
||||
|
||||
void ComponentBuilder::valueTreeChildAdded (ValueTree& tree, ValueTree&)
|
||||
{
|
||||
ComponentBuilderHelpers::updateComponent (*this, tree);
|
||||
}
|
||||
|
||||
void ComponentBuilder::valueTreeChildRemoved (ValueTree& tree, ValueTree&, int)
|
||||
{
|
||||
ComponentBuilderHelpers::updateComponent (*this, tree);
|
||||
}
|
||||
|
||||
void ComponentBuilder::valueTreeChildOrderChanged (ValueTree& tree, int, int)
|
||||
{
|
||||
ComponentBuilderHelpers::updateComponent (*this, tree);
|
||||
}
|
||||
|
||||
void ComponentBuilder::valueTreeParentChanged (ValueTree& tree)
|
||||
{
|
||||
ComponentBuilderHelpers::updateComponent (*this, tree);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
ComponentBuilder::TypeHandler::TypeHandler (const Identifier& valueTreeType)
|
||||
: type (valueTreeType), builder (nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
ComponentBuilder::TypeHandler::~TypeHandler()
|
||||
{
|
||||
}
|
||||
|
||||
ComponentBuilder* ComponentBuilder::TypeHandler::getBuilder() const noexcept
|
||||
{
|
||||
// A type handler needs to be registered with a ComponentBuilder before using it!
|
||||
jassert (builder != nullptr);
|
||||
return builder;
|
||||
}
|
||||
|
||||
void ComponentBuilder::updateChildComponents (Component& parent, const ValueTree& children)
|
||||
{
|
||||
using namespace ComponentBuilderHelpers;
|
||||
|
||||
auto numExistingChildComps = parent.getNumChildComponents();
|
||||
|
||||
Array<Component*> componentsInOrder;
|
||||
componentsInOrder.ensureStorageAllocated (numExistingChildComps);
|
||||
|
||||
{
|
||||
OwnedArray<Component> existingComponents;
|
||||
existingComponents.ensureStorageAllocated (numExistingChildComps);
|
||||
|
||||
for (int i = 0; i < numExistingChildComps; ++i)
|
||||
existingComponents.add (parent.getChildComponent (i));
|
||||
|
||||
auto newNumChildren = children.getNumChildren();
|
||||
|
||||
for (int i = 0; i < newNumChildren; ++i)
|
||||
{
|
||||
auto childState = children.getChild (i);
|
||||
auto* c = removeComponentWithID (existingComponents, getStateId (childState));
|
||||
|
||||
if (c == nullptr)
|
||||
{
|
||||
if (auto* type = getHandlerForState (childState))
|
||||
c = ComponentBuilderHelpers::createNewComponent (*type, childState, &parent);
|
||||
else
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
if (c != nullptr)
|
||||
componentsInOrder.add (c);
|
||||
}
|
||||
|
||||
// (remaining unused items in existingComponents get deleted here as it goes out of scope)
|
||||
}
|
||||
|
||||
// Make sure the z-order is correct..
|
||||
if (componentsInOrder.size() > 0)
|
||||
{
|
||||
componentsInOrder.getLast()->toFront (false);
|
||||
|
||||
for (int i = componentsInOrder.size() - 1; --i >= 0;)
|
||||
componentsInOrder.getUnchecked(i)->toBehind (componentsInOrder.getUnchecked (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace juce
|
248
modules/juce_gui_basics/layout/juce_ComponentBuilder.h
Normal file
248
modules/juce_gui_basics/layout/juce_ComponentBuilder.h
Normal file
@ -0,0 +1,248 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Loads and maintains a tree of Components from a ValueTree that represents them.
|
||||
|
||||
To allow the state of a tree of components to be saved as a ValueTree and re-loaded,
|
||||
this class lets you register a set of type-handlers for the different components that
|
||||
are involved, and then uses these types to re-create a set of components from its
|
||||
stored state.
|
||||
|
||||
Essentially, to use this, you need to create a ComponentBuilder with your ValueTree,
|
||||
then use registerTypeHandler() to give it a set of type handlers that can cope with
|
||||
all the items in your tree. Then you can call getComponent() to build the component.
|
||||
Once you've got the component you can either take it and delete the ComponentBuilder
|
||||
object, or if you keep the ComponentBuilder around, it'll monitor any changes in the
|
||||
ValueTree and automatically update the component to reflect these changes.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ComponentBuilder : private ValueTree::Listener
|
||||
{
|
||||
public:
|
||||
/** Creates a ComponentBuilder that will use the given state.
|
||||
Once you've created your builder, you should use registerTypeHandler() to register some
|
||||
type handlers for it, and then you can call createComponent() or getManagedComponent()
|
||||
to get the actual component.
|
||||
*/
|
||||
explicit ComponentBuilder (const ValueTree& state);
|
||||
|
||||
/** Creates a builder that doesn't have a state object. */
|
||||
ComponentBuilder();
|
||||
|
||||
/** Destructor. */
|
||||
~ComponentBuilder();
|
||||
|
||||
/** This is the ValueTree data object that the builder is working with. */
|
||||
ValueTree state;
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the builder's component (creating it if necessary).
|
||||
|
||||
The first time that this method is called, the builder will attempt to create a component
|
||||
from the ValueTree, so you must have registered some suitable type handlers before calling
|
||||
this. If there's a problem and the component can't be created, this method returns nullptr.
|
||||
|
||||
The component that is returned is owned by this ComponentBuilder, so you can put it inside
|
||||
your own parent components, but don't delete it! The ComponentBuilder will delete it automatically
|
||||
when the builder is destroyed. If you want to get a component that you can delete yourself,
|
||||
call createComponent() instead.
|
||||
|
||||
The ComponentBuilder will update this component if any changes are made to the ValueTree, so if
|
||||
there's a chance that the tree might change, be careful not to keep any pointers to sub-components,
|
||||
as they may be changed or removed.
|
||||
*/
|
||||
Component* getManagedComponent();
|
||||
|
||||
/** Creates and returns a new instance of the component that the ValueTree represents.
|
||||
The caller is responsible for using and deleting the object that is returned. Unlike
|
||||
getManagedComponent(), the component that is returned will not be updated by the builder.
|
||||
*/
|
||||
Component* createComponent();
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
The class is a base class for objects that manage the loading of a type of component
|
||||
from a ValueTree.
|
||||
|
||||
To store and re-load a tree of components as a ValueTree, each component type must have
|
||||
a TypeHandler to represent it.
|
||||
|
||||
@see ComponentBuilder::registerTypeHandler(), Drawable::registerDrawableTypeHandlers()
|
||||
*/
|
||||
class JUCE_API TypeHandler
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a TypeHandler.
|
||||
The valueTreeType must be the type name of the ValueTrees that this handler can parse.
|
||||
*/
|
||||
explicit TypeHandler (const Identifier& valueTreeType);
|
||||
|
||||
/** Destructor. */
|
||||
virtual ~TypeHandler();
|
||||
|
||||
/** Returns the type of the ValueTrees that this handler can parse. */
|
||||
const Identifier type;
|
||||
|
||||
/** Returns the builder that this type is registered with. */
|
||||
ComponentBuilder* getBuilder() const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** This method must create a new component from the given state, add it to the specified
|
||||
parent component (which may be null), and return it.
|
||||
|
||||
The ValueTree will have been pre-checked to make sure that its type matches the type
|
||||
that this handler supports.
|
||||
|
||||
There's no need to set the new Component's ID to match that of the state - the builder
|
||||
will take care of that itself.
|
||||
*/
|
||||
virtual Component* addNewComponentFromState (const ValueTree& state, Component* parent) = 0;
|
||||
|
||||
/** This method must update an existing component from a new ValueTree state.
|
||||
|
||||
A component that has been created with addNewComponentFromState() may need to be updated
|
||||
if the ValueTree changes, so this method is used to do that. Your implementation must do
|
||||
whatever's necessary to update the component from the new state provided.
|
||||
|
||||
The ValueTree will have been pre-checked to make sure that its type matches the type
|
||||
that this handler supports, and the component will have been created by this type's
|
||||
addNewComponentFromState() method.
|
||||
*/
|
||||
virtual void updateComponentFromState (Component* component, const ValueTree& state) = 0;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
friend class ComponentBuilder;
|
||||
ComponentBuilder* builder;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TypeHandler)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Adds a type handler that the builder can use when trying to load components.
|
||||
@see Drawable::registerDrawableTypeHandlers()
|
||||
*/
|
||||
void registerTypeHandler (TypeHandler* type);
|
||||
|
||||
/** Tries to find a registered type handler that can load a component from the given ValueTree. */
|
||||
TypeHandler* getHandlerForState (const ValueTree& state) const;
|
||||
|
||||
/** Returns the number of registered type handlers.
|
||||
@see getHandler, registerTypeHandler
|
||||
*/
|
||||
int getNumHandlers() const noexcept;
|
||||
|
||||
/** Returns one of the registered type handlers.
|
||||
@see getNumHandlers, registerTypeHandler
|
||||
*/
|
||||
TypeHandler* getHandler (int index) const noexcept;
|
||||
|
||||
/** Registers handlers for various standard juce components. */
|
||||
void registerStandardComponentTypes();
|
||||
|
||||
//==============================================================================
|
||||
/** This class is used when references to images need to be stored in ValueTrees.
|
||||
|
||||
An instance of an ImageProvider provides a mechanism for converting an Image to/from
|
||||
a reference, which may be a file, URL, ID string, or whatever system is appropriate in
|
||||
your app.
|
||||
|
||||
When you're loading components from a ValueTree that may need a way of loading images, you
|
||||
should call ComponentBuilder::setImageProvider() to supply a suitable provider before
|
||||
trying to load the component.
|
||||
|
||||
@see ComponentBuilder::setImageProvider()
|
||||
*/
|
||||
class JUCE_API ImageProvider
|
||||
{
|
||||
public:
|
||||
ImageProvider() {}
|
||||
virtual ~ImageProvider() {}
|
||||
|
||||
/** Retrieves the image associated with this identifier, which could be any
|
||||
kind of string, number, filename, etc.
|
||||
|
||||
The image that is returned will be owned by the caller, but it may come
|
||||
from the ImageCache.
|
||||
*/
|
||||
virtual Image getImageForIdentifier (const var& imageIdentifier) = 0;
|
||||
|
||||
/** Returns an identifier to be used to refer to a given image.
|
||||
This is used when a reference to an image is stored in a ValueTree.
|
||||
*/
|
||||
virtual var getIdentifierForImage (const Image& image) = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Gives the builder an ImageProvider object that the type handlers can use when
|
||||
loading images from stored references.
|
||||
|
||||
The object that is passed in is not owned by the builder, so the caller must delete
|
||||
it when it is no longer needed, but not while the builder may still be using it. To
|
||||
clear the image provider, just call setImageProvider (nullptr).
|
||||
*/
|
||||
void setImageProvider (ImageProvider* newImageProvider) noexcept;
|
||||
|
||||
/** Returns the current image provider that this builder is using, or nullptr if none has been set. */
|
||||
ImageProvider* getImageProvider() const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Updates the children of a parent component by updating them from the children of
|
||||
a given ValueTree.
|
||||
*/
|
||||
void updateChildComponents (Component& parent, const ValueTree& children);
|
||||
|
||||
/** An identifier for the property of the ValueTrees that is used to store a unique ID
|
||||
for that component.
|
||||
*/
|
||||
static const Identifier idProperty;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
OwnedArray<TypeHandler> types;
|
||||
std::unique_ptr<Component> component;
|
||||
ImageProvider* imageProvider;
|
||||
#if JUCE_DEBUG
|
||||
WeakReference<Component> componentRef;
|
||||
#endif
|
||||
|
||||
void valueTreePropertyChanged (ValueTree&, const Identifier&) override;
|
||||
void valueTreeChildAdded (ValueTree&, ValueTree&) override;
|
||||
void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override;
|
||||
void valueTreeChildOrderChanged (ValueTree&, int, int) override;
|
||||
void valueTreeParentChanged (ValueTree&) override;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentBuilder)
|
||||
};
|
||||
|
||||
} // namespace juce
|
143
modules/juce_gui_basics/layout/juce_ComponentMovementWatcher.cpp
Normal file
143
modules/juce_gui_basics/layout/juce_ComponentMovementWatcher.cpp
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
ComponentMovementWatcher::ComponentMovementWatcher (Component* const comp)
|
||||
: component (comp),
|
||||
wasShowing (comp->isShowing())
|
||||
{
|
||||
jassert (component != nullptr); // can't use this with a null pointer..
|
||||
|
||||
component->addComponentListener (this);
|
||||
registerWithParentComps();
|
||||
}
|
||||
|
||||
ComponentMovementWatcher::~ComponentMovementWatcher()
|
||||
{
|
||||
if (component != nullptr)
|
||||
component->removeComponentListener (this);
|
||||
|
||||
unregister();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ComponentMovementWatcher::componentParentHierarchyChanged (Component&)
|
||||
{
|
||||
if (component != nullptr && ! reentrant)
|
||||
{
|
||||
const ScopedValueSetter<bool> setter (reentrant, true);
|
||||
|
||||
auto* peer = component->getPeer();
|
||||
auto peerID = peer != nullptr ? peer->getUniqueID() : 0;
|
||||
|
||||
if (peerID != lastPeerID)
|
||||
{
|
||||
componentPeerChanged();
|
||||
|
||||
if (component == nullptr)
|
||||
return;
|
||||
|
||||
lastPeerID = peerID;
|
||||
}
|
||||
|
||||
unregister();
|
||||
registerWithParentComps();
|
||||
|
||||
componentMovedOrResized (*component, true, true);
|
||||
|
||||
if (component != nullptr)
|
||||
componentVisibilityChanged (*component);
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentMovementWatcher::componentMovedOrResized (Component&, bool wasMoved, bool wasResized)
|
||||
{
|
||||
if (component != nullptr)
|
||||
{
|
||||
if (wasMoved)
|
||||
{
|
||||
Point<int> newPos;
|
||||
auto* top = component->getTopLevelComponent();
|
||||
|
||||
if (top != component)
|
||||
newPos = top->getLocalPoint (component, Point<int>());
|
||||
else
|
||||
newPos = top->getPosition();
|
||||
|
||||
wasMoved = lastBounds.getPosition() != newPos;
|
||||
lastBounds.setPosition (newPos);
|
||||
}
|
||||
|
||||
wasResized = (lastBounds.getWidth() != component->getWidth() || lastBounds.getHeight() != component->getHeight());
|
||||
lastBounds.setSize (component->getWidth(), component->getHeight());
|
||||
|
||||
if (wasMoved || wasResized)
|
||||
componentMovedOrResized (wasMoved, wasResized);
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentMovementWatcher::componentBeingDeleted (Component& comp)
|
||||
{
|
||||
registeredParentComps.removeFirstMatchingValue (&comp);
|
||||
|
||||
if (component == &comp)
|
||||
unregister();
|
||||
}
|
||||
|
||||
void ComponentMovementWatcher::componentVisibilityChanged (Component&)
|
||||
{
|
||||
if (component != nullptr)
|
||||
{
|
||||
const bool isShowingNow = component->isShowing();
|
||||
|
||||
if (wasShowing != isShowingNow)
|
||||
{
|
||||
wasShowing = isShowingNow;
|
||||
componentVisibilityChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentMovementWatcher::registerWithParentComps()
|
||||
{
|
||||
for (auto* p = component->getParentComponent(); p != nullptr; p = p->getParentComponent())
|
||||
{
|
||||
p->addComponentListener (this);
|
||||
registeredParentComps.add (p);
|
||||
}
|
||||
}
|
||||
|
||||
void ComponentMovementWatcher::unregister()
|
||||
{
|
||||
for (auto* c : registeredParentComps)
|
||||
c->removeComponentListener (this);
|
||||
|
||||
registeredParentComps.clear();
|
||||
}
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/** An object that watches for any movement of a component or any of its parent components.
|
||||
|
||||
This makes it easy to check when a component is moved relative to its top-level
|
||||
peer window. The normal Component::moved() method is only called when a component
|
||||
moves relative to its immediate parent, and sometimes you want to know if any of
|
||||
components higher up the tree have moved (which of course will affect the overall
|
||||
position of all their sub-components).
|
||||
|
||||
It also includes a callback that lets you know when the top-level peer is changed.
|
||||
|
||||
This class is used by specialised components like WebBrowserComponent
|
||||
because they need to keep their custom windows in the right place and respond to
|
||||
changes in the peer.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ComponentMovementWatcher : public ComponentListener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a ComponentMovementWatcher to watch a given target component. */
|
||||
ComponentMovementWatcher (Component* component);
|
||||
|
||||
/** Destructor. */
|
||||
~ComponentMovementWatcher();
|
||||
|
||||
//==============================================================================
|
||||
/** This callback happens when the component that is being watched is moved
|
||||
relative to its top-level peer window, or when it is resized. */
|
||||
virtual void componentMovedOrResized (bool wasMoved, bool wasResized) = 0;
|
||||
|
||||
/** This callback happens when the component's top-level peer is changed. */
|
||||
virtual void componentPeerChanged() = 0;
|
||||
|
||||
/** This callback happens when the component's visibility state changes, possibly due to
|
||||
one of its parents being made visible or invisible.
|
||||
*/
|
||||
virtual void componentVisibilityChanged() = 0;
|
||||
|
||||
/** Returns the component that's being watched. */
|
||||
Component* getComponent() const noexcept { return component; }
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void componentParentHierarchyChanged (Component&) override;
|
||||
/** @internal */
|
||||
void componentMovedOrResized (Component&, bool wasMoved, bool wasResized) override;
|
||||
/** @internal */
|
||||
void componentBeingDeleted (Component&) override;
|
||||
/** @internal */
|
||||
void componentVisibilityChanged (Component&) override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
WeakReference<Component> component;
|
||||
uint32 lastPeerID = 0;
|
||||
Array<Component*> registeredParentComps;
|
||||
bool reentrant = false, wasShowing;
|
||||
Rectangle<int> lastBounds;
|
||||
|
||||
void unregister();
|
||||
void registerWithParentComps();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentMovementWatcher)
|
||||
};
|
||||
|
||||
} // namespace juce
|
458
modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp
Normal file
458
modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp
Normal file
@ -0,0 +1,458 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
struct ConcertinaPanel::PanelSizes
|
||||
{
|
||||
struct Panel
|
||||
{
|
||||
Panel() noexcept {}
|
||||
|
||||
Panel (const int sz, const int mn, const int mx) noexcept
|
||||
: size (sz), minSize (mn), maxSize (mx) {}
|
||||
|
||||
int setSize (const int newSize) noexcept
|
||||
{
|
||||
jassert (minSize <= maxSize);
|
||||
const int oldSize = size;
|
||||
size = jlimit (minSize, maxSize, newSize);
|
||||
return size - oldSize;
|
||||
}
|
||||
|
||||
int expand (int amount) noexcept
|
||||
{
|
||||
amount = jmin (amount, maxSize - size);
|
||||
size += amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
int reduce (int amount) noexcept
|
||||
{
|
||||
amount = jmin (amount, size - minSize);
|
||||
size -= amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
bool canExpand() const noexcept { return size < maxSize; }
|
||||
bool isMinimised() const noexcept { return size <= minSize; }
|
||||
|
||||
int size, minSize, maxSize;
|
||||
};
|
||||
|
||||
Array<Panel> sizes;
|
||||
|
||||
Panel& get (const int index) const noexcept { return sizes.getReference(index); }
|
||||
|
||||
PanelSizes withMovedPanel (const int index, int targetPosition, int totalSpace) const
|
||||
{
|
||||
const int num = sizes.size();
|
||||
totalSpace = jmax (totalSpace, getMinimumSize (0, num));
|
||||
targetPosition = jmax (targetPosition, totalSpace - getMaximumSize (index, num));
|
||||
|
||||
PanelSizes newSizes (*this);
|
||||
newSizes.stretchRange (0, index, targetPosition - newSizes.getTotalSize (0, index), stretchLast);
|
||||
newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, index) - newSizes.getTotalSize (index, num), stretchFirst);
|
||||
return newSizes;
|
||||
}
|
||||
|
||||
PanelSizes fittedInto (int totalSpace) const
|
||||
{
|
||||
PanelSizes newSizes (*this);
|
||||
const int num = newSizes.sizes.size();
|
||||
totalSpace = jmax (totalSpace, getMinimumSize (0, num));
|
||||
newSizes.stretchRange (0, num, totalSpace - newSizes.getTotalSize (0, num), stretchAll);
|
||||
return newSizes;
|
||||
}
|
||||
|
||||
PanelSizes withResizedPanel (const int index, int panelHeight, int totalSpace) const
|
||||
{
|
||||
PanelSizes newSizes (*this);
|
||||
|
||||
if (totalSpace <= 0)
|
||||
{
|
||||
newSizes.get(index).size = panelHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
const int num = sizes.size();
|
||||
const int minSize = getMinimumSize (0, num);
|
||||
totalSpace = jmax (totalSpace, minSize);
|
||||
|
||||
newSizes.get(index).setSize (panelHeight);
|
||||
newSizes.stretchRange (0, index, totalSpace - newSizes.getTotalSize (0, num), stretchLast);
|
||||
newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, num), stretchLast);
|
||||
newSizes = newSizes.fittedInto (totalSpace);
|
||||
}
|
||||
|
||||
return newSizes;
|
||||
}
|
||||
|
||||
private:
|
||||
enum ExpandMode
|
||||
{
|
||||
stretchAll,
|
||||
stretchFirst,
|
||||
stretchLast
|
||||
};
|
||||
|
||||
void growRangeFirst (const int start, const int end, int spaceDiff) noexcept
|
||||
{
|
||||
for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
|
||||
for (int i = start; i < end && spaceDiff > 0; ++i)
|
||||
spaceDiff -= get (i).expand (spaceDiff);
|
||||
}
|
||||
|
||||
void growRangeLast (const int start, const int end, int spaceDiff) noexcept
|
||||
{
|
||||
for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
|
||||
for (int i = end; --i >= start && spaceDiff > 0;)
|
||||
spaceDiff -= get (i).expand (spaceDiff);
|
||||
}
|
||||
|
||||
void growRangeAll (const int start, const int end, int spaceDiff) noexcept
|
||||
{
|
||||
Array<Panel*> expandableItems;
|
||||
|
||||
for (int i = start; i < end; ++i)
|
||||
if (get(i).canExpand() && ! get(i).isMinimised())
|
||||
expandableItems.add (& get(i));
|
||||
|
||||
for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
|
||||
for (int i = expandableItems.size(); --i >= 0 && spaceDiff > 0;)
|
||||
spaceDiff -= expandableItems.getUnchecked(i)->expand (spaceDiff / (i + 1));
|
||||
|
||||
growRangeLast (start, end, spaceDiff);
|
||||
}
|
||||
|
||||
void shrinkRangeFirst (const int start, const int end, int spaceDiff) noexcept
|
||||
{
|
||||
for (int i = start; i < end && spaceDiff > 0; ++i)
|
||||
spaceDiff -= get(i).reduce (spaceDiff);
|
||||
}
|
||||
|
||||
void shrinkRangeLast (const int start, const int end, int spaceDiff) noexcept
|
||||
{
|
||||
for (int i = end; --i >= start && spaceDiff > 0;)
|
||||
spaceDiff -= get(i).reduce (spaceDiff);
|
||||
}
|
||||
|
||||
void stretchRange (const int start, const int end, const int amountToAdd,
|
||||
const ExpandMode expandMode) noexcept
|
||||
{
|
||||
if (end > start)
|
||||
{
|
||||
if (amountToAdd > 0)
|
||||
{
|
||||
if (expandMode == stretchAll) growRangeAll (start, end, amountToAdd);
|
||||
else if (expandMode == stretchFirst) growRangeFirst (start, end, amountToAdd);
|
||||
else if (expandMode == stretchLast) growRangeLast (start, end, amountToAdd);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expandMode == stretchFirst) shrinkRangeFirst (start, end, -amountToAdd);
|
||||
else shrinkRangeLast (start, end, -amountToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getTotalSize (int start, const int end) const noexcept
|
||||
{
|
||||
int tot = 0;
|
||||
while (start < end) tot += get (start++).size;
|
||||
return tot;
|
||||
}
|
||||
|
||||
int getMinimumSize (int start, const int end) const noexcept
|
||||
{
|
||||
int tot = 0;
|
||||
while (start < end) tot += get (start++).minSize;
|
||||
return tot;
|
||||
}
|
||||
|
||||
int getMaximumSize (int start, const int end) const noexcept
|
||||
{
|
||||
int tot = 0;
|
||||
while (start < end)
|
||||
{
|
||||
const int mx = get (start++).maxSize;
|
||||
if (mx > 0x100000)
|
||||
return mx;
|
||||
|
||||
tot += mx;
|
||||
}
|
||||
|
||||
return tot;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class ConcertinaPanel::PanelHolder : public Component
|
||||
{
|
||||
public:
|
||||
PanelHolder (Component* const comp, bool takeOwnership)
|
||||
: component (comp, takeOwnership)
|
||||
{
|
||||
setRepaintsOnMouseActivity (true);
|
||||
setWantsKeyboardFocus (false);
|
||||
addAndMakeVisible (comp);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
if (customHeaderComponent == nullptr)
|
||||
{
|
||||
const Rectangle<int> area (getWidth(), getHeaderSize());
|
||||
g.reduceClipRegion (area);
|
||||
|
||||
getLookAndFeel().drawConcertinaPanelHeader (g, area, isMouseOver(), isMouseButtonDown(),
|
||||
getPanel(), *component);
|
||||
}
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
auto headerBounds = bounds.removeFromTop (getHeaderSize());
|
||||
|
||||
if (customHeaderComponent != nullptr)
|
||||
customHeaderComponent->setBounds (headerBounds);
|
||||
|
||||
component->setBounds (bounds);
|
||||
}
|
||||
|
||||
void mouseDown (const MouseEvent&) override
|
||||
{
|
||||
mouseDownY = getY();
|
||||
dragStartSizes = getPanel().getFittedSizes();
|
||||
}
|
||||
|
||||
void mouseDrag (const MouseEvent& e) override
|
||||
{
|
||||
ConcertinaPanel& panel = getPanel();
|
||||
panel.setLayout (dragStartSizes.withMovedPanel (panel.holders.indexOf (this),
|
||||
mouseDownY + e.getDistanceFromDragStartY(),
|
||||
panel.getHeight()), false);
|
||||
}
|
||||
|
||||
void mouseDoubleClick (const MouseEvent&) override
|
||||
{
|
||||
getPanel().panelHeaderDoubleClicked (component);
|
||||
}
|
||||
|
||||
void setCustomHeaderComponent (Component* headerComponent, bool shouldTakeOwnership)
|
||||
{
|
||||
customHeaderComponent.set (headerComponent, shouldTakeOwnership);
|
||||
|
||||
if (headerComponent != nullptr)
|
||||
{
|
||||
addAndMakeVisible (customHeaderComponent);
|
||||
customHeaderComponent->addMouseListener (this, false);
|
||||
}
|
||||
}
|
||||
|
||||
OptionalScopedPointer<Component> component;
|
||||
|
||||
private:
|
||||
PanelSizes dragStartSizes;
|
||||
int mouseDownY;
|
||||
OptionalScopedPointer<Component> customHeaderComponent;
|
||||
|
||||
int getHeaderSize() const noexcept
|
||||
{
|
||||
ConcertinaPanel& panel = getPanel();
|
||||
const int ourIndex = panel.holders.indexOf (this);
|
||||
return panel.currentSizes->get(ourIndex).minSize;
|
||||
}
|
||||
|
||||
ConcertinaPanel& getPanel() const
|
||||
{
|
||||
ConcertinaPanel* const panel = dynamic_cast<ConcertinaPanel*> (getParentComponent());
|
||||
jassert (panel != nullptr);
|
||||
return *panel;
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PanelHolder)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
ConcertinaPanel::ConcertinaPanel()
|
||||
: currentSizes (new PanelSizes()),
|
||||
headerHeight (20)
|
||||
{
|
||||
}
|
||||
|
||||
ConcertinaPanel::~ConcertinaPanel() {}
|
||||
|
||||
int ConcertinaPanel::getNumPanels() const noexcept
|
||||
{
|
||||
return holders.size();
|
||||
}
|
||||
|
||||
Component* ConcertinaPanel::getPanel (int index) const noexcept
|
||||
{
|
||||
if (PanelHolder* h = holders[index])
|
||||
return h->component;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ConcertinaPanel::addPanel (int insertIndex, Component* component, bool takeOwnership)
|
||||
{
|
||||
jassert (component != nullptr); // can't use a null pointer here!
|
||||
jassert (indexOfComp (component) < 0); // You can't add the same component more than once!
|
||||
|
||||
PanelHolder* const holder = new PanelHolder (component, takeOwnership);
|
||||
holders.insert (insertIndex, holder);
|
||||
currentSizes->sizes.insert (insertIndex, PanelSizes::Panel (headerHeight, headerHeight, std::numeric_limits<int>::max()));
|
||||
addAndMakeVisible (holder);
|
||||
resized();
|
||||
}
|
||||
|
||||
void ConcertinaPanel::removePanel (Component* component)
|
||||
{
|
||||
const int index = indexOfComp (component);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
currentSizes->sizes.remove (index);
|
||||
holders.remove (index);
|
||||
resized();
|
||||
}
|
||||
}
|
||||
|
||||
bool ConcertinaPanel::setPanelSize (Component* panelComponent, int height, const bool animate)
|
||||
{
|
||||
const int index = indexOfComp (panelComponent);
|
||||
jassert (index >= 0); // The specified component doesn't seem to have been added!
|
||||
|
||||
height += currentSizes->get(index).minSize;
|
||||
const int oldSize = currentSizes->get(index).size;
|
||||
setLayout (currentSizes->withResizedPanel (index, height, getHeight()), animate);
|
||||
return oldSize != currentSizes->get(index).size;
|
||||
}
|
||||
|
||||
bool ConcertinaPanel::expandPanelFully (Component* component, const bool animate)
|
||||
{
|
||||
return setPanelSize (component, getHeight(), animate);
|
||||
}
|
||||
|
||||
void ConcertinaPanel::setMaximumPanelSize (Component* component, int maximumSize)
|
||||
{
|
||||
const int index = indexOfComp (component);
|
||||
jassert (index >= 0); // The specified component doesn't seem to have been added!
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
currentSizes->get(index).maxSize = currentSizes->get(index).minSize + maximumSize;
|
||||
resized();
|
||||
}
|
||||
}
|
||||
|
||||
void ConcertinaPanel::setPanelHeaderSize (Component* component, int headerSize)
|
||||
{
|
||||
const auto index = indexOfComp (component);
|
||||
jassert (index >= 0); // The specified component doesn't seem to have been added!
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
auto oldMin = currentSizes->get (index).minSize;
|
||||
|
||||
currentSizes->get (index).minSize = headerSize;
|
||||
currentSizes->get (index).size += headerSize - oldMin;
|
||||
resized();
|
||||
}
|
||||
}
|
||||
|
||||
void ConcertinaPanel::setCustomPanelHeader (Component* component, Component* customComponent, bool takeOwnership)
|
||||
{
|
||||
OptionalScopedPointer<Component> optional (customComponent, takeOwnership);
|
||||
|
||||
const auto index = indexOfComp (component);
|
||||
jassert (index >= 0); // The specified component doesn't seem to have been added!
|
||||
|
||||
if (index >= 0)
|
||||
holders.getUnchecked (index)->setCustomHeaderComponent (optional.release(), takeOwnership);
|
||||
}
|
||||
|
||||
void ConcertinaPanel::resized()
|
||||
{
|
||||
applyLayout (getFittedSizes(), false);
|
||||
}
|
||||
|
||||
int ConcertinaPanel::indexOfComp (Component* comp) const noexcept
|
||||
{
|
||||
for (int i = 0; i < holders.size(); ++i)
|
||||
if (holders.getUnchecked(i)->component == comp)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
ConcertinaPanel::PanelSizes ConcertinaPanel::getFittedSizes() const
|
||||
{
|
||||
return currentSizes->fittedInto (getHeight());
|
||||
}
|
||||
|
||||
void ConcertinaPanel::applyLayout (const PanelSizes& sizes, const bool animate)
|
||||
{
|
||||
if (! animate)
|
||||
animator.cancelAllAnimations (false);
|
||||
|
||||
const int animationDuration = 150;
|
||||
const int w = getWidth();
|
||||
int y = 0;
|
||||
|
||||
for (int i = 0; i < holders.size(); ++i)
|
||||
{
|
||||
PanelHolder& p = *holders.getUnchecked (i);
|
||||
|
||||
const int h = sizes.get (i).size;
|
||||
const Rectangle<int> pos (0, y, w, h);
|
||||
|
||||
if (animate)
|
||||
animator.animateComponent (&p, pos, 1.0f, animationDuration, false, 1.0, 1.0);
|
||||
else
|
||||
p.setBounds (pos);
|
||||
|
||||
y += h;
|
||||
}
|
||||
}
|
||||
|
||||
void ConcertinaPanel::setLayout (const PanelSizes& sizes, const bool animate)
|
||||
{
|
||||
*currentSizes = sizes;
|
||||
applyLayout (getFittedSizes(), animate);
|
||||
}
|
||||
|
||||
void ConcertinaPanel::panelHeaderDoubleClicked (Component* component)
|
||||
{
|
||||
if (! expandPanelFully (component, true))
|
||||
setPanelSize (component, 0, true);
|
||||
}
|
||||
|
||||
} // namespace juce
|
147
modules/juce_gui_basics/layout/juce_ConcertinaPanel.h
Normal file
147
modules/juce_gui_basics/layout/juce_ConcertinaPanel.h
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A panel which holds a vertical stack of components which can be expanded
|
||||
and contracted.
|
||||
|
||||
Each section has its own header bar which can be dragged up and down
|
||||
to resize it, or double-clicked to fully expand that section.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ConcertinaPanel : public Component
|
||||
{
|
||||
public:
|
||||
/** Creates an empty concertina panel.
|
||||
You can call addPanel() to add some components to it.
|
||||
*/
|
||||
ConcertinaPanel();
|
||||
|
||||
/** Destructor. */
|
||||
~ConcertinaPanel();
|
||||
|
||||
/** Adds a component to the panel.
|
||||
@param insertIndex the index at which this component will be inserted, or
|
||||
-1 to append it to the end of the list.
|
||||
@param component the component that will be shown
|
||||
@param takeOwnership if true, then the ConcertinaPanel will take ownership
|
||||
of the content component, and will delete it later when
|
||||
it's no longer needed. If false, it won't delete it, and
|
||||
you must make sure it doesn't get deleted while in use.
|
||||
*/
|
||||
void addPanel (int insertIndex, Component* component, bool takeOwnership);
|
||||
|
||||
/** Removes one of the panels.
|
||||
If the takeOwnership flag was set when the panel was added, then
|
||||
this will also delete the component.
|
||||
*/
|
||||
void removePanel (Component* panelComponent);
|
||||
|
||||
/** Returns the number of panels.
|
||||
@see getPanel
|
||||
*/
|
||||
int getNumPanels() const noexcept;
|
||||
|
||||
/** Returns one of the panels.
|
||||
@see getNumPanels()
|
||||
*/
|
||||
Component* getPanel (int index) const noexcept;
|
||||
|
||||
/** Resizes one of the panels.
|
||||
The panelComponent must point to a valid panel component.
|
||||
If animate is true, the panels will be animated into their new positions;
|
||||
if false, they will just be immediately resized.
|
||||
*/
|
||||
bool setPanelSize (Component* panelComponent, int newHeight, bool animate);
|
||||
|
||||
/** Attempts to make one of the panels full-height.
|
||||
The panelComponent must point to a valid panel component.
|
||||
If this component has had a maximum size set, then it will be
|
||||
expanded to that size. Otherwise, it'll fill as much of the total
|
||||
space as possible.
|
||||
*/
|
||||
bool expandPanelFully (Component* panelComponent, bool animate);
|
||||
|
||||
/** Sets a maximum size for one of the panels. */
|
||||
void setMaximumPanelSize (Component* panelComponent, int maximumSize);
|
||||
|
||||
/** Sets the height of the header section for one of the panels. */
|
||||
void setPanelHeaderSize (Component* panelComponent, int headerSize);
|
||||
|
||||
/** Sets a custom header Component for one of the panels.
|
||||
|
||||
@param panelComponent the panel component to add the custom header to.
|
||||
@param customHeaderComponent the custom component to use for the panel header.
|
||||
This can be nullptr to clear the custom header component
|
||||
and just use the standard LookAndFeel panel.
|
||||
@param takeOwnership if true, then the PanelHolder will take ownership
|
||||
of the custom header component, and will delete it later when
|
||||
it's no longer needed. If false, it won't delete it, and
|
||||
you must make sure it doesn't get deleted while in use.
|
||||
*/
|
||||
void setCustomPanelHeader (Component* panelComponent, Component* customHeaderComponent, bool takeOwnership);
|
||||
|
||||
//==============================================================================
|
||||
/** This abstract base class is implemented by LookAndFeel classes. */
|
||||
struct JUCE_API LookAndFeelMethods
|
||||
{
|
||||
virtual ~LookAndFeelMethods() {}
|
||||
|
||||
virtual void drawConcertinaPanelHeader (Graphics&, const Rectangle<int>& area,
|
||||
bool isMouseOver, bool isMouseDown,
|
||||
ConcertinaPanel&, Component&) = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
void resized() override;
|
||||
|
||||
class PanelHolder;
|
||||
struct PanelSizes;
|
||||
friend class PanelHolder;
|
||||
friend struct PanelSizes;
|
||||
friend struct ContainerDeletePolicy<PanelSizes>;
|
||||
friend struct ContainerDeletePolicy<PanelHolder>;
|
||||
|
||||
std::unique_ptr<PanelSizes> currentSizes;
|
||||
OwnedArray<PanelHolder> holders;
|
||||
ComponentAnimator animator;
|
||||
int headerHeight;
|
||||
|
||||
int indexOfComp (Component*) const noexcept;
|
||||
PanelSizes getFittedSizes() const;
|
||||
void applyLayout (const PanelSizes&, bool animate);
|
||||
void setLayout (const PanelSizes&, bool animate);
|
||||
void panelHeaderDoubleClicked (Component*);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConcertinaPanel)
|
||||
};
|
||||
|
||||
} // namespace juce
|
860
modules/juce_gui_basics/layout/juce_FlexBox.cpp
Normal file
860
modules/juce_gui_basics/layout/juce_FlexBox.cpp
Normal file
@ -0,0 +1,860 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
struct FlexBoxLayoutCalculation
|
||||
{
|
||||
using Coord = double;
|
||||
|
||||
FlexBoxLayoutCalculation (FlexBox& fb, Coord w, Coord h)
|
||||
: owner (fb), parentWidth (w), parentHeight (h), numItems (owner.items.size()),
|
||||
isRowDirection (fb.flexDirection == FlexBox::Direction::row
|
||||
|| fb.flexDirection == FlexBox::Direction::rowReverse),
|
||||
containerLineLength (isRowDirection ? parentWidth : parentHeight)
|
||||
{
|
||||
lineItems.calloc (numItems * numItems);
|
||||
lineInfo.calloc (numItems);
|
||||
}
|
||||
|
||||
struct ItemWithState
|
||||
{
|
||||
ItemWithState (FlexItem& source) noexcept : item (&source) {}
|
||||
|
||||
FlexItem* item;
|
||||
Coord lockedWidth = 0, lockedHeight = 0;
|
||||
Coord lockedMarginLeft = 0, lockedMarginRight = 0, lockedMarginTop = 0, lockedMarginBottom = 0;
|
||||
Coord preferredWidth = 0, preferredHeight = 0;
|
||||
bool locked = false;
|
||||
|
||||
void resetItemLockedSize() noexcept
|
||||
{
|
||||
lockedWidth = preferredWidth;
|
||||
lockedHeight = preferredHeight;
|
||||
lockedMarginLeft = getValueOrZeroIfAuto (item->margin.left);
|
||||
lockedMarginRight = getValueOrZeroIfAuto (item->margin.right);
|
||||
lockedMarginTop = getValueOrZeroIfAuto (item->margin.top);
|
||||
lockedMarginBottom = getValueOrZeroIfAuto (item->margin.bottom);
|
||||
}
|
||||
|
||||
void setWidthChecked (Coord newWidth) noexcept
|
||||
{
|
||||
if (isAssigned (item->maxWidth)) newWidth = jmin (newWidth, static_cast<Coord> (item->maxWidth));
|
||||
if (isAssigned (item->minWidth)) newWidth = jmax (newWidth, static_cast<Coord> (item->minWidth));
|
||||
|
||||
lockedWidth = newWidth;
|
||||
}
|
||||
|
||||
void setHeightChecked (Coord newHeight) noexcept
|
||||
{
|
||||
if (isAssigned (item->maxHeight)) newHeight = jmin (newHeight, (Coord) item->maxHeight);
|
||||
if (isAssigned (item->minHeight)) newHeight = jmax (newHeight, (Coord) item->minHeight);
|
||||
|
||||
lockedHeight = newHeight;
|
||||
}
|
||||
};
|
||||
|
||||
struct RowInfo
|
||||
{
|
||||
int numItems;
|
||||
Coord crossSize, lineY, totalLength;
|
||||
};
|
||||
|
||||
FlexBox& owner;
|
||||
const Coord parentWidth, parentHeight;
|
||||
const int numItems;
|
||||
const bool isRowDirection;
|
||||
const Coord containerLineLength;
|
||||
|
||||
int numberOfRows = 1;
|
||||
Coord containerCrossLength = 0;
|
||||
|
||||
HeapBlock<ItemWithState*> lineItems;
|
||||
HeapBlock<RowInfo> lineInfo;
|
||||
Array<ItemWithState> itemStates;
|
||||
|
||||
ItemWithState& getItem (int x, int y) const noexcept { return *lineItems[y * numItems + x]; }
|
||||
|
||||
static bool isAuto (Coord value) noexcept { return value == FlexItem::autoValue; }
|
||||
static bool isAssigned (Coord value) noexcept { return value != FlexItem::notAssigned; }
|
||||
static Coord getValueOrZeroIfAuto (Coord value) noexcept { return isAuto (value) ? Coord() : value; }
|
||||
|
||||
//==============================================================================
|
||||
void createStates()
|
||||
{
|
||||
itemStates.ensureStorageAllocated (numItems);
|
||||
|
||||
for (auto& item : owner.items)
|
||||
itemStates.add (item);
|
||||
|
||||
std::stable_sort (itemStates.begin(), itemStates.end(),
|
||||
[] (const ItemWithState& i1, const ItemWithState& i2) { return i1.item->order < i2.item->order; });
|
||||
|
||||
for (auto& item : itemStates)
|
||||
{
|
||||
item.preferredWidth = getPreferredWidth (item);
|
||||
item.preferredHeight = getPreferredHeight (item);
|
||||
}
|
||||
}
|
||||
|
||||
void initialiseItems() noexcept
|
||||
{
|
||||
if (owner.flexWrap == FlexBox::Wrap::noWrap) // for single-line, all items go in line 1
|
||||
{
|
||||
lineInfo[0].numItems = numItems;
|
||||
int i = 0;
|
||||
|
||||
for (auto& item : itemStates)
|
||||
{
|
||||
item.resetItemLockedSize();
|
||||
lineItems[i++] = &item;
|
||||
}
|
||||
}
|
||||
else // if multi-line, group the flexbox items into multiple lines
|
||||
{
|
||||
auto currentLength = containerLineLength;
|
||||
int column = 0, row = 0;
|
||||
bool firstRow = true;
|
||||
|
||||
for (auto& item : itemStates)
|
||||
{
|
||||
item.resetItemLockedSize();
|
||||
|
||||
const auto flexitemLength = getItemLength (item);
|
||||
|
||||
if (flexitemLength > currentLength)
|
||||
{
|
||||
if (! firstRow)
|
||||
row++;
|
||||
|
||||
if (row >= numItems)
|
||||
break;
|
||||
|
||||
column = 0;
|
||||
currentLength = containerLineLength;
|
||||
numberOfRows = jmax (numberOfRows, row + 1);
|
||||
}
|
||||
|
||||
currentLength -= flexitemLength;
|
||||
lineItems[row * numItems + column] = &item;
|
||||
++column;
|
||||
lineInfo[row].numItems = jmax (lineInfo[row].numItems, column);
|
||||
firstRow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void resolveFlexibleLengths() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
resetRowItems (row);
|
||||
|
||||
for (int maxLoops = numItems; --maxLoops >= 0;)
|
||||
{
|
||||
resetUnlockedRowItems (row);
|
||||
|
||||
if (layoutRowItems (row))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void resolveAutoMarginsOnMainAxis() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
Coord allFlexGrow = 0;
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
const auto remainingLength = containerLineLength - lineInfo[row].totalLength;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (isRowDirection)
|
||||
{
|
||||
if (isAuto (item.item->margin.left)) ++allFlexGrow;
|
||||
if (isAuto (item.item->margin.right)) ++allFlexGrow;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isAuto (item.item->margin.top)) ++allFlexGrow;
|
||||
if (isAuto (item.item->margin.bottom)) ++allFlexGrow;
|
||||
}
|
||||
}
|
||||
|
||||
auto changeUnitWidth = remainingLength / allFlexGrow;
|
||||
|
||||
if (changeUnitWidth > 0)
|
||||
{
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (isRowDirection)
|
||||
{
|
||||
if (isAuto (item.item->margin.left)) item.lockedMarginLeft = changeUnitWidth;
|
||||
if (isAuto (item.item->margin.right)) item.lockedMarginRight = changeUnitWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isAuto (item.item->margin.top)) item.lockedMarginTop = changeUnitWidth;
|
||||
if (isAuto (item.item->margin.bottom)) item.lockedMarginBottom = changeUnitWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void calculateCrossSizesByLine() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
Coord maxSize = 0;
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
maxSize = jmax (maxSize, isRowDirection ? item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom
|
||||
: item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight);
|
||||
}
|
||||
|
||||
lineInfo[row].crossSize = maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
void calculateCrossSizeOfAllItems() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (isAssigned (item.item->maxHeight) && item.lockedHeight > item.item->maxHeight)
|
||||
item.lockedHeight = item.item->maxHeight;
|
||||
|
||||
if (isAssigned (item.item->maxWidth) && item.lockedWidth > item.item->maxWidth)
|
||||
item.lockedWidth = item.item->maxWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void alignLinesPerAlignContent() noexcept
|
||||
{
|
||||
containerCrossLength = isRowDirection ? parentHeight : parentWidth;
|
||||
|
||||
if (owner.alignContent == FlexBox::AlignContent::flexStart)
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
for (int row2 = row; row2 < numberOfRows; ++row2)
|
||||
lineInfo[row].lineY = row == 0 ? 0 : lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize;
|
||||
}
|
||||
else if (owner.alignContent == FlexBox::AlignContent::flexEnd)
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
Coord crossHeights = 0;
|
||||
|
||||
for (int row2 = row; row2 < numberOfRows; ++row2)
|
||||
crossHeights += lineInfo[row2].crossSize;
|
||||
|
||||
lineInfo[row].lineY = containerCrossLength - crossHeights;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Coord totalHeight = 0;
|
||||
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
totalHeight += lineInfo[row].crossSize;
|
||||
|
||||
if (owner.alignContent == FlexBox::AlignContent::stretch)
|
||||
{
|
||||
const auto difference = jmax (Coord(), (containerCrossLength - totalHeight) / numberOfRows);
|
||||
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
lineInfo[row].crossSize += difference;
|
||||
lineInfo[row].lineY = row == 0 ? 0 : lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize;
|
||||
}
|
||||
}
|
||||
else if (owner.alignContent == FlexBox::AlignContent::center)
|
||||
{
|
||||
const auto additionalength = (containerCrossLength - totalHeight) / 2;
|
||||
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
lineInfo[row].lineY = row == 0 ? additionalength : lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize;
|
||||
}
|
||||
else if (owner.alignContent == FlexBox::AlignContent::spaceBetween)
|
||||
{
|
||||
const auto additionalength = numberOfRows <= 1 ? Coord() : jmax (Coord(), (containerCrossLength - totalHeight)
|
||||
/ static_cast<Coord> (numberOfRows - 1));
|
||||
lineInfo[0].lineY = 0;
|
||||
|
||||
for (int row = 1; row < numberOfRows; ++row)
|
||||
lineInfo[row].lineY += additionalength + lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize;
|
||||
}
|
||||
else if (owner.alignContent == FlexBox::AlignContent::spaceAround)
|
||||
{
|
||||
const auto additionalength = numberOfRows <= 1 ? Coord() : jmax (Coord(), (containerCrossLength - totalHeight)
|
||||
/ static_cast<Coord> (2 + (2 * (numberOfRows - 1))));
|
||||
|
||||
lineInfo[0].lineY = additionalength;
|
||||
|
||||
for (int row = 1; row < numberOfRows; ++row)
|
||||
lineInfo[row].lineY += (2 * additionalength) + lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void resolveAutoMarginsOnCrossAxis() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
const auto crossSizeForLine = lineInfo[row].crossSize;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (isRowDirection)
|
||||
{
|
||||
if (isAuto (item.item->margin.top) && isAuto (item.item->margin.bottom))
|
||||
item.lockedMarginTop = (crossSizeForLine - item.lockedHeight) / 2;
|
||||
else if (isAuto (item.item->margin.top))
|
||||
item.lockedMarginTop = crossSizeForLine - item.lockedHeight - item.item->margin.bottom;
|
||||
}
|
||||
else if (isAuto (item.item->margin.left) && isAuto (item.item->margin.right))
|
||||
{
|
||||
item.lockedMarginLeft = jmax (Coord(), (crossSizeForLine - item.lockedWidth) / 2);
|
||||
}
|
||||
else if (isAuto (item.item->margin.top))
|
||||
{
|
||||
item.lockedMarginLeft = jmax (Coord(), crossSizeForLine - item.lockedHeight - item.item->margin.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void alignItemsInCrossAxisInLinesPerAlignItems() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
const auto lineSize = lineInfo[row].crossSize;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (item.item->alignSelf == FlexItem::AlignSelf::autoAlign)
|
||||
{
|
||||
if (owner.alignItems == FlexBox::AlignItems::stretch)
|
||||
{
|
||||
item.lockedMarginTop = item.item->margin.top;
|
||||
|
||||
if (isRowDirection)
|
||||
item.setHeightChecked (lineSize - item.item->margin.top - item.item->margin.bottom);
|
||||
else
|
||||
item.setWidthChecked (lineSize - item.item->margin.left - item.item->margin.right);
|
||||
}
|
||||
else if (owner.alignItems == FlexBox::AlignItems::flexStart)
|
||||
{
|
||||
item.lockedMarginTop = item.item->margin.top;
|
||||
}
|
||||
else if (owner.alignItems == FlexBox::AlignItems::flexEnd)
|
||||
{
|
||||
if (isRowDirection)
|
||||
item.lockedMarginTop = lineSize - item.lockedHeight - item.item->margin.bottom;
|
||||
else
|
||||
item.lockedMarginLeft = lineSize - item.lockedWidth - item.item->margin.right;
|
||||
}
|
||||
else if (owner.alignItems == FlexBox::AlignItems::center)
|
||||
{
|
||||
if (isRowDirection)
|
||||
item.lockedMarginTop = (lineSize - item.lockedHeight - item.item->margin.top - item.item->margin.bottom) / 2;
|
||||
else
|
||||
item.lockedMarginLeft = (lineSize - item.lockedWidth - item.item->margin.left - item.item->margin.right) / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void alignLinesPerAlignSelf() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
const auto lineSize = lineInfo[row].crossSize;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (! isAuto (item.item->margin.top))
|
||||
{
|
||||
if (item.item->alignSelf == FlexItem::AlignSelf::flexStart)
|
||||
{
|
||||
if (isRowDirection)
|
||||
item.lockedMarginTop = item.item->margin.top;
|
||||
else
|
||||
item.lockedMarginLeft = item.item->margin.left;
|
||||
}
|
||||
else if (item.item->alignSelf == FlexItem::AlignSelf::flexEnd)
|
||||
{
|
||||
if (isRowDirection)
|
||||
item.lockedMarginTop = lineSize - item.lockedHeight - item.item->margin.bottom;
|
||||
else
|
||||
item.lockedMarginLeft = lineSize - item.lockedWidth - item.item->margin.right;
|
||||
}
|
||||
else if (item.item->alignSelf == FlexItem::AlignSelf::center)
|
||||
{
|
||||
if (isRowDirection)
|
||||
item.lockedMarginTop = item.item->margin.top + (lineSize - item.lockedHeight - item.item->margin.top - item.item->margin.bottom) / 2;
|
||||
else
|
||||
item.lockedMarginLeft = item.item->margin.left + (lineSize - item.lockedWidth - item.item->margin.left - item.item->margin.right) / 2;
|
||||
}
|
||||
else if (item.item->alignSelf == FlexItem::AlignSelf::stretch)
|
||||
{
|
||||
item.lockedMarginTop = item.item->margin.top;
|
||||
item.lockedMarginLeft = item.item->margin.left;
|
||||
|
||||
if (isRowDirection)
|
||||
item.setHeightChecked (isAssigned (item.item->height) ? getPreferredHeight (item)
|
||||
: lineSize - item.item->margin.top - item.item->margin.bottom);
|
||||
else
|
||||
item.setWidthChecked (isAssigned (item.item->width) ? getPreferredWidth (item)
|
||||
: lineSize - item.item->margin.left - item.item->margin.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void alignItemsByJustifyContent() noexcept
|
||||
{
|
||||
Coord additionalMarginRight = 0, additionalMarginLeft = 0;
|
||||
|
||||
recalculateTotalItemLengthPerLineArray();
|
||||
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
Coord x = 0;
|
||||
|
||||
if (owner.justifyContent == FlexBox::JustifyContent::flexEnd)
|
||||
{
|
||||
x = containerLineLength - lineInfo[row].totalLength;
|
||||
}
|
||||
else if (owner.justifyContent == FlexBox::JustifyContent::center)
|
||||
{
|
||||
x = (containerLineLength - lineInfo[row].totalLength) / 2;
|
||||
}
|
||||
else if (owner.justifyContent == FlexBox::JustifyContent::spaceBetween)
|
||||
{
|
||||
additionalMarginRight
|
||||
= jmax (Coord(), (containerLineLength - lineInfo[row].totalLength) / jmax (1, numColumns - 1));
|
||||
}
|
||||
else if (owner.justifyContent == FlexBox::JustifyContent::spaceAround)
|
||||
{
|
||||
additionalMarginLeft = additionalMarginRight
|
||||
= jmax (Coord(), (containerLineLength - lineInfo[row].totalLength) / jmax (1, 2 * numColumns));
|
||||
}
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (isRowDirection)
|
||||
{
|
||||
item.lockedMarginLeft += additionalMarginLeft;
|
||||
item.lockedMarginRight += additionalMarginRight;
|
||||
item.item->currentBounds.setPosition ((float) (x + item.lockedMarginLeft), (float) item.lockedMarginTop);
|
||||
x += item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.lockedMarginTop += additionalMarginLeft;
|
||||
item.lockedMarginBottom += additionalMarginRight;
|
||||
item.item->currentBounds.setPosition ((float) item.lockedMarginLeft, (float) (x + item.lockedMarginTop));
|
||||
x += item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void layoutAllItems() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
const auto lineY = lineInfo[row].lineY;
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (isRowDirection)
|
||||
item.item->currentBounds.setY ((float) (lineY + item.lockedMarginTop));
|
||||
else
|
||||
item.item->currentBounds.setX ((float) (lineY + item.lockedMarginLeft));
|
||||
|
||||
item.item->currentBounds.setSize ((float) item.lockedWidth,
|
||||
(float) item.lockedHeight);
|
||||
}
|
||||
}
|
||||
|
||||
reverseLocations();
|
||||
reverseWrap();
|
||||
}
|
||||
|
||||
private:
|
||||
void resetRowItems (const int row) noexcept
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
resetItem (getItem (column, row));
|
||||
}
|
||||
|
||||
void resetUnlockedRowItems (const int row) noexcept
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (! item.locked)
|
||||
resetItem (item);
|
||||
}
|
||||
}
|
||||
|
||||
void resetItem (ItemWithState& item) noexcept
|
||||
{
|
||||
item.locked = false;
|
||||
item.lockedWidth = getPreferredWidth (item);
|
||||
item.lockedHeight = getPreferredHeight (item);
|
||||
}
|
||||
|
||||
bool layoutRowItems (const int row) noexcept
|
||||
{
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
auto flexContainerLength = containerLineLength;
|
||||
Coord totalItemsLength = 0, totalFlexGrow = 0, totalFlexShrink = 0;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
const auto& item = getItem (column, row);
|
||||
|
||||
if (item.locked)
|
||||
{
|
||||
flexContainerLength -= getItemLength (item);
|
||||
}
|
||||
else
|
||||
{
|
||||
totalItemsLength += getItemLength (item);
|
||||
totalFlexGrow += item.item->flexGrow;
|
||||
totalFlexShrink += item.item->flexShrink;
|
||||
}
|
||||
}
|
||||
|
||||
Coord changeUnit = 0;
|
||||
const auto difference = flexContainerLength - totalItemsLength;
|
||||
const bool positiveFlexibility = difference > 0;
|
||||
|
||||
if (positiveFlexibility)
|
||||
{
|
||||
if (totalFlexGrow != 0.0)
|
||||
changeUnit = difference / totalFlexGrow;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (totalFlexShrink != 0.0)
|
||||
changeUnit = difference / totalFlexShrink;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
auto& item = getItem (column, row);
|
||||
|
||||
if (! item.locked)
|
||||
if (! addToItemLength (item, (positiveFlexibility ? item.item->flexGrow
|
||||
: item.item->flexShrink) * changeUnit, row))
|
||||
ok = false;
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
void recalculateTotalItemLengthPerLineArray() noexcept
|
||||
{
|
||||
for (int row = 0; row < numberOfRows; ++row)
|
||||
{
|
||||
lineInfo[row].totalLength = 0;
|
||||
const auto numColumns = lineInfo[row].numItems;
|
||||
|
||||
for (int column = 0; column < numColumns; ++column)
|
||||
{
|
||||
const auto& item = getItem (column, row);
|
||||
|
||||
lineInfo[row].totalLength += isRowDirection ? item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight
|
||||
: item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reverseLocations() noexcept
|
||||
{
|
||||
if (owner.flexDirection == FlexBox::Direction::rowReverse)
|
||||
{
|
||||
for (auto& item : owner.items)
|
||||
item.currentBounds.setX ((float) (containerLineLength - item.currentBounds.getRight()));
|
||||
}
|
||||
else if (owner.flexDirection == FlexBox::Direction::columnReverse)
|
||||
{
|
||||
for (auto& item : owner.items)
|
||||
item.currentBounds.setY ((float) (containerLineLength - item.currentBounds.getBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
void reverseWrap() noexcept
|
||||
{
|
||||
if (owner.flexWrap == FlexBox::Wrap::wrapReverse)
|
||||
{
|
||||
if (isRowDirection)
|
||||
{
|
||||
for (auto& item : owner.items)
|
||||
item.currentBounds.setY ((float) (containerCrossLength - item.currentBounds.getBottom()));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto& item : owner.items)
|
||||
item.currentBounds.setX ((float) (containerCrossLength - item.currentBounds.getRight()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Coord getItemLength (const ItemWithState& item) const noexcept
|
||||
{
|
||||
return isRowDirection ? item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight
|
||||
: item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom;
|
||||
}
|
||||
|
||||
Coord getItemCrossSize (const ItemWithState& item) const noexcept
|
||||
{
|
||||
return isRowDirection ? item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom
|
||||
: item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight;
|
||||
}
|
||||
|
||||
bool addToItemLength (ItemWithState& item, const Coord length, int row) const noexcept
|
||||
{
|
||||
bool ok = false;
|
||||
|
||||
if (isRowDirection)
|
||||
{
|
||||
const auto prefWidth = getPreferredWidth (item);
|
||||
|
||||
if (isAssigned (item.item->maxWidth) && item.item->maxWidth < prefWidth + length)
|
||||
{
|
||||
item.lockedWidth = item.item->maxWidth;
|
||||
item.locked = true;
|
||||
}
|
||||
else if (isAssigned (prefWidth) && item.item->minWidth > prefWidth + length)
|
||||
{
|
||||
item.lockedWidth = item.item->minWidth;
|
||||
item.locked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = true;
|
||||
item.lockedWidth = prefWidth + length;
|
||||
}
|
||||
|
||||
lineInfo[row].totalLength += item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto prefHeight = getPreferredHeight (item);
|
||||
|
||||
if (isAssigned (item.item->maxHeight) && item.item->maxHeight < prefHeight + length)
|
||||
{
|
||||
item.lockedHeight = item.item->maxHeight;
|
||||
item.locked = true;
|
||||
}
|
||||
else if (isAssigned (prefHeight) && item.item->minHeight > prefHeight + length)
|
||||
{
|
||||
item.lockedHeight = item.item->minHeight;
|
||||
item.locked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = true;
|
||||
item.lockedHeight = prefHeight + length;
|
||||
}
|
||||
|
||||
lineInfo[row].totalLength += item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom;
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
Coord getPreferredWidth (const ItemWithState& itemWithState) const noexcept
|
||||
{
|
||||
const auto& item = *itemWithState.item;
|
||||
auto preferredWidth = (item.flexBasis > 0 && isRowDirection)
|
||||
? item.flexBasis
|
||||
: (isAssigned (item.width) ? item.width : item.minWidth);
|
||||
|
||||
if (isAssigned (item.minWidth) && preferredWidth < item.minWidth) return item.minWidth;
|
||||
if (isAssigned (item.maxWidth) && preferredWidth > item.maxWidth) return item.maxWidth;
|
||||
|
||||
return preferredWidth;
|
||||
}
|
||||
|
||||
Coord getPreferredHeight (const ItemWithState& itemWithState) const noexcept
|
||||
{
|
||||
const auto& item = *itemWithState.item;
|
||||
auto preferredHeight = (item.flexBasis > 0 && ! isRowDirection)
|
||||
? item.flexBasis
|
||||
: (isAssigned (item.height) ? item.height : item.minHeight);
|
||||
|
||||
if (isAssigned (item.minHeight) && preferredHeight < item.minHeight) return item.minHeight;
|
||||
if (isAssigned (item.maxHeight) && preferredHeight > item.maxHeight) return item.maxHeight;
|
||||
|
||||
return preferredHeight;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
FlexBox::FlexBox() noexcept {}
|
||||
FlexBox::~FlexBox() noexcept {}
|
||||
|
||||
FlexBox::FlexBox (JustifyContent jc) noexcept : justifyContent (jc) {}
|
||||
|
||||
FlexBox::FlexBox (Direction d, Wrap w, AlignContent ac, AlignItems ai, JustifyContent jc) noexcept
|
||||
: flexDirection (d), flexWrap (w), alignContent (ac), alignItems (ai), justifyContent (jc)
|
||||
{
|
||||
}
|
||||
|
||||
void FlexBox::performLayout (Rectangle<float> targetArea)
|
||||
{
|
||||
if (! items.isEmpty())
|
||||
{
|
||||
FlexBoxLayoutCalculation layout (*this, targetArea.getWidth(), targetArea.getHeight());
|
||||
|
||||
layout.createStates();
|
||||
layout.initialiseItems();
|
||||
layout.resolveFlexibleLengths();
|
||||
layout.resolveAutoMarginsOnMainAxis();
|
||||
layout.calculateCrossSizesByLine();
|
||||
layout.calculateCrossSizeOfAllItems();
|
||||
layout.alignLinesPerAlignContent();
|
||||
layout.resolveAutoMarginsOnCrossAxis();
|
||||
layout.alignItemsInCrossAxisInLinesPerAlignItems();
|
||||
layout.alignLinesPerAlignSelf();
|
||||
layout.alignItemsByJustifyContent();
|
||||
layout.layoutAllItems();
|
||||
|
||||
for (auto& item : items)
|
||||
{
|
||||
item.currentBounds += targetArea.getPosition();
|
||||
|
||||
if (auto* comp = item.associatedComponent)
|
||||
comp->setBounds (Rectangle<int>::leftTopRightBottom ((int) item.currentBounds.getX(),
|
||||
(int) item.currentBounds.getY(),
|
||||
(int) item.currentBounds.getRight(),
|
||||
(int) item.currentBounds.getBottom()));
|
||||
|
||||
if (auto* box = item.associatedFlexBox)
|
||||
box->performLayout (item.currentBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FlexBox::performLayout (Rectangle<int> targetArea)
|
||||
{
|
||||
performLayout (targetArea.toFloat());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
FlexItem::FlexItem() noexcept {}
|
||||
FlexItem::FlexItem (float w, float h) noexcept : currentBounds (w, h), minWidth (w), minHeight (h) {}
|
||||
FlexItem::FlexItem (float w, float h, Component& c) noexcept : FlexItem (w, h) { associatedComponent = &c; }
|
||||
FlexItem::FlexItem (float w, float h, FlexBox& fb) noexcept : FlexItem (w, h) { associatedFlexBox = &fb; }
|
||||
FlexItem::FlexItem (Component& c) noexcept : associatedComponent (&c) {}
|
||||
FlexItem::FlexItem (FlexBox& fb) noexcept : associatedFlexBox (&fb) {}
|
||||
|
||||
FlexItem::Margin::Margin() noexcept : left(), right(), top(), bottom() {}
|
||||
FlexItem::Margin::Margin (float v) noexcept : left (v), right (v), top (v), bottom (v) {}
|
||||
FlexItem::Margin::Margin (float t, float r, float b, float l) noexcept : left (l), right (r), top (t), bottom (b) {}
|
||||
|
||||
//==============================================================================
|
||||
FlexItem FlexItem::withFlex (float newFlexGrow) const noexcept
|
||||
{
|
||||
auto fi = *this;
|
||||
fi.flexGrow = newFlexGrow;
|
||||
return fi;
|
||||
}
|
||||
|
||||
FlexItem FlexItem::withFlex (float newFlexGrow, float newFlexShrink) const noexcept
|
||||
{
|
||||
auto fi = withFlex (newFlexGrow);
|
||||
fi.flexShrink = newFlexShrink;
|
||||
return fi;
|
||||
}
|
||||
|
||||
FlexItem FlexItem::withFlex (float newFlexGrow, float newFlexShrink, float newFlexBasis) const noexcept
|
||||
{
|
||||
auto fi = withFlex (newFlexGrow, newFlexShrink);
|
||||
fi.flexBasis = newFlexBasis;
|
||||
return fi;
|
||||
}
|
||||
|
||||
FlexItem FlexItem::withWidth (float newWidth) const noexcept { auto fi = *this; fi.width = newWidth; return fi; }
|
||||
FlexItem FlexItem::withMinWidth (float newMinWidth) const noexcept { auto fi = *this; fi.minWidth = newMinWidth; return fi; }
|
||||
FlexItem FlexItem::withMaxWidth (float newMaxWidth) const noexcept { auto fi = *this; fi.maxWidth = newMaxWidth; return fi; }
|
||||
|
||||
FlexItem FlexItem::withMinHeight (float newMinHeight) const noexcept { auto fi = *this; fi.minHeight = newMinHeight; return fi; }
|
||||
FlexItem FlexItem::withMaxHeight (float newMaxHeight) const noexcept { auto fi = *this; fi.maxHeight = newMaxHeight; return fi; }
|
||||
FlexItem FlexItem::withHeight (float newHeight) const noexcept { auto fi = *this; fi.height = newHeight; return fi; }
|
||||
|
||||
FlexItem FlexItem::withMargin (Margin m) const noexcept { auto fi = *this; fi.margin = m; return fi; }
|
||||
FlexItem FlexItem::withOrder (int newOrder) const noexcept { auto fi = *this; fi.order = newOrder; return fi; }
|
||||
FlexItem FlexItem::withAlignSelf (AlignSelf a) const noexcept { auto fi = *this; fi.alignSelf = a; return fi; }
|
||||
|
||||
} // namespace juce
|
147
modules/juce_gui_basics/layout/juce_FlexBox.h
Normal file
147
modules/juce_gui_basics/layout/juce_FlexBox.h
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
Represents a FlexBox container, which contains and manages the layout of a set
|
||||
of FlexItem objects.
|
||||
|
||||
To use this class, set its parameters appropriately (you can search online for
|
||||
more help on exactly how the FlexBox protocol works!), then add your sub-items
|
||||
to the items array, and call performLayout() in the resized() function of your
|
||||
Component.
|
||||
|
||||
@see FlexItem
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API FlexBox final
|
||||
{
|
||||
public:
|
||||
/** Possible values for the flexDirection property. */
|
||||
enum class Direction
|
||||
{
|
||||
row, /**< Set the main axis direction from left to right. */
|
||||
rowReverse, /**< Set the main axis direction from right to left. */
|
||||
column, /**< Set the main axis direction from top to bottom. */
|
||||
columnReverse /**< Set the main axis direction from bottom to top. */
|
||||
};
|
||||
|
||||
/** Possible values for the flexWrap property. */
|
||||
enum class Wrap
|
||||
{
|
||||
noWrap, /**< Items are forced into a single line. */
|
||||
wrap, /**< Items are wrapped onto multiple lines from top to bottom. */
|
||||
wrapReverse /**< Items are wrapped onto multiple lines from bottom to top. */
|
||||
};
|
||||
|
||||
/** Possible values for the alignContent property. */
|
||||
enum class AlignContent
|
||||
{
|
||||
stretch, /**< Lines of items are stretched from start to end of the cross axis. */
|
||||
flexStart, /**< Lines of items are aligned towards the start of the cross axis. */
|
||||
flexEnd, /**< Lines of items are aligned towards the end of the cross axis. */
|
||||
center, /**< Lines of items are aligned towards the center of the cross axis. */
|
||||
spaceBetween, /**< Lines of items are evenly spaced along the cross axis with spaces between them. */
|
||||
spaceAround /**< Lines of items are evenly spaced along the cross axis with spaces around them. */
|
||||
};
|
||||
|
||||
/** Possible values for the alignItems property. */
|
||||
enum class AlignItems
|
||||
{
|
||||
stretch, /**< Items are stretched from start to end of the cross axis. */
|
||||
flexStart, /**< Items are aligned towards the start of the cross axis. */
|
||||
flexEnd, /**< Items are aligned towards the end of the cross axis. */
|
||||
center /**< Items are aligned towards the center of the cross axis. */
|
||||
};
|
||||
|
||||
/** Possible values for the justifyContent property. */
|
||||
enum class JustifyContent
|
||||
{
|
||||
flexStart, /**< Items are justified towards the start of the main axis. */
|
||||
flexEnd, /**< Items are justified towards the end of the main axis. */
|
||||
center, /**< Items are justified towards the center of the main axis. */
|
||||
spaceBetween, /**< Items are evenly spaced along the main axis with spaces between them. */
|
||||
spaceAround /**< Items are evenly spaced along the main axis with spaces around them. */
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Creates an empty FlexBox container with default parameters. */
|
||||
FlexBox() noexcept;
|
||||
|
||||
/** Creates an empty FlexBox container with these parameters. */
|
||||
FlexBox (Direction, Wrap, AlignContent, AlignItems, JustifyContent) noexcept;
|
||||
|
||||
/** Creates an empty FlexBox container with the given content-justification mode. */
|
||||
FlexBox (JustifyContent) noexcept;
|
||||
|
||||
/** Destructor. */
|
||||
~FlexBox() noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Lays-out the box's items within the given rectangle. */
|
||||
void performLayout (Rectangle<float> targetArea);
|
||||
|
||||
/** Lays-out the box's items within the given rectangle. */
|
||||
void performLayout (Rectangle<int> targetArea);
|
||||
|
||||
//==============================================================================
|
||||
/** Specifies how flex items are placed in the flex container, and defines the
|
||||
direction of the main axis.
|
||||
*/
|
||||
Direction flexDirection = Direction::row;
|
||||
|
||||
/** Specifies whether items are forced into a single line or can be wrapped onto multiple lines.
|
||||
If wrapping is allowed, this property also controls the direction in which lines are stacked.
|
||||
*/
|
||||
Wrap flexWrap = Wrap::noWrap;
|
||||
|
||||
/** Specifies how a flex container's lines are placed within the flex container when
|
||||
there is extra space on the cross-axis.
|
||||
This property has no effect on single line layouts.
|
||||
*/
|
||||
AlignContent alignContent = AlignContent::stretch;
|
||||
|
||||
/** Specifies the alignment of flex items along the cross-axis of each line. */
|
||||
AlignItems alignItems = AlignItems::stretch;
|
||||
|
||||
/** Defines how the container distributes space between and around items along the main-axis.
|
||||
The alignment is done after the lengths and auto margins are applied, so that if there is at
|
||||
least one flexible element, with flex-grow different from 0, it will have no effect as there
|
||||
won't be any available space.
|
||||
*/
|
||||
JustifyContent justifyContent = JustifyContent::flexStart;
|
||||
|
||||
/** The set of items to lay-out. */
|
||||
Array<FlexItem> items;
|
||||
|
||||
private:
|
||||
JUCE_LEAK_DETECTOR (FlexBox)
|
||||
};
|
||||
|
||||
} // namespace juce
|
178
modules/juce_gui_basics/layout/juce_FlexItem.h
Normal file
178
modules/juce_gui_basics/layout/juce_FlexItem.h
Normal file
@ -0,0 +1,178 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
Describes the properties of an item inside a FlexBox container.
|
||||
|
||||
@see FlexBox
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API FlexItem final
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates an item with default parameters, and zero size. */
|
||||
FlexItem() noexcept;
|
||||
|
||||
/** Creates an item with the given size. */
|
||||
FlexItem (float width, float height) noexcept;
|
||||
|
||||
/** Creates an item with the given size and target Component. */
|
||||
FlexItem (float width, float height, Component& targetComponent) noexcept;
|
||||
|
||||
/** Creates an item that represents an embedded FlexBox with a given size. */
|
||||
FlexItem (float width, float height, FlexBox& flexBoxToControl) noexcept;
|
||||
|
||||
/** Creates an item with a given target Component. */
|
||||
FlexItem (Component& componentToControl) noexcept;
|
||||
|
||||
/** Creates an item that represents an embedded FlexBox. This class will not
|
||||
create a copy of the supplied flex box. You need to ensure that the
|
||||
life-time of flexBoxToControl is longer than the FlexItem. */
|
||||
FlexItem (FlexBox& flexBoxToControl) noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** The item's current bounds. */
|
||||
Rectangle<float> currentBounds;
|
||||
|
||||
/** If this is non-null, it represents a Component whose bounds are controlled by this item. */
|
||||
Component* associatedComponent = nullptr;
|
||||
|
||||
/** If this is non-null, it represents a FlexBox whose bounds are controlled by this item. */
|
||||
FlexBox* associatedFlexBox = nullptr;
|
||||
|
||||
/** Determines the order used to lay out items in their flex container.
|
||||
Elements are laid out in ascending order of thus order value. Elements with the same order value
|
||||
are laid out in the order in which they appear in the array.
|
||||
*/
|
||||
int order = 0;
|
||||
|
||||
/** Specifies the flex grow factor of this item.
|
||||
This indicates the amount of space inside the flex container the item should take up.
|
||||
*/
|
||||
float flexGrow = 0.0f;
|
||||
|
||||
/** Specifies the flex shrink factor of the item.
|
||||
This indicates the rate at which the item shrinks if there is insufficient space in
|
||||
the container.
|
||||
*/
|
||||
float flexShrink = 1.0f;
|
||||
|
||||
/** Specifies the flex-basis of the item.
|
||||
This is the initial main size of a flex item in the direction of flow. It determines the size
|
||||
of the content-box unless specified otherwise using box-sizing.
|
||||
*/
|
||||
float flexBasis = 0.0f;
|
||||
|
||||
/** Possible value for the alignSelf property */
|
||||
enum class AlignSelf
|
||||
{
|
||||
autoAlign, /**< Follows the FlexBox container's alignItems property. */
|
||||
flexStart, /**< Item is aligned towards the start of the cross axis. */
|
||||
flexEnd, /**< Item is aligned towards the end of the cross axis. */
|
||||
center, /**< Item is aligned towards the center of the cross axis. */
|
||||
stretch /**< Item is stretched from start to end of the cross axis. */
|
||||
};
|
||||
|
||||
/** This is the align-self property of the item.
|
||||
This determines the alignment of the item along the cross-axis (perpendicular to the direction
|
||||
of flow).
|
||||
*/
|
||||
AlignSelf alignSelf = AlignSelf::stretch;
|
||||
|
||||
//==============================================================================
|
||||
/** This constant can be used for sizes to indicate that 'auto' mode should be used. */
|
||||
static const int autoValue = -2;
|
||||
/** This constant can be used for sizes to indicate that no value has been set. */
|
||||
static const int notAssigned = -1;
|
||||
|
||||
float width = (float) notAssigned; /**< The item's width. */
|
||||
float minWidth = 0.0f; /**< The item's minimum width */
|
||||
float maxWidth = (float) notAssigned; /**< The item's maximum width */
|
||||
|
||||
float height = (float) notAssigned; /**< The item's height */
|
||||
float minHeight = 0.0f; /**< The item's minimum height */
|
||||
float maxHeight = (float) notAssigned; /**< The item's maximum height */
|
||||
|
||||
/** Represents a margin. */
|
||||
struct Margin final
|
||||
{
|
||||
Margin() noexcept; /**< Creates a margin of size zero. */
|
||||
Margin (float size) noexcept; /**< Creates a margin with this size on all sides. */
|
||||
Margin (float top, float right, float bottom, float left) noexcept; /**< Creates a margin with these sizes. */
|
||||
|
||||
float left; /**< Left margin size */
|
||||
float right; /**< Right margin size */
|
||||
float top; /**< Top margin size */
|
||||
float bottom; /**< Bottom margin size */
|
||||
};
|
||||
|
||||
/** The margin to leave around this item. */
|
||||
Margin margin;
|
||||
|
||||
//==============================================================================
|
||||
/** Returns a copy of this object with a new flex-grow value. */
|
||||
FlexItem withFlex (float newFlexGrow) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with new flex-grow and flex-shrink values. */
|
||||
FlexItem withFlex (float newFlexGrow, float newFlexShrink) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with new flex-grow, flex-shrink and flex-basis values. */
|
||||
FlexItem withFlex (float newFlexGrow, float newFlexShrink, float newFlexBasis) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new width. */
|
||||
FlexItem withWidth (float newWidth) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new minimum width. */
|
||||
FlexItem withMinWidth (float newMinWidth) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new maximum width. */
|
||||
FlexItem withMaxWidth (float newMaxWidth) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new height. */
|
||||
FlexItem withHeight (float newHeight) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new minimum height. */
|
||||
FlexItem withMinHeight (float newMinHeight) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new maximum height. */
|
||||
FlexItem withMaxHeight (float newMaxHeight) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new margin. */
|
||||
FlexItem withMargin (Margin) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new order. */
|
||||
FlexItem withOrder (int newOrder) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new alignSelf value. */
|
||||
FlexItem withAlignSelf (AlignSelf newAlignSelf) const noexcept;
|
||||
};
|
||||
|
||||
} // namespace juce
|
1028
modules/juce_gui_basics/layout/juce_Grid.cpp
Normal file
1028
modules/juce_gui_basics/layout/juce_Grid.cpp
Normal file
File diff suppressed because it is too large
Load Diff
228
modules/juce_gui_basics/layout/juce_Grid.h
Normal file
228
modules/juce_gui_basics/layout/juce_Grid.h
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
Container that handles geometry for grid layouts (fixed columns and rows) using a set of declarative rules.
|
||||
|
||||
Implemented from the `CSS Grid Layout` specification as described at:
|
||||
https://css-tricks.com/snippets/css/complete-guide-grid/
|
||||
|
||||
@see GridItem
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API Grid final
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** A size in pixels */
|
||||
struct Px final
|
||||
{
|
||||
explicit Px (float p) : pixels (static_cast<long double>(p)) { /*sta (p >= 0.0f);*/ }
|
||||
explicit Px (int p) : pixels (static_cast<long double>(p)) { /*sta (p >= 0.0f);*/ }
|
||||
explicit constexpr Px (long double p) : pixels (p) {}
|
||||
explicit constexpr Px (unsigned long long p) : pixels (static_cast<long double>(p)) {}
|
||||
|
||||
long double pixels;
|
||||
};
|
||||
|
||||
/** A fractional ratio integer */
|
||||
struct Fr final
|
||||
{
|
||||
explicit Fr (int f) : fraction (static_cast<unsigned long long> (f)) {}
|
||||
explicit constexpr Fr (unsigned long long p) : fraction (p) {}
|
||||
|
||||
unsigned long long fraction;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Represents a track. */
|
||||
struct TrackInfo final
|
||||
{
|
||||
/** Creates a track with auto dimension. */
|
||||
TrackInfo() noexcept;
|
||||
/** */
|
||||
TrackInfo (Px sizeInPixels) noexcept;
|
||||
/** */
|
||||
TrackInfo (Fr fractionOfFreeSpace) noexcept;
|
||||
|
||||
/** */
|
||||
TrackInfo (Px sizeInPixels, const juce::String& endLineNameToUse) noexcept;
|
||||
/** */
|
||||
TrackInfo (Fr fractionOfFreeSpace, const juce::String& endLineNameToUse) noexcept;
|
||||
|
||||
/** */
|
||||
TrackInfo (const juce::String& startLineNameToUse, Px sizeInPixels) noexcept;
|
||||
/** */
|
||||
TrackInfo (const juce::String& startLineNameToUse, Fr fractionOfFreeSpace) noexcept;
|
||||
|
||||
/** */
|
||||
TrackInfo (const juce::String& startLineNameToUse, Px sizeInPixels, const juce::String& endLineNameToUse) noexcept;
|
||||
/** */
|
||||
TrackInfo (const juce::String& startLineNameToUse, Fr fractionOfFreeSpace, const juce::String& endLineNameToUse) noexcept;
|
||||
|
||||
private:
|
||||
friend class Grid;
|
||||
friend class GridItem;
|
||||
|
||||
float size = 0; // Either a fraction or an absolute size in pixels
|
||||
bool isFraction = false;
|
||||
bool hasKeyword = false;
|
||||
|
||||
juce::String startLineName, endLineName;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Possible values for the justifyItems property. */
|
||||
enum class JustifyItems : int
|
||||
{
|
||||
start = 0, /**< Content inside the item is justified towards the left. */
|
||||
end, /**< Content inside the item is justified towards the right. */
|
||||
center, /**< Content inside the item is justified towards the center. */
|
||||
stretch /**< Content inside the item is stretched from left to right. */
|
||||
};
|
||||
|
||||
/** Possible values for the alignItems property. */
|
||||
enum class AlignItems : int
|
||||
{
|
||||
start = 0, /**< Content inside the item is aligned towards the top. */
|
||||
end, /**< Content inside the item is aligned towards the bottom. */
|
||||
center, /**< Content inside the item is aligned towards the center. */
|
||||
stretch /**< Content inside the item is stretched from top to bottom. */
|
||||
};
|
||||
|
||||
/** Possible values for the justifyContent property. */
|
||||
enum class JustifyContent
|
||||
{
|
||||
start, /**< Items are justified towards the left of the container. */
|
||||
end, /**< Items are justified towards the right of the container. */
|
||||
center, /**< Items are justified towards the center of the container. */
|
||||
stretch, /**< Items are stretched from left to right of the container. */
|
||||
spaceAround, /**< Items are evenly spaced along the row with spaces between them. */
|
||||
spaceBetween, /**< Items are evenly spaced along the row with spaces around them. */
|
||||
spaceEvenly /**< Items are evenly spaced along the row with even amount of spaces between them. */
|
||||
};
|
||||
|
||||
/** Possible values for the alignContent property. */
|
||||
enum class AlignContent
|
||||
{
|
||||
start, /**< Items are aligned towards the top of the container. */
|
||||
end, /**< Items are aligned towards the bottom of the container. */
|
||||
center, /**< Items are aligned towards the center of the container. */
|
||||
stretch, /**< Items are stretched from top to bottom of the container. */
|
||||
spaceAround, /**< Items are evenly spaced along the column with spaces between them. */
|
||||
spaceBetween, /**< Items are evenly spaced along the column with spaces around them. */
|
||||
spaceEvenly /**< Items are evenly spaced along the column with even amount of spaces between them. */
|
||||
};
|
||||
|
||||
/** Possible values for the autoFlow property. */
|
||||
enum class AutoFlow
|
||||
{
|
||||
row, /**< Fills the grid by adding rows of items. */
|
||||
column, /**< Fills the grid by adding columns of items. */
|
||||
rowDense, /**< Fills the grid by adding rows of items and attempts to fill in gaps. */
|
||||
columnDense /**< Fills the grid by adding columns of items and attempts to fill in gaps. */
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** Creates an empty Grid container with default parameters. */
|
||||
Grid() noexcept;
|
||||
|
||||
/** Destructor */
|
||||
~Grid() noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Specifies the alignment of content inside the items along the rows. */
|
||||
JustifyItems justifyItems = JustifyItems::stretch;
|
||||
|
||||
/** Specifies the alignment of content inside the items along the columns. */
|
||||
AlignItems alignItems = AlignItems::stretch;
|
||||
|
||||
/** Specifies the alignment of items along the rows. */
|
||||
JustifyContent justifyContent = JustifyContent::stretch;
|
||||
|
||||
/** Specifies the alignment of items along the columns. */
|
||||
AlignContent alignContent = AlignContent::stretch;
|
||||
|
||||
/** Specifies how the auto-placement algorithm places items. */
|
||||
AutoFlow autoFlow = AutoFlow::row;
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** The set of column tracks to lay out. */
|
||||
juce::Array<TrackInfo> templateColumns;
|
||||
|
||||
/** The set of row tracks to lay out. */
|
||||
juce::Array<TrackInfo> templateRows;
|
||||
|
||||
/** Template areas */
|
||||
juce::StringArray templateAreas;
|
||||
|
||||
/** The row track for auto dimension. */
|
||||
TrackInfo autoRows;
|
||||
|
||||
/** The column track for auto dimension. */
|
||||
TrackInfo autoColumns;
|
||||
|
||||
/** The gap in pixels between columns. */
|
||||
Px columnGap { 0 };
|
||||
/** The gap in pixels between rows. */
|
||||
Px rowGap { 0 };
|
||||
|
||||
/** Sets the gap between rows and columns in pixels. */
|
||||
void setGap (Px sizeInPixels) noexcept { rowGap = columnGap = sizeInPixels; }
|
||||
|
||||
//==============================================================================
|
||||
/** The set of items to lay-out. */
|
||||
juce::Array<GridItem> items;
|
||||
|
||||
//==============================================================================
|
||||
/** Lays-out the grid's items within the given rectangle. */
|
||||
void performLayout (juce::Rectangle<int>);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the number of columns. */
|
||||
int getNumberOfColumns() const noexcept { return templateColumns.size(); }
|
||||
/** Returns the number of rows. */
|
||||
int getNumberOfRows() const noexcept { return templateRows.size(); }
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct SizeCalculation;
|
||||
struct PlacementHelpers;
|
||||
struct AutoPlacement;
|
||||
struct BoxAlignment;
|
||||
};
|
||||
|
||||
constexpr Grid::Px operator"" _px (long double px) { return Grid::Px { px }; }
|
||||
constexpr Grid::Px operator"" _px (unsigned long long px) { return Grid::Px { px }; }
|
||||
constexpr Grid::Fr operator"" _fr (unsigned long long fr) { return Grid::Fr { fr }; }
|
||||
|
||||
} // namespace juce
|
182
modules/juce_gui_basics/layout/juce_GridItem.cpp
Normal file
182
modules/juce_gui_basics/layout/juce_GridItem.cpp
Normal file
@ -0,0 +1,182 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
GridItem::Property::Property() noexcept : isAuto (true)
|
||||
{
|
||||
}
|
||||
|
||||
GridItem::Property::Property (GridItem::Keyword keyword) noexcept : isAuto (keyword == GridItem::Keyword::autoValue)
|
||||
{
|
||||
jassert (keyword == GridItem::Keyword::autoValue);
|
||||
}
|
||||
|
||||
GridItem::Property::Property (const char* lineNameToUse) noexcept : GridItem::Property (juce::String (lineNameToUse))
|
||||
{
|
||||
}
|
||||
|
||||
GridItem::Property::Property (const juce::String& lineNameToUse) noexcept : name (lineNameToUse), number (1)
|
||||
{
|
||||
}
|
||||
|
||||
GridItem::Property::Property (int numberToUse) noexcept : number (numberToUse)
|
||||
{
|
||||
}
|
||||
|
||||
GridItem::Property::Property (int numberToUse, const juce::String& lineNameToUse) noexcept
|
||||
: name (lineNameToUse), number (numberToUse)
|
||||
{
|
||||
}
|
||||
|
||||
GridItem::Property::Property (Span spanToUse) noexcept
|
||||
: name (spanToUse.name), number (spanToUse.number), isSpan (true)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
GridItem::Margin::Margin() noexcept : left(), right(), top(), bottom() {}
|
||||
|
||||
GridItem::Margin::Margin (int v) noexcept : GridItem::Margin::Margin (static_cast<float> (v)) {}
|
||||
|
||||
GridItem::Margin::Margin (float v) noexcept : left (v), right (v), top (v), bottom (v) {}
|
||||
|
||||
GridItem::Margin::Margin (float t, float r, float b, float l) noexcept : left (l), right (r), top (t), bottom (b) {}
|
||||
|
||||
//==============================================================================
|
||||
GridItem::GridItem() noexcept {}
|
||||
GridItem::~GridItem() noexcept {}
|
||||
|
||||
GridItem::GridItem (juce::Component& componentToUse) noexcept : associatedComponent (&componentToUse) {}
|
||||
GridItem::GridItem (juce::Component* componentToUse) noexcept : associatedComponent (componentToUse) {}
|
||||
|
||||
void GridItem::setArea (Property rowStart, Property columnStart, Property rowEnd, Property columnEnd)
|
||||
{
|
||||
column.start = columnStart;
|
||||
column.end = columnEnd;
|
||||
row.start = rowStart;
|
||||
row.end = rowEnd;
|
||||
}
|
||||
|
||||
void GridItem::setArea (Property rowStart, Property columnStart)
|
||||
{
|
||||
column.start = columnStart;
|
||||
row.start = rowStart;
|
||||
}
|
||||
|
||||
void GridItem::setArea (const juce::String& areaName)
|
||||
{
|
||||
area = areaName;
|
||||
}
|
||||
|
||||
GridItem GridItem::withArea (Property rowStart, Property columnStart, Property rowEnd, Property columnEnd) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.setArea (rowStart, columnStart, rowEnd, columnEnd);
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withArea (Property rowStart, Property columnStart) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.setArea (rowStart, columnStart);
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withArea (const juce::String& areaName) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.setArea (areaName);
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withRow (StartAndEndProperty newRow) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.row = newRow;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withColumn (StartAndEndProperty newColumn) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.column = newColumn;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withAlignSelf (AlignSelf newAlignSelf) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.alignSelf = newAlignSelf;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withJustifySelf (JustifySelf newJustifySelf) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.justifySelf = newJustifySelf;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withWidth (float newWidth) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.width = newWidth;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withHeight (float newHeight) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.height = newHeight;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withSize (float newWidth, float newHeight) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.width = newWidth;
|
||||
gi.height = newHeight;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withMargin (Margin newHeight) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.margin = newHeight;
|
||||
return gi;
|
||||
}
|
||||
|
||||
GridItem GridItem::withOrder (int newOrder) const noexcept
|
||||
{
|
||||
auto gi = *this;
|
||||
gi.order = newOrder;
|
||||
return gi;
|
||||
}
|
||||
|
||||
} // namespace juce
|
251
modules/juce_gui_basics/layout/juce_GridItem.h
Normal file
251
modules/juce_gui_basics/layout/juce_GridItem.h
Normal file
@ -0,0 +1,251 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
Defines an item in a Grid
|
||||
@see Grid
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API GridItem
|
||||
{
|
||||
public:
|
||||
enum class Keyword { autoValue };
|
||||
|
||||
//==============================================================================
|
||||
/** Represents a span. */
|
||||
struct Span
|
||||
{
|
||||
explicit Span (int numberToUse) noexcept : number (numberToUse)
|
||||
{
|
||||
/* Span must be at least one and positive */
|
||||
jassert (numberToUse > 0);
|
||||
}
|
||||
|
||||
explicit Span (int numberToUse, const juce::String& nameToUse) : Span (numberToUse)
|
||||
{
|
||||
/* Name must not be empty */
|
||||
jassert (nameToUse.isNotEmpty());
|
||||
name = nameToUse;
|
||||
}
|
||||
|
||||
explicit Span (const juce::String& nameToUse) : name (nameToUse)
|
||||
{
|
||||
/* Name must not be empty */
|
||||
jassert (nameToUse.isNotEmpty());
|
||||
}
|
||||
|
||||
int number = 1;
|
||||
juce::String name;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Represents a property. */
|
||||
struct Property
|
||||
{
|
||||
/** */
|
||||
Property() noexcept;
|
||||
|
||||
/** */
|
||||
Property (Keyword keyword) noexcept;
|
||||
|
||||
/** */
|
||||
Property (const char* lineNameToUse) noexcept;
|
||||
|
||||
/** */
|
||||
Property (const juce::String& lineNameToUse) noexcept;
|
||||
|
||||
/** */
|
||||
Property (int numberToUse) noexcept;
|
||||
|
||||
/** */
|
||||
Property (int numberToUse, const juce::String& lineNameToUse) noexcept;
|
||||
|
||||
/** */
|
||||
Property (Span spanToUse) noexcept;
|
||||
|
||||
private:
|
||||
bool hasSpan() const noexcept { return isSpan && ! isAuto; }
|
||||
bool hasAbsolute() const noexcept { return ! (isSpan || isAuto); }
|
||||
bool hasAuto() const noexcept { return isAuto; }
|
||||
bool hasName() const noexcept { return name.isNotEmpty(); }
|
||||
|
||||
friend class Grid;
|
||||
|
||||
juce::String name;
|
||||
int number = 1; /** Either an absolute line number or number of lines to span across. */
|
||||
bool isSpan = false;
|
||||
bool isAuto = false;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Represents start and end properties. */
|
||||
struct StartAndEndProperty { Property start, end; };
|
||||
|
||||
//==============================================================================
|
||||
/** Possible values for the justifySelf property. */
|
||||
enum class JustifySelf : int
|
||||
{
|
||||
start = 0, /**< Content inside the item is justified towards the left. */
|
||||
end, /**< Content inside the item is justified towards the right. */
|
||||
center, /**< Content inside the item is justified towards the center. */
|
||||
stretch, /**< Content inside the item is stretched from left to right. */
|
||||
autoValue /**< Follows the Grid container's justifyItems property. */
|
||||
};
|
||||
|
||||
/** Possible values for the alignSelf property. */
|
||||
enum class AlignSelf : int
|
||||
{
|
||||
start = 0, /**< Content inside the item is aligned towards the top. */
|
||||
end, /**< Content inside the item is aligned towards the bottom. */
|
||||
center, /**< Content inside the item is aligned towards the center. */
|
||||
stretch, /**< Content inside the item is stretched from top to bottom. */
|
||||
autoValue /**< Follows the Grid container's alignItems property. */
|
||||
};
|
||||
|
||||
/** Creates an item with default parameters. */
|
||||
GridItem() noexcept;
|
||||
/** Creates an item with a given Component to use. */
|
||||
GridItem (juce::Component& componentToUse) noexcept;
|
||||
/** Creates an item with a given Component to use. */
|
||||
GridItem (juce::Component* componentToUse) noexcept;
|
||||
|
||||
/** Destructor. */
|
||||
~GridItem() noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** If this is non-null, it represents a Component whose bounds are controlled by this item. */
|
||||
juce::Component* associatedComponent = nullptr;
|
||||
|
||||
//==============================================================================
|
||||
/** Determines the order used to lay out items in their grid container. */
|
||||
int order = 0;
|
||||
|
||||
/** This is the justify-self property of the item.
|
||||
This determines the alignment of the item along the row.
|
||||
*/
|
||||
JustifySelf justifySelf = JustifySelf::autoValue;
|
||||
|
||||
/** This is the align-self property of the item.
|
||||
This determines the alignment of the item along the column.
|
||||
*/
|
||||
AlignSelf alignSelf = AlignSelf::autoValue;
|
||||
|
||||
/** These are the start and end properties of the column. */
|
||||
StartAndEndProperty column = { Keyword::autoValue, Keyword::autoValue };
|
||||
|
||||
/** These are the start and end properties of the row. */
|
||||
StartAndEndProperty row = { Keyword::autoValue, Keyword::autoValue };
|
||||
|
||||
/** */
|
||||
juce::String area;
|
||||
|
||||
//==============================================================================
|
||||
enum
|
||||
{
|
||||
useDefaultValue = -2, /* TODO: useDefaultValue should be named useAuto */
|
||||
notAssigned = -1
|
||||
};
|
||||
|
||||
/* TODO: move all of this into a common class that is shared with the FlexItem */
|
||||
float width = notAssigned;
|
||||
float minWidth = 0;
|
||||
float maxWidth = notAssigned;
|
||||
|
||||
float height = notAssigned;
|
||||
float minHeight = 0;
|
||||
float maxHeight = notAssigned;
|
||||
|
||||
/** Represents a margin. */
|
||||
struct Margin
|
||||
{
|
||||
Margin() noexcept;
|
||||
Margin (int size) noexcept;
|
||||
Margin (float size) noexcept;
|
||||
Margin (float top, float right, float bottom, float left) noexcept; /**< Creates a margin with these sizes. */
|
||||
|
||||
float left;
|
||||
float right;
|
||||
float top;
|
||||
float bottom;
|
||||
};
|
||||
|
||||
/** The margin to leave around this item. */
|
||||
Margin margin;
|
||||
|
||||
/** The item's current bounds. */
|
||||
juce::Rectangle<float> currentBounds;
|
||||
|
||||
/** Short-hand */
|
||||
void setArea (Property rowStart, Property columnStart, Property rowEnd, Property columnEnd);
|
||||
|
||||
/** Short-hand, span of 1 by default */
|
||||
void setArea (Property rowStart, Property columnStart);
|
||||
|
||||
/** Short-hand */
|
||||
void setArea (const juce::String& areaName);
|
||||
|
||||
/** Short-hand */
|
||||
GridItem withArea (Property rowStart, Property columnStart, Property rowEnd, Property columnEnd) const noexcept;
|
||||
|
||||
/** Short-hand, span of 1 by default */
|
||||
GridItem withArea (Property rowStart, Property columnStart) const noexcept;
|
||||
|
||||
/** Short-hand */
|
||||
GridItem withArea (const juce::String& areaName) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new row property. */
|
||||
GridItem withRow (StartAndEndProperty row) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new column property. */
|
||||
GridItem withColumn (StartAndEndProperty column) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new alignSelf property. */
|
||||
GridItem withAlignSelf (AlignSelf newAlignSelf) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new justifySelf property. */
|
||||
GridItem withJustifySelf (JustifySelf newJustifySelf) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new width. */
|
||||
GridItem withWidth (float newWidth) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new height. */
|
||||
GridItem withHeight (float newHeight) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new size. */
|
||||
GridItem withSize (float newWidth, float newHeight) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new margin. */
|
||||
GridItem withMargin (Margin newMargin) const noexcept;
|
||||
|
||||
/** Returns a copy of this object with a new order. */
|
||||
GridItem withOrder (int newOrder) const noexcept;
|
||||
};
|
||||
|
||||
} // namespace juce
|
262
modules/juce_gui_basics/layout/juce_GridUnitTests.cpp
Normal file
262
modules/juce_gui_basics/layout/juce_GridUnitTests.cpp
Normal file
@ -0,0 +1,262 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
struct GridTests : public juce::UnitTest
|
||||
{
|
||||
GridTests() : juce::UnitTest ("Grid class") {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
using Fr = juce::Grid::Fr;
|
||||
using Tr = juce::Grid::TrackInfo;
|
||||
using Rect = juce::Rectangle<float>;
|
||||
using Grid = juce::Grid;
|
||||
|
||||
{
|
||||
Grid grid;
|
||||
|
||||
grid.templateColumns.add (Tr (1_fr));
|
||||
grid.templateRows.addArray ({ Tr (20_px), Tr (1_fr) });
|
||||
|
||||
grid.items.addArray ({ GridItem().withArea (1, 1),
|
||||
GridItem().withArea (2, 1) });
|
||||
|
||||
grid.performLayout (juce::Rectangle<int> (200, 400));
|
||||
|
||||
beginTest ("Layout calculation test: 1 column x 2 rows: no gap");
|
||||
expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 200.f, 20.0f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.0f, 20.0f, 200.f, 380.0f));
|
||||
|
||||
grid.templateColumns.add (Tr (50_px));
|
||||
grid.templateRows.add (Tr (2_fr));
|
||||
|
||||
grid.items.addArray ( { GridItem().withArea (1, 2),
|
||||
GridItem().withArea (2, 2),
|
||||
GridItem().withArea (3, 1),
|
||||
GridItem().withArea (3, 2) });
|
||||
|
||||
grid.performLayout (juce::Rectangle<int> (150, 170));
|
||||
|
||||
beginTest ("Layout calculation test: 2 columns x 3 rows: no gap");
|
||||
expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 100.0f, 20.0f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.0f, 20.0f, 100.0f, 50.0f));
|
||||
expect (grid.items[2].currentBounds == Rect (100.0f, 0.0f, 50.0f, 20.0f));
|
||||
expect (grid.items[3].currentBounds == Rect (100.0f, 20.0f, 50.0f, 50.0f));
|
||||
expect (grid.items[4].currentBounds == Rect (0.0f, 70.0f, 100.0f, 100.0f));
|
||||
expect (grid.items[5].currentBounds == Rect (100.0f, 70.0f, 50.0f, 100.0f));
|
||||
|
||||
grid.columnGap = 20_px;
|
||||
grid.rowGap = 10_px;
|
||||
|
||||
grid.performLayout (juce::Rectangle<int> (200, 310));
|
||||
|
||||
beginTest ("Layout calculation test: 2 columns x 3 rows: rowGap of 10 and columnGap of 20");
|
||||
expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 130.0f, 20.0f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.0f, 30.0f, 130.0f, 90.0f));
|
||||
expect (grid.items[2].currentBounds == Rect (150.0f, 0.0f, 50.0f, 20.0f));
|
||||
expect (grid.items[3].currentBounds == Rect (150.0f, 30.0f, 50.0f, 90.0f));
|
||||
expect (grid.items[4].currentBounds == Rect (0.0f, 130.0f, 130.0f, 180.0f));
|
||||
expect (grid.items[5].currentBounds == Rect (150.0f, 130.0f, 50.0f, 180.0f));
|
||||
}
|
||||
|
||||
{
|
||||
Grid grid;
|
||||
|
||||
grid.templateColumns.addArray ({ Tr ("first", 20_px, "in"), Tr ("in", 1_fr, "in"), Tr (20_px, "last") });
|
||||
grid.templateRows.addArray ({ Tr (1_fr),
|
||||
Tr (20_px)});
|
||||
|
||||
{
|
||||
beginTest ("Grid items placement tests: integer and custom ident, counting forward");
|
||||
|
||||
GridItem i1, i2, i3, i4, i5;
|
||||
i1.column = { 1, 4 };
|
||||
i1.row = { 1, 2 };
|
||||
|
||||
i2.column = { 1, 3 };
|
||||
i2.row = { 1, 3 };
|
||||
|
||||
i3.column = { "first", "in" };
|
||||
i3.row = { 2, 3 };
|
||||
|
||||
i4.column = { "first", { 2, "in" } };
|
||||
i4.row = { 1, 2 };
|
||||
|
||||
i5.column = { "first", "last" };
|
||||
i5.row = { 1, 2 };
|
||||
|
||||
grid.items.addArray ({ i1, i2, i3, i4, i5 });
|
||||
|
||||
grid.performLayout ({ 140, 100 });
|
||||
|
||||
expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.0f, 0.0f, 120.0f, 100.0f));
|
||||
expect (grid.items[2].currentBounds == Rect (0.0f, 80.0f, 20.0f, 20.0f));
|
||||
expect (grid.items[3].currentBounds == Rect (0.0f, 0.0f, 120.0f, 80.0f));
|
||||
expect (grid.items[4].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Grid grid;
|
||||
|
||||
grid.templateColumns.addArray ({ Tr ("first", 20_px, "in"), Tr ("in", 1_fr, "in"), Tr (20_px, "last") });
|
||||
grid.templateRows.addArray ({ Tr (1_fr),
|
||||
Tr (20_px)});
|
||||
|
||||
beginTest ("Grid items placement tests: integer and custom ident, counting forward, reversed end and start");
|
||||
|
||||
GridItem i1, i2, i3, i4, i5;
|
||||
i1.column = { 4, 1 };
|
||||
i1.row = { 2, 1 };
|
||||
|
||||
i2.column = { 3, 1 };
|
||||
i2.row = { 3, 1 };
|
||||
|
||||
i3.column = { "in", "first" };
|
||||
i3.row = { 3, 2 };
|
||||
|
||||
i4.column = { "first", { 2, "in" } };
|
||||
i4.row = { 1, 2 };
|
||||
|
||||
i5.column = { "last", "first" };
|
||||
i5.row = { 1, 2 };
|
||||
|
||||
grid.items.addArray ({ i1, i2, i3, i4, i5 });
|
||||
|
||||
grid.performLayout ({ 140, 100 });
|
||||
|
||||
expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.0f, 0.0f, 120.0f, 100.0f));
|
||||
expect (grid.items[2].currentBounds == Rect (0.0f, 80.0f, 20.0f, 20.0f));
|
||||
expect (grid.items[3].currentBounds == Rect (0.0f, 0.0f, 120.0f, 80.0f));
|
||||
expect (grid.items[4].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
|
||||
}
|
||||
|
||||
{
|
||||
beginTest ("Grid items placement tests: areas");
|
||||
|
||||
Grid grid;
|
||||
|
||||
grid.templateColumns = { Tr (50_px), Tr (100_px), Tr (Fr (1_fr)), Tr (50_px) };
|
||||
grid.templateRows = { Tr (50_px),
|
||||
Tr (1_fr),
|
||||
Tr (50_px) };
|
||||
|
||||
grid.templateAreas = { "header header header header",
|
||||
"main main . sidebar",
|
||||
"footer footer footer footer" };
|
||||
|
||||
grid.items.addArray ({ GridItem().withArea ("header"),
|
||||
GridItem().withArea ("main"),
|
||||
GridItem().withArea ("sidebar"),
|
||||
GridItem().withArea ("footer"),
|
||||
});
|
||||
|
||||
grid.performLayout ({ 300, 150 });
|
||||
|
||||
expect (grid.items[0].currentBounds == Rect (0.f, 0.f, 300.f, 50.f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.f, 50.f, 150.f, 50.f));
|
||||
expect (grid.items[2].currentBounds == Rect (250.f, 50.f, 50.f, 50.f));
|
||||
expect (grid.items[3].currentBounds == Rect (0.f, 100.f, 300.f, 50.f));
|
||||
}
|
||||
|
||||
{
|
||||
beginTest ("Grid implicit rows and columns: triggered by areas");
|
||||
|
||||
Grid grid;
|
||||
|
||||
grid.templateColumns = { Tr (50_px), Tr (100_px), Tr (1_fr), Tr (50_px) };
|
||||
grid.templateRows = { Tr (50_px),
|
||||
Tr (1_fr),
|
||||
Tr (50_px) };
|
||||
|
||||
grid.autoRows = Tr (30_px);
|
||||
grid.autoColumns = Tr (30_px);
|
||||
|
||||
grid.templateAreas = { "header header header header header",
|
||||
"main main . sidebar sidebar",
|
||||
"footer footer footer footer footer",
|
||||
"sub sub sub sub sub"};
|
||||
|
||||
grid.items.addArray ({ GridItem().withArea ("header"),
|
||||
GridItem().withArea ("main"),
|
||||
GridItem().withArea ("sidebar"),
|
||||
GridItem().withArea ("footer"),
|
||||
GridItem().withArea ("sub"),
|
||||
});
|
||||
|
||||
grid.performLayout ({ 330, 180 });
|
||||
|
||||
expect (grid.items[0].currentBounds == Rect (0.f, 0.f, 330.f, 50.f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.f, 50.f, 150.f, 50.f));
|
||||
expect (grid.items[2].currentBounds == Rect (250.f, 50.f, 80.f, 50.f));
|
||||
expect (grid.items[3].currentBounds == Rect (0.f, 100.f, 330.f, 50.f));
|
||||
expect (grid.items[4].currentBounds == Rect (0.f, 150.f, 330.f, 30.f));
|
||||
}
|
||||
|
||||
{
|
||||
beginTest ("Grid implicit rows and columns: triggered by areas");
|
||||
|
||||
Grid grid;
|
||||
|
||||
grid.templateColumns = { Tr (50_px), Tr (100_px), Tr (1_fr), Tr (50_px) };
|
||||
grid.templateRows = { Tr (50_px),
|
||||
Tr (1_fr),
|
||||
Tr (50_px) };
|
||||
|
||||
grid.autoRows = Tr (1_fr);
|
||||
grid.autoColumns = Tr (1_fr);
|
||||
|
||||
grid.templateAreas = { "header header header header",
|
||||
"main main . sidebar",
|
||||
"footer footer footer footer" };
|
||||
|
||||
grid.items.addArray ({ GridItem().withArea ("header"),
|
||||
GridItem().withArea ("main"),
|
||||
GridItem().withArea ("sidebar"),
|
||||
GridItem().withArea ("footer"),
|
||||
GridItem().withArea (4, 5, 6, 7)
|
||||
});
|
||||
|
||||
grid.performLayout ({ 350, 250 });
|
||||
|
||||
expect (grid.items[0].currentBounds == Rect (0.f, 0.f, 250.f, 50.f));
|
||||
expect (grid.items[1].currentBounds == Rect (0.f, 50.f, 150.f, 50.f));
|
||||
expect (grid.items[2].currentBounds == Rect (200.f, 50.f, 50.f, 50.f));
|
||||
expect (grid.items[3].currentBounds == Rect (0.f, 100.f, 250.f, 50.f));
|
||||
expect (grid.items[4].currentBounds == Rect (250.f, 150.f, 100.f, 100.f));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
static GridTests gridUnitTests;
|
||||
|
||||
} // namespace juce
|
73
modules/juce_gui_basics/layout/juce_GroupComponent.cpp
Normal file
73
modules/juce_gui_basics/layout/juce_GroupComponent.cpp
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
GroupComponent::GroupComponent (const String& name,
|
||||
const String& labelText)
|
||||
: Component (name),
|
||||
text (labelText),
|
||||
justification (Justification::left)
|
||||
{
|
||||
setInterceptsMouseClicks (false, true);
|
||||
}
|
||||
|
||||
GroupComponent::~GroupComponent() {}
|
||||
|
||||
void GroupComponent::setText (const String& newText)
|
||||
{
|
||||
if (text != newText)
|
||||
{
|
||||
text = newText;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
String GroupComponent::getText() const
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
void GroupComponent::setTextLabelPosition (Justification newJustification)
|
||||
{
|
||||
if (justification != newJustification)
|
||||
{
|
||||
justification = newJustification;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupComponent::paint (Graphics& g)
|
||||
{
|
||||
getLookAndFeel().drawGroupComponentOutline (g, getWidth(), getHeight(),
|
||||
text, justification, *this);
|
||||
}
|
||||
|
||||
void GroupComponent::enablementChanged() { repaint(); }
|
||||
void GroupComponent::colourChanged() { repaint(); }
|
||||
|
||||
} // namespace juce
|
113
modules/juce_gui_basics/layout/juce_GroupComponent.h
Normal file
113
modules/juce_gui_basics/layout/juce_GroupComponent.h
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component that draws an outline around itself and has an optional title at
|
||||
the top, for drawing an outline around a group of controls.
|
||||
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API GroupComponent : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a GroupComponent.
|
||||
|
||||
@param componentName the name to give the component
|
||||
@param labelText the text to show at the top of the outline
|
||||
*/
|
||||
GroupComponent (const String& componentName = String(),
|
||||
const String& labelText = String());
|
||||
|
||||
/** Destructor. */
|
||||
~GroupComponent();
|
||||
|
||||
//==============================================================================
|
||||
/** Changes the text that's shown at the top of the component. */
|
||||
void setText (const String& newText);
|
||||
|
||||
/** Returns the currently displayed text label. */
|
||||
String getText() const;
|
||||
|
||||
/** Sets the positioning of the text label.
|
||||
|
||||
(The default is Justification::left)
|
||||
|
||||
@see getTextLabelPosition
|
||||
*/
|
||||
void setTextLabelPosition (Justification justification);
|
||||
|
||||
/** Returns the current text label position.
|
||||
|
||||
@see setTextLabelPosition
|
||||
*/
|
||||
Justification getTextLabelPosition() const noexcept { return justification; }
|
||||
|
||||
//==============================================================================
|
||||
/** A set of colour IDs to use to change the colour of various aspects of the component.
|
||||
|
||||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
|
||||
methods.
|
||||
|
||||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
|
||||
*/
|
||||
enum ColourIds
|
||||
{
|
||||
outlineColourId = 0x1005400, /**< The colour to use for drawing the line around the edge. */
|
||||
textColourId = 0x1005410 /**< The colour to use to draw the text label. */
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** This abstract base class is implemented by LookAndFeel classes. */
|
||||
struct JUCE_API LookAndFeelMethods
|
||||
{
|
||||
virtual ~LookAndFeelMethods() {}
|
||||
|
||||
virtual void drawGroupComponentOutline (Graphics&, int w, int h, const String& text,
|
||||
const Justification&, GroupComponent&) = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void enablementChanged() override;
|
||||
/** @internal */
|
||||
void colourChanged() override;
|
||||
|
||||
private:
|
||||
String text;
|
||||
Justification justification;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (GroupComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
502
modules/juce_gui_basics/layout/juce_MultiDocumentPanel.cpp
Normal file
502
modules/juce_gui_basics/layout/juce_MultiDocumentPanel.cpp
Normal file
@ -0,0 +1,502 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
MultiDocumentPanelWindow::MultiDocumentPanelWindow (Colour backgroundColour)
|
||||
: DocumentWindow (String(), backgroundColour,
|
||||
DocumentWindow::maximiseButton | DocumentWindow::closeButton, false)
|
||||
{
|
||||
}
|
||||
|
||||
MultiDocumentPanelWindow::~MultiDocumentPanelWindow()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MultiDocumentPanelWindow::maximiseButtonPressed()
|
||||
{
|
||||
if (auto* owner = getOwner())
|
||||
owner->setLayoutMode (MultiDocumentPanel::MaximisedWindowsWithTabs);
|
||||
else
|
||||
jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel!
|
||||
}
|
||||
|
||||
void MultiDocumentPanelWindow::closeButtonPressed()
|
||||
{
|
||||
if (auto* owner = getOwner())
|
||||
owner->closeDocument (getContentComponent(), true);
|
||||
else
|
||||
jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel!
|
||||
}
|
||||
|
||||
void MultiDocumentPanelWindow::activeWindowStatusChanged()
|
||||
{
|
||||
DocumentWindow::activeWindowStatusChanged();
|
||||
updateOrder();
|
||||
}
|
||||
|
||||
void MultiDocumentPanelWindow::broughtToFront()
|
||||
{
|
||||
DocumentWindow::broughtToFront();
|
||||
updateOrder();
|
||||
}
|
||||
|
||||
void MultiDocumentPanelWindow::updateOrder()
|
||||
{
|
||||
if (auto* owner = getOwner())
|
||||
owner->updateOrder();
|
||||
}
|
||||
|
||||
MultiDocumentPanel* MultiDocumentPanelWindow::getOwner() const noexcept
|
||||
{
|
||||
return findParentComponentOfClass<MultiDocumentPanel>();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct MultiDocumentPanel::TabbedComponentInternal : public TabbedComponent
|
||||
{
|
||||
TabbedComponentInternal() : TabbedComponent (TabbedButtonBar::TabsAtTop) {}
|
||||
|
||||
void currentTabChanged (int, const String&) override
|
||||
{
|
||||
if (auto* owner = findParentComponentOfClass<MultiDocumentPanel>())
|
||||
owner->updateOrder();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
MultiDocumentPanel::MultiDocumentPanel()
|
||||
{
|
||||
setOpaque (true);
|
||||
}
|
||||
|
||||
MultiDocumentPanel::~MultiDocumentPanel()
|
||||
{
|
||||
closeAllDocuments (false);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
namespace MultiDocHelpers
|
||||
{
|
||||
static bool shouldDeleteComp (Component* const c)
|
||||
{
|
||||
return c->getProperties() ["mdiDocumentDelete_"];
|
||||
}
|
||||
}
|
||||
|
||||
bool MultiDocumentPanel::closeAllDocuments (const bool checkItsOkToCloseFirst)
|
||||
{
|
||||
while (! components.isEmpty())
|
||||
if (! closeDocument (components.getLast(), checkItsOkToCloseFirst))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
MultiDocumentPanelWindow* MultiDocumentPanel::createNewDocumentWindow()
|
||||
{
|
||||
return new MultiDocumentPanelWindow (backgroundColour);
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::addWindow (Component* component)
|
||||
{
|
||||
auto* dw = createNewDocumentWindow();
|
||||
|
||||
dw->setResizable (true, false);
|
||||
dw->setContentNonOwned (component, true);
|
||||
dw->setName (component->getName());
|
||||
|
||||
auto bkg = component->getProperties() ["mdiDocumentBkg_"];
|
||||
dw->setBackgroundColour (bkg.isVoid() ? backgroundColour : Colour ((uint32) static_cast<int> (bkg)));
|
||||
|
||||
int x = 4;
|
||||
|
||||
if (auto* topComp = getChildren().getLast())
|
||||
if (topComp->getX() == x && topComp->getY() == x)
|
||||
x += 16;
|
||||
|
||||
dw->setTopLeftPosition (x, x);
|
||||
|
||||
auto pos = component->getProperties() ["mdiDocumentPos_"];
|
||||
if (pos.toString().isNotEmpty())
|
||||
dw->restoreWindowStateFromString (pos.toString());
|
||||
|
||||
addAndMakeVisible (dw);
|
||||
dw->toFront (true);
|
||||
}
|
||||
|
||||
bool MultiDocumentPanel::addDocument (Component* const component,
|
||||
Colour docColour,
|
||||
const bool deleteWhenRemoved)
|
||||
{
|
||||
// If you try passing a full DocumentWindow or ResizableWindow in here, you'll end up
|
||||
// with a frame-within-a-frame! Just pass in the bare content component.
|
||||
jassert (dynamic_cast<ResizableWindow*> (component) == nullptr);
|
||||
|
||||
if (component == nullptr || (maximumNumDocuments > 0 && components.size() >= maximumNumDocuments))
|
||||
return false;
|
||||
|
||||
components.add (component);
|
||||
component->getProperties().set ("mdiDocumentDelete_", deleteWhenRemoved);
|
||||
component->getProperties().set ("mdiDocumentBkg_", (int) docColour.getARGB());
|
||||
component->addComponentListener (this);
|
||||
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
if (isFullscreenWhenOneDocument())
|
||||
{
|
||||
if (components.size() == 1)
|
||||
{
|
||||
addAndMakeVisible (component);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (components.size() == 2)
|
||||
addWindow (components.getFirst());
|
||||
|
||||
addWindow (component);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
addWindow (component);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tabComponent == nullptr && components.size() > numDocsBeforeTabsUsed)
|
||||
{
|
||||
tabComponent.reset (new TabbedComponentInternal());
|
||||
addAndMakeVisible (tabComponent.get());
|
||||
|
||||
auto temp = components;
|
||||
|
||||
for (auto& c : temp)
|
||||
tabComponent->addTab (c->getName(), docColour, c, false);
|
||||
|
||||
resized();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tabComponent != nullptr)
|
||||
tabComponent->addTab (component->getName(), docColour, component, false);
|
||||
else
|
||||
addAndMakeVisible (component);
|
||||
}
|
||||
|
||||
setActiveDocument (component);
|
||||
}
|
||||
|
||||
resized();
|
||||
activeDocumentChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MultiDocumentPanel::closeDocument (Component* component,
|
||||
const bool checkItsOkToCloseFirst)
|
||||
{
|
||||
if (components.contains (component))
|
||||
{
|
||||
if (checkItsOkToCloseFirst && ! tryToCloseDocument (component))
|
||||
return false;
|
||||
|
||||
component->removeComponentListener (this);
|
||||
|
||||
const bool shouldDelete = MultiDocHelpers::shouldDeleteComp (component);
|
||||
component->getProperties().remove ("mdiDocumentDelete_");
|
||||
component->getProperties().remove ("mdiDocumentBkg_");
|
||||
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
for (auto* child : getChildren())
|
||||
{
|
||||
if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
|
||||
{
|
||||
if (dw->getContentComponent() == component)
|
||||
{
|
||||
std::unique_ptr<MultiDocumentPanelWindow> (dw)->clearContentComponent();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelete)
|
||||
delete component;
|
||||
|
||||
components.removeFirstMatchingValue (component);
|
||||
|
||||
if (isFullscreenWhenOneDocument() && components.size() == 1)
|
||||
{
|
||||
for (int i = getNumChildComponents(); --i >= 0;)
|
||||
{
|
||||
std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i)));
|
||||
|
||||
if (dw != nullptr)
|
||||
dw->clearContentComponent();
|
||||
}
|
||||
|
||||
addAndMakeVisible (components.getFirst());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
jassert (components.indexOf (component) >= 0);
|
||||
|
||||
if (tabComponent != nullptr)
|
||||
{
|
||||
for (int i = tabComponent->getNumTabs(); --i >= 0;)
|
||||
if (tabComponent->getTabContentComponent (i) == component)
|
||||
tabComponent->removeTab (i);
|
||||
}
|
||||
else
|
||||
{
|
||||
removeChildComponent (component);
|
||||
}
|
||||
|
||||
if (shouldDelete)
|
||||
delete component;
|
||||
|
||||
if (tabComponent != nullptr && tabComponent->getNumTabs() <= numDocsBeforeTabsUsed)
|
||||
tabComponent.reset();
|
||||
|
||||
components.removeFirstMatchingValue (component);
|
||||
|
||||
if (components.size() > 0 && tabComponent == nullptr)
|
||||
addAndMakeVisible (components.getFirst());
|
||||
}
|
||||
|
||||
resized();
|
||||
|
||||
// This ensures that the active tab is painted properly when a tab is closed!
|
||||
if (auto* activeComponent = getActiveDocument())
|
||||
setActiveDocument (activeComponent);
|
||||
|
||||
activeDocumentChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int MultiDocumentPanel::getNumDocuments() const noexcept
|
||||
{
|
||||
return components.size();
|
||||
}
|
||||
|
||||
Component* MultiDocumentPanel::getDocument (const int index) const noexcept
|
||||
{
|
||||
return components [index];
|
||||
}
|
||||
|
||||
Component* MultiDocumentPanel::getActiveDocument() const noexcept
|
||||
{
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
for (auto* child : getChildren())
|
||||
if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
|
||||
if (dw->isActiveWindow())
|
||||
return dw->getContentComponent();
|
||||
}
|
||||
|
||||
return components.getLast();
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::setActiveDocument (Component* component)
|
||||
{
|
||||
jassert (component != nullptr);
|
||||
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
component = getContainerComp (component);
|
||||
|
||||
if (component != nullptr)
|
||||
component->toFront (true);
|
||||
}
|
||||
else if (tabComponent != nullptr)
|
||||
{
|
||||
jassert (components.indexOf (component) >= 0);
|
||||
|
||||
for (int i = tabComponent->getNumTabs(); --i >= 0;)
|
||||
{
|
||||
if (tabComponent->getTabContentComponent (i) == component)
|
||||
{
|
||||
tabComponent->setCurrentTabIndex (i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
component->grabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::activeDocumentChanged()
|
||||
{
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::setMaximumNumDocuments (const int newNumber)
|
||||
{
|
||||
maximumNumDocuments = newNumber;
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::useFullscreenWhenOneDocument (const bool shouldUseTabs)
|
||||
{
|
||||
numDocsBeforeTabsUsed = shouldUseTabs ? 1 : 0;
|
||||
}
|
||||
|
||||
bool MultiDocumentPanel::isFullscreenWhenOneDocument() const noexcept
|
||||
{
|
||||
return numDocsBeforeTabsUsed != 0;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MultiDocumentPanel::setLayoutMode (const LayoutMode newLayoutMode)
|
||||
{
|
||||
if (mode != newLayoutMode)
|
||||
{
|
||||
mode = newLayoutMode;
|
||||
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
tabComponent.reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = getNumChildComponents(); --i >= 0;)
|
||||
{
|
||||
std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i)));
|
||||
|
||||
if (dw != nullptr)
|
||||
{
|
||||
dw->getContentComponent()->getProperties().set ("mdiDocumentPos_", dw->getWindowStateAsString());
|
||||
dw->clearContentComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resized();
|
||||
|
||||
auto tempComps = components;
|
||||
components.clear();
|
||||
|
||||
for (auto* c : tempComps)
|
||||
addDocument (c,
|
||||
Colour ((uint32) static_cast<int> (c->getProperties().getWithDefault ("mdiDocumentBkg_", (int) Colours::white.getARGB()))),
|
||||
MultiDocHelpers::shouldDeleteComp (c));
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::setBackgroundColour (Colour newBackgroundColour)
|
||||
{
|
||||
if (backgroundColour != newBackgroundColour)
|
||||
{
|
||||
backgroundColour = newBackgroundColour;
|
||||
setOpaque (newBackgroundColour.isOpaque());
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MultiDocumentPanel::paint (Graphics& g)
|
||||
{
|
||||
g.fillAll (backgroundColour);
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::resized()
|
||||
{
|
||||
if (mode == MaximisedWindowsWithTabs || components.size() == numDocsBeforeTabsUsed)
|
||||
{
|
||||
for (auto* child : getChildren())
|
||||
child->setBounds (getLocalBounds());
|
||||
}
|
||||
|
||||
setWantsKeyboardFocus (components.size() == 0);
|
||||
}
|
||||
|
||||
Component* MultiDocumentPanel::getContainerComp (Component* c) const
|
||||
{
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
for (auto* child : getChildren())
|
||||
if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
|
||||
if (dw->getContentComponent() == c)
|
||||
return dw;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::componentNameChanged (Component&)
|
||||
{
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
for (auto* child : getChildren())
|
||||
if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
|
||||
dw->setName (dw->getContentComponent()->getName());
|
||||
}
|
||||
else if (tabComponent != nullptr)
|
||||
{
|
||||
for (int i = tabComponent->getNumTabs(); --i >= 0;)
|
||||
tabComponent->setTabName (i, tabComponent->getTabContentComponent (i)->getName());
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDocumentPanel::updateOrder()
|
||||
{
|
||||
auto oldList = components;
|
||||
|
||||
if (mode == FloatingWindows)
|
||||
{
|
||||
components.clear();
|
||||
|
||||
for (auto* child : getChildren())
|
||||
if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
|
||||
components.add (dw->getContentComponent());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tabComponent != nullptr)
|
||||
{
|
||||
if (auto* current = tabComponent->getCurrentContentComponent())
|
||||
{
|
||||
components.removeFirstMatchingValue (current);
|
||||
components.add (current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (components != oldList)
|
||||
activeDocumentChanged();
|
||||
}
|
||||
|
||||
} // namespace juce
|
308
modules/juce_gui_basics/layout/juce_MultiDocumentPanel.h
Normal file
308
modules/juce_gui_basics/layout/juce_MultiDocumentPanel.h
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class MultiDocumentPanel;
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
This is a derivative of DocumentWindow that is used inside a MultiDocumentPanel
|
||||
component.
|
||||
|
||||
It's like a normal DocumentWindow but has some extra functionality to make sure
|
||||
everything works nicely inside a MultiDocumentPanel.
|
||||
|
||||
@see MultiDocumentPanel
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API MultiDocumentPanelWindow : public DocumentWindow
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/**
|
||||
*/
|
||||
MultiDocumentPanelWindow (Colour backgroundColour);
|
||||
|
||||
/** Destructor. */
|
||||
~MultiDocumentPanelWindow();
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void maximiseButtonPressed() override;
|
||||
/** @internal */
|
||||
void closeButtonPressed() override;
|
||||
/** @internal */
|
||||
void activeWindowStatusChanged() override;
|
||||
/** @internal */
|
||||
void broughtToFront() override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void updateOrder();
|
||||
MultiDocumentPanel* getOwner() const noexcept;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiDocumentPanelWindow)
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component that contains a set of other components either in floating windows
|
||||
or tabs.
|
||||
|
||||
This acts as a panel that can be used to hold a set of open document windows, with
|
||||
different layout modes.
|
||||
|
||||
Use addDocument() and closeDocument() to add or remove components from the
|
||||
panel - never use any of the Component methods to access the panel's child
|
||||
components directly, as these are managed internally.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API MultiDocumentPanel : public Component,
|
||||
private ComponentListener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates an empty panel.
|
||||
|
||||
Use addDocument() and closeDocument() to add or remove components from the
|
||||
panel - never use any of the Component methods to access the panel's child
|
||||
components directly, as these are managed internally.
|
||||
*/
|
||||
MultiDocumentPanel();
|
||||
|
||||
/** Destructor.
|
||||
|
||||
When deleted, this will call closeAllDocuments (false) to make sure all its
|
||||
components are deleted. If you need to make sure all documents are saved
|
||||
before closing, then you should call closeAllDocuments (true) and check that
|
||||
it returns true before deleting the panel.
|
||||
*/
|
||||
~MultiDocumentPanel();
|
||||
|
||||
//==============================================================================
|
||||
/** Tries to close all the documents.
|
||||
|
||||
If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will
|
||||
be called for each open document, and any of these calls fails, this method
|
||||
will stop and return false, leaving some documents still open.
|
||||
|
||||
If checkItsOkToCloseFirst is false, then all documents will be closed
|
||||
unconditionally.
|
||||
|
||||
@see closeDocument
|
||||
*/
|
||||
bool closeAllDocuments (bool checkItsOkToCloseFirst);
|
||||
|
||||
/** Adds a document component to the panel.
|
||||
|
||||
If the number of documents would exceed the limit set by setMaximumNumDocuments() then
|
||||
this will fail and return false. (If it does fail, the component passed-in will not be
|
||||
deleted, even if deleteWhenRemoved was set to true).
|
||||
|
||||
The MultiDocumentPanel will deal with creating a window border to go around your component,
|
||||
so just pass in the bare content component here, no need to give it a ResizableWindow
|
||||
or DocumentWindow.
|
||||
|
||||
@param component the component to add
|
||||
@param backgroundColour the background colour to use to fill the component's
|
||||
window or tab
|
||||
@param deleteWhenRemoved if true, then when the component is removed by closeDocument()
|
||||
or closeAllDocuments(), then it will be deleted. If false, then
|
||||
the caller must handle the component's deletion
|
||||
*/
|
||||
bool addDocument (Component* component,
|
||||
Colour backgroundColour,
|
||||
bool deleteWhenRemoved);
|
||||
|
||||
/** Closes one of the documents.
|
||||
|
||||
If checkItsOkToCloseFirst is true, then the tryToCloseDocument() method will
|
||||
be called, and if it fails, this method will return false without closing the
|
||||
document.
|
||||
|
||||
If checkItsOkToCloseFirst is false, then the documents will be closed
|
||||
unconditionally.
|
||||
|
||||
The component will be deleted if the deleteWhenRemoved parameter was set to
|
||||
true when it was added with addDocument.
|
||||
|
||||
@see addDocument, closeAllDocuments
|
||||
*/
|
||||
bool closeDocument (Component* component,
|
||||
bool checkItsOkToCloseFirst);
|
||||
|
||||
/** Returns the number of open document windows.
|
||||
|
||||
@see getDocument
|
||||
*/
|
||||
int getNumDocuments() const noexcept;
|
||||
|
||||
/** Returns one of the open documents.
|
||||
|
||||
The order of the documents in this array may change when they are added, removed
|
||||
or moved around.
|
||||
|
||||
@see getNumDocuments
|
||||
*/
|
||||
Component* getDocument (int index) const noexcept;
|
||||
|
||||
/** Returns the document component that is currently focused or on top.
|
||||
|
||||
If currently using floating windows, then this will be the component in the currently
|
||||
active window, or the top component if none are active.
|
||||
|
||||
If it's currently in tabbed mode, then it'll return the component in the active tab.
|
||||
|
||||
@see setActiveDocument
|
||||
*/
|
||||
Component* getActiveDocument() const noexcept;
|
||||
|
||||
/** Makes one of the components active and brings it to the top.
|
||||
|
||||
@see getActiveDocument
|
||||
*/
|
||||
void setActiveDocument (Component* component);
|
||||
|
||||
/** Callback which gets invoked when the currently-active document changes. */
|
||||
virtual void activeDocumentChanged();
|
||||
|
||||
/** Sets a limit on how many windows can be open at once.
|
||||
|
||||
If this is zero or less there's no limit (the default). addDocument() will fail
|
||||
if this number is exceeded.
|
||||
*/
|
||||
void setMaximumNumDocuments (int maximumNumDocuments);
|
||||
|
||||
/** Sets an option to make the document fullscreen if there's only one document open.
|
||||
|
||||
If set to true, then if there's only one document, it'll fill the whole of this
|
||||
component without tabs or a window border. If false, then tabs or a window
|
||||
will always be shown, even if there's only one document. If there's more than
|
||||
one document open, then this option makes no difference.
|
||||
*/
|
||||
void useFullscreenWhenOneDocument (bool shouldUseTabs);
|
||||
|
||||
/** Returns the result of the last time useFullscreenWhenOneDocument() was called.
|
||||
*/
|
||||
bool isFullscreenWhenOneDocument() const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** The different layout modes available. */
|
||||
enum LayoutMode
|
||||
{
|
||||
FloatingWindows, /**< In this mode, there are overlapping DocumentWindow components for each document. */
|
||||
MaximisedWindowsWithTabs /**< In this mode, a TabbedComponent is used to show one document at a time. */
|
||||
};
|
||||
|
||||
/** Changes the panel's mode.
|
||||
|
||||
@see LayoutMode, getLayoutMode
|
||||
*/
|
||||
void setLayoutMode (LayoutMode newLayoutMode);
|
||||
|
||||
/** Returns the current layout mode. */
|
||||
LayoutMode getLayoutMode() const noexcept { return mode; }
|
||||
|
||||
/** Sets the background colour for the whole panel.
|
||||
|
||||
Each document has its own background colour, but this is the one used to fill the areas
|
||||
behind them.
|
||||
*/
|
||||
void setBackgroundColour (Colour newBackgroundColour);
|
||||
|
||||
/** Returns the current background colour.
|
||||
|
||||
@see setBackgroundColour
|
||||
*/
|
||||
Colour getBackgroundColour() const noexcept { return backgroundColour; }
|
||||
|
||||
/** If the panel is being used in tabbed mode, this returns the TabbedComponent that's involved. */
|
||||
TabbedComponent* getCurrentTabbedComponent() const noexcept { return tabComponent.get(); }
|
||||
|
||||
//==============================================================================
|
||||
/** A subclass must override this to say whether its currently ok for a document
|
||||
to be closed.
|
||||
|
||||
This method is called by closeDocument() and closeAllDocuments() to indicate that
|
||||
a document should be saved if possible, ready for it to be closed.
|
||||
|
||||
If this method returns true, then it means the document is ok and can be closed.
|
||||
|
||||
If it returns false, then it means that the closeDocument() method should stop
|
||||
and not close.
|
||||
|
||||
Normally, you'd use this method to ask the user if they want to save any changes,
|
||||
then return true if the save operation went ok. If the user cancelled the save
|
||||
operation you could return false here to abort the close operation.
|
||||
|
||||
If your component is based on the FileBasedDocument class, then you'd probably want
|
||||
to call FileBasedDocument::saveIfNeededAndUserAgrees() and return true if this returned
|
||||
FileBasedDocument::savedOk
|
||||
|
||||
@see closeDocument, FileBasedDocument::saveIfNeededAndUserAgrees()
|
||||
*/
|
||||
virtual bool tryToCloseDocument (Component* component) = 0;
|
||||
|
||||
/** Creates a new window to be used for a document.
|
||||
|
||||
The default implementation of this just returns a basic MultiDocumentPanelWindow object,
|
||||
but you might want to override it to return a custom component.
|
||||
*/
|
||||
virtual MultiDocumentPanelWindow* createNewDocumentWindow();
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
/** @internal */
|
||||
void componentNameChanged (Component&) override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
LayoutMode mode = MaximisedWindowsWithTabs;
|
||||
Array<Component*> components;
|
||||
std::unique_ptr<TabbedComponent> tabComponent;
|
||||
Colour backgroundColour { Colours::lightblue };
|
||||
int maximumNumDocuments = 0, numDocsBeforeTabsUsed = 0;
|
||||
|
||||
struct TabbedComponentInternal;
|
||||
friend class MultiDocumentPanelWindow;
|
||||
|
||||
Component* getContainerComp (Component*) const;
|
||||
void updateOrder();
|
||||
void addWindow (Component*);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiDocumentPanel)
|
||||
};
|
||||
|
||||
} // namespace juce
|
202
modules/juce_gui_basics/layout/juce_ResizableBorderComponent.cpp
Normal file
202
modules/juce_gui_basics/layout/juce_ResizableBorderComponent.cpp
Normal file
@ -0,0 +1,202 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
ResizableBorderComponent::Zone::Zone() noexcept : zone (0) {}
|
||||
|
||||
ResizableBorderComponent::Zone::Zone (const int zoneFlags) noexcept : zone (zoneFlags) {}
|
||||
|
||||
ResizableBorderComponent::Zone::Zone (const ResizableBorderComponent::Zone& other) noexcept : zone (other.zone) {}
|
||||
|
||||
ResizableBorderComponent::Zone& ResizableBorderComponent::Zone::operator= (const ResizableBorderComponent::Zone& other) noexcept
|
||||
{
|
||||
zone = other.zone;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool ResizableBorderComponent::Zone::operator== (const ResizableBorderComponent::Zone& other) const noexcept { return zone == other.zone; }
|
||||
bool ResizableBorderComponent::Zone::operator!= (const ResizableBorderComponent::Zone& other) const noexcept { return zone != other.zone; }
|
||||
|
||||
ResizableBorderComponent::Zone ResizableBorderComponent::Zone::fromPositionOnBorder (const Rectangle<int>& totalSize,
|
||||
const BorderSize<int>& border,
|
||||
Point<int> position)
|
||||
{
|
||||
int z = 0;
|
||||
|
||||
if (totalSize.contains (position)
|
||||
&& ! border.subtractedFrom (totalSize).contains (position))
|
||||
{
|
||||
const int minW = jmax (totalSize.getWidth() / 10, jmin (10, totalSize.getWidth() / 3));
|
||||
if (position.x < jmax (border.getLeft(), minW) && border.getLeft() > 0)
|
||||
z |= left;
|
||||
else if (position.x >= totalSize.getWidth() - jmax (border.getRight(), minW) && border.getRight() > 0)
|
||||
z |= right;
|
||||
|
||||
const int minH = jmax (totalSize.getHeight() / 10, jmin (10, totalSize.getHeight() / 3));
|
||||
if (position.y < jmax (border.getTop(), minH) && border.getTop() > 0)
|
||||
z |= top;
|
||||
else if (position.y >= totalSize.getHeight() - jmax (border.getBottom(), minH) && border.getBottom() > 0)
|
||||
z |= bottom;
|
||||
}
|
||||
|
||||
return Zone (z);
|
||||
}
|
||||
|
||||
MouseCursor ResizableBorderComponent::Zone::getMouseCursor() const noexcept
|
||||
{
|
||||
MouseCursor::StandardCursorType mc = MouseCursor::NormalCursor;
|
||||
|
||||
switch (zone)
|
||||
{
|
||||
case (left | top): mc = MouseCursor::TopLeftCornerResizeCursor; break;
|
||||
case top: mc = MouseCursor::TopEdgeResizeCursor; break;
|
||||
case (right | top): mc = MouseCursor::TopRightCornerResizeCursor; break;
|
||||
case left: mc = MouseCursor::LeftEdgeResizeCursor; break;
|
||||
case right: mc = MouseCursor::RightEdgeResizeCursor; break;
|
||||
case (left | bottom): mc = MouseCursor::BottomLeftCornerResizeCursor; break;
|
||||
case bottom: mc = MouseCursor::BottomEdgeResizeCursor; break;
|
||||
case (right | bottom): mc = MouseCursor::BottomRightCornerResizeCursor; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return mc;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
ResizableBorderComponent::ResizableBorderComponent (Component* const componentToResize,
|
||||
ComponentBoundsConstrainer* const constrainer_)
|
||||
: component (componentToResize),
|
||||
constrainer (constrainer_),
|
||||
borderSize (5),
|
||||
mouseZone (0)
|
||||
{
|
||||
}
|
||||
|
||||
ResizableBorderComponent::~ResizableBorderComponent()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ResizableBorderComponent::paint (Graphics& g)
|
||||
{
|
||||
getLookAndFeel().drawResizableFrame (g, getWidth(), getHeight(), borderSize);
|
||||
}
|
||||
|
||||
void ResizableBorderComponent::mouseEnter (const MouseEvent& e)
|
||||
{
|
||||
updateMouseZone (e);
|
||||
}
|
||||
|
||||
void ResizableBorderComponent::mouseMove (const MouseEvent& e)
|
||||
{
|
||||
updateMouseZone (e);
|
||||
}
|
||||
|
||||
void ResizableBorderComponent::mouseDown (const MouseEvent& e)
|
||||
{
|
||||
if (component == nullptr)
|
||||
{
|
||||
jassertfalse; // You've deleted the component that this resizer was supposed to be using!
|
||||
return;
|
||||
}
|
||||
|
||||
updateMouseZone (e);
|
||||
|
||||
originalBounds = component->getBounds();
|
||||
|
||||
if (constrainer != nullptr)
|
||||
constrainer->resizeStart();
|
||||
}
|
||||
|
||||
void ResizableBorderComponent::mouseDrag (const MouseEvent& e)
|
||||
{
|
||||
if (component == nullptr)
|
||||
{
|
||||
jassertfalse; // You've deleted the component that this resizer was supposed to be using!
|
||||
return;
|
||||
}
|
||||
|
||||
const Rectangle<int> newBounds (mouseZone.resizeRectangleBy (originalBounds, e.getOffsetFromDragStart()));
|
||||
|
||||
if (constrainer != nullptr)
|
||||
{
|
||||
constrainer->setBoundsForComponent (component, newBounds,
|
||||
mouseZone.isDraggingTopEdge(),
|
||||
mouseZone.isDraggingLeftEdge(),
|
||||
mouseZone.isDraggingBottomEdge(),
|
||||
mouseZone.isDraggingRightEdge());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Component::Positioner* const pos = component->getPositioner())
|
||||
pos->applyNewBounds (newBounds);
|
||||
else
|
||||
component->setBounds (newBounds);
|
||||
}
|
||||
}
|
||||
|
||||
void ResizableBorderComponent::mouseUp (const MouseEvent&)
|
||||
{
|
||||
if (constrainer != nullptr)
|
||||
constrainer->resizeEnd();
|
||||
}
|
||||
|
||||
bool ResizableBorderComponent::hitTest (int x, int y)
|
||||
{
|
||||
return x < borderSize.getLeft()
|
||||
|| x >= getWidth() - borderSize.getRight()
|
||||
|| y < borderSize.getTop()
|
||||
|| y >= getHeight() - borderSize.getBottom();
|
||||
}
|
||||
|
||||
void ResizableBorderComponent::setBorderThickness (const BorderSize<int>& newBorderSize)
|
||||
{
|
||||
if (borderSize != newBorderSize)
|
||||
{
|
||||
borderSize = newBorderSize;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
BorderSize<int> ResizableBorderComponent::getBorderThickness() const
|
||||
{
|
||||
return borderSize;
|
||||
}
|
||||
|
||||
void ResizableBorderComponent::updateMouseZone (const MouseEvent& e)
|
||||
{
|
||||
Zone newZone (Zone::fromPositionOnBorder (getLocalBounds(), borderSize, e.getPosition()));
|
||||
|
||||
if (mouseZone != newZone)
|
||||
{
|
||||
mouseZone = newZone;
|
||||
setMouseCursor (newZone.getMouseCursor());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace juce
|
197
modules/juce_gui_basics/layout/juce_ResizableBorderComponent.h
Normal file
197
modules/juce_gui_basics/layout/juce_ResizableBorderComponent.h
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component that resizes its parent component when dragged.
|
||||
|
||||
This component forms a frame around the edge of a component, allowing it to
|
||||
be dragged by the edges or corners to resize it - like the way windows are
|
||||
resized in MSWindows or Linux.
|
||||
|
||||
To use it, just add it to your component, making it fill the entire parent component
|
||||
(there's a mouse hit-test that only traps mouse-events which land around the
|
||||
edge of the component, so it's even ok to put it on top of any other components
|
||||
you're using). Make sure you rescale the resizer component to fill the parent
|
||||
each time the parent's size changes.
|
||||
|
||||
@see ResizableCornerComponent
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ResizableBorderComponent : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a resizer.
|
||||
|
||||
Pass in the target component which you want to be resized when this one is
|
||||
dragged.
|
||||
|
||||
The target component will usually be a parent of the resizer component, but this
|
||||
isn't mandatory.
|
||||
|
||||
Remember that when the target component is resized, it'll need to move and
|
||||
resize this component to keep it in place, as this won't happen automatically.
|
||||
|
||||
If the constrainer parameter is not a nullptr, then this object will be used to
|
||||
enforce limits on the size and position that the component can be stretched to.
|
||||
Make sure that the constrainer isn't deleted while still in use by this object.
|
||||
|
||||
@see ComponentBoundsConstrainer
|
||||
*/
|
||||
ResizableBorderComponent (Component* componentToResize,
|
||||
ComponentBoundsConstrainer* constrainer);
|
||||
|
||||
/** Destructor. */
|
||||
~ResizableBorderComponent();
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** Specifies how many pixels wide the draggable edges of this component are.
|
||||
|
||||
@see getBorderThickness
|
||||
*/
|
||||
void setBorderThickness (const BorderSize<int>& newBorderSize);
|
||||
|
||||
/** Returns the number of pixels wide that the draggable edges of this component are.
|
||||
|
||||
@see setBorderThickness
|
||||
*/
|
||||
BorderSize<int> getBorderThickness() const;
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** Represents the different sections of a resizable border, which allow it to
|
||||
resized in different ways.
|
||||
*/
|
||||
class Zone
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
enum Zones
|
||||
{
|
||||
centre = 0,
|
||||
left = 1,
|
||||
top = 2,
|
||||
right = 4,
|
||||
bottom = 8
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Creates a Zone from a combination of the flags in \enum Zones. */
|
||||
explicit Zone (int zoneFlags) noexcept;
|
||||
|
||||
Zone() noexcept;
|
||||
Zone (const Zone&) noexcept;
|
||||
Zone& operator= (const Zone&) noexcept;
|
||||
|
||||
bool operator== (const Zone&) const noexcept;
|
||||
bool operator!= (const Zone&) const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Given a point within a rectangle with a resizable border, this returns the
|
||||
zone that the point lies within.
|
||||
*/
|
||||
static Zone fromPositionOnBorder (const Rectangle<int>& totalSize,
|
||||
const BorderSize<int>& border,
|
||||
Point<int> position);
|
||||
|
||||
/** Returns an appropriate mouse-cursor for this resize zone. */
|
||||
MouseCursor getMouseCursor() const noexcept;
|
||||
|
||||
/** Returns true if dragging this zone will move the enire object without resizing it. */
|
||||
bool isDraggingWholeObject() const noexcept { return zone == centre; }
|
||||
/** Returns true if dragging this zone will move the object's left edge. */
|
||||
bool isDraggingLeftEdge() const noexcept { return (zone & left) != 0; }
|
||||
/** Returns true if dragging this zone will move the object's right edge. */
|
||||
bool isDraggingRightEdge() const noexcept { return (zone & right) != 0; }
|
||||
/** Returns true if dragging this zone will move the object's top edge. */
|
||||
bool isDraggingTopEdge() const noexcept { return (zone & top) != 0; }
|
||||
/** Returns true if dragging this zone will move the object's bottom edge. */
|
||||
bool isDraggingBottomEdge() const noexcept { return (zone & bottom) != 0; }
|
||||
|
||||
/** Resizes this rectangle by the given amount, moving just the edges that this zone
|
||||
applies to.
|
||||
*/
|
||||
template <typename ValueType>
|
||||
Rectangle<ValueType> resizeRectangleBy (Rectangle<ValueType> original,
|
||||
const Point<ValueType>& distance) const noexcept
|
||||
{
|
||||
if (isDraggingWholeObject())
|
||||
return original + distance;
|
||||
|
||||
if (isDraggingLeftEdge()) original.setLeft (jmin (original.getRight(), original.getX() + distance.x));
|
||||
if (isDraggingRightEdge()) original.setWidth (jmax (ValueType(), original.getWidth() + distance.x));
|
||||
if (isDraggingTopEdge()) original.setTop (jmin (original.getBottom(), original.getY() + distance.y));
|
||||
if (isDraggingBottomEdge()) original.setHeight (jmax (ValueType(), original.getHeight() + distance.y));
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/** Returns the raw flags for this zone. */
|
||||
int getZoneFlags() const noexcept { return zone; }
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
int zone;
|
||||
};
|
||||
|
||||
/** Returns the zone in which the mouse was last seen. */
|
||||
Zone getCurrentZone() const noexcept { return mouseZone; }
|
||||
|
||||
protected:
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void mouseEnter (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseMove (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseDown (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseDrag (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseUp (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
bool hitTest (int x, int y) override;
|
||||
|
||||
private:
|
||||
WeakReference<Component> component;
|
||||
ComponentBoundsConstrainer* constrainer;
|
||||
BorderSize<int> borderSize;
|
||||
Rectangle<int> originalBounds;
|
||||
Zone mouseZone;
|
||||
|
||||
void updateMouseZone (const MouseEvent&);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ResizableBorderComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
106
modules/juce_gui_basics/layout/juce_ResizableCornerComponent.cpp
Normal file
106
modules/juce_gui_basics/layout/juce_ResizableCornerComponent.cpp
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
ResizableCornerComponent::ResizableCornerComponent (Component* const componentToResize,
|
||||
ComponentBoundsConstrainer* const constrainer_)
|
||||
: component (componentToResize),
|
||||
constrainer (constrainer_)
|
||||
{
|
||||
setRepaintsOnMouseActivity (true);
|
||||
setMouseCursor (MouseCursor::BottomRightCornerResizeCursor);
|
||||
}
|
||||
|
||||
ResizableCornerComponent::~ResizableCornerComponent()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ResizableCornerComponent::paint (Graphics& g)
|
||||
{
|
||||
getLookAndFeel()
|
||||
.drawCornerResizer (g, getWidth(), getHeight(),
|
||||
isMouseOverOrDragging(),
|
||||
isMouseButtonDown());
|
||||
}
|
||||
|
||||
void ResizableCornerComponent::mouseDown (const MouseEvent&)
|
||||
{
|
||||
if (component == nullptr)
|
||||
{
|
||||
jassertfalse; // You've deleted the component that this resizer is supposed to be controlling!
|
||||
return;
|
||||
}
|
||||
|
||||
originalBounds = component->getBounds();
|
||||
|
||||
if (constrainer != nullptr)
|
||||
constrainer->resizeStart();
|
||||
}
|
||||
|
||||
void ResizableCornerComponent::mouseDrag (const MouseEvent& e)
|
||||
{
|
||||
if (component == nullptr)
|
||||
{
|
||||
jassertfalse; // You've deleted the component that this resizer is supposed to be controlling!
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle<int> r (originalBounds.withSize (originalBounds.getWidth() + e.getDistanceFromDragStartX(),
|
||||
originalBounds.getHeight() + e.getDistanceFromDragStartY()));
|
||||
|
||||
if (constrainer != nullptr)
|
||||
{
|
||||
constrainer->setBoundsForComponent (component, r, false, false, true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Component::Positioner* const pos = component->getPositioner())
|
||||
pos->applyNewBounds (r);
|
||||
else
|
||||
component->setBounds (r);
|
||||
}
|
||||
}
|
||||
|
||||
void ResizableCornerComponent::mouseUp (const MouseEvent&)
|
||||
{
|
||||
if (constrainer != nullptr)
|
||||
constrainer->resizeEnd();
|
||||
}
|
||||
|
||||
bool ResizableCornerComponent::hitTest (int x, int y)
|
||||
{
|
||||
if (getWidth() <= 0)
|
||||
return false;
|
||||
|
||||
const int yAtX = getHeight() - (getHeight() * x / getWidth());
|
||||
|
||||
return y >= yAtX - getHeight() / 4;
|
||||
}
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/** A component that resizes a parent component when dragged.
|
||||
|
||||
This is the small triangular stripey resizer component you get in the bottom-right
|
||||
of windows (more commonly on the Mac than Windows). Put one in the corner of
|
||||
a larger component and it will automatically resize its parent when it gets dragged
|
||||
around.
|
||||
|
||||
@see ResizableBorderComponent
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ResizableCornerComponent : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a resizer.
|
||||
|
||||
Pass in the target component which you want to be resized when this one is
|
||||
dragged.
|
||||
|
||||
The target component will usually be a parent of the resizer component, but this
|
||||
isn't mandatory.
|
||||
|
||||
Remember that when the target component is resized, it'll need to move and
|
||||
resize this component to keep it in place, as this won't happen automatically.
|
||||
|
||||
If a constrainer object is provided, then this object will be used to enforce
|
||||
limits on the size and position that the component can be stretched to. Make sure
|
||||
that the constrainer isn't deleted while still in use by this object. If you
|
||||
pass a nullptr in here, no limits will be put on the sizes it can be stretched to.
|
||||
|
||||
@see ComponentBoundsConstrainer
|
||||
*/
|
||||
ResizableCornerComponent (Component* componentToResize,
|
||||
ComponentBoundsConstrainer* constrainer);
|
||||
|
||||
/** Destructor. */
|
||||
~ResizableCornerComponent();
|
||||
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void mouseDown (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseDrag (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseUp (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
bool hitTest (int x, int y) override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
WeakReference<Component> component;
|
||||
ComponentBoundsConstrainer* constrainer;
|
||||
Rectangle<int> originalBounds;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ResizableCornerComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
114
modules/juce_gui_basics/layout/juce_ResizableEdgeComponent.cpp
Normal file
114
modules/juce_gui_basics/layout/juce_ResizableEdgeComponent.cpp
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
ResizableEdgeComponent::ResizableEdgeComponent (Component* const componentToResize,
|
||||
ComponentBoundsConstrainer* const constrainer_,
|
||||
Edge edge_)
|
||||
: component (componentToResize),
|
||||
constrainer (constrainer_),
|
||||
edge (edge_)
|
||||
{
|
||||
setRepaintsOnMouseActivity (true);
|
||||
setMouseCursor (isVertical() ? MouseCursor::LeftRightResizeCursor
|
||||
: MouseCursor::UpDownResizeCursor);
|
||||
}
|
||||
|
||||
ResizableEdgeComponent::~ResizableEdgeComponent()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool ResizableEdgeComponent::isVertical() const noexcept
|
||||
{
|
||||
return edge == leftEdge || edge == rightEdge;
|
||||
}
|
||||
|
||||
void ResizableEdgeComponent::paint (Graphics& g)
|
||||
{
|
||||
getLookAndFeel().drawStretchableLayoutResizerBar (g, getWidth(), getHeight(), isVertical(),
|
||||
isMouseOver(), isMouseButtonDown());
|
||||
}
|
||||
|
||||
void ResizableEdgeComponent::mouseDown (const MouseEvent&)
|
||||
{
|
||||
if (component == nullptr)
|
||||
{
|
||||
jassertfalse; // You've deleted the component that this resizer was supposed to be using!
|
||||
return;
|
||||
}
|
||||
|
||||
originalBounds = component->getBounds();
|
||||
|
||||
if (constrainer != nullptr)
|
||||
constrainer->resizeStart();
|
||||
}
|
||||
|
||||
void ResizableEdgeComponent::mouseDrag (const MouseEvent& e)
|
||||
{
|
||||
if (component == nullptr)
|
||||
{
|
||||
jassertfalse; // You've deleted the component that this resizer was supposed to be using!
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle<int> newBounds (originalBounds);
|
||||
|
||||
switch (edge)
|
||||
{
|
||||
case leftEdge: newBounds.setLeft (jmin (newBounds.getRight(), newBounds.getX() + e.getDistanceFromDragStartX())); break;
|
||||
case rightEdge: newBounds.setWidth (jmax (0, newBounds.getWidth() + e.getDistanceFromDragStartX())); break;
|
||||
case topEdge: newBounds.setTop (jmin (newBounds.getBottom(), newBounds.getY() + e.getDistanceFromDragStartY())); break;
|
||||
case bottomEdge: newBounds.setHeight (jmax (0, newBounds.getHeight() + e.getDistanceFromDragStartY())); break;
|
||||
default: jassertfalse; break;
|
||||
}
|
||||
|
||||
if (constrainer != nullptr)
|
||||
{
|
||||
constrainer->setBoundsForComponent (component, newBounds,
|
||||
edge == topEdge,
|
||||
edge == leftEdge,
|
||||
edge == bottomEdge,
|
||||
edge == rightEdge);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Component::Positioner* const pos = component->getPositioner())
|
||||
pos->applyNewBounds (newBounds);
|
||||
else
|
||||
component->setBounds (newBounds);
|
||||
}
|
||||
}
|
||||
|
||||
void ResizableEdgeComponent::mouseUp (const MouseEvent&)
|
||||
{
|
||||
if (constrainer != nullptr)
|
||||
constrainer->resizeEnd();
|
||||
}
|
||||
|
||||
} // namespace juce
|
101
modules/juce_gui_basics/layout/juce_ResizableEdgeComponent.h
Normal file
101
modules/juce_gui_basics/layout/juce_ResizableEdgeComponent.h
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component that resizes its parent component when dragged.
|
||||
|
||||
This component forms a bar along one edge of a component, allowing it to
|
||||
be dragged by that edges to resize it.
|
||||
|
||||
To use it, just add it to your component, positioning it along the appropriate
|
||||
edge. Make sure you reposition the resizer component each time the parent's size
|
||||
changes, to keep it in the correct position.
|
||||
|
||||
@see ResizbleBorderComponent, ResizableCornerComponent
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ResizableEdgeComponent : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
enum Edge
|
||||
{
|
||||
leftEdge, /**< Indicates a vertical bar that can be dragged left/right to move the component's left-hand edge. */
|
||||
rightEdge, /**< Indicates a vertical bar that can be dragged left/right to move the component's right-hand edge. */
|
||||
topEdge, /**< Indicates a horizontal bar that can be dragged up/down to move the top of the component. */
|
||||
bottomEdge /**< Indicates a horizontal bar that can be dragged up/down to move the bottom of the component. */
|
||||
};
|
||||
|
||||
/** Creates a resizer bar.
|
||||
|
||||
Pass in the target component which you want to be resized when this one is
|
||||
dragged. The target component will usually be this component's parent, but this
|
||||
isn't mandatory.
|
||||
|
||||
Remember that when the target component is resized, it'll need to move and
|
||||
resize this component to keep it in place, as this won't happen automatically.
|
||||
|
||||
If the constrainer parameter is not a nullptr, then this object will be used to
|
||||
enforce limits on the size and position that the component can be stretched to.
|
||||
Make sure that the constrainer isn't deleted while still in use by this object.
|
||||
|
||||
@see ComponentBoundsConstrainer
|
||||
*/
|
||||
ResizableEdgeComponent (Component* componentToResize,
|
||||
ComponentBoundsConstrainer* constrainer,
|
||||
Edge edgeToResize);
|
||||
|
||||
/** Destructor. */
|
||||
~ResizableEdgeComponent();
|
||||
|
||||
bool isVertical() const noexcept;
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void mouseDown (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseDrag (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseUp (const MouseEvent&) override;
|
||||
|
||||
private:
|
||||
WeakReference<Component> component;
|
||||
ComponentBoundsConstrainer* constrainer;
|
||||
Rectangle<int> originalBounds;
|
||||
const Edge edge;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ResizableEdgeComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
442
modules/juce_gui_basics/layout/juce_ScrollBar.cpp
Normal file
442
modules/juce_gui_basics/layout/juce_ScrollBar.cpp
Normal file
@ -0,0 +1,442 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class ScrollBar::ScrollbarButton : public Button
|
||||
{
|
||||
public:
|
||||
ScrollbarButton (int direc, ScrollBar& s)
|
||||
: Button (String()), direction (direc), owner (s)
|
||||
{
|
||||
setWantsKeyboardFocus (false);
|
||||
}
|
||||
|
||||
void paintButton (Graphics& g, bool over, bool down) override
|
||||
{
|
||||
getLookAndFeel().drawScrollbarButton (g, owner, getWidth(), getHeight(),
|
||||
direction, owner.isVertical(), over, down);
|
||||
}
|
||||
|
||||
void clicked() override
|
||||
{
|
||||
owner.moveScrollbarInSteps ((direction == 1 || direction == 2) ? 1 : -1);
|
||||
}
|
||||
|
||||
int direction;
|
||||
|
||||
private:
|
||||
ScrollBar& owner;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScrollbarButton)
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
ScrollBar::ScrollBar (bool shouldBeVertical) : vertical (shouldBeVertical)
|
||||
{
|
||||
setRepaintsOnMouseActivity (true);
|
||||
setFocusContainer (true);
|
||||
}
|
||||
|
||||
ScrollBar::~ScrollBar()
|
||||
{
|
||||
upButton.reset();
|
||||
downButton.reset();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ScrollBar::setRangeLimits (Range<double> newRangeLimit, NotificationType notification)
|
||||
{
|
||||
if (totalRange != newRangeLimit)
|
||||
{
|
||||
totalRange = newRangeLimit;
|
||||
setCurrentRange (visibleRange, notification);
|
||||
updateThumbPosition();
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::setRangeLimits (const double newMinimum, const double newMaximum, NotificationType notification)
|
||||
{
|
||||
jassert (newMaximum >= newMinimum); // these can't be the wrong way round!
|
||||
setRangeLimits (Range<double> (newMinimum, newMaximum), notification);
|
||||
}
|
||||
|
||||
bool ScrollBar::setCurrentRange (Range<double> newRange, const NotificationType notification)
|
||||
{
|
||||
const Range<double> constrainedRange (totalRange.constrainRange (newRange));
|
||||
|
||||
if (visibleRange != constrainedRange)
|
||||
{
|
||||
visibleRange = constrainedRange;
|
||||
|
||||
updateThumbPosition();
|
||||
|
||||
if (notification != dontSendNotification)
|
||||
triggerAsyncUpdate();
|
||||
|
||||
if (notification == sendNotificationSync)
|
||||
handleUpdateNowIfNeeded();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScrollBar::setCurrentRange (const double newStart, const double newSize, NotificationType notification)
|
||||
{
|
||||
setCurrentRange (Range<double> (newStart, newStart + newSize), notification);
|
||||
}
|
||||
|
||||
void ScrollBar::setCurrentRangeStart (const double newStart, NotificationType notification)
|
||||
{
|
||||
setCurrentRange (visibleRange.movedToStartAt (newStart), notification);
|
||||
}
|
||||
|
||||
void ScrollBar::setSingleStepSize (const double newSingleStepSize) noexcept
|
||||
{
|
||||
singleStepSize = newSingleStepSize;
|
||||
}
|
||||
|
||||
bool ScrollBar::moveScrollbarInSteps (const int howManySteps, NotificationType notification)
|
||||
{
|
||||
return setCurrentRange (visibleRange + howManySteps * singleStepSize, notification);
|
||||
}
|
||||
|
||||
bool ScrollBar::moveScrollbarInPages (const int howManyPages, NotificationType notification)
|
||||
{
|
||||
return setCurrentRange (visibleRange + howManyPages * visibleRange.getLength(), notification);
|
||||
}
|
||||
|
||||
bool ScrollBar::scrollToTop (NotificationType notification)
|
||||
{
|
||||
return setCurrentRange (visibleRange.movedToStartAt (getMinimumRangeLimit()), notification);
|
||||
}
|
||||
|
||||
bool ScrollBar::scrollToBottom (NotificationType notification)
|
||||
{
|
||||
return setCurrentRange (visibleRange.movedToEndAt (getMaximumRangeLimit()), notification);
|
||||
}
|
||||
|
||||
void ScrollBar::setButtonRepeatSpeed (const int newInitialDelay,
|
||||
const int newRepeatDelay,
|
||||
const int newMinimumDelay)
|
||||
{
|
||||
initialDelayInMillisecs = newInitialDelay;
|
||||
repeatDelayInMillisecs = newRepeatDelay;
|
||||
minimumDelayInMillisecs = newMinimumDelay;
|
||||
|
||||
if (upButton != nullptr)
|
||||
{
|
||||
upButton ->setRepeatSpeed (newInitialDelay, newRepeatDelay, newMinimumDelay);
|
||||
downButton->setRepeatSpeed (newInitialDelay, newRepeatDelay, newMinimumDelay);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ScrollBar::addListener (Listener* const listener)
|
||||
{
|
||||
listeners.add (listener);
|
||||
}
|
||||
|
||||
void ScrollBar::removeListener (Listener* const listener)
|
||||
{
|
||||
listeners.remove (listener);
|
||||
}
|
||||
|
||||
void ScrollBar::handleAsyncUpdate()
|
||||
{
|
||||
auto start = visibleRange.getStart(); // (need to use a temp variable for VC7 compatibility)
|
||||
listeners.call ([=] (Listener& l) { l.scrollBarMoved (this, start); });
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ScrollBar::updateThumbPosition()
|
||||
{
|
||||
auto minimumScrollBarThumbSize = getLookAndFeel().getMinimumScrollbarThumbSize (*this);
|
||||
|
||||
int newThumbSize = roundToInt (totalRange.getLength() > 0 ? (visibleRange.getLength() * thumbAreaSize) / totalRange.getLength()
|
||||
: thumbAreaSize);
|
||||
|
||||
if (newThumbSize < minimumScrollBarThumbSize)
|
||||
newThumbSize = jmin (minimumScrollBarThumbSize, thumbAreaSize - 1);
|
||||
|
||||
if (newThumbSize > thumbAreaSize)
|
||||
newThumbSize = thumbAreaSize;
|
||||
|
||||
int newThumbStart = thumbAreaStart;
|
||||
|
||||
if (totalRange.getLength() > visibleRange.getLength())
|
||||
newThumbStart += roundToInt (((visibleRange.getStart() - totalRange.getStart()) * (thumbAreaSize - newThumbSize))
|
||||
/ (totalRange.getLength() - visibleRange.getLength()));
|
||||
|
||||
Component::setVisible (getVisibility());
|
||||
|
||||
if (thumbStart != newThumbStart || thumbSize != newThumbSize)
|
||||
{
|
||||
const int repaintStart = jmin (thumbStart, newThumbStart) - 4;
|
||||
const int repaintSize = jmax (thumbStart + thumbSize, newThumbStart + newThumbSize) + 8 - repaintStart;
|
||||
|
||||
if (vertical)
|
||||
repaint (0, repaintStart, getWidth(), repaintSize);
|
||||
else
|
||||
repaint (repaintStart, 0, repaintSize, getHeight());
|
||||
|
||||
thumbStart = newThumbStart;
|
||||
thumbSize = newThumbSize;
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::setOrientation (const bool shouldBeVertical)
|
||||
{
|
||||
if (vertical != shouldBeVertical)
|
||||
{
|
||||
vertical = shouldBeVertical;
|
||||
|
||||
if (upButton != nullptr)
|
||||
{
|
||||
upButton->direction = vertical ? 0 : 3;
|
||||
downButton->direction = vertical ? 2 : 1;
|
||||
}
|
||||
|
||||
updateThumbPosition();
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::setAutoHide (const bool shouldHideWhenFullRange)
|
||||
{
|
||||
autohides = shouldHideWhenFullRange;
|
||||
updateThumbPosition();
|
||||
}
|
||||
|
||||
bool ScrollBar::autoHides() const noexcept
|
||||
{
|
||||
return autohides;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void ScrollBar::paint (Graphics& g)
|
||||
{
|
||||
if (thumbAreaSize > 0)
|
||||
{
|
||||
auto& lf = getLookAndFeel();
|
||||
|
||||
const int thumb = (thumbAreaSize > lf.getMinimumScrollbarThumbSize (*this))
|
||||
? thumbSize : 0;
|
||||
|
||||
if (vertical)
|
||||
lf.drawScrollbar (g, *this, 0, thumbAreaStart, getWidth(), thumbAreaSize,
|
||||
vertical, thumbStart, thumb, isMouseOver(), isMouseButtonDown());
|
||||
else
|
||||
lf.drawScrollbar (g, *this, thumbAreaStart, 0, thumbAreaSize, getHeight(),
|
||||
vertical, thumbStart, thumb, isMouseOver(), isMouseButtonDown());
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::lookAndFeelChanged()
|
||||
{
|
||||
setComponentEffect (getLookAndFeel().getScrollbarEffect());
|
||||
|
||||
if (isVisible())
|
||||
resized();
|
||||
}
|
||||
|
||||
void ScrollBar::resized()
|
||||
{
|
||||
auto length = vertical ? getHeight() : getWidth();
|
||||
|
||||
auto& lf = getLookAndFeel();
|
||||
const bool buttonsVisible = lf.areScrollbarButtonsVisible();
|
||||
int buttonSize = 0;
|
||||
|
||||
if (buttonsVisible)
|
||||
{
|
||||
if (upButton == nullptr)
|
||||
{
|
||||
upButton .reset (new ScrollbarButton (vertical ? 0 : 3, *this));
|
||||
downButton.reset (new ScrollbarButton (vertical ? 2 : 1, *this));
|
||||
|
||||
addAndMakeVisible (upButton.get());
|
||||
addAndMakeVisible (downButton.get());
|
||||
|
||||
setButtonRepeatSpeed (initialDelayInMillisecs, repeatDelayInMillisecs, minimumDelayInMillisecs);
|
||||
}
|
||||
|
||||
buttonSize = jmin (lf.getScrollbarButtonSize (*this), length / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
upButton.reset();
|
||||
downButton.reset();
|
||||
}
|
||||
|
||||
if (length < 32 + lf.getMinimumScrollbarThumbSize (*this))
|
||||
{
|
||||
thumbAreaStart = length / 2;
|
||||
thumbAreaSize = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
thumbAreaStart = buttonSize;
|
||||
thumbAreaSize = length - 2 * buttonSize;
|
||||
}
|
||||
|
||||
if (upButton != nullptr)
|
||||
{
|
||||
Rectangle<int> r (getLocalBounds());
|
||||
|
||||
if (vertical)
|
||||
{
|
||||
upButton->setBounds (r.removeFromTop (buttonSize));
|
||||
downButton->setBounds (r.removeFromBottom (buttonSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
upButton->setBounds (r.removeFromLeft (buttonSize));
|
||||
downButton->setBounds (r.removeFromRight (buttonSize));
|
||||
}
|
||||
}
|
||||
|
||||
updateThumbPosition();
|
||||
}
|
||||
|
||||
void ScrollBar::parentHierarchyChanged()
|
||||
{
|
||||
lookAndFeelChanged();
|
||||
}
|
||||
|
||||
void ScrollBar::mouseDown (const MouseEvent& e)
|
||||
{
|
||||
isDraggingThumb = false;
|
||||
lastMousePos = vertical ? e.y : e.x;
|
||||
dragStartMousePos = lastMousePos;
|
||||
dragStartRange = visibleRange.getStart();
|
||||
|
||||
if (dragStartMousePos < thumbStart)
|
||||
{
|
||||
moveScrollbarInPages (-1);
|
||||
startTimer (400);
|
||||
}
|
||||
else if (dragStartMousePos >= thumbStart + thumbSize)
|
||||
{
|
||||
moveScrollbarInPages (1);
|
||||
startTimer (400);
|
||||
}
|
||||
else
|
||||
{
|
||||
isDraggingThumb = (thumbAreaSize > getLookAndFeel().getMinimumScrollbarThumbSize (*this))
|
||||
&& (thumbAreaSize > thumbSize);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::mouseDrag (const MouseEvent& e)
|
||||
{
|
||||
const int mousePos = vertical ? e.y : e.x;
|
||||
|
||||
if (isDraggingThumb && lastMousePos != mousePos && thumbAreaSize > thumbSize)
|
||||
{
|
||||
const int deltaPixels = mousePos - dragStartMousePos;
|
||||
|
||||
setCurrentRangeStart (dragStartRange
|
||||
+ deltaPixels * (totalRange.getLength() - visibleRange.getLength())
|
||||
/ (thumbAreaSize - thumbSize));
|
||||
}
|
||||
|
||||
lastMousePos = mousePos;
|
||||
}
|
||||
|
||||
void ScrollBar::mouseUp (const MouseEvent&)
|
||||
{
|
||||
isDraggingThumb = false;
|
||||
stopTimer();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void ScrollBar::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
|
||||
{
|
||||
float increment = 10.0f * (vertical ? wheel.deltaY : wheel.deltaX);
|
||||
|
||||
if (increment < 0)
|
||||
increment = jmin (increment, -1.0f);
|
||||
else if (increment > 0)
|
||||
increment = jmax (increment, 1.0f);
|
||||
|
||||
setCurrentRange (visibleRange - singleStepSize * increment);
|
||||
}
|
||||
|
||||
void ScrollBar::timerCallback()
|
||||
{
|
||||
if (isMouseButtonDown())
|
||||
{
|
||||
startTimer (40);
|
||||
|
||||
if (lastMousePos < thumbStart)
|
||||
setCurrentRange (visibleRange - visibleRange.getLength());
|
||||
else if (lastMousePos > thumbStart + thumbSize)
|
||||
setCurrentRangeStart (visibleRange.getEnd());
|
||||
}
|
||||
else
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
}
|
||||
|
||||
bool ScrollBar::keyPressed (const KeyPress& key)
|
||||
{
|
||||
if (isVisible())
|
||||
{
|
||||
if (key == KeyPress::upKey || key == KeyPress::leftKey) return moveScrollbarInSteps (-1);
|
||||
if (key == KeyPress::downKey || key == KeyPress::rightKey) return moveScrollbarInSteps (1);
|
||||
if (key == KeyPress::pageUpKey) return moveScrollbarInPages (-1);
|
||||
if (key == KeyPress::pageDownKey) return moveScrollbarInPages (1);
|
||||
if (key == KeyPress::homeKey) return scrollToTop();
|
||||
if (key == KeyPress::endKey) return scrollToBottom();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScrollBar::setVisible (bool shouldBeVisible)
|
||||
{
|
||||
if (userVisibilityFlag != shouldBeVisible)
|
||||
{
|
||||
userVisibilityFlag = shouldBeVisible;
|
||||
Component::setVisible (getVisibility());
|
||||
}
|
||||
}
|
||||
|
||||
bool ScrollBar::getVisibility() const noexcept
|
||||
{
|
||||
if (! userVisibilityFlag)
|
||||
return false;
|
||||
|
||||
return (! autohides) || (totalRange.getLength() > visibleRange.getLength()
|
||||
&& visibleRange.getLength() > 0.0);
|
||||
}
|
||||
|
||||
} // namespace juce
|
436
modules/juce_gui_basics/layout/juce_ScrollBar.h
Normal file
436
modules/juce_gui_basics/layout/juce_ScrollBar.h
Normal file
@ -0,0 +1,436 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A scrollbar component.
|
||||
|
||||
To use a scrollbar, set up its total range using the setRangeLimits() method - this
|
||||
sets the range of values it can represent. Then you can use setCurrentRange() to
|
||||
change the position and size of the scrollbar's 'thumb'.
|
||||
|
||||
Registering a ScrollBar::Listener with the scrollbar will allow you to find out when
|
||||
the user moves it, and you can use the getCurrentRangeStart() to find out where
|
||||
they moved it to.
|
||||
|
||||
The scrollbar will adjust its own visibility according to whether its thumb size
|
||||
allows it to actually be scrolled.
|
||||
|
||||
For most purposes, it's probably easier to use a Viewport or ListBox
|
||||
instead of handling a scrollbar directly.
|
||||
|
||||
@see ScrollBar::Listener
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ScrollBar : public Component,
|
||||
public AsyncUpdater,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a Scrollbar.
|
||||
@param isVertical specifies whether the bar should be a vertical or horizontal
|
||||
*/
|
||||
ScrollBar (bool isVertical);
|
||||
|
||||
/** Destructor. */
|
||||
~ScrollBar();
|
||||
|
||||
//==============================================================================
|
||||
/** Returns true if the scrollbar is vertical, false if it's horizontal. */
|
||||
bool isVertical() const noexcept { return vertical; }
|
||||
|
||||
/** Changes the scrollbar's direction.
|
||||
|
||||
You'll also need to resize the bar appropriately - this just changes its internal
|
||||
layout.
|
||||
|
||||
@param shouldBeVertical true makes it vertical; false makes it horizontal.
|
||||
*/
|
||||
void setOrientation (bool shouldBeVertical);
|
||||
|
||||
/** Tells the scrollbar whether to make itself invisible when not needed.
|
||||
|
||||
The default behaviour is for a scrollbar to become invisible when the thumb
|
||||
fills the whole of its range (i.e. when it can't be moved). Setting this
|
||||
value to false forces the bar to always be visible.
|
||||
@see autoHides()
|
||||
*/
|
||||
void setAutoHide (bool shouldHideWhenFullRange);
|
||||
|
||||
/** Returns true if this scrollbar is set to auto-hide when its thumb is as big
|
||||
as its maximum range.
|
||||
@see setAutoHide
|
||||
*/
|
||||
bool autoHides() const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Sets the minimum and maximum values that the bar will move between.
|
||||
|
||||
The bar's thumb will always be constrained so that the entire thumb lies
|
||||
within this range.
|
||||
|
||||
@param newRangeLimit the new range.
|
||||
@param notification whether to send a notification of the change to listeners.
|
||||
A notification will only be sent if the range has changed.
|
||||
|
||||
@see setCurrentRange
|
||||
*/
|
||||
void setRangeLimits (Range<double> newRangeLimit,
|
||||
NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Sets the minimum and maximum values that the bar will move between.
|
||||
|
||||
The bar's thumb will always be constrained so that the entire thumb lies
|
||||
within this range.
|
||||
|
||||
@param minimum the new range minimum.
|
||||
@param maximum the new range maximum.
|
||||
@param notification whether to send a notification of the change to listeners.
|
||||
A notification will only be sent if the range has changed.
|
||||
|
||||
@see setCurrentRange
|
||||
*/
|
||||
void setRangeLimits (double minimum, double maximum,
|
||||
NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Returns the current limits on the thumb position.
|
||||
@see setRangeLimits
|
||||
*/
|
||||
Range<double> getRangeLimit() const noexcept { return totalRange; }
|
||||
|
||||
/** Returns the lower value that the thumb can be set to.
|
||||
|
||||
This is the value set by setRangeLimits().
|
||||
*/
|
||||
double getMinimumRangeLimit() const noexcept { return totalRange.getStart(); }
|
||||
|
||||
/** Returns the upper value that the thumb can be set to.
|
||||
|
||||
This is the value set by setRangeLimits().
|
||||
*/
|
||||
double getMaximumRangeLimit() const noexcept { return totalRange.getEnd(); }
|
||||
|
||||
//==============================================================================
|
||||
/** Changes the position of the scrollbar's 'thumb'.
|
||||
|
||||
This sets both the position and size of the thumb - to just set the position without
|
||||
changing the size, you can use setCurrentRangeStart().
|
||||
|
||||
If this method call actually changes the scrollbar's position, it will trigger an
|
||||
asynchronous call to ScrollBar::Listener::scrollBarMoved() for all the listeners that
|
||||
are registered.
|
||||
|
||||
The notification parameter can be used to optionally send or inhibit a callback to
|
||||
any scrollbar listeners.
|
||||
|
||||
@returns true if the range was changed, or false if nothing was changed.
|
||||
@see getCurrentRange. setCurrentRangeStart
|
||||
*/
|
||||
bool setCurrentRange (Range<double> newRange,
|
||||
NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Changes the position of the scrollbar's 'thumb'.
|
||||
|
||||
This sets both the position and size of the thumb - to just set the position without
|
||||
changing the size, you can use setCurrentRangeStart().
|
||||
|
||||
@param newStart the top (or left) of the thumb, in the range
|
||||
getMinimumRangeLimit() <= newStart <= getMaximumRangeLimit(). If the
|
||||
value is beyond these limits, it will be clipped.
|
||||
@param newSize the size of the thumb, such that
|
||||
getMinimumRangeLimit() <= newStart + newSize <= getMaximumRangeLimit(). If the
|
||||
size is beyond these limits, it will be clipped.
|
||||
@param notification specifies if and how a callback should be made to any listeners
|
||||
if the range actually changes
|
||||
@see setCurrentRangeStart, getCurrentRangeStart, getCurrentRangeSize
|
||||
*/
|
||||
void setCurrentRange (double newStart, double newSize,
|
||||
NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Moves the bar's thumb position.
|
||||
|
||||
This will move the thumb position without changing the thumb size. Note
|
||||
that the maximum thumb start position is (getMaximumRangeLimit() - getCurrentRangeSize()).
|
||||
|
||||
If this method call actually changes the scrollbar's position, it will trigger an
|
||||
asynchronous call to ScrollBar::Listener::scrollBarMoved() for all the listeners that
|
||||
are registered.
|
||||
|
||||
@see setCurrentRange
|
||||
*/
|
||||
void setCurrentRangeStart (double newStart,
|
||||
NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Returns the current thumb range.
|
||||
@see getCurrentRange, setCurrentRange
|
||||
*/
|
||||
Range<double> getCurrentRange() const noexcept { return visibleRange; }
|
||||
|
||||
/** Returns the position of the top of the thumb.
|
||||
@see getCurrentRange, setCurrentRangeStart
|
||||
*/
|
||||
double getCurrentRangeStart() const noexcept { return visibleRange.getStart(); }
|
||||
|
||||
/** Returns the current size of the thumb.
|
||||
@see getCurrentRange, setCurrentRange
|
||||
*/
|
||||
double getCurrentRangeSize() const noexcept { return visibleRange.getLength(); }
|
||||
|
||||
//==============================================================================
|
||||
/** Sets the amount by which the up and down buttons will move the bar.
|
||||
|
||||
The value here is in terms of the total range, and is added or subtracted
|
||||
from the thumb position when the user clicks an up/down (or left/right) button.
|
||||
*/
|
||||
void setSingleStepSize (double newSingleStepSize) noexcept;
|
||||
|
||||
/** Moves the scrollbar by a number of single-steps.
|
||||
|
||||
This will move the bar by a multiple of its single-step interval (as
|
||||
specified using the setSingleStepSize() method).
|
||||
|
||||
A positive value here will move the bar down or to the right, a negative
|
||||
value moves it up or to the left.
|
||||
|
||||
@param howManySteps the number of steps to move the scrollbar
|
||||
@param notification whether to send a notification of the change to listeners.
|
||||
A notification will only be sent if the position has changed.
|
||||
|
||||
@returns true if the scrollbar's position actually changed.
|
||||
*/
|
||||
bool moveScrollbarInSteps (int howManySteps,
|
||||
NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Moves the scroll bar up or down in pages.
|
||||
|
||||
This will move the bar by a multiple of its current thumb size, effectively
|
||||
doing a page-up or down.
|
||||
|
||||
A positive value here will move the bar down or to the right, a negative
|
||||
value moves it up or to the left.
|
||||
|
||||
@param howManyPages the number of pages to move the scrollbar
|
||||
@param notification whether to send a notification of the change to listeners.
|
||||
A notification will only be sent if the position has changed.
|
||||
|
||||
@returns true if the scrollbar's position actually changed.
|
||||
*/
|
||||
bool moveScrollbarInPages (int howManyPages,
|
||||
NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Scrolls to the top (or left).
|
||||
This is the same as calling setCurrentRangeStart (getMinimumRangeLimit());
|
||||
|
||||
@param notification whether to send a notification of the change to listeners.
|
||||
A notification will only be sent if the position has changed.
|
||||
|
||||
@returns true if the scrollbar's position actually changed.
|
||||
*/
|
||||
bool scrollToTop (NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Scrolls to the bottom (or right).
|
||||
This is the same as calling setCurrentRangeStart (getMaximumRangeLimit() - getCurrentRangeSize());
|
||||
|
||||
@param notification whether to send a notification of the change to listeners.
|
||||
A notification will only be sent if the position has changed.
|
||||
|
||||
@returns true if the scrollbar's position actually changed.
|
||||
*/
|
||||
bool scrollToBottom (NotificationType notification = sendNotificationAsync);
|
||||
|
||||
/** Changes the delay before the up and down buttons autorepeat when they are held
|
||||
down.
|
||||
|
||||
For an explanation of what the parameters are for, see Button::setRepeatSpeed().
|
||||
|
||||
@see Button::setRepeatSpeed
|
||||
*/
|
||||
void setButtonRepeatSpeed (int initialDelayInMillisecs,
|
||||
int repeatDelayInMillisecs,
|
||||
int minimumDelayInMillisecs = -1);
|
||||
|
||||
//==============================================================================
|
||||
/** A set of colour IDs to use to change the colour of various aspects of the component.
|
||||
|
||||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
|
||||
methods.
|
||||
|
||||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
|
||||
*/
|
||||
enum ColourIds
|
||||
{
|
||||
backgroundColourId = 0x1000300, /**< The background colour of the scrollbar. */
|
||||
thumbColourId = 0x1000400, /**< A base colour to use for the thumb. The look and feel will probably use variations on this colour. */
|
||||
trackColourId = 0x1000401 /**< A base colour to use for the slot area of the bar. The look and feel will probably use variations on this colour. */
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A class for receiving events from a ScrollBar.
|
||||
|
||||
You can register a ScrollBar::Listener with a ScrollBar using the ScrollBar::addListener()
|
||||
method, and it will be called when the bar's position changes.
|
||||
|
||||
@see ScrollBar::addListener, ScrollBar::removeListener
|
||||
*/
|
||||
class JUCE_API Listener
|
||||
{
|
||||
public:
|
||||
/** Destructor. */
|
||||
virtual ~Listener() {}
|
||||
|
||||
/** Called when a ScrollBar is moved.
|
||||
|
||||
@param scrollBarThatHasMoved the bar that has moved
|
||||
@param newRangeStart the new range start of this bar
|
||||
*/
|
||||
virtual void scrollBarMoved (ScrollBar* scrollBarThatHasMoved,
|
||||
double newRangeStart) = 0;
|
||||
};
|
||||
|
||||
/** Registers a listener that will be called when the scrollbar is moved. */
|
||||
void addListener (Listener* listener);
|
||||
|
||||
/** Deregisters a previously-registered listener. */
|
||||
void removeListener (Listener* listener);
|
||||
|
||||
//==============================================================================
|
||||
/** This abstract base class is implemented by LookAndFeel classes to provide
|
||||
scrollbar-drawing functionality.
|
||||
*/
|
||||
struct JUCE_API LookAndFeelMethods
|
||||
{
|
||||
virtual ~LookAndFeelMethods() {}
|
||||
|
||||
virtual bool areScrollbarButtonsVisible() = 0;
|
||||
|
||||
/** Draws one of the buttons on a scrollbar.
|
||||
|
||||
@param g the context to draw into
|
||||
@param scrollbar the bar itself
|
||||
@param width the width of the button
|
||||
@param height the height of the button
|
||||
@param buttonDirection the direction of the button, where 0 = up, 1 = right, 2 = down, 3 = left
|
||||
@param isScrollbarVertical true if it's a vertical bar, false if horizontal
|
||||
@param isMouseOverButton whether the mouse is currently over the button (also true if it's held down)
|
||||
@param isButtonDown whether the mouse button's held down
|
||||
*/
|
||||
virtual void drawScrollbarButton (Graphics& g,
|
||||
ScrollBar& scrollbar,
|
||||
int width, int height,
|
||||
int buttonDirection,
|
||||
bool isScrollbarVertical,
|
||||
bool isMouseOverButton,
|
||||
bool isButtonDown) = 0;
|
||||
|
||||
/** Draws the thumb area of a scrollbar.
|
||||
|
||||
@param g the context to draw into
|
||||
@param scrollbar the bar itself
|
||||
@param x the x position of the left edge of the thumb area to draw in
|
||||
@param y the y position of the top edge of the thumb area to draw in
|
||||
@param width the width of the thumb area to draw in
|
||||
@param height the height of the thumb area to draw in
|
||||
@param isScrollbarVertical true if it's a vertical bar, false if horizontal
|
||||
@param thumbStartPosition for vertical bars, the y coordinate of the top of the
|
||||
thumb, or its x position for horizontal bars
|
||||
@param thumbSize for vertical bars, the height of the thumb, or its width for
|
||||
horizontal bars. This may be 0 if the thumb shouldn't be drawn.
|
||||
@param isMouseOver whether the mouse is over the thumb area, also true if the mouse is
|
||||
currently dragging the thumb
|
||||
@param isMouseDown whether the mouse is currently dragging the scrollbar
|
||||
*/
|
||||
virtual void drawScrollbar (Graphics& g, ScrollBar& scrollbar,
|
||||
int x, int y, int width, int height,
|
||||
bool isScrollbarVertical,
|
||||
int thumbStartPosition,
|
||||
int thumbSize,
|
||||
bool isMouseOver,
|
||||
bool isMouseDown) = 0;
|
||||
|
||||
/** Returns the component effect to use for a scrollbar */
|
||||
virtual ImageEffectFilter* getScrollbarEffect() = 0;
|
||||
|
||||
/** Returns the minimum length in pixels to use for a scrollbar thumb. */
|
||||
virtual int getMinimumScrollbarThumbSize (ScrollBar&) = 0;
|
||||
|
||||
/** Returns the default thickness to use for a scrollbar. */
|
||||
virtual int getDefaultScrollbarWidth() = 0;
|
||||
|
||||
/** Returns the length in pixels to use for a scrollbar button. */
|
||||
virtual int getScrollbarButtonSize (ScrollBar&) = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
bool keyPressed (const KeyPress&) override;
|
||||
/** @internal */
|
||||
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override;
|
||||
/** @internal */
|
||||
void lookAndFeelChanged() override;
|
||||
/** @internal */
|
||||
void mouseDown (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseDrag (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseUp (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
/** @internal */
|
||||
void parentHierarchyChanged() override;
|
||||
/** @internal */
|
||||
void setVisible (bool) override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
Range<double> totalRange { 0.0, 1.0 }, visibleRange { 0.0, 1.0 };
|
||||
double singleStepSize = 0.1, dragStartRange = 0;
|
||||
int thumbAreaStart = 0, thumbAreaSize = 0, thumbStart = 0, thumbSize = 0;
|
||||
int dragStartMousePos = 0, lastMousePos = 0;
|
||||
int initialDelayInMillisecs = 100, repeatDelayInMillisecs = 50, minimumDelayInMillisecs = 10;
|
||||
bool vertical, isDraggingThumb = false, autohides = true, userVisibilityFlag = false;
|
||||
class ScrollbarButton;
|
||||
friend struct ContainerDeletePolicy<ScrollbarButton>;
|
||||
std::unique_ptr<ScrollbarButton> upButton, downButton;
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
void handleAsyncUpdate() override;
|
||||
void updateThumbPosition();
|
||||
void timerCallback() override;
|
||||
bool getVisibility() const noexcept;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScrollBar)
|
||||
};
|
||||
|
||||
|
||||
} // namespace juce
|
278
modules/juce_gui_basics/layout/juce_SidePanel.cpp
Normal file
278
modules/juce_gui_basics/layout/juce_SidePanel.cpp
Normal file
@ -0,0 +1,278 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
SidePanel::SidePanel (StringRef title, int width, bool positionOnLeft,
|
||||
Component* contentToDisplay, bool deleteComponentWhenNoLongerNeeded)
|
||||
: titleLabel ("titleLabel", title),
|
||||
isOnLeft (positionOnLeft),
|
||||
panelWidth (width)
|
||||
{
|
||||
lookAndFeelChanged();
|
||||
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
dismissButton.onClick = [this] { showOrHide (false); };
|
||||
addAndMakeVisible (dismissButton);
|
||||
|
||||
Desktop::getInstance().addGlobalMouseListener (this);
|
||||
|
||||
if (contentToDisplay != nullptr)
|
||||
setContent (contentToDisplay, deleteComponentWhenNoLongerNeeded);
|
||||
|
||||
setOpaque (false);
|
||||
}
|
||||
|
||||
SidePanel::~SidePanel()
|
||||
{
|
||||
Desktop::getInstance().removeGlobalMouseListener (this);
|
||||
|
||||
if (parent != nullptr)
|
||||
parent->removeComponentListener (this);
|
||||
}
|
||||
|
||||
void SidePanel::setContent (Component* newContent, bool deleteComponentWhenNoLongerNeeded)
|
||||
{
|
||||
if (contentComponent.get() != newContent)
|
||||
{
|
||||
if (deleteComponentWhenNoLongerNeeded)
|
||||
contentComponent.setOwned (newContent);
|
||||
else
|
||||
contentComponent.setNonOwned (newContent);
|
||||
|
||||
addAndMakeVisible (contentComponent);
|
||||
|
||||
resized();
|
||||
}
|
||||
}
|
||||
|
||||
void SidePanel::setTitleBarComponent (Component* titleBarComponentToUse,
|
||||
bool keepDismissButton,
|
||||
bool deleteComponentWhenNoLongerNeeded)
|
||||
{
|
||||
if (titleBarComponent.get() != titleBarComponentToUse)
|
||||
{
|
||||
if (deleteComponentWhenNoLongerNeeded)
|
||||
titleBarComponent.setOwned (titleBarComponentToUse);
|
||||
else
|
||||
titleBarComponent.setNonOwned (titleBarComponentToUse);
|
||||
|
||||
addAndMakeVisible (titleBarComponent);
|
||||
|
||||
resized();
|
||||
}
|
||||
|
||||
shouldShowDismissButton = keepDismissButton;
|
||||
}
|
||||
|
||||
void SidePanel::showOrHide (bool show)
|
||||
{
|
||||
if (parent != nullptr)
|
||||
{
|
||||
isShowing = show;
|
||||
|
||||
Desktop::getInstance().getAnimator().animateComponent (this, calculateBoundsInParent (*parent),
|
||||
1.0f, 250, true, 1.0, 0.0);
|
||||
|
||||
if (onPanelShowHide != nullptr)
|
||||
onPanelShowHide (isShowing);
|
||||
}
|
||||
}
|
||||
|
||||
void SidePanel::moved()
|
||||
{
|
||||
if (onPanelMove != nullptr)
|
||||
onPanelMove();
|
||||
}
|
||||
|
||||
void SidePanel::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
|
||||
calculateAndRemoveShadowBounds (bounds);
|
||||
|
||||
auto titleBounds = bounds.removeFromTop (titleBarHeight);
|
||||
|
||||
if (titleBarComponent != nullptr)
|
||||
{
|
||||
if (shouldShowDismissButton)
|
||||
dismissButton.setBounds (isOnLeft ? titleBounds.removeFromRight (30).withTrimmedRight (10)
|
||||
: titleBounds.removeFromLeft (30).withTrimmedLeft (10));
|
||||
|
||||
titleBarComponent->setBounds (titleBounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
dismissButton.setBounds (isOnLeft ? titleBounds.removeFromRight (30).withTrimmedRight (10)
|
||||
: titleBounds.removeFromLeft (30).withTrimmedLeft (10));
|
||||
|
||||
titleLabel.setBounds (isOnLeft ? titleBounds.withTrimmedRight (40)
|
||||
: titleBounds.withTrimmedLeft (40));
|
||||
}
|
||||
|
||||
if (contentComponent != nullptr)
|
||||
contentComponent->setBounds (bounds);
|
||||
}
|
||||
|
||||
void SidePanel::paint (Graphics& g)
|
||||
{
|
||||
auto& lf = getLookAndFeel();
|
||||
|
||||
auto bgColour = lf.findColour (SidePanel::backgroundColour);
|
||||
auto shadowColour = lf.findColour (SidePanel::shadowBaseColour);
|
||||
|
||||
g.setGradientFill (ColourGradient (shadowColour.withAlpha (0.7f), (isOnLeft ? shadowArea.getTopLeft()
|
||||
: shadowArea.getTopRight()).toFloat(),
|
||||
shadowColour.withAlpha (0.0f), (isOnLeft ? shadowArea.getTopRight()
|
||||
: shadowArea.getTopLeft()).toFloat(), false));
|
||||
g.fillRect (shadowArea);
|
||||
|
||||
g.excludeClipRegion (shadowArea);
|
||||
g.fillAll (bgColour);
|
||||
}
|
||||
|
||||
void SidePanel::parentHierarchyChanged()
|
||||
{
|
||||
auto* newParent = getParentComponent();
|
||||
|
||||
if ((newParent != nullptr) && (parent != newParent))
|
||||
{
|
||||
if (parent != nullptr)
|
||||
parent->removeComponentListener (this);
|
||||
|
||||
parent = newParent;
|
||||
parent->addComponentListener (this);
|
||||
}
|
||||
}
|
||||
|
||||
void SidePanel::mouseDrag (const MouseEvent& e)
|
||||
{
|
||||
if (shouldResize)
|
||||
{
|
||||
Point<int> convertedPoint;
|
||||
|
||||
if (getParentComponent() == nullptr)
|
||||
convertedPoint = e.eventComponent->localPointToGlobal (e.getPosition());
|
||||
else
|
||||
convertedPoint = getParentComponent()->getLocalPoint (e.eventComponent, e.getPosition());
|
||||
|
||||
auto currentMouseDragX = convertedPoint.x;
|
||||
|
||||
if (isOnLeft)
|
||||
{
|
||||
amountMoved = startingBounds.getRight() - currentMouseDragX;
|
||||
setBounds (getBounds().withX (startingBounds.getX() - jmax (amountMoved, 0)));
|
||||
}
|
||||
else
|
||||
{
|
||||
amountMoved = currentMouseDragX - startingBounds.getX();
|
||||
setBounds (getBounds().withX (startingBounds.getX() + jmax (amountMoved, 0)));
|
||||
}
|
||||
}
|
||||
else if (isShowing)
|
||||
{
|
||||
auto relativeMouseDownPosition = getLocalPoint (e.eventComponent, e.getMouseDownPosition());
|
||||
auto relativeMouseDragPosition = getLocalPoint (e.eventComponent, e.getPosition());
|
||||
|
||||
if (! getLocalBounds().contains (relativeMouseDownPosition)
|
||||
&& getLocalBounds().contains (relativeMouseDragPosition))
|
||||
{
|
||||
shouldResize = true;
|
||||
startingBounds = getBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SidePanel::mouseUp (const MouseEvent&)
|
||||
{
|
||||
if (shouldResize)
|
||||
{
|
||||
showOrHide (amountMoved < (panelWidth / 2));
|
||||
|
||||
amountMoved = 0;
|
||||
shouldResize = false;
|
||||
}
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
void SidePanel::lookAndFeelChanged()
|
||||
{
|
||||
auto& lf = getLookAndFeel();
|
||||
|
||||
dismissButton.setShape (lf.getSidePanelDismissButtonShape (*this), false, true, false);
|
||||
|
||||
dismissButton.setColours (lf.findColour (SidePanel::dismissButtonNormalColour),
|
||||
lf.findColour (SidePanel::dismissButtonOverColour),
|
||||
lf.findColour (SidePanel::dismissButtonDownColour));
|
||||
|
||||
titleLabel.setFont (lf.getSidePanelTitleFont (*this));
|
||||
titleLabel.setColour (Label::textColourId, findColour (SidePanel::titleTextColour));
|
||||
titleLabel.setJustificationType (lf.getSidePanelTitleJustification (*this));
|
||||
}
|
||||
|
||||
void SidePanel::componentMovedOrResized (Component& component, bool wasMoved, bool wasResized)
|
||||
{
|
||||
ignoreUnused (wasMoved);
|
||||
|
||||
if (wasResized && (&component == parent))
|
||||
setBounds (calculateBoundsInParent (component));
|
||||
}
|
||||
|
||||
Rectangle<int> SidePanel::calculateBoundsInParent (Component& parentComp) const
|
||||
{
|
||||
auto parentBounds = parentComp.getBounds();
|
||||
|
||||
if (isOnLeft)
|
||||
{
|
||||
return isShowing ? parentBounds.removeFromLeft (panelWidth)
|
||||
: parentBounds.withX (parentBounds.getX() - panelWidth).withWidth (panelWidth);
|
||||
}
|
||||
|
||||
return isShowing ? parentBounds.removeFromRight (panelWidth)
|
||||
: parentBounds.withX (parentBounds.getRight()).withWidth (panelWidth);
|
||||
}
|
||||
|
||||
void SidePanel::calculateAndRemoveShadowBounds (Rectangle<int>& bounds)
|
||||
{
|
||||
shadowArea = isOnLeft ? bounds.removeFromRight (shadowWidth)
|
||||
: bounds.removeFromLeft (shadowWidth);
|
||||
}
|
||||
|
||||
bool SidePanel::isMouseEventInThisOrChildren (Component* eventComponent)
|
||||
{
|
||||
if (eventComponent == this)
|
||||
return true;
|
||||
|
||||
for (auto& child : getChildren())
|
||||
if (eventComponent == child)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace juce
|
232
modules/juce_gui_basics/layout/juce_SidePanel.h
Normal file
232
modules/juce_gui_basics/layout/juce_SidePanel.h
Normal file
@ -0,0 +1,232 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component that is positioned on either the left- or right-hand side of its parent,
|
||||
containing a header and some content. This sort of component is typically used for
|
||||
navigation and forms in mobile applications.
|
||||
|
||||
When triggered with the showOrHide() method, the SidePanel will animate itself to its
|
||||
new position. This component also contains some logic to reactively resize and dismiss
|
||||
itself when the user drags it.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class SidePanel : public Component,
|
||||
private ComponentListener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a SidePanel component.
|
||||
|
||||
@param title the text to use for the SidePanel's title bar
|
||||
@param width the width of the SidePanel
|
||||
@param positionOnLeft if true, the SidePanel will be positioned on the left of its parent component and
|
||||
if false, the SidePanel will be positioned on the right of its parent component
|
||||
@param contentComponent the component to add to this SidePanel - this content will take up the full
|
||||
size of the SidePanel, minus the height of the title bar. You can pass nullptr
|
||||
to this if you like and set the content component later using the setContent() method
|
||||
@param deleteComponentWhenNoLongerNeeded if true, the component will be deleted automatically when
|
||||
the SidePanel is deleted or when a different component is added. If false,
|
||||
the caller must manage the lifetime of the component
|
||||
*/
|
||||
SidePanel (StringRef title, int width, bool positionOnLeft,
|
||||
Component* contentComponent = nullptr,
|
||||
bool deleteComponentWhenNoLongerNeeded = true);
|
||||
|
||||
/** Destructor */
|
||||
~SidePanel();
|
||||
|
||||
//==============================================================================
|
||||
/** Sets the component that this SidePanel will contain.
|
||||
|
||||
This will add the given component to this SidePanel and position it below the title bar.
|
||||
|
||||
(Don't add or remove any child components directly using the normal
|
||||
Component::addChildComponent() methods).
|
||||
|
||||
@param newContentComponent the component to add to this SidePanel, or nullptr to remove
|
||||
the current component.
|
||||
@param deleteComponentWhenNoLongerNeeded if true, the component will be deleted automatically when
|
||||
the SidePanel is deleted or when a different component is added. If false,
|
||||
the caller must manage the lifetime of the component
|
||||
|
||||
@see getContent
|
||||
*/
|
||||
void setContent (Component* newContentComponent,
|
||||
bool deleteComponentWhenNoLongerNeeded = true);
|
||||
|
||||
/** Returns the component that's currently being used inside the SidePanel.
|
||||
|
||||
@see setViewedComponent
|
||||
*/
|
||||
Component* getContent() const noexcept { return contentComponent.get(); }
|
||||
|
||||
/** Sets a custom component to be used for the title bar of this SidePanel, replacing
|
||||
the default. You can pass a nullptr to revert to the default title bar.
|
||||
|
||||
@param titleBarComponentToUse the component to use as the title bar, or nullptr to use
|
||||
the default
|
||||
@param keepDismissButton if false the specified component will take up the full width of
|
||||
the title bar including the dismiss button but if true, the default
|
||||
dismiss button will be kept
|
||||
@param deleteComponentWhenNoLongerNeeded if true, the component will be deleted automatically when
|
||||
the SidePanel is deleted or when a different component is added. If false,
|
||||
the caller must manage the lifetime of the component
|
||||
|
||||
@see getTitleBarComponent
|
||||
*/
|
||||
void setTitleBarComponent (Component* titleBarComponentToUse,
|
||||
bool keepDismissButton,
|
||||
bool deleteComponentWhenNoLongerNeeded = true);
|
||||
|
||||
/** Returns the component that is currently being used as the title bar of the SidePanel.
|
||||
|
||||
@see setTitleBarComponent
|
||||
*/
|
||||
Component* getTitleBarComponent() const noexcept { return titleBarComponent.get(); }
|
||||
|
||||
/** Shows or hides the SidePanel.
|
||||
|
||||
This will animate the SidePanel to either its full width or to be hidden on the
|
||||
left- or right-hand side of its parent component depending on the value of positionOnLeft
|
||||
that was passed to the constructor.
|
||||
|
||||
@param show if true, this will show the SidePanel and if false the SidePanel will be hidden
|
||||
*/
|
||||
void showOrHide (bool show);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns true if the SidePanel is currently showing. */
|
||||
bool isPanelShowing() const noexcept { return isShowing; }
|
||||
|
||||
/** Returns true if the SidePanel is positioned on the left of its parent. */
|
||||
bool isPanelOnLeft() const noexcept { return isOnLeft; }
|
||||
|
||||
/** Sets the width of the shadow that will be drawn on the side of the panel. */
|
||||
void setShadowWidth (int newWidth) noexcept { shadowWidth = newWidth; }
|
||||
|
||||
/** Returns the width of the shadow that will be drawn on the side of the panel. */
|
||||
int getShadowWidth() const noexcept { return shadowWidth; }
|
||||
|
||||
/** Sets the height of the title bar at the top of the SidePanel. */
|
||||
void setTitleBarHeight (int newHeight) noexcept { titleBarHeight = newHeight; }
|
||||
|
||||
/** Returns the height of the title bar at the top of the SidePanel. */
|
||||
int getTitleBarHeight() const noexcept { return titleBarHeight; }
|
||||
|
||||
/** Returns the text that is displayed in the title bar at the top of the SidePanel. */
|
||||
String getTitleText() const noexcept { return titleLabel.getText(); }
|
||||
|
||||
//==============================================================================
|
||||
void moved() override;
|
||||
void resized() override;
|
||||
void paint (Graphics& g) override;
|
||||
|
||||
void parentHierarchyChanged() override;
|
||||
|
||||
void mouseDrag (const MouseEvent&) override;
|
||||
void mouseUp (const MouseEvent&) override;
|
||||
|
||||
//==============================================================================
|
||||
/** This abstract base class is implemented by LookAndFeel classes to provide
|
||||
SidePanel drawing functionality.
|
||||
*/
|
||||
struct JUCE_API LookAndFeelMethods
|
||||
{
|
||||
virtual ~LookAndFeelMethods() {}
|
||||
|
||||
virtual Font getSidePanelTitleFont (SidePanel&) = 0;
|
||||
virtual Justification getSidePanelTitleJustification (SidePanel&) = 0;
|
||||
virtual Path getSidePanelDismissButtonShape (SidePanel&) = 0;
|
||||
};
|
||||
|
||||
/** A set of colour IDs to use to change the colour of various aspects of the SidePanel.
|
||||
|
||||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
|
||||
methods.
|
||||
|
||||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
|
||||
*/
|
||||
enum ColourIds
|
||||
{
|
||||
backgroundColour = 0x100f001,
|
||||
titleTextColour = 0x100f002,
|
||||
shadowBaseColour = 0x100f003,
|
||||
dismissButtonNormalColour = 0x100f004,
|
||||
dismissButtonOverColour = 0x100f004,
|
||||
dismissButtonDownColour = 0x100f005
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** You can assign a lambda to this callback object and it will be called when the panel is moved. */
|
||||
std::function<void()> onPanelMove;
|
||||
|
||||
/** You can assign a lambda to this callback object and it will be called when the panel is shown or hidden. */
|
||||
std::function<void(bool)> onPanelShowHide;
|
||||
|
||||
private:
|
||||
//==========================================================================
|
||||
Component* parent = nullptr;
|
||||
OptionalScopedPointer<Component> contentComponent;
|
||||
OptionalScopedPointer<Component> titleBarComponent;
|
||||
|
||||
Label titleLabel;
|
||||
ShapeButton dismissButton { "dismissButton", Colours::lightgrey, Colours::lightgrey, Colours::white };
|
||||
|
||||
Rectangle<int> shadowArea;
|
||||
|
||||
bool isOnLeft = false;
|
||||
bool isShowing = false;
|
||||
|
||||
int panelWidth = 0;
|
||||
int shadowWidth = 15;
|
||||
int titleBarHeight = 40;
|
||||
|
||||
Rectangle<int> startingBounds;
|
||||
bool shouldResize = false;
|
||||
int amountMoved = 0;
|
||||
|
||||
bool shouldShowDismissButton = true;
|
||||
|
||||
//==========================================================================
|
||||
void lookAndFeelChanged() override;
|
||||
void componentMovedOrResized (Component&, bool wasMoved, bool wasResized) override;
|
||||
|
||||
Rectangle<int> calculateBoundsInParent (Component&) const;
|
||||
void calculateAndRemoveShadowBounds (Rectangle<int>& bounds);
|
||||
|
||||
bool isMouseEventInThisOrChildren (Component*);
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SidePanel)
|
||||
};
|
||||
|
||||
} // namespace juce
|
343
modules/juce_gui_basics/layout/juce_StretchableLayoutManager.cpp
Normal file
343
modules/juce_gui_basics/layout/juce_StretchableLayoutManager.cpp
Normal file
@ -0,0 +1,343 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
StretchableLayoutManager::StretchableLayoutManager() {}
|
||||
StretchableLayoutManager::~StretchableLayoutManager() {}
|
||||
|
||||
//==============================================================================
|
||||
void StretchableLayoutManager::clearAllItems()
|
||||
{
|
||||
items.clear();
|
||||
totalSize = 0;
|
||||
}
|
||||
|
||||
void StretchableLayoutManager::setItemLayout (const int itemIndex,
|
||||
const double minimumSize,
|
||||
const double maximumSize,
|
||||
const double preferredSize)
|
||||
{
|
||||
auto* layout = getInfoFor (itemIndex);
|
||||
|
||||
if (layout == nullptr)
|
||||
{
|
||||
layout = new ItemLayoutProperties();
|
||||
layout->itemIndex = itemIndex;
|
||||
|
||||
int i;
|
||||
for (i = 0; i < items.size(); ++i)
|
||||
if (items.getUnchecked (i)->itemIndex > itemIndex)
|
||||
break;
|
||||
|
||||
items.insert (i, layout);
|
||||
}
|
||||
|
||||
layout->minSize = minimumSize;
|
||||
layout->maxSize = maximumSize;
|
||||
layout->preferredSize = preferredSize;
|
||||
layout->currentSize = 0;
|
||||
}
|
||||
|
||||
bool StretchableLayoutManager::getItemLayout (const int itemIndex,
|
||||
double& minimumSize,
|
||||
double& maximumSize,
|
||||
double& preferredSize) const
|
||||
{
|
||||
if (auto* layout = getInfoFor (itemIndex))
|
||||
{
|
||||
minimumSize = layout->minSize;
|
||||
maximumSize = layout->maxSize;
|
||||
preferredSize = layout->preferredSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void StretchableLayoutManager::setTotalSize (const int newTotalSize)
|
||||
{
|
||||
totalSize = newTotalSize;
|
||||
|
||||
fitComponentsIntoSpace (0, items.size(), totalSize, 0);
|
||||
}
|
||||
|
||||
int StretchableLayoutManager::getItemCurrentPosition (const int itemIndex) const
|
||||
{
|
||||
int pos = 0;
|
||||
|
||||
for (int i = 0; i < itemIndex; ++i)
|
||||
if (auto* layout = getInfoFor (i))
|
||||
pos += layout->currentSize;
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
int StretchableLayoutManager::getItemCurrentAbsoluteSize (const int itemIndex) const
|
||||
{
|
||||
if (auto* layout = getInfoFor (itemIndex))
|
||||
return layout->currentSize;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
double StretchableLayoutManager::getItemCurrentRelativeSize (const int itemIndex) const
|
||||
{
|
||||
if (auto* layout = getInfoFor (itemIndex))
|
||||
return -layout->currentSize / (double) totalSize;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void StretchableLayoutManager::setItemPosition (const int itemIndex,
|
||||
int newPosition)
|
||||
{
|
||||
for (int i = items.size(); --i >= 0;)
|
||||
{
|
||||
auto* layout = items.getUnchecked(i);
|
||||
|
||||
if (layout->itemIndex == itemIndex)
|
||||
{
|
||||
auto realTotalSize = jmax (totalSize, getMinimumSizeOfItems (0, items.size()));
|
||||
auto minSizeAfterThisComp = getMinimumSizeOfItems (i, items.size());
|
||||
auto maxSizeAfterThisComp = getMaximumSizeOfItems (i + 1, items.size());
|
||||
|
||||
newPosition = jmax (newPosition, totalSize - maxSizeAfterThisComp - layout->currentSize);
|
||||
newPosition = jmin (newPosition, realTotalSize - minSizeAfterThisComp);
|
||||
|
||||
auto endPos = fitComponentsIntoSpace (0, i, newPosition, 0);
|
||||
|
||||
endPos += layout->currentSize;
|
||||
|
||||
fitComponentsIntoSpace (i + 1, items.size(), totalSize - endPos, endPos);
|
||||
updatePrefSizesToMatchCurrentPositions();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void StretchableLayoutManager::layOutComponents (Component** const components,
|
||||
int numComponents,
|
||||
int x, int y, int w, int h,
|
||||
const bool vertically,
|
||||
const bool resizeOtherDimension)
|
||||
{
|
||||
setTotalSize (vertically ? h : w);
|
||||
int pos = vertically ? y : x;
|
||||
|
||||
for (int i = 0; i < numComponents; ++i)
|
||||
{
|
||||
if (auto* layout = getInfoFor (i))
|
||||
{
|
||||
if (auto* c = components[i])
|
||||
{
|
||||
if (i == numComponents - 1)
|
||||
{
|
||||
// if it's the last item, crop it to exactly fit the available space..
|
||||
if (resizeOtherDimension)
|
||||
{
|
||||
if (vertically)
|
||||
c->setBounds (x, pos, w, jmax (layout->currentSize, h - pos));
|
||||
else
|
||||
c->setBounds (pos, y, jmax (layout->currentSize, w - pos), h);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (vertically)
|
||||
c->setBounds (c->getX(), pos, c->getWidth(), jmax (layout->currentSize, h - pos));
|
||||
else
|
||||
c->setBounds (pos, c->getY(), jmax (layout->currentSize, w - pos), c->getHeight());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resizeOtherDimension)
|
||||
{
|
||||
if (vertically)
|
||||
c->setBounds (x, pos, w, layout->currentSize);
|
||||
else
|
||||
c->setBounds (pos, y, layout->currentSize, h);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (vertically)
|
||||
c->setBounds (c->getX(), pos, c->getWidth(), layout->currentSize);
|
||||
else
|
||||
c->setBounds (pos, c->getY(), layout->currentSize, c->getHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos += layout->currentSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
StretchableLayoutManager::ItemLayoutProperties* StretchableLayoutManager::getInfoFor (const int itemIndex) const
|
||||
{
|
||||
for (auto* i : items)
|
||||
if (i->itemIndex == itemIndex)
|
||||
return i;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int StretchableLayoutManager::fitComponentsIntoSpace (const int startIndex,
|
||||
const int endIndex,
|
||||
const int availableSpace,
|
||||
int startPos)
|
||||
{
|
||||
// calculate the total sizes
|
||||
double totalIdealSize = 0.0;
|
||||
int totalMinimums = 0;
|
||||
|
||||
for (int i = startIndex; i < endIndex; ++i)
|
||||
{
|
||||
auto* layout = items.getUnchecked (i);
|
||||
|
||||
layout->currentSize = sizeToRealSize (layout->minSize, totalSize);
|
||||
|
||||
totalMinimums += layout->currentSize;
|
||||
totalIdealSize += sizeToRealSize (layout->preferredSize, totalSize);
|
||||
}
|
||||
|
||||
if (totalIdealSize <= 0)
|
||||
totalIdealSize = 1.0;
|
||||
|
||||
// now calc the best sizes..
|
||||
int extraSpace = availableSpace - totalMinimums;
|
||||
|
||||
while (extraSpace > 0)
|
||||
{
|
||||
int numWantingMoreSpace = 0;
|
||||
int numHavingTakenExtraSpace = 0;
|
||||
|
||||
// first figure out how many comps want a slice of the extra space..
|
||||
for (int i = startIndex; i < endIndex; ++i)
|
||||
{
|
||||
auto* layout = items.getUnchecked (i);
|
||||
|
||||
auto sizeWanted = sizeToRealSize (layout->preferredSize, totalSize);
|
||||
|
||||
auto bestSize = jlimit (layout->currentSize,
|
||||
jmax (layout->currentSize,
|
||||
sizeToRealSize (layout->maxSize, totalSize)),
|
||||
roundToInt (sizeWanted * availableSpace / totalIdealSize));
|
||||
|
||||
if (bestSize > layout->currentSize)
|
||||
++numWantingMoreSpace;
|
||||
}
|
||||
|
||||
// ..share out the extra space..
|
||||
for (int i = startIndex; i < endIndex; ++i)
|
||||
{
|
||||
auto* layout = items.getUnchecked (i);
|
||||
|
||||
auto sizeWanted = sizeToRealSize (layout->preferredSize, totalSize);
|
||||
|
||||
auto bestSize = jlimit (layout->currentSize,
|
||||
jmax (layout->currentSize, sizeToRealSize (layout->maxSize, totalSize)),
|
||||
roundToInt (sizeWanted * availableSpace / totalIdealSize));
|
||||
|
||||
auto extraWanted = bestSize - layout->currentSize;
|
||||
|
||||
if (extraWanted > 0)
|
||||
{
|
||||
auto extraAllowed = jmin (extraWanted,
|
||||
extraSpace / jmax (1, numWantingMoreSpace));
|
||||
|
||||
if (extraAllowed > 0)
|
||||
{
|
||||
++numHavingTakenExtraSpace;
|
||||
--numWantingMoreSpace;
|
||||
|
||||
layout->currentSize += extraAllowed;
|
||||
extraSpace -= extraAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (numHavingTakenExtraSpace <= 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// ..and calculate the end position
|
||||
for (int i = startIndex; i < endIndex; ++i)
|
||||
{
|
||||
auto* layout = items.getUnchecked(i);
|
||||
startPos += layout->currentSize;
|
||||
}
|
||||
|
||||
return startPos;
|
||||
}
|
||||
|
||||
int StretchableLayoutManager::getMinimumSizeOfItems (const int startIndex,
|
||||
const int endIndex) const
|
||||
{
|
||||
int totalMinimums = 0;
|
||||
|
||||
for (int i = startIndex; i < endIndex; ++i)
|
||||
totalMinimums += sizeToRealSize (items.getUnchecked (i)->minSize, totalSize);
|
||||
|
||||
return totalMinimums;
|
||||
}
|
||||
|
||||
int StretchableLayoutManager::getMaximumSizeOfItems (const int startIndex, const int endIndex) const
|
||||
{
|
||||
int totalMaximums = 0;
|
||||
|
||||
for (int i = startIndex; i < endIndex; ++i)
|
||||
totalMaximums += sizeToRealSize (items.getUnchecked (i)->maxSize, totalSize);
|
||||
|
||||
return totalMaximums;
|
||||
}
|
||||
|
||||
void StretchableLayoutManager::updatePrefSizesToMatchCurrentPositions()
|
||||
{
|
||||
for (int i = 0; i < items.size(); ++i)
|
||||
{
|
||||
auto* layout = items.getUnchecked (i);
|
||||
|
||||
layout->preferredSize
|
||||
= (layout->preferredSize < 0) ? getItemCurrentRelativeSize (i)
|
||||
: getItemCurrentAbsoluteSize (i);
|
||||
}
|
||||
}
|
||||
|
||||
int StretchableLayoutManager::sizeToRealSize (double size, int totalSpace)
|
||||
{
|
||||
if (size < 0)
|
||||
size *= -totalSpace;
|
||||
|
||||
return roundToInt (size);
|
||||
}
|
||||
|
||||
} // namespace juce
|
262
modules/juce_gui_basics/layout/juce_StretchableLayoutManager.h
Normal file
262
modules/juce_gui_basics/layout/juce_StretchableLayoutManager.h
Normal file
@ -0,0 +1,262 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
For laying out a set of components, where the components have preferred sizes
|
||||
and size limits, but where they are allowed to stretch to fill the available
|
||||
space.
|
||||
|
||||
For example, if you have a component containing several other components, and
|
||||
each one should be given a share of the total size, you could use one of these
|
||||
to resize the child components when the parent component is resized. Then
|
||||
you could add a StretchableLayoutResizerBar to easily let the user rescale them.
|
||||
|
||||
A StretchableLayoutManager operates only in one dimension, so if you have a set
|
||||
of components stacked vertically on top of each other, you'd use one to manage their
|
||||
heights. To build up complex arrangements of components, e.g. for applications
|
||||
with multiple nested panels, you would use more than one StretchableLayoutManager.
|
||||
E.g. by using two (one vertical, one horizontal), you could create a resizable
|
||||
spreadsheet-style table.
|
||||
|
||||
E.g.
|
||||
@code
|
||||
class MyComp : public Component
|
||||
{
|
||||
StretchableLayoutManager myLayout;
|
||||
|
||||
MyComp()
|
||||
{
|
||||
myLayout.setItemLayout (0, // for item 0
|
||||
50, 100, // must be between 50 and 100 pixels in size
|
||||
-0.6); // and its preferred size is 60% of the total available space
|
||||
|
||||
myLayout.setItemLayout (1, // for item 1
|
||||
-0.2, -0.6, // size must be between 20% and 60% of the available space
|
||||
50); // and its preferred size is 50 pixels
|
||||
}
|
||||
|
||||
void resized()
|
||||
{
|
||||
// make a list of two of our child components that we want to reposition
|
||||
Component* comps[] = { myComp1, myComp2 };
|
||||
|
||||
// this will position the 2 components, one above the other, to fit
|
||||
// vertically into the rectangle provided.
|
||||
myLayout.layOutComponents (comps, 2,
|
||||
0, 0, getWidth(), getHeight(),
|
||||
true);
|
||||
}
|
||||
};
|
||||
@endcode
|
||||
|
||||
@see StretchableLayoutResizerBar
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API StretchableLayoutManager
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates an empty layout.
|
||||
|
||||
You'll need to add some item properties to the layout before it can be used
|
||||
to resize things - see setItemLayout().
|
||||
*/
|
||||
StretchableLayoutManager();
|
||||
|
||||
/** Destructor. */
|
||||
~StretchableLayoutManager();
|
||||
|
||||
//==============================================================================
|
||||
/** For a numbered item, this sets its size limits and preferred size.
|
||||
|
||||
@param itemIndex the index of the item to change.
|
||||
@param minimumSize the minimum size that this item is allowed to be - a positive number
|
||||
indicates an absolute size in pixels. A negative number indicates a
|
||||
proportion of the available space (e.g -0.5 is 50%)
|
||||
@param maximumSize the maximum size that this item is allowed to be - a positive number
|
||||
indicates an absolute size in pixels. A negative number indicates a
|
||||
proportion of the available space
|
||||
@param preferredSize the size that this item would like to be, if there's enough room. A
|
||||
positive number indicates an absolute size in pixels. A negative number
|
||||
indicates a proportion of the available space
|
||||
@see getItemLayout
|
||||
*/
|
||||
void setItemLayout (int itemIndex,
|
||||
double minimumSize,
|
||||
double maximumSize,
|
||||
double preferredSize);
|
||||
|
||||
/** For a numbered item, this returns its size limits and preferred size.
|
||||
|
||||
@param itemIndex the index of the item.
|
||||
@param minimumSize the minimum size that this item is allowed to be - a positive number
|
||||
indicates an absolute size in pixels. A negative number indicates a
|
||||
proportion of the available space (e.g -0.5 is 50%)
|
||||
@param maximumSize the maximum size that this item is allowed to be - a positive number
|
||||
indicates an absolute size in pixels. A negative number indicates a
|
||||
proportion of the available space
|
||||
@param preferredSize the size that this item would like to be, if there's enough room. A
|
||||
positive number indicates an absolute size in pixels. A negative number
|
||||
indicates a proportion of the available space
|
||||
@returns false if the item's properties hadn't been set
|
||||
@see setItemLayout
|
||||
*/
|
||||
bool getItemLayout (int itemIndex,
|
||||
double& minimumSize,
|
||||
double& maximumSize,
|
||||
double& preferredSize) const;
|
||||
|
||||
/** Clears all the properties that have been set with setItemLayout() and resets
|
||||
this object to its initial state.
|
||||
*/
|
||||
void clearAllItems();
|
||||
|
||||
//==============================================================================
|
||||
/** Takes a set of components that correspond to the layout's items, and positions
|
||||
them to fill a space.
|
||||
|
||||
This will try to give each item its preferred size, whether that's a relative size
|
||||
or an absolute one.
|
||||
|
||||
@param components an array of components that correspond to each of the
|
||||
numbered items that the StretchableLayoutManager object
|
||||
has been told about with setItemLayout()
|
||||
@param numComponents the number of components in the array that is passed-in. This
|
||||
should be the same as the number of items this object has been
|
||||
told about.
|
||||
@param x the left of the rectangle in which the components should
|
||||
be laid out
|
||||
@param y the top of the rectangle in which the components should
|
||||
be laid out
|
||||
@param width the width of the rectangle in which the components should
|
||||
be laid out
|
||||
@param height the height of the rectangle in which the components should
|
||||
be laid out
|
||||
@param vertically if true, the components will be positioned in a vertical stack,
|
||||
so that they fill the height of the rectangle. If false, they
|
||||
will be placed side-by-side in a horizontal line, filling the
|
||||
available width
|
||||
@param resizeOtherDimension if true, this means that the components will have their
|
||||
other dimension resized to fit the space - i.e. if the 'vertically'
|
||||
parameter is true, their x-positions and widths are adjusted to fit
|
||||
the x and width parameters; if 'vertically' is false, their y-positions
|
||||
and heights are adjusted to fit the y and height parameters.
|
||||
*/
|
||||
void layOutComponents (Component** components,
|
||||
int numComponents,
|
||||
int x, int y, int width, int height,
|
||||
bool vertically,
|
||||
bool resizeOtherDimension);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the current position of one of the items.
|
||||
|
||||
This is only a valid call after layOutComponents() has been called, as it
|
||||
returns the last position that this item was placed at. If the layout was
|
||||
vertical, the value returned will be the y position of the top of the item,
|
||||
relative to the top of the rectangle in which the items were placed (so for
|
||||
example, item 0 will always have position of 0, even in the rectangle passed
|
||||
in to layOutComponents() wasn't at y = 0). If the layout was done horizontally,
|
||||
the position returned is the item's left-hand position, again relative to the
|
||||
x position of the rectangle used.
|
||||
|
||||
@see getItemCurrentSize, setItemPosition
|
||||
*/
|
||||
int getItemCurrentPosition (int itemIndex) const;
|
||||
|
||||
/** Returns the current size of one of the items.
|
||||
|
||||
This is only meaningful after layOutComponents() has been called, as it
|
||||
returns the last size that this item was given. If the layout was done
|
||||
vertically, it'll return the item's height in pixels; if it was horizontal,
|
||||
it'll return its width.
|
||||
|
||||
@see getItemCurrentRelativeSize
|
||||
*/
|
||||
int getItemCurrentAbsoluteSize (int itemIndex) const;
|
||||
|
||||
/** Returns the current size of one of the items.
|
||||
|
||||
This is only meaningful after layOutComponents() has been called, as it
|
||||
returns the last size that this item was given. If the layout was done
|
||||
vertically, it'll return a negative value representing the item's height relative
|
||||
to the last size used for laying the components out; if the layout was done
|
||||
horizontally it'll be the proportion of its width.
|
||||
|
||||
@see getItemCurrentAbsoluteSize
|
||||
*/
|
||||
double getItemCurrentRelativeSize (int itemIndex) const;
|
||||
|
||||
//==============================================================================
|
||||
/** Moves one of the items, shifting along any other items as necessary in
|
||||
order to get it to the desired position.
|
||||
|
||||
Calling this method will also update the preferred sizes of the items it
|
||||
shuffles along, so that they reflect their new positions.
|
||||
|
||||
(This is the method that a StretchableLayoutResizerBar uses to shift the items
|
||||
about when it's dragged).
|
||||
|
||||
@param itemIndex the item to move
|
||||
@param newPosition the absolute position that you'd like this item to move
|
||||
to. The item might not be able to always reach exactly this position,
|
||||
because other items may have minimum sizes that constrain how
|
||||
far it can go
|
||||
*/
|
||||
void setItemPosition (int itemIndex,
|
||||
int newPosition);
|
||||
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct ItemLayoutProperties
|
||||
{
|
||||
int itemIndex;
|
||||
int currentSize;
|
||||
double minSize, maxSize, preferredSize;
|
||||
};
|
||||
|
||||
OwnedArray<ItemLayoutProperties> items;
|
||||
int totalSize = 0;
|
||||
|
||||
//==============================================================================
|
||||
static int sizeToRealSize (double size, int totalSpace);
|
||||
ItemLayoutProperties* getInfoFor (int itemIndex) const;
|
||||
void setTotalSize (int newTotalSize);
|
||||
int fitComponentsIntoSpace (int startIndex, int endIndex, int availableSpace, int startPos);
|
||||
int getMinimumSizeOfItems (int startIndex, int endIndex) const;
|
||||
int getMaximumSizeOfItems (int startIndex, int endIndex) const;
|
||||
void updatePrefSizesToMatchCurrentPositions();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StretchableLayoutManager)
|
||||
};
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
StretchableLayoutResizerBar::StretchableLayoutResizerBar (StretchableLayoutManager* layout_,
|
||||
const int index,
|
||||
const bool vertical)
|
||||
: layout (layout_),
|
||||
itemIndex (index),
|
||||
isVertical (vertical)
|
||||
{
|
||||
setRepaintsOnMouseActivity (true);
|
||||
setMouseCursor (vertical ? MouseCursor::LeftRightResizeCursor
|
||||
: MouseCursor::UpDownResizeCursor);
|
||||
}
|
||||
|
||||
StretchableLayoutResizerBar::~StretchableLayoutResizerBar()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void StretchableLayoutResizerBar::paint (Graphics& g)
|
||||
{
|
||||
getLookAndFeel().drawStretchableLayoutResizerBar (g,
|
||||
getWidth(), getHeight(),
|
||||
isVertical,
|
||||
isMouseOver(),
|
||||
isMouseButtonDown());
|
||||
}
|
||||
|
||||
void StretchableLayoutResizerBar::mouseDown (const MouseEvent&)
|
||||
{
|
||||
mouseDownPos = layout->getItemCurrentPosition (itemIndex);
|
||||
}
|
||||
|
||||
void StretchableLayoutResizerBar::mouseDrag (const MouseEvent& e)
|
||||
{
|
||||
const int desiredPos = mouseDownPos + (isVertical ? e.getDistanceFromDragStartX()
|
||||
: e.getDistanceFromDragStartY());
|
||||
|
||||
|
||||
if (layout->getItemCurrentPosition (itemIndex) != desiredPos)
|
||||
{
|
||||
layout->setItemPosition (itemIndex, desiredPos);
|
||||
hasBeenMoved();
|
||||
}
|
||||
}
|
||||
|
||||
void StretchableLayoutResizerBar::hasBeenMoved()
|
||||
{
|
||||
if (Component* parent = getParentComponent())
|
||||
parent->resized();
|
||||
}
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component that acts as one of the vertical or horizontal bars you see being
|
||||
used to resize panels in a window.
|
||||
|
||||
One of these acts with a StretchableLayoutManager to resize the other components.
|
||||
|
||||
@see StretchableLayoutManager
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API StretchableLayoutResizerBar : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a resizer bar for use on a specified layout.
|
||||
|
||||
@param layoutToUse the layout that will be affected when this bar
|
||||
is dragged
|
||||
@param itemIndexInLayout the item index in the layout that corresponds to
|
||||
this bar component. You'll need to set up the item
|
||||
properties in a suitable way for a divider bar, e.g.
|
||||
for an 8-pixel wide bar which, you could call
|
||||
myLayout->setItemLayout (barIndex, 8, 8, 8)
|
||||
@param isBarVertical true if it's an upright bar that you drag left and
|
||||
right; false for a horizontal one that you drag up and
|
||||
down
|
||||
*/
|
||||
StretchableLayoutResizerBar (StretchableLayoutManager* layoutToUse,
|
||||
int itemIndexInLayout,
|
||||
bool isBarVertical);
|
||||
|
||||
/** Destructor. */
|
||||
~StretchableLayoutResizerBar();
|
||||
|
||||
//==============================================================================
|
||||
/** This is called when the bar is dragged.
|
||||
|
||||
This method must update the positions of any components whose position is
|
||||
determined by the StretchableLayoutManager, because they might have just
|
||||
moved.
|
||||
|
||||
The default implementation calls the resized() method of this component's
|
||||
parent component, because that's often where you're likely to apply the
|
||||
layout, but it can be overridden for more specific needs.
|
||||
*/
|
||||
virtual void hasBeenMoved();
|
||||
|
||||
//==============================================================================
|
||||
/** This abstract base class is implemented by LookAndFeel classes. */
|
||||
struct JUCE_API LookAndFeelMethods
|
||||
{
|
||||
virtual ~LookAndFeelMethods() {}
|
||||
|
||||
virtual void drawStretchableLayoutResizerBar (Graphics&, int w, int h,
|
||||
bool isVerticalBar, bool isMouseOver, bool isMouseDragging) = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void mouseDown (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseDrag (const MouseEvent&) override;
|
||||
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
StretchableLayoutManager* layout;
|
||||
int itemIndex, mouseDownPos;
|
||||
bool isVertical;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StretchableLayoutResizerBar)
|
||||
};
|
||||
|
||||
} // namespace juce
|
123
modules/juce_gui_basics/layout/juce_StretchableObjectResizer.cpp
Normal file
123
modules/juce_gui_basics/layout/juce_StretchableObjectResizer.cpp
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
StretchableObjectResizer::StretchableObjectResizer() {}
|
||||
StretchableObjectResizer::~StretchableObjectResizer() {}
|
||||
|
||||
void StretchableObjectResizer::addItem (const double size,
|
||||
const double minSize, const double maxSize,
|
||||
const int order)
|
||||
{
|
||||
// the order must be >= 0 but less than the maximum integer value.
|
||||
jassert (order >= 0 && order < std::numeric_limits<int>::max());
|
||||
jassert (maxSize >= minSize);
|
||||
|
||||
Item item;
|
||||
item.size = size;
|
||||
item.minSize = minSize;
|
||||
item.maxSize = maxSize;
|
||||
item.order = order;
|
||||
items.add (item);
|
||||
}
|
||||
|
||||
double StretchableObjectResizer::getItemSize (const int index) const noexcept
|
||||
{
|
||||
return isPositiveAndBelow (index, items.size()) ? items.getReference (index).size
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
void StretchableObjectResizer::resizeToFit (const double targetSize)
|
||||
{
|
||||
int order = 0;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
double currentSize = 0;
|
||||
double minSize = 0;
|
||||
double maxSize = 0;
|
||||
|
||||
int nextHighestOrder = std::numeric_limits<int>::max();
|
||||
|
||||
for (int i = 0; i < items.size(); ++i)
|
||||
{
|
||||
const Item& it = items.getReference(i);
|
||||
currentSize += it.size;
|
||||
|
||||
if (it.order <= order)
|
||||
{
|
||||
minSize += it.minSize;
|
||||
maxSize += it.maxSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
minSize += it.size;
|
||||
maxSize += it.size;
|
||||
nextHighestOrder = jmin (nextHighestOrder, it.order);
|
||||
}
|
||||
}
|
||||
|
||||
const double thisIterationTarget = jlimit (minSize, maxSize, targetSize);
|
||||
|
||||
if (thisIterationTarget >= currentSize)
|
||||
{
|
||||
const double availableExtraSpace = maxSize - currentSize;
|
||||
const double targetAmountOfExtraSpace = thisIterationTarget - currentSize;
|
||||
const double scale = availableExtraSpace > 0 ? targetAmountOfExtraSpace / availableExtraSpace : 1.0;
|
||||
|
||||
for (int i = 0; i < items.size(); ++i)
|
||||
{
|
||||
Item& it = items.getReference(i);
|
||||
|
||||
if (it.order <= order)
|
||||
it.size = jlimit (it.minSize, it.maxSize, it.size + (it.maxSize - it.size) * scale);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const double amountOfSlack = currentSize - minSize;
|
||||
const double targetAmountOfSlack = thisIterationTarget - minSize;
|
||||
const double scale = targetAmountOfSlack / amountOfSlack;
|
||||
|
||||
for (int i = 0; i < items.size(); ++i)
|
||||
{
|
||||
Item& it = items.getReference(i);
|
||||
|
||||
if (it.order <= order)
|
||||
it.size = jmax (it.minSize, it.minSize + (it.size - it.minSize) * scale);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextHighestOrder < std::numeric_limits<int>::max())
|
||||
order = nextHighestOrder;
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace juce
|
103
modules/juce_gui_basics/layout/juce_StretchableObjectResizer.h
Normal file
103
modules/juce_gui_basics/layout/juce_StretchableObjectResizer.h
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A utility class for fitting a set of objects whose sizes can vary between
|
||||
a minimum and maximum size, into a space.
|
||||
|
||||
This is a trickier algorithm than it would first seem, so I've put it in this
|
||||
class to allow it to be shared by various bits of code.
|
||||
|
||||
To use it, create one of these objects, call addItem() to add the list of items
|
||||
you need, then call resizeToFit(), which will change all their sizes. You can
|
||||
then retrieve the new sizes with getItemSize() and getNumItems().
|
||||
|
||||
It's currently used by the TableHeaderComponent for stretching out the table
|
||||
headings to fill the table's width.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class StretchableObjectResizer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates an empty object resizer. */
|
||||
StretchableObjectResizer();
|
||||
|
||||
/** Destructor. */
|
||||
~StretchableObjectResizer();
|
||||
|
||||
//==============================================================================
|
||||
/** Adds an item to the list.
|
||||
|
||||
The order parameter lets you specify groups of items that are resized first when some
|
||||
space needs to be found. Those items with an order of 0 will be the first ones to be
|
||||
resized, and if that doesn't provide enough space to meet the requirements, the algorithm
|
||||
will then try resizing the items with an order of 1, then 2, and so on.
|
||||
*/
|
||||
void addItem (double currentSize,
|
||||
double minSize,
|
||||
double maxSize,
|
||||
int order = 0);
|
||||
|
||||
/** Resizes all the items to fit this amount of space.
|
||||
|
||||
This will attempt to fit them in without exceeding each item's minimum and
|
||||
maximum sizes. In cases where none of the items can be expanded or enlarged any
|
||||
further, the final size may be greater or less than the size passed in.
|
||||
|
||||
After calling this method, you can retrieve the new sizes with the getItemSize()
|
||||
method.
|
||||
*/
|
||||
void resizeToFit (double targetSize);
|
||||
|
||||
/** Returns the number of items that have been added. */
|
||||
int getNumItems() const noexcept { return items.size(); }
|
||||
|
||||
/** Returns the size of one of the items. */
|
||||
double getItemSize (int index) const noexcept;
|
||||
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct Item
|
||||
{
|
||||
double size;
|
||||
double minSize;
|
||||
double maxSize;
|
||||
int order;
|
||||
};
|
||||
|
||||
Array<Item> items;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StretchableObjectResizer)
|
||||
};
|
||||
|
||||
} // namespace juce
|
581
modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp
Normal file
581
modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp
Normal file
@ -0,0 +1,581 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
TabBarButton::TabBarButton (const String& name, TabbedButtonBar& bar)
|
||||
: Button (name), owner (bar)
|
||||
{
|
||||
setWantsKeyboardFocus (false);
|
||||
}
|
||||
|
||||
TabBarButton::~TabBarButton() {}
|
||||
|
||||
int TabBarButton::getIndex() const { return owner.indexOfTabButton (this); }
|
||||
Colour TabBarButton::getTabBackgroundColour() const { return owner.getTabBackgroundColour (getIndex()); }
|
||||
bool TabBarButton::isFrontTab() const { return getToggleState(); }
|
||||
|
||||
void TabBarButton::paintButton (Graphics& g, const bool isMouseOverButton, const bool isButtonDown)
|
||||
{
|
||||
getLookAndFeel().drawTabButton (*this, g, isMouseOverButton, isButtonDown);
|
||||
}
|
||||
|
||||
void TabBarButton::clicked (const ModifierKeys& mods)
|
||||
{
|
||||
if (mods.isPopupMenu())
|
||||
owner.popupMenuClickOnTab (getIndex(), getButtonText());
|
||||
else
|
||||
owner.setCurrentTabIndex (getIndex());
|
||||
}
|
||||
|
||||
bool TabBarButton::hitTest (int mx, int my)
|
||||
{
|
||||
auto area = getActiveArea();
|
||||
|
||||
if (owner.isVertical())
|
||||
{
|
||||
if (isPositiveAndBelow (mx, getWidth())
|
||||
&& my >= area.getY() + overlapPixels && my < area.getBottom() - overlapPixels)
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isPositiveAndBelow (my, getHeight())
|
||||
&& mx >= area.getX() + overlapPixels && mx < area.getRight() - overlapPixels)
|
||||
return true;
|
||||
}
|
||||
|
||||
Path p;
|
||||
getLookAndFeel().createTabButtonShape (*this, p, false, false);
|
||||
|
||||
return p.contains ((float) (mx - area.getX()),
|
||||
(float) (my - area.getY()));
|
||||
}
|
||||
|
||||
int TabBarButton::getBestTabLength (const int depth)
|
||||
{
|
||||
return getLookAndFeel().getTabButtonBestWidth (*this, depth);
|
||||
}
|
||||
|
||||
void TabBarButton::calcAreas (Rectangle<int>& extraComp, Rectangle<int>& textArea) const
|
||||
{
|
||||
auto& lf = getLookAndFeel();
|
||||
textArea = getActiveArea();
|
||||
|
||||
auto depth = owner.isVertical() ? textArea.getWidth() : textArea.getHeight();
|
||||
auto overlap = lf.getTabButtonOverlap (depth);
|
||||
|
||||
if (overlap > 0)
|
||||
{
|
||||
if (owner.isVertical())
|
||||
textArea.reduce (0, overlap);
|
||||
else
|
||||
textArea.reduce (overlap, 0);
|
||||
}
|
||||
|
||||
if (extraComponent != nullptr)
|
||||
{
|
||||
extraComp = lf.getTabButtonExtraComponentBounds (*this, textArea, *extraComponent);
|
||||
|
||||
auto orientation = owner.getOrientation();
|
||||
|
||||
if (orientation == TabbedButtonBar::TabsAtLeft || orientation == TabbedButtonBar::TabsAtRight)
|
||||
{
|
||||
if (extraComp.getCentreY() > textArea.getCentreY())
|
||||
textArea.setBottom (jmin (textArea.getBottom(), extraComp.getY()));
|
||||
else
|
||||
textArea.setTop (jmax (textArea.getY(), extraComp.getBottom()));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (extraComp.getCentreX() > textArea.getCentreX())
|
||||
textArea.setRight (jmin (textArea.getRight(), extraComp.getX()));
|
||||
else
|
||||
textArea.setLeft (jmax (textArea.getX(), extraComp.getRight()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle<int> TabBarButton::getTextArea() const
|
||||
{
|
||||
Rectangle<int> extraComp, textArea;
|
||||
calcAreas (extraComp, textArea);
|
||||
return textArea;
|
||||
}
|
||||
|
||||
Rectangle<int> TabBarButton::getActiveArea() const
|
||||
{
|
||||
auto r = getLocalBounds();
|
||||
auto spaceAroundImage = getLookAndFeel().getTabButtonSpaceAroundImage();
|
||||
auto orientation = owner.getOrientation();
|
||||
|
||||
if (orientation != TabbedButtonBar::TabsAtLeft) r.removeFromRight (spaceAroundImage);
|
||||
if (orientation != TabbedButtonBar::TabsAtRight) r.removeFromLeft (spaceAroundImage);
|
||||
if (orientation != TabbedButtonBar::TabsAtBottom) r.removeFromTop (spaceAroundImage);
|
||||
if (orientation != TabbedButtonBar::TabsAtTop) r.removeFromBottom (spaceAroundImage);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void TabBarButton::setExtraComponent (Component* comp, ExtraComponentPlacement placement)
|
||||
{
|
||||
jassert (extraCompPlacement == beforeText || extraCompPlacement == afterText);
|
||||
extraCompPlacement = placement;
|
||||
extraComponent.reset (comp);
|
||||
addAndMakeVisible (extraComponent.get());
|
||||
resized();
|
||||
}
|
||||
|
||||
void TabBarButton::childBoundsChanged (Component* c)
|
||||
{
|
||||
if (c == extraComponent.get())
|
||||
{
|
||||
owner.resized();
|
||||
resized();
|
||||
}
|
||||
}
|
||||
|
||||
void TabBarButton::resized()
|
||||
{
|
||||
if (extraComponent != nullptr)
|
||||
{
|
||||
Rectangle<int> extraComp, textArea;
|
||||
calcAreas (extraComp, textArea);
|
||||
|
||||
if (! extraComp.isEmpty())
|
||||
extraComponent->setBounds (extraComp);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class TabbedButtonBar::BehindFrontTabComp : public Component
|
||||
{
|
||||
public:
|
||||
BehindFrontTabComp (TabbedButtonBar& tb) : owner (tb)
|
||||
{
|
||||
setInterceptsMouseClicks (false, false);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
getLookAndFeel().drawTabAreaBehindFrontButton (owner, g, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
void enablementChanged() override
|
||||
{
|
||||
repaint();
|
||||
}
|
||||
|
||||
TabbedButtonBar& owner;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (BehindFrontTabComp)
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
TabbedButtonBar::TabbedButtonBar (Orientation orientationToUse)
|
||||
: orientation (orientationToUse)
|
||||
{
|
||||
setInterceptsMouseClicks (false, true);
|
||||
behindFrontTab.reset (new BehindFrontTabComp (*this));
|
||||
addAndMakeVisible (behindFrontTab.get());
|
||||
setFocusContainer (true);
|
||||
}
|
||||
|
||||
TabbedButtonBar::~TabbedButtonBar()
|
||||
{
|
||||
tabs.clear();
|
||||
extraTabsButton.reset();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void TabbedButtonBar::setOrientation (const Orientation newOrientation)
|
||||
{
|
||||
orientation = newOrientation;
|
||||
|
||||
for (auto* child : getChildren())
|
||||
child->resized();
|
||||
|
||||
resized();
|
||||
}
|
||||
|
||||
TabBarButton* TabbedButtonBar::createTabButton (const String& name, const int /*index*/)
|
||||
{
|
||||
return new TabBarButton (name, *this);
|
||||
}
|
||||
|
||||
void TabbedButtonBar::setMinimumTabScaleFactor (double newMinimumScale)
|
||||
{
|
||||
minimumScale = newMinimumScale;
|
||||
resized();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void TabbedButtonBar::clearTabs()
|
||||
{
|
||||
tabs.clear();
|
||||
extraTabsButton.reset();
|
||||
setCurrentTabIndex (-1);
|
||||
}
|
||||
|
||||
void TabbedButtonBar::addTab (const String& tabName,
|
||||
Colour tabBackgroundColour,
|
||||
int insertIndex)
|
||||
{
|
||||
jassert (tabName.isNotEmpty()); // you have to give them all a name..
|
||||
|
||||
if (tabName.isNotEmpty())
|
||||
{
|
||||
if (! isPositiveAndBelow (insertIndex, tabs.size()))
|
||||
insertIndex = tabs.size();
|
||||
|
||||
auto* currentTab = tabs[currentTabIndex];
|
||||
|
||||
auto* newTab = new TabInfo();
|
||||
newTab->name = tabName;
|
||||
newTab->colour = tabBackgroundColour;
|
||||
newTab->button.reset (createTabButton (tabName, insertIndex));
|
||||
jassert (newTab->button != nullptr);
|
||||
|
||||
tabs.insert (insertIndex, newTab);
|
||||
currentTabIndex = tabs.indexOf (currentTab);
|
||||
addAndMakeVisible (newTab->button.get(), insertIndex);
|
||||
|
||||
resized();
|
||||
|
||||
if (currentTabIndex < 0)
|
||||
setCurrentTabIndex (0);
|
||||
}
|
||||
}
|
||||
|
||||
void TabbedButtonBar::setTabName (int tabIndex, const String& newName)
|
||||
{
|
||||
if (auto* tab = tabs[tabIndex])
|
||||
{
|
||||
if (tab->name != newName)
|
||||
{
|
||||
tab->name = newName;
|
||||
tab->button->setButtonText (newName);
|
||||
resized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabbedButtonBar::removeTab (const int indexToRemove, const bool animate)
|
||||
{
|
||||
if (isPositiveAndBelow (indexToRemove, tabs.size()))
|
||||
{
|
||||
auto oldSelectedIndex = currentTabIndex;
|
||||
|
||||
if (indexToRemove == currentTabIndex)
|
||||
oldSelectedIndex = -1;
|
||||
else if (indexToRemove < oldSelectedIndex)
|
||||
--oldSelectedIndex;
|
||||
|
||||
tabs.remove (indexToRemove);
|
||||
|
||||
setCurrentTabIndex (oldSelectedIndex);
|
||||
updateTabPositions (animate);
|
||||
}
|
||||
}
|
||||
|
||||
void TabbedButtonBar::moveTab (const int currentIndex, const int newIndex, const bool animate)
|
||||
{
|
||||
auto* currentTab = tabs[currentTabIndex];
|
||||
tabs.move (currentIndex, newIndex);
|
||||
currentTabIndex = tabs.indexOf (currentTab);
|
||||
updateTabPositions (animate);
|
||||
}
|
||||
|
||||
int TabbedButtonBar::getNumTabs() const
|
||||
{
|
||||
return tabs.size();
|
||||
}
|
||||
|
||||
String TabbedButtonBar::getCurrentTabName() const
|
||||
{
|
||||
if (auto* tab = tabs [currentTabIndex])
|
||||
return tab->name;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
StringArray TabbedButtonBar::getTabNames() const
|
||||
{
|
||||
StringArray names;
|
||||
|
||||
for (auto* t : tabs)
|
||||
names.add (t->name);
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
void TabbedButtonBar::setCurrentTabIndex (int newIndex, bool shouldSendChangeMessage)
|
||||
{
|
||||
if (currentTabIndex != newIndex)
|
||||
{
|
||||
if (! isPositiveAndBelow (newIndex, tabs.size()))
|
||||
newIndex = -1;
|
||||
|
||||
currentTabIndex = newIndex;
|
||||
|
||||
for (int i = 0; i < tabs.size(); ++i)
|
||||
tabs.getUnchecked(i)->button->setToggleState (i == newIndex, dontSendNotification);
|
||||
|
||||
resized();
|
||||
|
||||
if (shouldSendChangeMessage)
|
||||
sendChangeMessage();
|
||||
|
||||
currentTabChanged (newIndex, getCurrentTabName());
|
||||
}
|
||||
}
|
||||
|
||||
TabBarButton* TabbedButtonBar::getTabButton (const int index) const
|
||||
{
|
||||
if (auto* tab = tabs[index])
|
||||
return static_cast<TabBarButton*> (tab->button.get());
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int TabbedButtonBar::indexOfTabButton (const TabBarButton* button) const
|
||||
{
|
||||
for (int i = tabs.size(); --i >= 0;)
|
||||
if (tabs.getUnchecked(i)->button.get() == button)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
Rectangle<int> TabbedButtonBar::getTargetBounds (TabBarButton* button) const
|
||||
{
|
||||
if (button == nullptr || indexOfTabButton (button) == -1)
|
||||
return {};
|
||||
|
||||
auto& animator = Desktop::getInstance().getAnimator();
|
||||
|
||||
return animator.isAnimating (button) ? animator.getComponentDestination (button)
|
||||
: button->getBounds();
|
||||
}
|
||||
|
||||
void TabbedButtonBar::lookAndFeelChanged()
|
||||
{
|
||||
extraTabsButton.reset();
|
||||
resized();
|
||||
}
|
||||
|
||||
void TabbedButtonBar::paint (Graphics& g)
|
||||
{
|
||||
getLookAndFeel().drawTabbedButtonBarBackground (*this, g);
|
||||
}
|
||||
|
||||
void TabbedButtonBar::resized()
|
||||
{
|
||||
updateTabPositions (false);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void TabbedButtonBar::updateTabPositions (bool animate)
|
||||
{
|
||||
auto& lf = getLookAndFeel();
|
||||
|
||||
auto depth = getWidth();
|
||||
auto length = getHeight();
|
||||
|
||||
if (! isVertical())
|
||||
std::swap (depth, length);
|
||||
|
||||
auto overlap = lf.getTabButtonOverlap (depth) + lf.getTabButtonSpaceAroundImage() * 2;
|
||||
|
||||
auto totalLength = jmax (0, overlap);
|
||||
auto numVisibleButtons = tabs.size();
|
||||
|
||||
for (int i = 0; i < tabs.size(); ++i)
|
||||
{
|
||||
auto* tb = tabs.getUnchecked(i)->button.get();
|
||||
|
||||
totalLength += tb->getBestTabLength (depth) - overlap;
|
||||
tb->overlapPixels = jmax (0, overlap / 2);
|
||||
}
|
||||
|
||||
double scale = 1.0;
|
||||
|
||||
if (totalLength > length)
|
||||
scale = jmax (minimumScale, length / (double) totalLength);
|
||||
|
||||
const bool isTooBig = (int) (totalLength * scale) > length;
|
||||
int tabsButtonPos = 0;
|
||||
|
||||
if (isTooBig)
|
||||
{
|
||||
if (extraTabsButton == nullptr)
|
||||
{
|
||||
extraTabsButton.reset (lf.createTabBarExtrasButton());
|
||||
addAndMakeVisible (extraTabsButton.get());
|
||||
extraTabsButton->setAlwaysOnTop (true);
|
||||
extraTabsButton->setTriggeredOnMouseDown (true);
|
||||
extraTabsButton->onClick = [this] { showExtraItemsMenu(); };
|
||||
}
|
||||
|
||||
auto buttonSize = jmin (proportionOfWidth (0.7f), proportionOfHeight (0.7f));
|
||||
extraTabsButton->setSize (buttonSize, buttonSize);
|
||||
|
||||
if (isVertical())
|
||||
{
|
||||
tabsButtonPos = getHeight() - buttonSize / 2 - 1;
|
||||
extraTabsButton->setCentrePosition (getWidth() / 2, tabsButtonPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
tabsButtonPos = getWidth() - buttonSize / 2 - 1;
|
||||
extraTabsButton->setCentrePosition (tabsButtonPos, getHeight() / 2);
|
||||
}
|
||||
|
||||
totalLength = 0;
|
||||
|
||||
for (int i = 0; i < tabs.size(); ++i)
|
||||
{
|
||||
auto* tb = tabs.getUnchecked(i)->button.get();
|
||||
auto newLength = totalLength + tb->getBestTabLength (depth);
|
||||
|
||||
if (i > 0 && newLength * minimumScale > tabsButtonPos)
|
||||
{
|
||||
totalLength += overlap;
|
||||
break;
|
||||
}
|
||||
|
||||
numVisibleButtons = i + 1;
|
||||
totalLength = newLength - overlap;
|
||||
}
|
||||
|
||||
scale = jmax (minimumScale, tabsButtonPos / (double) totalLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
extraTabsButton.reset();
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
|
||||
TabBarButton* frontTab = nullptr;
|
||||
auto& animator = Desktop::getInstance().getAnimator();
|
||||
|
||||
for (int i = 0; i < tabs.size(); ++i)
|
||||
{
|
||||
if (auto* tb = getTabButton (i))
|
||||
{
|
||||
auto bestLength = roundToInt (scale * tb->getBestTabLength (depth));
|
||||
|
||||
if (i < numVisibleButtons)
|
||||
{
|
||||
auto newBounds = isVertical() ? Rectangle<int> (0, pos, getWidth(), bestLength)
|
||||
: Rectangle<int> (pos, 0, bestLength, getHeight());
|
||||
|
||||
if (animate)
|
||||
{
|
||||
animator.animateComponent (tb, newBounds, 1.0f, 200, false, 3.0, 0.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
animator.cancelAnimation (tb, false);
|
||||
tb->setBounds (newBounds);
|
||||
}
|
||||
|
||||
tb->toBack();
|
||||
|
||||
if (i == currentTabIndex)
|
||||
frontTab = tb;
|
||||
|
||||
tb->setVisible (true);
|
||||
}
|
||||
else
|
||||
{
|
||||
tb->setVisible (false);
|
||||
}
|
||||
|
||||
pos += bestLength - overlap;
|
||||
}
|
||||
}
|
||||
|
||||
behindFrontTab->setBounds (getLocalBounds());
|
||||
|
||||
if (frontTab != nullptr)
|
||||
{
|
||||
frontTab->toFront (false);
|
||||
behindFrontTab->toBehind (frontTab);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Colour TabbedButtonBar::getTabBackgroundColour (int tabIndex)
|
||||
{
|
||||
if (auto* tab = tabs[tabIndex])
|
||||
return tab->colour;
|
||||
|
||||
return Colours::transparentBlack;
|
||||
}
|
||||
|
||||
void TabbedButtonBar::setTabBackgroundColour (int tabIndex, Colour newColour)
|
||||
{
|
||||
if (auto* tab = tabs [tabIndex])
|
||||
{
|
||||
if (tab->colour != newColour)
|
||||
{
|
||||
tab->colour = newColour;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabbedButtonBar::extraItemsMenuCallback (int result, TabbedButtonBar* bar)
|
||||
{
|
||||
if (bar != nullptr && result > 0)
|
||||
bar->setCurrentTabIndex (result - 1);
|
||||
}
|
||||
|
||||
void TabbedButtonBar::showExtraItemsMenu()
|
||||
{
|
||||
PopupMenu m;
|
||||
|
||||
for (int i = 0; i < tabs.size(); ++i)
|
||||
{
|
||||
auto* tab = tabs.getUnchecked(i);
|
||||
|
||||
if (! tab->button->isVisible())
|
||||
m.addItem (i + 1, tab->name, true, i == currentTabIndex);
|
||||
}
|
||||
|
||||
m.showMenuAsync (PopupMenu::Options().withTargetComponent (extraTabsButton.get()),
|
||||
ModalCallbackFunction::forComponent (extraItemsMenuCallback, this));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void TabbedButtonBar::currentTabChanged (int, const String&) {}
|
||||
void TabbedButtonBar::popupMenuClickOnTab (int, const String&) {}
|
||||
|
||||
} // namespace juce
|
374
modules/juce_gui_basics/layout/juce_TabbedButtonBar.h
Normal file
374
modules/juce_gui_basics/layout/juce_TabbedButtonBar.h
Normal file
@ -0,0 +1,374 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class TabbedButtonBar;
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** In a TabbedButtonBar, this component is used for each of the buttons.
|
||||
|
||||
If you want to create a TabbedButtonBar with custom tab components, derive
|
||||
your component from this class, and override the TabbedButtonBar::createTabButton()
|
||||
method to create it instead of the default one.
|
||||
|
||||
@see TabbedButtonBar
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API TabBarButton : public Button
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates the tab button. */
|
||||
TabBarButton (const String& name, TabbedButtonBar& ownerBar);
|
||||
|
||||
/** Destructor. */
|
||||
~TabBarButton();
|
||||
|
||||
/** Returns the bar that contains this button. */
|
||||
TabbedButtonBar& getTabbedButtonBar() const { return owner; }
|
||||
|
||||
//==============================================================================
|
||||
/** When adding an extra component to the tab, this indicates which side of
|
||||
the text it should be placed on. */
|
||||
enum ExtraComponentPlacement
|
||||
{
|
||||
beforeText,
|
||||
afterText
|
||||
};
|
||||
|
||||
/** Sets an extra component that will be shown in the tab.
|
||||
|
||||
This optional component will be positioned inside the tab, either to the left or right
|
||||
of the text. You could use this to implement things like a close button or a graphical
|
||||
status indicator. If a non-null component is passed-in, the TabbedButtonBar will take
|
||||
ownership of it and delete it when required.
|
||||
*/
|
||||
void setExtraComponent (Component* extraTabComponent,
|
||||
ExtraComponentPlacement extraComponentPlacement);
|
||||
|
||||
/** Returns the custom component, if there is one. */
|
||||
Component* getExtraComponent() const noexcept { return extraComponent.get(); }
|
||||
|
||||
/** Returns the placement of the custom component, if there is one. */
|
||||
ExtraComponentPlacement getExtraComponentPlacement() const noexcept { return extraCompPlacement; }
|
||||
|
||||
/** Returns an area of the component that's safe to draw in.
|
||||
|
||||
This deals with the orientation of the tabs, which affects which side is
|
||||
touching the tabbed box's content component.
|
||||
*/
|
||||
Rectangle<int> getActiveArea() const;
|
||||
|
||||
/** Returns the area of the component that should contain its text. */
|
||||
Rectangle<int> getTextArea() const;
|
||||
|
||||
/** Returns this tab's index in its tab bar. */
|
||||
int getIndex() const;
|
||||
|
||||
/** Returns the colour of the tab. */
|
||||
Colour getTabBackgroundColour() const;
|
||||
|
||||
/** Returns true if this is the frontmost (selected) tab. */
|
||||
bool isFrontTab() const;
|
||||
|
||||
//==============================================================================
|
||||
/** Chooses the best length for the tab, given the specified depth.
|
||||
|
||||
If the tab is horizontal, this should return its width, and the depth
|
||||
specifies its height. If it's vertical, it should return the height, and
|
||||
the depth is actually its width.
|
||||
*/
|
||||
virtual int getBestTabLength (int depth);
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paintButton (Graphics&, bool isMouseOverButton, bool isButtonDown) override;
|
||||
/** @internal */
|
||||
void clicked (const ModifierKeys&) override;
|
||||
/** @internal */
|
||||
bool hitTest (int x, int y) override;
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
/** @internal */
|
||||
void childBoundsChanged (Component*) override;
|
||||
|
||||
protected:
|
||||
friend class TabbedButtonBar;
|
||||
TabbedButtonBar& owner;
|
||||
int overlapPixels = 0;
|
||||
|
||||
std::unique_ptr<Component> extraComponent;
|
||||
ExtraComponentPlacement extraCompPlacement = afterText;
|
||||
|
||||
private:
|
||||
void calcAreas (Rectangle<int>&, Rectangle<int>&) const;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TabBarButton)
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A vertical or horizontal bar containing tabs that you can select.
|
||||
|
||||
You can use one of these to generate things like a dialog box that has
|
||||
tabbed pages you can flip between. Attach a ChangeListener to the
|
||||
button bar to be told when the user changes the page.
|
||||
|
||||
An easier method than doing this is to use a TabbedComponent, which
|
||||
contains its own TabbedButtonBar and which takes care of the layout
|
||||
and other housekeeping.
|
||||
|
||||
@see TabbedComponent
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API TabbedButtonBar : public Component,
|
||||
public ChangeBroadcaster
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** The placement of the tab-bar
|
||||
@see setOrientation, getOrientation
|
||||
*/
|
||||
enum Orientation
|
||||
{
|
||||
TabsAtTop,
|
||||
TabsAtBottom,
|
||||
TabsAtLeft,
|
||||
TabsAtRight
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Creates a TabbedButtonBar with a given orientation.
|
||||
You can change the orientation later if you need to.
|
||||
*/
|
||||
TabbedButtonBar (Orientation orientation);
|
||||
|
||||
/** Destructor. */
|
||||
~TabbedButtonBar();
|
||||
|
||||
//==============================================================================
|
||||
/** Changes the bar's orientation.
|
||||
|
||||
This won't change the bar's actual size - you'll need to do that yourself,
|
||||
but this determines which direction the tabs go in, and which side they're
|
||||
stuck to.
|
||||
*/
|
||||
void setOrientation (Orientation orientation);
|
||||
|
||||
/** Returns the bar's current orientation.
|
||||
@see setOrientation
|
||||
*/
|
||||
Orientation getOrientation() const noexcept { return orientation; }
|
||||
|
||||
/** Returns true if the orientation is TabsAtLeft or TabsAtRight. */
|
||||
bool isVertical() const noexcept { return orientation == TabsAtLeft || orientation == TabsAtRight; }
|
||||
|
||||
/** Returns the thickness of the bar, which may be its width or height, depending on the orientation. */
|
||||
int getThickness() const noexcept { return isVertical() ? getWidth() : getHeight(); }
|
||||
|
||||
/** Changes the minimum scale factor to which the tabs can be compressed when trying to
|
||||
fit a lot of tabs on-screen.
|
||||
*/
|
||||
void setMinimumTabScaleFactor (double newMinimumScale);
|
||||
|
||||
//==============================================================================
|
||||
/** Deletes all the tabs from the bar.
|
||||
@see addTab
|
||||
*/
|
||||
void clearTabs();
|
||||
|
||||
/** Adds a tab to the bar.
|
||||
Tabs are added in left-to-right reading order.
|
||||
If this is the first tab added, it'll also be automatically selected.
|
||||
*/
|
||||
void addTab (const String& tabName,
|
||||
Colour tabBackgroundColour,
|
||||
int insertIndex);
|
||||
|
||||
/** Changes the name of one of the tabs. */
|
||||
void setTabName (int tabIndex, const String& newName);
|
||||
|
||||
/** Gets rid of one of the tabs. */
|
||||
void removeTab (int tabIndex, bool animate = false);
|
||||
|
||||
/** Moves a tab to a new index in the list.
|
||||
Pass -1 as the index to move it to the end of the list.
|
||||
*/
|
||||
void moveTab (int currentIndex, int newIndex, bool animate = false);
|
||||
|
||||
/** Returns the number of tabs in the bar. */
|
||||
int getNumTabs() const;
|
||||
|
||||
/** Returns a list of all the tab names in the bar. */
|
||||
StringArray getTabNames() const;
|
||||
|
||||
/** Changes the currently selected tab.
|
||||
This will send a change message and cause a synchronous callback to
|
||||
the currentTabChanged() method. (But if the given tab is already selected,
|
||||
nothing will be done).
|
||||
|
||||
To deselect all the tabs, use an index of -1.
|
||||
*/
|
||||
void setCurrentTabIndex (int newTabIndex, bool sendChangeMessage = true);
|
||||
|
||||
/** Returns the name of the currently selected tab.
|
||||
This could be an empty string if none are selected.
|
||||
*/
|
||||
String getCurrentTabName() const;
|
||||
|
||||
/** Returns the index of the currently selected tab.
|
||||
This could return -1 if none are selected.
|
||||
*/
|
||||
int getCurrentTabIndex() const noexcept { return currentTabIndex; }
|
||||
|
||||
/** Returns the button for a specific tab.
|
||||
The button that is returned may be deleted later by this component, so don't hang
|
||||
on to the pointer that is returned. A null pointer may be returned if the index is
|
||||
out of range.
|
||||
*/
|
||||
TabBarButton* getTabButton (int index) const;
|
||||
|
||||
/** Returns the index of a TabBarButton if it belongs to this bar. */
|
||||
int indexOfTabButton (const TabBarButton* button) const;
|
||||
|
||||
/** Returns the final bounds of this button if it is currently being animated. */
|
||||
Rectangle<int> getTargetBounds (TabBarButton* button) const;
|
||||
|
||||
//==============================================================================
|
||||
/** Callback method to indicate the selected tab has been changed.
|
||||
@see setCurrentTabIndex
|
||||
*/
|
||||
virtual void currentTabChanged (int newCurrentTabIndex,
|
||||
const String& newCurrentTabName);
|
||||
|
||||
/** Callback method to indicate that the user has right-clicked on a tab. */
|
||||
virtual void popupMenuClickOnTab (int tabIndex, const String& tabName);
|
||||
|
||||
/** Returns the colour of a tab.
|
||||
This is the colour that was specified in addTab().
|
||||
*/
|
||||
Colour getTabBackgroundColour (int tabIndex);
|
||||
|
||||
/** Changes the background colour of a tab.
|
||||
@see addTab, getTabBackgroundColour
|
||||
*/
|
||||
void setTabBackgroundColour (int tabIndex, Colour newColour);
|
||||
|
||||
//==============================================================================
|
||||
/** A set of colour IDs to use to change the colour of various aspects of the component.
|
||||
|
||||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
|
||||
methods.
|
||||
|
||||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
|
||||
*/
|
||||
enum ColourIds
|
||||
{
|
||||
tabOutlineColourId = 0x1005812, /**< The colour to use to draw an outline around the tabs. */
|
||||
tabTextColourId = 0x1005813, /**< The colour to use to draw the tab names. If this isn't specified,
|
||||
the look and feel will choose an appropriate colour. */
|
||||
frontOutlineColourId = 0x1005814, /**< The colour to use to draw an outline around the currently-selected tab. */
|
||||
frontTextColourId = 0x1005815, /**< The colour to use to draw the currently-selected tab name. If
|
||||
this isn't specified, the look and feel will choose an appropriate
|
||||
colour. */
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** This abstract base class is implemented by LookAndFeel classes to provide
|
||||
window drawing functionality.
|
||||
*/
|
||||
struct JUCE_API LookAndFeelMethods
|
||||
{
|
||||
virtual ~LookAndFeelMethods() {}
|
||||
|
||||
virtual int getTabButtonSpaceAroundImage() = 0;
|
||||
virtual int getTabButtonOverlap (int tabDepth) = 0;
|
||||
virtual int getTabButtonBestWidth (TabBarButton&, int tabDepth) = 0;
|
||||
virtual Rectangle<int> getTabButtonExtraComponentBounds (const TabBarButton&, Rectangle<int>& textArea, Component& extraComp) = 0;
|
||||
|
||||
virtual void drawTabButton (TabBarButton&, Graphics&, bool isMouseOver, bool isMouseDown) = 0;
|
||||
virtual Font getTabButtonFont (TabBarButton&, float height) = 0;
|
||||
virtual void drawTabButtonText (TabBarButton&, Graphics&, bool isMouseOver, bool isMouseDown) = 0;
|
||||
virtual void drawTabbedButtonBarBackground (TabbedButtonBar&, Graphics&) = 0;
|
||||
virtual void drawTabAreaBehindFrontButton (TabbedButtonBar&, Graphics&, int w, int h) = 0;
|
||||
|
||||
virtual void createTabButtonShape (TabBarButton&, Path& path, bool isMouseOver, bool isMouseDown) = 0;
|
||||
virtual void fillTabButtonShape (TabBarButton&, Graphics&, const Path& path, bool isMouseOver, bool isMouseDown) = 0;
|
||||
|
||||
virtual Button* createTabBarExtrasButton() = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
/** @internal */
|
||||
void lookAndFeelChanged() override;
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** This creates one of the tabs.
|
||||
|
||||
If you need to use custom tab components, you can override this method and
|
||||
return your own class instead of the default.
|
||||
*/
|
||||
virtual TabBarButton* createTabButton (const String& tabName, int tabIndex);
|
||||
|
||||
private:
|
||||
struct TabInfo
|
||||
{
|
||||
std::unique_ptr<TabBarButton> button;
|
||||
String name;
|
||||
Colour colour;
|
||||
};
|
||||
|
||||
OwnedArray<TabInfo> tabs;
|
||||
|
||||
Orientation orientation;
|
||||
double minimumScale = 0.7;
|
||||
int currentTabIndex = -1;
|
||||
|
||||
class BehindFrontTabComp;
|
||||
friend class BehindFrontTabComp;
|
||||
friend struct ContainerDeletePolicy<BehindFrontTabComp>;
|
||||
std::unique_ptr<BehindFrontTabComp> behindFrontTab;
|
||||
std::unique_ptr<Button> extraTabsButton;
|
||||
|
||||
void showExtraItemsMenu();
|
||||
static void extraItemsMenuCallback (int, TabbedButtonBar*);
|
||||
void updateTabPositions (bool animate);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TabbedButtonBar)
|
||||
};
|
||||
|
||||
} // namespace juce
|
315
modules/juce_gui_basics/layout/juce_TabbedComponent.cpp
Normal file
315
modules/juce_gui_basics/layout/juce_TabbedComponent.cpp
Normal file
@ -0,0 +1,315 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
namespace TabbedComponentHelpers
|
||||
{
|
||||
const Identifier deleteComponentId ("deleteByTabComp_");
|
||||
|
||||
static void deleteIfNecessary (Component* const comp)
|
||||
{
|
||||
if (comp != nullptr && (bool) comp->getProperties() [deleteComponentId])
|
||||
delete comp;
|
||||
}
|
||||
|
||||
static Rectangle<int> getTabArea (Rectangle<int>& content, BorderSize<int>& outline,
|
||||
const TabbedButtonBar::Orientation orientation, const int tabDepth)
|
||||
{
|
||||
switch (orientation)
|
||||
{
|
||||
case TabbedButtonBar::TabsAtTop: outline.setTop (0); return content.removeFromTop (tabDepth);
|
||||
case TabbedButtonBar::TabsAtBottom: outline.setBottom (0); return content.removeFromBottom (tabDepth);
|
||||
case TabbedButtonBar::TabsAtLeft: outline.setLeft (0); return content.removeFromLeft (tabDepth);
|
||||
case TabbedButtonBar::TabsAtRight: outline.setRight (0); return content.removeFromRight (tabDepth);
|
||||
default: jassertfalse; break;
|
||||
}
|
||||
|
||||
return Rectangle<int>();
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct TabbedComponent::ButtonBar : public TabbedButtonBar
|
||||
{
|
||||
ButtonBar (TabbedComponent& tabComp, TabbedButtonBar::Orientation o)
|
||||
: TabbedButtonBar (o), owner (tabComp)
|
||||
{
|
||||
}
|
||||
|
||||
void currentTabChanged (int newCurrentTabIndex, const String& newTabName)
|
||||
{
|
||||
owner.changeCallback (newCurrentTabIndex, newTabName);
|
||||
}
|
||||
|
||||
void popupMenuClickOnTab (int tabIndex, const String& tabName)
|
||||
{
|
||||
owner.popupMenuClickOnTab (tabIndex, tabName);
|
||||
}
|
||||
|
||||
Colour getTabBackgroundColour (const int tabIndex)
|
||||
{
|
||||
return owner.tabs->getTabBackgroundColour (tabIndex);
|
||||
}
|
||||
|
||||
TabBarButton* createTabButton (const String& tabName, int tabIndex)
|
||||
{
|
||||
return owner.createTabButton (tabName, tabIndex);
|
||||
}
|
||||
|
||||
TabbedComponent& owner;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonBar)
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
TabbedComponent::TabbedComponent (const TabbedButtonBar::Orientation orientation)
|
||||
{
|
||||
tabs.reset (new ButtonBar (*this, orientation));
|
||||
addAndMakeVisible (tabs.get());
|
||||
}
|
||||
|
||||
TabbedComponent::~TabbedComponent()
|
||||
{
|
||||
clearTabs();
|
||||
tabs.reset();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void TabbedComponent::setOrientation (const TabbedButtonBar::Orientation orientation)
|
||||
{
|
||||
tabs->setOrientation (orientation);
|
||||
resized();
|
||||
}
|
||||
|
||||
TabbedButtonBar::Orientation TabbedComponent::getOrientation() const noexcept
|
||||
{
|
||||
return tabs->getOrientation();
|
||||
}
|
||||
|
||||
void TabbedComponent::setTabBarDepth (const int newDepth)
|
||||
{
|
||||
if (tabDepth != newDepth)
|
||||
{
|
||||
tabDepth = newDepth;
|
||||
resized();
|
||||
}
|
||||
}
|
||||
|
||||
TabBarButton* TabbedComponent::createTabButton (const String& tabName, const int /*tabIndex*/)
|
||||
{
|
||||
return new TabBarButton (tabName, *tabs);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void TabbedComponent::clearTabs()
|
||||
{
|
||||
if (panelComponent != nullptr)
|
||||
{
|
||||
panelComponent->setVisible (false);
|
||||
removeChildComponent (panelComponent);
|
||||
panelComponent = nullptr;
|
||||
}
|
||||
|
||||
tabs->clearTabs();
|
||||
|
||||
for (int i = contentComponents.size(); --i >= 0;)
|
||||
TabbedComponentHelpers::deleteIfNecessary (contentComponents.getReference (i));
|
||||
|
||||
contentComponents.clear();
|
||||
}
|
||||
|
||||
void TabbedComponent::addTab (const String& tabName,
|
||||
Colour tabBackgroundColour,
|
||||
Component* const contentComponent,
|
||||
const bool deleteComponentWhenNotNeeded,
|
||||
const int insertIndex)
|
||||
{
|
||||
contentComponents.insert (insertIndex, WeakReference<Component> (contentComponent));
|
||||
|
||||
if (deleteComponentWhenNotNeeded && contentComponent != nullptr)
|
||||
contentComponent->getProperties().set (TabbedComponentHelpers::deleteComponentId, true);
|
||||
|
||||
tabs->addTab (tabName, tabBackgroundColour, insertIndex);
|
||||
resized();
|
||||
}
|
||||
|
||||
void TabbedComponent::setTabName (const int tabIndex, const String& newName)
|
||||
{
|
||||
tabs->setTabName (tabIndex, newName);
|
||||
}
|
||||
|
||||
void TabbedComponent::removeTab (const int tabIndex)
|
||||
{
|
||||
if (isPositiveAndBelow (tabIndex, contentComponents.size()))
|
||||
{
|
||||
TabbedComponentHelpers::deleteIfNecessary (contentComponents.getReference (tabIndex));
|
||||
contentComponents.remove (tabIndex);
|
||||
tabs->removeTab (tabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void TabbedComponent::moveTab (const int currentIndex, const int newIndex, const bool animate)
|
||||
{
|
||||
contentComponents.move (currentIndex, newIndex);
|
||||
tabs->moveTab (currentIndex, newIndex, animate);
|
||||
}
|
||||
|
||||
int TabbedComponent::getNumTabs() const
|
||||
{
|
||||
return tabs->getNumTabs();
|
||||
}
|
||||
|
||||
StringArray TabbedComponent::getTabNames() const
|
||||
{
|
||||
return tabs->getTabNames();
|
||||
}
|
||||
|
||||
Component* TabbedComponent::getTabContentComponent (const int tabIndex) const noexcept
|
||||
{
|
||||
return contentComponents [tabIndex];
|
||||
}
|
||||
|
||||
Colour TabbedComponent::getTabBackgroundColour (const int tabIndex) const noexcept
|
||||
{
|
||||
return tabs->getTabBackgroundColour (tabIndex);
|
||||
}
|
||||
|
||||
void TabbedComponent::setTabBackgroundColour (const int tabIndex, Colour newColour)
|
||||
{
|
||||
tabs->setTabBackgroundColour (tabIndex, newColour);
|
||||
|
||||
if (getCurrentTabIndex() == tabIndex)
|
||||
repaint();
|
||||
}
|
||||
|
||||
void TabbedComponent::setCurrentTabIndex (const int newTabIndex, const bool sendChangeMessage)
|
||||
{
|
||||
tabs->setCurrentTabIndex (newTabIndex, sendChangeMessage);
|
||||
}
|
||||
|
||||
int TabbedComponent::getCurrentTabIndex() const
|
||||
{
|
||||
return tabs->getCurrentTabIndex();
|
||||
}
|
||||
|
||||
String TabbedComponent::getCurrentTabName() const
|
||||
{
|
||||
return tabs->getCurrentTabName();
|
||||
}
|
||||
|
||||
void TabbedComponent::setOutline (const int thickness)
|
||||
{
|
||||
outlineThickness = thickness;
|
||||
resized();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void TabbedComponent::setIndent (const int indentThickness)
|
||||
{
|
||||
edgeIndent = indentThickness;
|
||||
resized();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void TabbedComponent::paint (Graphics& g)
|
||||
{
|
||||
g.fillAll (findColour (backgroundColourId));
|
||||
|
||||
auto content = getLocalBounds();
|
||||
BorderSize<int> outline (outlineThickness);
|
||||
TabbedComponentHelpers::getTabArea (content, outline, getOrientation(), tabDepth);
|
||||
|
||||
g.reduceClipRegion (content);
|
||||
g.fillAll (tabs->getTabBackgroundColour (getCurrentTabIndex()));
|
||||
|
||||
if (outlineThickness > 0)
|
||||
{
|
||||
RectangleList<int> rl (content);
|
||||
rl.subtract (outline.subtractedFrom (content));
|
||||
|
||||
g.reduceClipRegion (rl);
|
||||
g.fillAll (findColour (outlineColourId));
|
||||
}
|
||||
}
|
||||
|
||||
void TabbedComponent::resized()
|
||||
{
|
||||
auto content = getLocalBounds();
|
||||
BorderSize<int> outline (outlineThickness);
|
||||
|
||||
tabs->setBounds (TabbedComponentHelpers::getTabArea (content, outline, getOrientation(), tabDepth));
|
||||
content = BorderSize<int> (edgeIndent).subtractedFrom (outline.subtractedFrom (content));
|
||||
|
||||
for (int i = contentComponents.size(); --i >= 0;)
|
||||
if (Component* c = contentComponents.getReference(i))
|
||||
c->setBounds (content);
|
||||
}
|
||||
|
||||
void TabbedComponent::lookAndFeelChanged()
|
||||
{
|
||||
for (int i = contentComponents.size(); --i >= 0;)
|
||||
if (Component* c = contentComponents.getReference(i))
|
||||
c->lookAndFeelChanged();
|
||||
}
|
||||
|
||||
void TabbedComponent::changeCallback (const int newCurrentTabIndex, const String& newTabName)
|
||||
{
|
||||
auto* newPanelComp = getTabContentComponent (getCurrentTabIndex());
|
||||
|
||||
if (newPanelComp != panelComponent)
|
||||
{
|
||||
if (panelComponent != nullptr)
|
||||
{
|
||||
panelComponent->setVisible (false);
|
||||
removeChildComponent (panelComponent);
|
||||
}
|
||||
|
||||
panelComponent = newPanelComp;
|
||||
|
||||
if (panelComponent != nullptr)
|
||||
{
|
||||
// do these ops as two stages instead of addAndMakeVisible() so that the
|
||||
// component has always got a parent when it gets the visibilityChanged() callback
|
||||
addChildComponent (panelComponent);
|
||||
panelComponent->sendLookAndFeelChange();
|
||||
panelComponent->setVisible (true);
|
||||
panelComponent->toFront (true);
|
||||
}
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
resized();
|
||||
currentTabChanged (newCurrentTabIndex, newTabName);
|
||||
}
|
||||
|
||||
void TabbedComponent::currentTabChanged (int, const String&) {}
|
||||
void TabbedComponent::popupMenuClickOnTab (int, const String&) {}
|
||||
|
||||
} // namespace juce
|
226
modules/juce_gui_basics/layout/juce_TabbedComponent.h
Normal file
226
modules/juce_gui_basics/layout/juce_TabbedComponent.h
Normal file
@ -0,0 +1,226 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component with a TabbedButtonBar along one of its sides.
|
||||
|
||||
This makes it easy to create a set of tabbed pages, just add a bunch of tabs
|
||||
with addTab(), and this will take care of showing the pages for you when the
|
||||
user clicks on a different tab.
|
||||
|
||||
@see TabbedButtonBar
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API TabbedComponent : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates a TabbedComponent, specifying where the tabs should be placed.
|
||||
Once created, add some tabs with the addTab() method.
|
||||
*/
|
||||
explicit TabbedComponent (TabbedButtonBar::Orientation orientation);
|
||||
|
||||
/** Destructor. */
|
||||
~TabbedComponent();
|
||||
|
||||
//==============================================================================
|
||||
/** Changes the placement of the tabs.
|
||||
|
||||
This will rearrange the layout to place the tabs along the appropriate
|
||||
side of this component, and will shift the content component accordingly.
|
||||
|
||||
@see TabbedButtonBar::setOrientation
|
||||
*/
|
||||
void setOrientation (TabbedButtonBar::Orientation orientation);
|
||||
|
||||
/** Returns the current tab placement.
|
||||
@see setOrientation, TabbedButtonBar::getOrientation
|
||||
*/
|
||||
TabbedButtonBar::Orientation getOrientation() const noexcept;
|
||||
|
||||
/** Specifies how many pixels wide or high the tab-bar should be.
|
||||
|
||||
If the tabs are placed along the top or bottom, this specified the height
|
||||
of the bar; if they're along the left or right edges, it'll be the width
|
||||
of the bar.
|
||||
*/
|
||||
void setTabBarDepth (int newDepth);
|
||||
|
||||
/** Returns the current thickness of the tab bar.
|
||||
@see setTabBarDepth
|
||||
*/
|
||||
int getTabBarDepth() const noexcept { return tabDepth; }
|
||||
|
||||
/** Specifies the thickness of an outline that should be drawn around the content component.
|
||||
|
||||
If this thickness is > 0, a line will be drawn around the three sides of the content
|
||||
component which don't touch the tab-bar, and the content component will be inset by this amount.
|
||||
|
||||
To set the colour of the line, use setColour (outlineColourId, ...).
|
||||
*/
|
||||
void setOutline (int newThickness);
|
||||
|
||||
/** Specifies a gap to leave around the edge of the content component.
|
||||
Each edge of the content component will be indented by the given number of pixels.
|
||||
*/
|
||||
void setIndent (int indentThickness);
|
||||
|
||||
//==============================================================================
|
||||
/** Removes all the tabs from the bar.
|
||||
@see TabbedButtonBar::clearTabs
|
||||
*/
|
||||
void clearTabs();
|
||||
|
||||
/** Adds a tab to the tab-bar.
|
||||
|
||||
The component passed in will be shown for the tab. If deleteComponentWhenNotNeeded
|
||||
is true, then the TabbedComponent will take ownership of the component and will delete
|
||||
it when the tab is removed or when this object is deleted.
|
||||
|
||||
@see TabbedButtonBar::addTab
|
||||
*/
|
||||
void addTab (const String& tabName,
|
||||
Colour tabBackgroundColour,
|
||||
Component* contentComponent,
|
||||
bool deleteComponentWhenNotNeeded,
|
||||
int insertIndex = -1);
|
||||
|
||||
/** Changes the name of one of the tabs. */
|
||||
void setTabName (int tabIndex, const String& newName);
|
||||
|
||||
/** Gets rid of one of the tabs. */
|
||||
void removeTab (int tabIndex);
|
||||
|
||||
/** Moves a tab to a new index in the list.
|
||||
Pass -1 as the index to move it to the end of the list.
|
||||
*/
|
||||
void moveTab (int currentIndex, int newIndex, bool animate = false);
|
||||
|
||||
/** Returns the number of tabs in the bar. */
|
||||
int getNumTabs() const;
|
||||
|
||||
/** Returns a list of all the tab names in the bar. */
|
||||
StringArray getTabNames() const;
|
||||
|
||||
/** Returns the content component that was added for the given index.
|
||||
Be careful not to reposition or delete the components that are returned, as
|
||||
this will interfere with the TabbedComponent's behaviour.
|
||||
*/
|
||||
Component* getTabContentComponent (int tabIndex) const noexcept;
|
||||
|
||||
/** Returns the colour of one of the tabs. */
|
||||
Colour getTabBackgroundColour (int tabIndex) const noexcept;
|
||||
|
||||
/** Changes the background colour of one of the tabs. */
|
||||
void setTabBackgroundColour (int tabIndex, Colour newColour);
|
||||
|
||||
//==============================================================================
|
||||
/** Changes the currently-selected tab.
|
||||
To deselect all the tabs, pass -1 as the index.
|
||||
@see TabbedButtonBar::setCurrentTabIndex
|
||||
*/
|
||||
void setCurrentTabIndex (int newTabIndex, bool sendChangeMessage = true);
|
||||
|
||||
/** Returns the index of the currently selected tab.
|
||||
@see addTab, TabbedButtonBar::getCurrentTabIndex()
|
||||
*/
|
||||
int getCurrentTabIndex() const;
|
||||
|
||||
/** Returns the name of the currently selected tab.
|
||||
@see addTab, TabbedButtonBar::getCurrentTabName()
|
||||
*/
|
||||
String getCurrentTabName() const;
|
||||
|
||||
/** Returns the current component that's filling the panel.
|
||||
This will return nullptr if there isn't one.
|
||||
*/
|
||||
Component* getCurrentContentComponent() const noexcept { return panelComponent; }
|
||||
|
||||
//==============================================================================
|
||||
/** Callback method to indicate the selected tab has been changed.
|
||||
@see setCurrentTabIndex
|
||||
*/
|
||||
virtual void currentTabChanged (int newCurrentTabIndex, const String& newCurrentTabName);
|
||||
|
||||
/** Callback method to indicate that the user has right-clicked on a tab. */
|
||||
virtual void popupMenuClickOnTab (int tabIndex, const String& tabName);
|
||||
|
||||
/** Returns the tab button bar component that is being used. */
|
||||
TabbedButtonBar& getTabbedButtonBar() const noexcept { return *tabs; }
|
||||
|
||||
//==============================================================================
|
||||
/** A set of colour IDs to use to change the colour of various aspects of the component.
|
||||
|
||||
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
|
||||
methods.
|
||||
|
||||
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
|
||||
*/
|
||||
enum ColourIds
|
||||
{
|
||||
backgroundColourId = 0x1005800, /**< The colour to fill the background behind the tabs. */
|
||||
outlineColourId = 0x1005801, /**< The colour to use to draw an outline around the content.
|
||||
(See setOutline) */
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
/** @internal */
|
||||
void lookAndFeelChanged() override;
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** This creates one of the tab buttons.
|
||||
|
||||
If you need to use custom tab components, you can override this method and
|
||||
return your own class instead of the default.
|
||||
*/
|
||||
virtual TabBarButton* createTabButton (const String& tabName, int tabIndex);
|
||||
|
||||
/** @internal */
|
||||
std::unique_ptr<TabbedButtonBar> tabs;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
Array<WeakReference<Component>> contentComponents;
|
||||
WeakReference<Component> panelComponent;
|
||||
int tabDepth = 30, outlineThickness = 1, edgeIndent = 0;
|
||||
|
||||
struct ButtonBar;
|
||||
void changeCallback (int newCurrentTabIndex, const String& newTabName);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TabbedComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
634
modules/juce_gui_basics/layout/juce_Viewport.cpp
Normal file
634
modules/juce_gui_basics/layout/juce_Viewport.cpp
Normal file
@ -0,0 +1,634 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
Viewport::Viewport (const String& name) : 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);
|
||||
|
||||
#if JUCE_ANDROID || JUCE_IOS
|
||||
setScrollOnDragEnabled (true);
|
||||
#endif
|
||||
|
||||
recreateScrollbars();
|
||||
}
|
||||
|
||||
Viewport::~Viewport()
|
||||
{
|
||||
setScrollOnDragEnabled (false);
|
||||
deleteOrRemoveContentComp();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void Viewport::visibleAreaChanged (const Rectangle<int>&) {}
|
||||
void Viewport::viewedComponentChanged (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<Component> oldCompDeleter (contentComp);
|
||||
contentComp = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
contentHolder.removeChildComponent (contentComp);
|
||||
contentComp = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::setViewedComponent (Component* const newViewedComponent, const bool deleteComponentWhenNoLongerNeeded)
|
||||
{
|
||||
if (contentComp.get() != newViewedComponent)
|
||||
{
|
||||
deleteOrRemoveContentComp();
|
||||
contentComp = newViewedComponent;
|
||||
deleteContent = deleteComponentWhenNoLongerNeeded;
|
||||
|
||||
if (contentComp != nullptr)
|
||||
{
|
||||
contentHolder.addAndMakeVisible (contentComp);
|
||||
setViewPosition (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(); }
|
||||
|
||||
Point<int> Viewport::viewportPosToCompPos (Point<int> pos) const
|
||||
{
|
||||
jassert (contentComp != nullptr);
|
||||
|
||||
auto contentBounds = contentHolder.getLocalArea (contentComp, contentComp->getLocalBounds());
|
||||
|
||||
Point<int> p (jmax (jmin (0, contentHolder.getWidth() - contentBounds.getWidth()), jmin (0, -(pos.x))),
|
||||
jmax (jmin (0, contentHolder.getHeight() - contentBounds.getHeight()), 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 (Point<int> newPosition)
|
||||
{
|
||||
if (contentComp != nullptr)
|
||||
contentComp->setTopLeftPosition (viewportPosToCompPos (newPosition));
|
||||
}
|
||||
|
||||
void Viewport::setViewPositionProportionately (const double x, const double y)
|
||||
{
|
||||
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 (getHorizontalScrollBar().isVisible() || canScrollHorizontally())
|
||||
{
|
||||
if (mouseX < activeBorderThickness)
|
||||
dx = activeBorderThickness - mouseX;
|
||||
else if (mouseX >= contentHolder.getWidth() - activeBorderThickness)
|
||||
dx = (contentHolder.getWidth() - activeBorderThickness) - mouseX;
|
||||
|
||||
if (dx < 0)
|
||||
dx = jmax (dx, -maximumSpeed, contentHolder.getWidth() - contentComp->getRight());
|
||||
else
|
||||
dx = 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 = jmax (dy, -maximumSpeed, contentHolder.getHeight() - contentComp->getBottom());
|
||||
else
|
||||
dy = 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 (Component&, bool, bool)
|
||||
{
|
||||
updateVisibleArea();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
typedef AnimatedPosition<AnimatedPositionBehaviours::ContinuousWithMomentum> ViewportDragPosition;
|
||||
|
||||
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()
|
||||
{
|
||||
viewport.contentHolder.removeMouseListener (this);
|
||||
Desktop::getInstance().removeGlobalMouseListener (this);
|
||||
}
|
||||
|
||||
void positionChanged (ViewportDragPosition&, double) override
|
||||
{
|
||||
viewport.setViewPosition (originalViewPos - Point<int> ((int) offsetX.getPosition(),
|
||||
(int) offsetY.getPosition()));
|
||||
}
|
||||
|
||||
void mouseDown (const 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);
|
||||
Desktop::getInstance().addGlobalMouseListener (this);
|
||||
|
||||
isGlobalMouseListener = true;
|
||||
}
|
||||
}
|
||||
|
||||
void mouseDrag (const MouseEvent& e) override
|
||||
{
|
||||
if (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 MouseEvent&) override
|
||||
{
|
||||
if (isGlobalMouseListener && Desktop::getInstance().getNumDraggingMouseSources() == 0)
|
||||
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<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;
|
||||
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-caclulate..
|
||||
if (oldContentBounds == contentComp->getBounds())
|
||||
break;
|
||||
}
|
||||
|
||||
Rectangle<int> contentBounds;
|
||||
if (contentComp != nullptr)
|
||||
contentBounds = contentHolder.getLocalArea (contentComp, contentComp->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);
|
||||
hbar.cancelPendingUpdate();
|
||||
|
||||
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);
|
||||
vbar.cancelPendingUpdate();
|
||||
|
||||
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 Rectangle<int> visibleArea (visibleOrigin.x, visibleOrigin.y,
|
||||
jmin (contentBounds.getWidth() - visibleOrigin.x, contentArea.getWidth()),
|
||||
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 (ScrollBar* scrollBarThatHasMoved, double newRangeStart)
|
||||
{
|
||||
auto newRangeStartInt = roundToInt (newRangeStart);
|
||||
|
||||
if (scrollBarThatHasMoved == horizontalScrollBar.get())
|
||||
{
|
||||
setViewPosition (newRangeStartInt, getViewPositionY());
|
||||
}
|
||||
else if (scrollBarThatHasMoved == verticalScrollBar.get())
|
||||
{
|
||||
setViewPosition (getViewPositionX(), newRangeStartInt);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
|
||||
{
|
||||
if (! useMouseWheelMoveIfNeeded (e, wheel))
|
||||
Component::mouseWheelMove (e, wheel);
|
||||
}
|
||||
|
||||
static int rescaleMouseWheelDistance (float distance, int singleStepSize) noexcept
|
||||
{
|
||||
if (distance == 0.0f)
|
||||
return 0;
|
||||
|
||||
distance *= 14.0f * singleStepSize;
|
||||
|
||||
return roundToInt (distance < 0 ? jmin (distance, -1.0f)
|
||||
: jmax (distance, 1.0f));
|
||||
}
|
||||
|
||||
bool Viewport::useMouseWheelMoveIfNeeded (const MouseEvent& e, const 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 KeyPress& key)
|
||||
{
|
||||
return key == KeyPress::upKey
|
||||
|| key == KeyPress::downKey
|
||||
|| key == KeyPress::pageUpKey
|
||||
|| key == KeyPress::pageDownKey
|
||||
|| key == KeyPress::homeKey
|
||||
|| key == KeyPress::endKey;
|
||||
}
|
||||
|
||||
static bool isLeftRightKeyPress (const KeyPress& key)
|
||||
{
|
||||
return key == KeyPress::leftKey
|
||||
|| key == KeyPress::rightKey;
|
||||
}
|
||||
|
||||
bool Viewport::keyPressed (const 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 KeyPress& key)
|
||||
{
|
||||
return isUpDownKeyPress (key) || isLeftRightKeyPress (key);
|
||||
}
|
||||
|
||||
ScrollBar* Viewport::createScrollBarComponent (bool isVertical)
|
||||
{
|
||||
return new ScrollBar (isVertical);
|
||||
}
|
||||
|
||||
void Viewport::setScrollBarPosition (bool verticalScrollbarOnRight,
|
||||
bool horizontalScrollbarAtBottom)
|
||||
{
|
||||
vScrollbarRight = verticalScrollbarOnRight;
|
||||
hScrollbarBottom = horizontalScrollbarAtBottom;
|
||||
|
||||
resized();
|
||||
}
|
||||
|
||||
} // namespace juce
|
342
modules/juce_gui_basics/layout/juce_Viewport.h
Normal file
342
modules/juce_gui_basics/layout/juce_Viewport.h
Normal file
@ -0,0 +1,342 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
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.
|
||||
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API Viewport : public Component,
|
||||
private ComponentListener,
|
||||
private 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 String& componentName = 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 (Component* newViewedComponent,
|
||||
bool deleteComponentWhenNoLongerNeeded = true);
|
||||
|
||||
/** Returns the component that's currently being used inside the Viewport.
|
||||
|
||||
@see setViewedComponent
|
||||
*/
|
||||
Component* getViewedComponent() const noexcept { return contentComp; }
|
||||
|
||||
//==============================================================================
|
||||
/** 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 (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. */
|
||||
Point<int> getViewPosition() const noexcept { return lastVisibleArea.getPosition(); }
|
||||
|
||||
/** Returns the visible area of the child component, relative to its top-left */
|
||||
Rectangle<int> getViewArea() const noexcept { return lastVisibleArea; }
|
||||
|
||||
/** 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 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.
|
||||
|
||||
The allowVerticalScrollingWithoutScrollbar parameters allow you to enable
|
||||
mouse-wheel scrolling even when there the scrollbars are hidden. When the
|
||||
scrollbars are visible, these parameters are ignored.
|
||||
*/
|
||||
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);
|
||||
|
||||
/** Returns a pointer to the scrollbar component being used.
|
||||
Handy if you need to customise the bar somehow.
|
||||
*/
|
||||
ScrollBar& getVerticalScrollBar() noexcept { return *verticalScrollBar; }
|
||||
|
||||
/** Returns a pointer to the scrollbar component being used.
|
||||
Handy if you need to customise the bar somehow.
|
||||
*/
|
||||
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. */
|
||||
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 (ScrollBar*, double newRangeStart) override;
|
||||
/** @internal */
|
||||
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override;
|
||||
/** @internal */
|
||||
bool keyPressed (const KeyPress&) override;
|
||||
/** @internal */
|
||||
void componentMovedOrResized (Component&, bool wasMoved, bool wasResized) override;
|
||||
/** @internal */
|
||||
void lookAndFeelChanged() override;
|
||||
/** @internal */
|
||||
bool useMouseWheelMoveIfNeeded (const MouseEvent&, const MouseWheelDetails&);
|
||||
/** @internal */
|
||||
static bool respondsToKey (const KeyPress&);
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** Creates the Scrollbar components that will be added to the Viewport.
|
||||
Subclasses can override this if they need to customise the scrollbars in some way.
|
||||
*/
|
||||
virtual ScrollBar* createScrollBarComponent (bool isVertical);
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
std::unique_ptr<ScrollBar> verticalScrollBar, horizontalScrollBar;
|
||||
Component contentHolder;
|
||||
WeakReference<Component> contentComp;
|
||||
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;
|
||||
|
||||
struct DragToScrollListener;
|
||||
friend struct DragToScrollListener;
|
||||
friend struct ContainerDeletePolicy<DragToScrollListener>;
|
||||
std::unique_ptr<DragToScrollListener> dragToScrollListener;
|
||||
|
||||
Point<int> viewportPosToCompPos (Point<int>) const;
|
||||
|
||||
void updateVisibleArea();
|
||||
void deleteOrRemoveContentComp();
|
||||
|
||||
#if JUCE_CATCH_DEPRECATED_CODE_MISUSE
|
||||
// If you get an error here, it's because this method's parameters have changed! See the new definition above..
|
||||
virtual int visibleAreaChanged (int, int, int, int) { return 0; }
|
||||
#endif
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Viewport)
|
||||
};
|
||||
|
||||
} // namespace juce
|
Reference in New Issue
Block a user