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:
parent
fefcf7aca6
commit
ff6520a89a
1141 changed files with 438491 additions and 94 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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(); }
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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&)
|
||||
{
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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&) {}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue