1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-14 00:14:18 +00:00

Added Animated App template and examples

This commit is contained in:
Felix Faire 2014-10-29 15:55:23 +00:00
parent fefcf7aca6
commit ff6520a89a
1141 changed files with 438491 additions and 94 deletions

View file

@ -0,0 +1,209 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_ANIMATEDPOSITION_H_INCLUDED
#define JUCE_ANIMATEDPOSITION_H_INCLUDED
//==============================================================================
/**
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
*/
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)
{
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()
{
startTimer (1000 / 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)
{
startTimer (100);
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)
{
const double elapsedSecs = jmax (0.005, (now - last).inSeconds());
const double v = (newPos - lastPos) / elapsedSecs;
return std::abs (v) > 0.2 ? v : 0.0;
}
void moveTo (double newPos)
{
const Time 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 (&Listener::positionChanged, *this, newPosition);
}
}
void timerCallback() override
{
const Time now = Time::getCurrentTime();
const double elapsed = jlimit (0.001, 0.020, (now - lastUpdate).inSeconds());
lastUpdate = now;
const double newPos = behaviour.getNextPosition (position, elapsed);
if (behaviour.isStopped (newPos))
stopTimer();
else
startTimer (1000 / 60);
setPositionAndSendChange (newPos);
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnimatedPosition)
};
#endif // JUCE_ANIMATEDPOSITION_H_INCLUDED

View file

@ -0,0 +1,151 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_ANIMATEDPOSITIONBEHAVIOURS_H_INCLUDED
#define JUCE_ANIMATEDPOSITIONBEHAVIOURS_H_INCLUDED
//==============================================================================
/** 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
*/
struct ContinuousWithMomentum
{
ContinuousWithMomentum() noexcept
: velocity (0), damping (0.92)
{
}
/** 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;
}
/** 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) < 0.05)
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;
}
private:
double velocity, damping;
};
//==============================================================================
/** 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
*/
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;
};
};
#endif // JUCE_ANIMATEDPOSITIONBEHAVIOURS_H_INCLUDED

View file

@ -0,0 +1,328 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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 = new ProxyComponent (*component);
else
proxy = nullptr;
component->setVisible (! useProxyComponent);
}
bool useTimeslice (const int elapsed)
{
if (Component* const c = proxy != nullptr ? static_cast<Component*> (proxy)
: static_cast<Component*> (component))
{
msElapsed += elapsed;
double newProgress = msElapsed / (double) msTotal;
if (newProgress >= 0 && newProgress < 1.0)
{
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;
}
}
if (isChangingAlpha)
{
alpha += (destAlpha - alpha) * delta;
c->setAlpha ((float) alpha);
stillBusy = true;
}
if (stillBusy)
return true;
}
}
}
moveToFinalDestination();
return false;
}
void moveToFinalDestination()
{
if (component != nullptr)
{
component->setAlpha ((float) destAlpha);
component->setBounds (destination);
if (proxy != nullptr)
component->setVisible (destAlpha > 0);
}
}
//==============================================================================
class ProxyComponent : public Component
{
public:
ProxyComponent (Component& c)
{
setWantsKeyboardFocus (false);
setBounds (c.getBounds());
setTransform (c.getTransform());
setAlpha (c.getAlpha());
setInterceptsMouseClicks (false, false);
if (Component* const 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..
const float 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;
ScopedPointer<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));
}
};
//==============================================================================
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)
{
AnimationTask* 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();
startTimer (1000 / 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 (AnimationTask* const at = findTaskFor (component))
{
if (moveComponentToItsFinalPosition)
at->moveToFinalDestination();
tasks.removeObject (at);
sendChangeMessage();
}
}
Rectangle<int> ComponentAnimator::getComponentDestination (Component* const component)
{
jassert (component != nullptr);
if (AnimationTask* const 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()
{
const uint32 timeNow = Time::getMillisecondCounter();
if (lastTime == 0 || lastTime == timeNow)
lastTime = timeNow;
const int elapsed = (int) (timeNow - lastTime);
for (int i = tasks.size(); --i >= 0;)
{
if (! tasks.getUnchecked(i)->useTimeslice (elapsed))
{
tasks.remove (i);
sendChangeMessage();
}
}
lastTime = timeNow;
if (tasks.size() == 0)
stopTimer();
}

View file

@ -0,0 +1,161 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_COMPONENTANIMATOR_H_INCLUDED
#define JUCE_COMPONENTANIMATOR_H_INCLUDED
//==============================================================================
/**
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
*/
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();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentAnimator)
};
#endif // JUCE_COMPONENTANIMATOR_H_INCLUDED

View file

@ -0,0 +1,301 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
ComponentBoundsConstrainer::ComponentBoundsConstrainer() noexcept
: minW (0), maxW (0x3fffffff),
minH (0), maxH (0x3fffffff),
minOffTop (0),
minOffLeft (0),
minOffBottom (0),
minOffRight (0),
aspectRatio (0.0)
{
}
ComponentBoundsConstrainer::~ComponentBoundsConstrainer()
{
}
//==============================================================================
void ComponentBoundsConstrainer::setMinimumWidth (const int minimumWidth) noexcept { minW = minimumWidth; }
void ComponentBoundsConstrainer::setMaximumWidth (const int maximumWidth) noexcept { maxW = maximumWidth; }
void ComponentBoundsConstrainer::setMinimumHeight (const int minimumHeight) noexcept { minH = minimumHeight; }
void ComponentBoundsConstrainer::setMaximumHeight (const int maximumHeight) noexcept { maxH = maximumHeight; }
void ComponentBoundsConstrainer::setMinimumSize (const int minimumWidth, const 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 (const int maximumWidth, const 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 (const int minimumWidth,
const int minimumHeight,
const int maximumWidth,
const 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 (const int minimumWhenOffTheTop,
const int minimumWhenOffTheLeft,
const int minimumWhenOffTheBottom,
const int minimumWhenOffTheRight) noexcept
{
minOffTop = minimumWhenOffTheTop;
minOffLeft = minimumWhenOffTheLeft;
minOffBottom = minimumWhenOffTheBottom;
minOffRight = minimumWhenOffTheRight;
}
void ComponentBoundsConstrainer::setFixedAspectRatio (const double widthOverHeight) noexcept
{
aspectRatio = jmax (0.0, widthOverHeight);
}
double ComponentBoundsConstrainer::getFixedAspectRatio() const noexcept
{
return aspectRatio;
}
void ComponentBoundsConstrainer::setBoundsForComponent (Component* const component,
const Rectangle<int>& targetBounds,
const bool isStretchingTop,
const bool isStretchingLeft,
const bool isStretchingBottom,
const bool isStretchingRight)
{
jassert (component != nullptr);
Rectangle<int> limits, bounds (targetBounds);
BorderSize<int> border;
if (Component* const parent = component->getParentComponent())
{
limits.setSize (parent->getWidth(), parent->getHeight());
}
else
{
if (ComponentPeer* const peer = component->getPeer())
border = peer->getFrameSize();
limits = Desktop::getInstance().getDisplays().getDisplayContaining (bounds.getCentre()).userArea;
}
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,
const Rectangle<int>& bounds)
{
if (Component::Positioner* const 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,
const bool isStretchingTop,
const bool isStretchingLeft,
const bool isStretchingBottom,
const 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());
}

View file

@ -0,0 +1,197 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_COMPONENTBOUNDSCONSTRAINER_H_INCLUDED
#define JUCE_COMPONENTBOUNDSCONSTRAINER_H_INCLUDED
//==============================================================================
/**
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
*/
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,
const 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* component,
const Rectangle<int>& bounds);
private:
//==============================================================================
int minW, maxW, minH, maxH;
int minOffTop, minOffLeft, minOffBottom, minOffRight;
double aspectRatio;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentBoundsConstrainer)
};
#endif // JUCE_COMPONENTBOUNDSCONSTRAINER_H_INCLUDED

View file

@ -0,0 +1,280 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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 (int i = c.getNumChildComponents(); --i >= 0;)
if (Component* const child = findComponentWithID (*c.getChildComponent (i), compId))
return child;
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() == static_cast <Component*> (component));
#endif
}
Component* ComponentBuilder::getManagedComponent()
{
if (component == nullptr)
{
component = createComponent();
#if JUCE_DEBUG
componentRef = component;
#endif
}
return component;
}
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()
{
Drawable::registerDrawableTypeHandlers (*this);
}
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&)
{
ComponentBuilderHelpers::updateComponent (*this, tree);
}
void ComponentBuilder::valueTreeChildOrderChanged (ValueTree& tree)
{
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;
const int 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));
const int newNumChildren = children.getNumChildren();
for (int i = 0; i < newNumChildren; ++i)
{
const ValueTree childState (children.getChild (i));
Component* c = removeComponentWithID (existingComponents, getStateId (childState));
if (c == nullptr)
{
if (TypeHandler* const 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));
}
}

View file

@ -0,0 +1,245 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_COMPONENTBUILDER_H_INCLUDED
#define JUCE_COMPONENTBUILDER_H_INCLUDED
//==============================================================================
/**
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.
*/
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;
ScopedPointer<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&) override;
void valueTreeChildOrderChanged (ValueTree&) override;
void valueTreeParentChanged (ValueTree&) override;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentBuilder)
};
#endif // JUCE_COMPONENTBUILDER_H_INCLUDED

View file

@ -0,0 +1,139 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
ComponentMovementWatcher::ComponentMovementWatcher (Component* const comp)
: component (comp),
lastPeerID (0),
reentrant (false),
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);
ComponentPeer* const peer = component->getPeer();
const uint32 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;
Component* const 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 (Component* p = component->getParentComponent(); p != nullptr; p = p->getParentComponent())
{
p->addComponentListener (this);
registeredParentComps.add (p);
}
}
void ComponentMovementWatcher::unregister()
{
for (int i = registeredParentComps.size(); --i >= 0;)
registeredParentComps.getUnchecked(i)->removeComponentListener (this);
registeredParentComps.clear();
}

View file

@ -0,0 +1,95 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_COMPONENTMOVEMENTWATCHER_H_INCLUDED
#define JUCE_COMPONENTMOVEMENTWATCHER_H_INCLUDED
//==============================================================================
/** 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 or QuickTimeComponent
because they need to keep their custom windows in the right place and respond to
changes in the peer.
*/
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;
Array <Component*> registeredParentComps;
bool reentrant, wasShowing;
Rectangle<int> lastBounds;
void unregister();
void registerWithParentComps();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentMovementWatcher)
};
#endif // JUCE_COMPONENTMOVEMENTWATCHER_H_INCLUDED

View file

@ -0,0 +1,411 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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
{
const Rectangle<int> area (getWidth(), getHeaderSize());
g.reduceClipRegion (area);
getLookAndFeel().drawConcertinaPanelHeader (g, area, isMouseOver(), isMouseButtonDown(),
getPanel(), *component);
}
void resized() override
{
component->setBounds (getLocalBounds().withTop (getHeaderSize()));
}
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);
}
OptionalScopedPointer<Component> component;
private:
PanelSizes dragStartSizes;
int mouseDownY;
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() {}
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 int index = indexOfComp (component);
jassert (index >= 0); // The specified component doesn't seem to have been added!
if (index >= 0)
{
currentSizes->get(index).minSize = headerSize;
resized();
}
}
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);
}

View file

@ -0,0 +1,131 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_CONCERTINAPANEL_H_INCLUDED
#define JUCE_CONCERTINAPANEL_H_INCLUDED
//==============================================================================
/**
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.
*/
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, const 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);
//==============================================================================
/** 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>;
ScopedPointer<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)
};
#endif // JUCE_CONCERTINAPANEL_H_INCLUDED

View file

@ -0,0 +1,66 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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(); }

View file

@ -0,0 +1,111 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_GROUPCOMPONENT_H_INCLUDED
#define JUCE_GROUPCOMPONENT_H_INCLUDED
//==============================================================================
/**
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.
*/
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::empty,
const String& labelText = String::empty);
/** 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)
};
#endif // JUCE_GROUPCOMPONENT_H_INCLUDED

View file

@ -0,0 +1,507 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
MultiDocumentPanelWindow::MultiDocumentPanelWindow (Colour backgroundColour)
: DocumentWindow (String::empty, backgroundColour,
DocumentWindow::maximiseButton | DocumentWindow::closeButton, false)
{
}
MultiDocumentPanelWindow::~MultiDocumentPanelWindow()
{
}
//==============================================================================
void MultiDocumentPanelWindow::maximiseButtonPressed()
{
if (MultiDocumentPanel* const owner = getOwner())
owner->setLayoutMode (MultiDocumentPanel::MaximisedWindowsWithTabs);
else
jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel!
}
void MultiDocumentPanelWindow::closeButtonPressed()
{
if (MultiDocumentPanel* const 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 (MultiDocumentPanel* const owner = getOwner())
owner->updateOrder();
}
MultiDocumentPanel* MultiDocumentPanelWindow::getOwner() const noexcept
{
return findParentComponentOfClass<MultiDocumentPanel>();
}
//==============================================================================
class MultiDocumentPanel::TabbedComponentInternal : public TabbedComponent
{
public:
TabbedComponentInternal()
: TabbedComponent (TabbedButtonBar::TabsAtTop)
{
}
void currentTabChanged (int, const String&)
{
if (MultiDocumentPanel* const owner = findParentComponentOfClass<MultiDocumentPanel>())
owner->updateOrder();
}
};
//==============================================================================
MultiDocumentPanel::MultiDocumentPanel()
: mode (MaximisedWindowsWithTabs),
backgroundColour (Colours::lightblue),
maximumNumDocuments (0),
numDocsBeforeTabsUsed (0)
{
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.size() > 0)
if (! closeDocument (components.getLast(), checkItsOkToCloseFirst))
return false;
return true;
}
MultiDocumentPanelWindow* MultiDocumentPanel::createNewDocumentWindow()
{
return new MultiDocumentPanelWindow (backgroundColour);
}
void MultiDocumentPanel::addWindow (Component* component)
{
MultiDocumentPanelWindow* const dw = createNewDocumentWindow();
dw->setResizable (true, false);
dw->setContentNonOwned (component, true);
dw->setName (component->getName());
const var bkg (component->getProperties() ["mdiDocumentBkg_"]);
dw->setBackgroundColour (bkg.isVoid() ? backgroundColour : Colour ((uint32) static_cast <int> (bkg)));
int x = 4;
if (Component* const topComp = getChildComponent (getNumChildComponents() - 1))
if (topComp->getX() == x && topComp->getY() == x)
x += 16;
dw->setTopLeftPosition (x, x);
const var 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)
{
addAndMakeVisible (tabComponent = new TabbedComponentInternal());
Array <Component*> temp (components);
for (int i = 0; i < temp.size(); ++i)
tabComponent->addTab (temp[i]->getName(), docColour, temp[i], 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 (int i = getNumChildComponents(); --i >= 0;)
{
if (MultiDocumentPanelWindow* const dw = dynamic_cast <MultiDocumentPanelWindow*> (getChildComponent (i)))
{
if (dw->getContentComponent() == component)
{
ScopedPointer<MultiDocumentPanelWindow> (dw)->clearContentComponent();
break;
}
}
}
if (shouldDelete)
delete component;
components.removeFirstMatchingValue (component);
if (isFullscreenWhenOneDocument() && components.size() == 1)
{
for (int i = getNumChildComponents(); --i >= 0;)
{
ScopedPointer<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 = nullptr;
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 (Component* 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 (int i = getNumChildComponents(); --i >= 0;)
if (MultiDocumentPanelWindow* const dw = dynamic_cast <MultiDocumentPanelWindow*> (getChildComponent (i)))
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 = nullptr;
}
else
{
for (int i = getNumChildComponents(); --i >= 0;)
{
ScopedPointer<MultiDocumentPanelWindow> dw (dynamic_cast <MultiDocumentPanelWindow*> (getChildComponent (i)));
if (dw != nullptr)
{
dw->getContentComponent()->getProperties().set ("mdiDocumentPos_", dw->getWindowStateAsString());
dw->clearContentComponent();
}
}
}
resized();
const Array <Component*> tempComps (components);
components.clear();
for (int i = 0; i < tempComps.size(); ++i)
{
Component* const c = tempComps.getUnchecked(i);
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 (int i = getNumChildComponents(); --i >= 0;)
getChildComponent (i)->setBounds (getLocalBounds());
}
setWantsKeyboardFocus (components.size() == 0);
}
Component* MultiDocumentPanel::getContainerComp (Component* c) const
{
if (mode == FloatingWindows)
{
for (int i = 0; i < getNumChildComponents(); ++i)
if (MultiDocumentPanelWindow* const dw = dynamic_cast <MultiDocumentPanelWindow*> (getChildComponent (i)))
if (dw->getContentComponent() == c)
return dw;
}
return c;
}
void MultiDocumentPanel::componentNameChanged (Component&)
{
if (mode == FloatingWindows)
{
for (int i = 0; i < getNumChildComponents(); ++i)
if (MultiDocumentPanelWindow* const dw = dynamic_cast <MultiDocumentPanelWindow*> (getChildComponent (i)))
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()
{
const Array <Component*> oldList (components);
if (mode == FloatingWindows)
{
components.clear();
for (int i = 0; i < getNumChildComponents(); ++i)
if (MultiDocumentPanelWindow* const dw = dynamic_cast <MultiDocumentPanelWindow*> (getChildComponent (i)))
components.add (dw->getContentComponent());
}
else
{
if (tabComponent != nullptr)
{
if (Component* const current = tabComponent->getCurrentContentComponent())
{
components.removeFirstMatchingValue (current);
components.add (current);
}
}
}
if (components != oldList)
activeDocumentChanged();
}

View file

@ -0,0 +1,304 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_MULTIDOCUMENTPANEL_H_INCLUDED
#define JUCE_MULTIDOCUMENTPANEL_H_INCLUDED
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
*/
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.
*/
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; }
//==============================================================================
/** 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;
Array <Component*> components;
ScopedPointer<TabbedComponent> tabComponent;
Colour backgroundColour;
int maximumNumDocuments, numDocsBeforeTabsUsed;
class TabbedComponentInternal;
friend class MultiDocumentPanelWindow;
friend class TabbedComponentInternal;
Component* getContainerComp (Component*) const;
void updateOrder();
void addWindow (Component*);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiDocumentPanel)
};
#endif // JUCE_MULTIDOCUMENTPANEL_H_INCLUDED

View file

@ -0,0 +1,201 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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());
}
}

View file

@ -0,0 +1,195 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_RESIZABLEBORDERCOMPONENT_H_INCLUDED
#define JUCE_RESIZABLEBORDERCOMPONENT_H_INCLUDED
//==============================================================================
/**
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
*/
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 non-zero, 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)
};
#endif // JUCE_RESIZABLEBORDERCOMPONENT_H_INCLUDED

View file

@ -0,0 +1,99 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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;
}

View file

@ -0,0 +1,91 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_RESIZABLECORNERCOMPONENT_H_INCLUDED
#define JUCE_RESIZABLECORNERCOMPONENT_H_INCLUDED
//==============================================================================
/** 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
*/
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 the constrainer parameter is non-zero, 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 zero 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)
};
#endif // JUCE_RESIZABLECORNERCOMPONENT_H_INCLUDED

View file

@ -0,0 +1,107 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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();
}

View file

@ -0,0 +1,99 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_RESIZABLEEDGECOMPONENT_H_INCLUDED
#define JUCE_RESIZABLEEDGECOMPONENT_H_INCLUDED
//==============================================================================
/**
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
*/
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 non-zero, 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)
};
#endif // JUCE_RESIZABLEEDGECOMPONENT_H_INCLUDED

View file

@ -0,0 +1,421 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
class ScrollBar::ScrollbarButton : public Button
{
public:
ScrollbarButton (const int direction_, ScrollBar& owner_)
: Button (String::empty), direction (direction_), owner (owner_)
{
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 (const bool vertical_)
: totalRange (0.0, 1.0),
visibleRange (0.0, 0.1),
singleStepSize (0.1),
thumbAreaStart (0),
thumbAreaSize (0),
thumbStart (0),
thumbSize (0),
initialDelayInMillisecs (100),
repeatDelayInMillisecs (50),
minimumDelayInMillisecs (10),
vertical (vertical_),
isDraggingThumb (false),
autohides (true)
{
setRepaintsOnMouseActivity (true);
setFocusContainer (true);
}
ScrollBar::~ScrollBar()
{
upButton = nullptr;
downButton = nullptr;
}
//==============================================================================
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 initialDelayInMillisecs_,
const int repeatDelayInMillisecs_,
const int minimumDelayInMillisecs_)
{
initialDelayInMillisecs = initialDelayInMillisecs_;
repeatDelayInMillisecs = repeatDelayInMillisecs_;
minimumDelayInMillisecs = minimumDelayInMillisecs_;
if (upButton != nullptr)
{
upButton ->setRepeatSpeed (initialDelayInMillisecs, repeatDelayInMillisecs, minimumDelayInMillisecs);
downButton->setRepeatSpeed (initialDelayInMillisecs, repeatDelayInMillisecs, minimumDelayInMillisecs);
}
}
//==============================================================================
void ScrollBar::addListener (Listener* const listener)
{
listeners.add (listener);
}
void ScrollBar::removeListener (Listener* const listener)
{
listeners.remove (listener);
}
void ScrollBar::handleAsyncUpdate()
{
double start = visibleRange.getStart(); // (need to use a temp variable for VC7 compatibility)
listeners.call (&ScrollBar::Listener::scrollBarMoved, this, start);
}
//==============================================================================
void ScrollBar::updateThumbPosition()
{
int newThumbSize = roundToInt (totalRange.getLength() > 0 ? (visibleRange.getLength() * thumbAreaSize) / totalRange.getLength()
: thumbAreaSize);
LookAndFeel& lf = getLookAndFeel();
if (newThumbSize < lf.getMinimumScrollbarThumbSize (*this))
newThumbSize = jmin (lf.getMinimumScrollbarThumbSize (*this), 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()));
setVisible ((! autohides) || (totalRange.getLength() > visibleRange.getLength() && visibleRange.getLength() > 0.0));
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)
{
LookAndFeel& 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()
{
const int length = vertical ? getHeight() : getWidth();
LookAndFeel& lf = getLookAndFeel();
const bool buttonsVisible = lf.areScrollbarButtonsVisible();
int buttonSize = 0;
if (buttonsVisible)
{
if (upButton == nullptr)
{
addAndMakeVisible (upButton = new ScrollbarButton (vertical ? 0 : 3, *this));
addAndMakeVisible (downButton = new ScrollbarButton (vertical ? 2 : 1, *this));
setButtonRepeatSpeed (initialDelayInMillisecs, repeatDelayInMillisecs, minimumDelayInMillisecs);
}
buttonSize = jmin (lf.getScrollbarButtonSize (*this), length / 2);
}
else
{
upButton = nullptr;
downButton = nullptr;
}
if (length < 32 + lf.getMinimumScrollbarThumbSize (*this))
{
thumbAreaStart = length / 2;
thumbAreaSize = 0;
}
else
{
thumbAreaStart = buttonSize;
thumbAreaSize = length - (buttonSize << 1);
}
if (upButton != nullptr)
{
if (vertical)
{
upButton->setBounds (0, 0, getWidth(), buttonSize);
downButton->setBounds (0, thumbAreaStart + thumbAreaSize, getWidth(), buttonSize);
}
else
{
upButton->setBounds (0, 0, buttonSize, getHeight());
downButton->setBounds (thumbAreaStart + thumbAreaSize, 0, buttonSize, getHeight());
}
}
updateThumbPosition();
}
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;
}

View file

@ -0,0 +1,404 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_SCROLLBAR_H_INCLUDED
#define JUCE_SCROLLBAR_H_INCLUDED
//==============================================================================
/**
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
*/
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.
@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.
@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.
@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.
@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());
@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());
@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;
private:
//==============================================================================
Range <double> totalRange, visibleRange;
double singleStepSize, dragStartRange;
int thumbAreaStart, thumbAreaSize, thumbStart, thumbSize;
int dragStartMousePos, lastMousePos;
int initialDelayInMillisecs, repeatDelayInMillisecs, minimumDelayInMillisecs;
bool vertical, isDraggingThumb, autohides;
class ScrollbarButton;
friend struct ContainerDeletePolicy<ScrollbarButton>;
ScopedPointer<ScrollbarButton> upButton, downButton;
ListenerList<Listener> listeners;
void handleAsyncUpdate() override;
void updateThumbPosition();
void timerCallback() override;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScrollBar)
};
/** This typedef is just for compatibility with old code - newer code should use the ScrollBar::Listener class directly. */
typedef ScrollBar::Listener ScrollBarListener;
#endif // JUCE_SCROLLBAR_H_INCLUDED

View file

@ -0,0 +1,342 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
StretchableLayoutManager::StretchableLayoutManager()
: totalSize (0)
{
}
StretchableLayoutManager::~StretchableLayoutManager()
{
}
//==============================================================================
void StretchableLayoutManager::clearAllItems()
{
items.clear();
totalSize = 0;
}
void StretchableLayoutManager::setItemLayout (const int itemIndex,
const double minimumSize,
const double maximumSize,
const double preferredSize)
{
ItemLayoutProperties* 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 (const ItemLayoutProperties* const 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 (const ItemLayoutProperties* const layout = getInfoFor (i))
pos += layout->currentSize;
return pos;
}
int StretchableLayoutManager::getItemCurrentAbsoluteSize (const int itemIndex) const
{
if (const ItemLayoutProperties* const layout = getInfoFor (itemIndex))
return layout->currentSize;
return 0;
}
double StretchableLayoutManager::getItemCurrentRelativeSize (const int itemIndex) const
{
if (const ItemLayoutProperties* const 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;)
{
const ItemLayoutProperties* const layout = items.getUnchecked(i);
if (layout->itemIndex == itemIndex)
{
int realTotalSize = jmax (totalSize, getMinimumSizeOfItems (0, items.size()));
const int minSizeAfterThisComp = getMinimumSizeOfItems (i, items.size());
const int maxSizeAfterThisComp = getMaximumSizeOfItems (i + 1, items.size());
newPosition = jmax (newPosition, totalSize - maxSizeAfterThisComp - layout->currentSize);
newPosition = jmin (newPosition, realTotalSize - minSizeAfterThisComp);
int 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 (const ItemLayoutProperties* const layout = getInfoFor (i))
{
if (Component* const 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 (int i = items.size(); --i >= 0;)
if (items.getUnchecked(i)->itemIndex == itemIndex)
return items.getUnchecked(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)
{
ItemLayoutProperties* const 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)
{
ItemLayoutProperties* const layout = items.getUnchecked (i);
double sizeWanted = sizeToRealSize (layout->preferredSize, totalSize);
const int 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)
{
ItemLayoutProperties* const layout = items.getUnchecked (i);
double sizeWanted = sizeToRealSize (layout->preferredSize, totalSize);
int bestSize = jlimit (layout->currentSize,
jmax (layout->currentSize, sizeToRealSize (layout->maxSize, totalSize)),
roundToInt (sizeWanted * availableSpace / totalIdealSize));
const int extraWanted = bestSize - layout->currentSize;
if (extraWanted > 0)
{
const int 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)
{
ItemLayoutProperties* const 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)
{
ItemLayoutProperties* const 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);
}

View file

@ -0,0 +1,260 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_STRETCHABLELAYOUTMANAGER_H_INCLUDED
#define JUCE_STRETCHABLELAYOUTMANAGER_H_INCLUDED
//==============================================================================
/**
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
*/
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;
//==============================================================================
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)
};
#endif // JUCE_STRETCHABLELAYOUTMANAGER_H_INCLUDED

View file

@ -0,0 +1,73 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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();
}

View file

@ -0,0 +1,104 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_STRETCHABLELAYOUTRESIZERBAR_H_INCLUDED
#define JUCE_STRETCHABLELAYOUTRESIZERBAR_H_INCLUDED
//==============================================================================
/**
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
*/
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)
};
#endif // JUCE_STRETCHABLELAYOUTRESIZERBAR_H_INCLUDED

View file

@ -0,0 +1,116 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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;
}
}

View file

@ -0,0 +1,101 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_STRETCHABLEOBJECTRESIZER_H_INCLUDED
#define JUCE_STRETCHABLEOBJECTRESIZER_H_INCLUDED
//==============================================================================
/**
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.
*/
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 miniumum 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)
};
#endif // JUCE_STRETCHABLEOBJECTRESIZER_H_INCLUDED

View file

@ -0,0 +1,579 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
TabBarButton::TabBarButton (const String& name, TabbedButtonBar& owner_)
: Button (name), owner (owner_), overlapPixels (0), extraCompPlacement (afterText)
{
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)
{
const Rectangle<int> 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
{
LookAndFeel& lf = getLookAndFeel();
textArea = getActiveArea();
const int depth = owner.isVertical() ? textArea.getWidth() : textArea.getHeight();
const int 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);
const TabbedButtonBar::Orientation 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
{
Rectangle<int> r (getLocalBounds());
const int spaceAroundImage = getLookAndFeel().getTabButtonSpaceAroundImage();
const TabbedButtonBar::Orientation 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;
addAndMakeVisible (extraComponent = comp);
resized();
}
void TabBarButton::childBoundsChanged (Component* c)
{
if (c == extraComponent)
{
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 ButtonListener // (can't use Button::Listener due to idiotic VC2005 bug)
{
public:
BehindFrontTabComp (TabbedButtonBar& tb) : owner (tb)
{
setInterceptsMouseClicks (false, false);
}
void paint (Graphics& g) override
{
getLookAndFeel().drawTabAreaBehindFrontButton (owner, g, getWidth(), getHeight());
}
void enablementChanged() override
{
repaint();
}
void buttonClicked (Button*) override
{
owner.showExtraItemsMenu();
}
private:
TabbedButtonBar& owner;
JUCE_DECLARE_NON_COPYABLE (BehindFrontTabComp)
};
//==============================================================================
TabbedButtonBar::TabbedButtonBar (const Orientation orientation_)
: orientation (orientation_),
minimumScale (0.7),
currentTabIndex (-1)
{
setInterceptsMouseClicks (false, true);
addAndMakeVisible (behindFrontTab = new BehindFrontTabComp (*this));
setFocusContainer (true);
}
TabbedButtonBar::~TabbedButtonBar()
{
tabs.clear();
extraTabsButton = nullptr;
}
//==============================================================================
void TabbedButtonBar::setOrientation (const Orientation newOrientation)
{
orientation = newOrientation;
for (int i = getNumChildComponents(); --i >= 0;)
getChildComponent (i)->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 = nullptr;
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();
TabInfo* const currentTab = tabs [currentTabIndex];
TabInfo* newTab = new TabInfo();
newTab->name = tabName;
newTab->colour = tabBackgroundColour;
newTab->button = createTabButton (tabName, insertIndex);
jassert (newTab->button != nullptr);
tabs.insert (insertIndex, newTab);
currentTabIndex = tabs.indexOf (currentTab);
addAndMakeVisible (newTab->button, insertIndex);
resized();
if (currentTabIndex < 0)
setCurrentTabIndex (0);
}
}
void TabbedButtonBar::setTabName (const int tabIndex, const String& newName)
{
if (TabInfo* const tab = tabs [tabIndex])
{
if (tab->name != newName)
{
tab->name = newName;
tab->button->setButtonText (newName);
resized();
}
}
}
void TabbedButtonBar::removeTab (const int tabIndex, const bool animate)
{
const int oldIndex = currentTabIndex;
if (tabIndex == currentTabIndex)
setCurrentTabIndex (-1);
tabs.remove (tabIndex);
setCurrentTabIndex (oldIndex);
updateTabPositions (animate);
}
void TabbedButtonBar::moveTab (const int currentIndex, const int newIndex, const bool animate)
{
TabInfo* const currentTab = tabs [currentTabIndex];
tabs.move (currentIndex, newIndex);
currentTabIndex = tabs.indexOf (currentTab);
updateTabPositions (animate);
}
int TabbedButtonBar::getNumTabs() const
{
return tabs.size();
}
String TabbedButtonBar::getCurrentTabName() const
{
TabInfo* tab = tabs [currentTabIndex];
return tab == nullptr ? String::empty : tab->name;
}
StringArray TabbedButtonBar::getTabNames() const
{
StringArray names;
for (int i = 0; i < tabs.size(); ++i)
names.add (tabs.getUnchecked(i)->name);
return names;
}
void TabbedButtonBar::setCurrentTabIndex (int newIndex, const bool sendChangeMessage_)
{
if (currentTabIndex != newIndex)
{
if (! isPositiveAndBelow (newIndex, tabs.size()))
newIndex = -1;
currentTabIndex = newIndex;
for (int i = 0; i < tabs.size(); ++i)
{
TabBarButton* tb = tabs.getUnchecked(i)->button;
tb->setToggleState (i == newIndex, dontSendNotification);
}
resized();
if (sendChangeMessage_)
sendChangeMessage();
currentTabChanged (newIndex, getCurrentTabName());
}
}
TabBarButton* TabbedButtonBar::getTabButton (const int index) const
{
if (TabInfo* tab = tabs[index])
return static_cast<TabBarButton*> (tab->button);
return nullptr;
}
int TabbedButtonBar::indexOfTabButton (const TabBarButton* button) const
{
for (int i = tabs.size(); --i >= 0;)
if (tabs.getUnchecked(i)->button == button)
return i;
return -1;
}
Rectangle<int> TabbedButtonBar::getTargetBounds (TabBarButton* button) const
{
if (button == nullptr || indexOfTabButton (button) == -1)
return Rectangle<int>();
ComponentAnimator& animator = Desktop::getInstance().getAnimator();
return animator.isAnimating (button) ? animator.getComponentDestination (button) : button->getBounds();
}
void TabbedButtonBar::lookAndFeelChanged()
{
extraTabsButton = nullptr;
resized();
}
void TabbedButtonBar::paint (Graphics& g)
{
getLookAndFeel().drawTabbedButtonBarBackground (*this, g);
}
void TabbedButtonBar::resized()
{
updateTabPositions (false);
}
//==============================================================================
void TabbedButtonBar::updateTabPositions (bool animate)
{
LookAndFeel& lf = getLookAndFeel();
int depth = getWidth();
int length = getHeight();
if (! isVertical())
std::swap (depth, length);
const int overlap = lf.getTabButtonOverlap (depth) + lf.getTabButtonSpaceAroundImage() * 2;
int totalLength = jmax (0, overlap);
int numVisibleButtons = tabs.size();
for (int i = 0; i < tabs.size(); ++i)
{
TabBarButton* const tb = tabs.getUnchecked(i)->button;
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)
{
addAndMakeVisible (extraTabsButton = lf.createTabBarExtrasButton());
extraTabsButton->addListener (behindFrontTab);
extraTabsButton->setAlwaysOnTop (true);
extraTabsButton->setTriggeredOnMouseDown (true);
}
const int 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)
{
TabBarButton* const tb = tabs.getUnchecked(i)->button;
const int 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 = nullptr;
}
int pos = 0;
TabBarButton* frontTab = nullptr;
ComponentAnimator& animator = Desktop::getInstance().getAnimator();
for (int i = 0; i < tabs.size(); ++i)
{
if (TabBarButton* const tb = getTabButton (i))
{
const int bestLength = roundToInt (scale * tb->getBestTabLength (depth));
if (i < numVisibleButtons)
{
const Rectangle<int> 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 (const int tabIndex)
{
if (TabInfo* tab = tabs [tabIndex])
return tab->colour;
return Colours::transparentBlack;
}
void TabbedButtonBar::setTabBackgroundColour (const int tabIndex, Colour newColour)
{
if (TabInfo* const 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)
{
const TabInfo* const tab = tabs.getUnchecked(i);
if (! tab->button->isVisible())
m.addItem (i + 1, tab->name, true, i == currentTabIndex);
}
m.showMenuAsync (PopupMenu::Options().withTargetComponent (extraTabsButton),
ModalCallbackFunction::forComponent (extraItemsMenuCallback, this));
}
//==============================================================================
void TabbedButtonBar::currentTabChanged (const int, const String&)
{
}
void TabbedButtonBar::popupMenuClickOnTab (const int, const String&)
{
}

View file

@ -0,0 +1,369 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_TABBEDBUTTONBAR_H_INCLUDED
#define JUCE_TABBEDBUTTONBAR_H_INCLUDED
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
*/
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; }
/** 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;
ScopedPointer<Component> extraComponent;
ExtraComponentPlacement extraCompPlacement;
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
*/
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 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:
Orientation orientation;
struct TabInfo
{
ScopedPointer<TabBarButton> button;
String name;
Colour colour;
};
OwnedArray <TabInfo> tabs;
double minimumScale;
int currentTabIndex;
class BehindFrontTabComp;
friend class BehindFrontTabComp;
friend struct ContainerDeletePolicy<BehindFrontTabComp>;
ScopedPointer<BehindFrontTabComp> behindFrontTab;
ScopedPointer<Button> extraTabsButton;
void showExtraItemsMenu();
static void extraItemsMenuCallback (int, TabbedButtonBar*);
void updateTabPositions (bool animate);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TabbedButtonBar)
};
#endif // JUCE_TABBEDBUTTONBAR_H_INCLUDED

View file

@ -0,0 +1,307 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
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>();
}
}
//==============================================================================
class TabbedComponent::ButtonBar : public TabbedButtonBar
{
public:
ButtonBar (TabbedComponent& owner_, const TabbedButtonBar::Orientation orientation_)
: TabbedButtonBar (orientation_),
owner (owner_)
{
}
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);
}
private:
TabbedComponent& owner;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonBar)
};
//==============================================================================
TabbedComponent::TabbedComponent (const TabbedButtonBar::Orientation orientation)
: tabDepth (30),
outlineThickness (1),
edgeIndent (0)
{
addAndMakeVisible (tabs = new ButtonBar (*this, orientation));
}
TabbedComponent::~TabbedComponent()
{
clearTabs();
tabs = nullptr;
}
//==============================================================================
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);
}
}
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));
Rectangle<int> 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()
{
Rectangle<int> 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)
{
Component* const 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->setVisible (true);
panelComponent->toFront (true);
}
repaint();
}
resized();
currentTabChanged (newCurrentTabIndex, newTabName);
}
void TabbedComponent::currentTabChanged (const int, const String&) {}
void TabbedComponent::popupMenuClickOnTab (const int, const String&) {}

View file

@ -0,0 +1,220 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_TABBEDCOMPONENT_H_INCLUDED
#define JUCE_TABBEDCOMPONENT_H_INCLUDED
//==============================================================================
/**
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
*/
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);
/** 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 */
ScopedPointer<TabbedButtonBar> tabs;
private:
//==============================================================================
Array <WeakReference<Component> > contentComponents;
WeakReference<Component> panelComponent;
int tabDepth, outlineThickness, edgeIndent;
class ButtonBar;
friend class ButtonBar;
void changeCallback (int newCurrentTabIndex, const String& newTabName);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TabbedComponent)
};
#endif // JUCE_TABBEDCOMPONENT_H_INCLUDED

View file

@ -0,0 +1,433 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
Viewport::Viewport (const String& name)
: Component (name),
scrollBarThickness (0),
singleStepX (16),
singleStepY (16),
showHScrollbar (true),
showVScrollbar (true),
deleteContent (true),
allowScrollingWithoutScrollbarV (false),
allowScrollingWithoutScrollbarH (false),
verticalScrollBar (true),
horizontalScrollBar (false)
{
// content holder is used to clip the contents so they don't overlap the scrollbars
addAndMakeVisible (contentHolder);
contentHolder.setInterceptsMouseClicks (false, true);
addChildComponent (verticalScrollBar);
addChildComponent (horizontalScrollBar);
verticalScrollBar.addListener (this);
horizontalScrollBar.addListener (this);
setInterceptsMouseClicks (false, true);
setWantsKeyboardFocus (true);
}
Viewport::~Viewport()
{
deleteContentComp();
}
//==============================================================================
void Viewport::visibleAreaChanged (const Rectangle<int>&) {}
void Viewport::viewedComponentChanged (Component*) {}
//==============================================================================
void Viewport::deleteContentComp()
{
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..
ScopedPointer<Component> oldCompDeleter (contentComp);
}
else
{
contentComp = nullptr;
}
}
void Viewport::setViewedComponent (Component* const newViewedComponent, const bool deleteComponentWhenNoLongerNeeded)
{
if (contentComp.get() != newViewedComponent)
{
deleteContentComp();
contentComp = newViewedComponent;
deleteContent = deleteComponentWhenNoLongerNeeded;
if (contentComp != nullptr)
{
contentHolder.addAndMakeVisible (contentComp);
setViewPosition (Point<int>());
contentComp->addComponentListener (this);
}
viewedComponentChanged (contentComp);
updateVisibleArea();
}
}
int Viewport::getMaximumVisibleWidth() const { return contentHolder.getWidth(); }
int Viewport::getMaximumVisibleHeight() const { return contentHolder.getHeight(); }
Point<int> Viewport::viewportPosToCompPos (Point<int> pos) const
{
jassert (contentComp != nullptr);
return Point<int> (jmax (jmin (0, contentHolder.getWidth() - contentComp->getWidth()), jmin (0, -(pos.x))),
jmax (jmin (0, contentHolder.getHeight() - contentComp->getHeight()), jmin (0, -(pos.y))));
}
void Viewport::setViewPosition (const int xPixelsOffset, const int yPixelsOffset)
{
setViewPosition (Point<int> (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 (horizontalScrollBar.isVisible() || contentComp->getX() < 0 || contentComp->getRight() > getWidth())
{
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 (verticalScrollBar.isVisible() || contentComp->getY() < 0 || contentComp->getBottom() > getHeight())
{
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();
}
void Viewport::resized()
{
updateVisibleArea();
}
//==============================================================================
void Viewport::updateVisibleArea()
{
const int 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 && ! horizontalScrollBar.autoHides();
vBarVisible = canShowVBar && ! verticalScrollBar.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 (contentComp == nullptr)
{
contentHolder.setBounds (contentArea);
break;
}
const Rectangle<int> 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());
Point<int> visibleOrigin (-contentBounds.getPosition());
horizontalScrollBar.setBounds (0, contentArea.getHeight(), contentArea.getWidth(), scrollbarWidth);
horizontalScrollBar.setRangeLimits (0.0, contentBounds.getWidth());
horizontalScrollBar.setCurrentRange (visibleOrigin.x, contentArea.getWidth());
horizontalScrollBar.setSingleStepSize (singleStepX);
horizontalScrollBar.cancelPendingUpdate();
if (canShowHBar && ! hBarVisible)
visibleOrigin.setX (0);
verticalScrollBar.setBounds (contentArea.getWidth(), 0, scrollbarWidth, contentArea.getHeight());
verticalScrollBar.setRangeLimits (0.0, contentBounds.getHeight());
verticalScrollBar.setCurrentRange (visibleOrigin.y, contentArea.getHeight());
verticalScrollBar.setSingleStepSize (singleStepY);
verticalScrollBar.cancelPendingUpdate();
if (canShowVBar && ! vBarVisible)
visibleOrigin.setY (0);
// Force the visibility *after* setting the ranges to avoid flicker caused by edge conditions in the numbers.
horizontalScrollBar.setVisible (hBarVisible);
verticalScrollBar.setVisible (vBarVisible);
if (contentComp != nullptr)
{
const Point<int> 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);
}
horizontalScrollBar.handleUpdateNowIfNeeded();
verticalScrollBar.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)
{
if (scrollBarThickness != thickness)
{
scrollBarThickness = thickness;
updateVisibleArea();
}
}
int Viewport::getScrollBarThickness() const
{
return scrollBarThickness > 0 ? scrollBarThickness
: getLookAndFeel().getDefaultScrollbarWidth();
}
void Viewport::scrollBarMoved (ScrollBar* scrollBarThatHasMoved, double newRangeStart)
{
const int newRangeStartInt = roundToInt (newRangeStart);
if (scrollBarThatHasMoved == &horizontalScrollBar)
{
setViewPosition (newRangeStartInt, getViewPositionY());
}
else if (scrollBarThatHasMoved == &verticalScrollBar)
{
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)
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 || verticalScrollBar.isVisible());
const bool canScrollHorz = (allowScrollingWithoutScrollbarH || horizontalScrollBar.isVisible());
if (canScrollHorz || canScrollVert)
{
const int deltaX = rescaleMouseWheelDistance (wheel.deltaX, singleStepX);
const int deltaY = rescaleMouseWheelDistance (wheel.deltaY, singleStepY);
Point<int> 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 (verticalScrollBar.isVisible() && isUpDownKey)
return verticalScrollBar.keyPressed (key);
const bool isLeftRightKey = isLeftRightKeyPress (key);
if (horizontalScrollBar.isVisible() && (isUpDownKey || isLeftRightKey))
return horizontalScrollBar.keyPressed (key);
return false;
}
bool Viewport::respondsToKey (const KeyPress& key)
{
return isUpDownKeyPress (key) || isLeftRightKeyPress (key);
}

View file

@ -0,0 +1,284 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2013 - Raw Material Software Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#ifndef JUCE_VIEWPORT_H_INCLUDED
#define JUCE_VIEWPORT_H_INCLUDED
//==============================================================================
/**
A Viewport is used to contain a larger child component, and allows the child
to be automatically scrolled around.
To use a Viewport, just create one and set the component that goes inside it
using the setViewedComponent() method. When the child component changes size,
the Viewport will adjust its scrollbars accordingly.
A subclass of the viewport can be created which will receive calls to its
visibleAreaChanged() method when the subcomponent changes position or size.
*/
class 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::empty);
/** 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);
/** 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; }
//==============================================================================
/** @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 */
bool useMouseWheelMoveIfNeeded (const MouseEvent&, const MouseWheelDetails&);
/** @internal */
static bool respondsToKey (const KeyPress&);
private:
//==============================================================================
WeakReference<Component> contentComp;
Rectangle<int> lastVisibleArea;
int scrollBarThickness;
int singleStepX, singleStepY;
bool showHScrollbar, showVScrollbar, deleteContent;
bool allowScrollingWithoutScrollbarV, allowScrollingWithoutScrollbarH;
Component contentHolder;
ScrollBar verticalScrollBar, horizontalScrollBar;
Point<int> viewportPosToCompPos (Point<int>) const;
void updateVisibleArea();
void deleteContentComp();
#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)
};
#endif // JUCE_VIEWPORT_H_INCLUDED