ResizableLayout


#1

Here’s a nifty class that I’ve carried with me through the years, modified to work with Juce. It will allow you to place anchors on your child Components so that when the parent is resized, your controls will automatically stretch, move, flow, whatever! The header file has some comments showing a usage example. There’s a compilable sample program at the bottom.

ResizableLayout.h

// Copyright (C) 2008-2011 by One Guy Group, Inc., All rights reserved worldwide.
/*******************************************************************************

Resizable Layout for Juce
By Vincent Falco

Juce:
http://www.rawmaterialsoftware

--------------------------------------------------------------------------------

License: MIT License (http://www.opensource.org/licenses/mit-license.php)
Copyright (c) 2011 by Vincent Falco

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*******************************************************************************/

#ifndef RESIZABLELAYOUT_H
#define RESIZABLELAYOUT_H

#include "juce.h"

//==============================================================================
/**
  Smoothly repositions and resizes child Components without rounding errors,
  according to caller provided stretching parameters.

  Usage:

  1) Derive your Component from ResizableLayout

  2) Give your Component a well defined default size

  3) Add child Components and position them according to how
     they should appear at the default.

  4) For each child Component, also call addToLayout() and specify what
     portion of the owner's growth or reduction should apply to each corner.

  5) At the end of your constructor, call activateLayout() to turn it on

  6) If you later manually reposition a control (for example, using a
     ComponentDragger) call updateLayoutFor() on the control that moved,
     or if you moved all the controls call updateLayout().

  7) You do not need to override resized() to make this work, all you
     have to do is lay things out when you create your child Components
     and add anchors as appropriate.

  Example

  struct ContentComponent : Component, ResizableLayout
  {
    ContentComponent() : ResizableLayout (this)
    {
      // for layouts to work you must start at some size
      // place controls in a location that is initially correct.
      setSize(512, 384);

      TextButton* b;

      b = new TextButton ("Juce");
      b->setBounds (32, 32, 512-64, 32);
      // stretch to fit the width of the window
      addToLayout (b, anchorTopLeft, anchorTopRight);
      addAndMakeVisible (b);

      b = new TextButton ("One");
      b->setBounds (32, 96, 128, 32);
      // take up 1/3 of the width of the window
      addToLayout (b, Point<int>(0,0), Point<int>(33,0));
      addAndMakeVisible (b);

      // turn it on
      activateLayout();
    }
  }

*/
class ResizableLayout
  : public ComponentBoundsConstrainer
  , private ComponentListener
{
public:
	enum
	{
		anchorUnit=100
	};

	enum Style
	{
		styleStretch,
		styleFixedAspect
	};

	static const Point<int> anchorNone;
	static const Point<int> anchorTopLeft;
	static const Point<int> anchorTopCenter;
	static const Point<int> anchorTopRight;
	static const Point<int> anchorMidLeft;
	static const Point<int> anchorMidCenter;
	static const Point<int> anchorMidRight;
	static const Point<int> anchorBottomLeft;
	static const Point<int> anchorBottomCenter;
	static const Point<int> anchorBottomRight;

public:
	ResizableLayout (Component* owner);
	~ResizableLayout ();

	// Add a Component to the Layout.
	// topLeft and bottomRight are the percentages that the top left and bottom right of
	// the Component should move by, when the layout is resized.
	// So if you wanted to have the control take on the full width of the parent, and
	// half the height, you would use bottomRight.x=100, bottomRight.y=50. or
  // use the constant anchorMidRight
	void addToLayout (
		Component *component,
		const Point<int> &topLeft,
		const Point<int> &bottomRight=anchorNone,
		Style style = styleStretch );

	// Remove a Component from the Layout.
	void removeFromLayout (Component* component);

	// Activate (or deactivate) the Layout. The Layout is initially inactive,
	// to prevent spurious recalculation while a Component and its children are being
	// constructed (causing resized() messages). Activating the Layout for the
	// first time will cause an Update().
	void activateLayout (bool bActive=true);

	// Update the state information for all items. This is used on the first Activate(),
	// and can also be used if multiple controls are moved or resized from elsewhere.
	void updateLayout ();

	// Call this to manually update the state information for a single control
	// after it has been moved or resized from elsewhere.
	void updateLayoutFor (Component *component);

  // Convenience function
  static Rectangle<int> calcBoundsOfChildren (Component* parent);

private:
  struct Rect
  {
	  Rect() {}
	  Rect( int top0, int left0, int bottom0, int right0 ) { top=top0; left=left0; bottom=bottom0; right=right0; }
	  Rect( const Rectangle<int> &r ) { top=int(r.getY()); left=int(r.getX()); bottom=int(r.getBottom()); right=int(r.getRight()); }
	  operator Rectangle<int>() const { return Rectangle<int>( left, top, Width(), Height() ); }
	  int Height( void ) const { return bottom-top; }
	  int Width( void ) const { return right-left; }
	  void Inset( int dx, int dy ) { top+=dy; left+=dx; bottom-=dy; right-=dx; }

	  int top;
	  int left;
	  int bottom;
	  int right;
  };

  struct Anchor
	{
		Style	style;
		Component* component;
		Point<int> topLeft;
		Point<int> bottomRight;

    Anchor (Component* component=0);
    bool operator== (const Anchor& rhs) const;
    bool operator>= (const Anchor& rhs) const;
	};

  struct State
	{
		Component* component;
		double aspect;
		Rect margin;

    State (Component* component=0);
    bool operator== (const State& rhs) const;
    bool operator>= (const State& rhs) const;
	};

  void addStateFor (const Anchor& anchor);

	void recalculateLayout ();

  void componentMovedOrResized (Component& component,
                                bool wasMoved,
                                bool wasResized);

  void componentBeingDeleted (Component& component);

private:
	Component* m_owner;

  SortedSet<Anchor> m_anchors;
  SortedSet<State> m_states;

	bool m_bFirstTime;
	bool m_bActive;
};

#endif

ResizableLayout.cpp

// Copyright (C) 2008-2011 by One Guy Group, Inc., All rights reserved worldwide.
/*******************************************************************************

Resizable Layout for Juce
By Vincent Falco

Juce:
http://www.rawmaterialsoftware

--------------------------------------------------------------------------------

License: MIT License (http://www.opensource.org/licenses/mit-license.php)
Copyright (c) 2011 by Vincent Falco

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*******************************************************************************/

#include "ResizableLayout.h"

const Point<int> ResizableLayout::anchorNone			  ( -1, -1 );
const Point<int> ResizableLayout::anchorTopLeft		  ( 0, 0 );
const Point<int> ResizableLayout::anchorTopCenter		( anchorUnit/2, 0 );
const Point<int> ResizableLayout::anchorTopRight		( anchorUnit, 0 );
const Point<int> ResizableLayout::anchorMidLeft		  ( 0, anchorUnit/2 );
const Point<int> ResizableLayout::anchorMidCenter		( anchorUnit/2, anchorUnit/2 );
const Point<int> ResizableLayout::anchorMidRight		( anchorUnit, anchorUnit/2 );
const Point<int> ResizableLayout::anchorBottomLeft	( 0, anchorUnit );
const Point<int> ResizableLayout::anchorBottomCenter( anchorUnit/2, anchorUnit );
const Point<int> ResizableLayout::anchorBottomRight	( anchorUnit, anchorUnit );

ResizableLayout::Anchor::Anchor (Component* component_)
: component (component_)
{
  jassert (component);
}

bool ResizableLayout::Anchor::operator== (const Anchor& rhs) const
  { return component == rhs.component; }

bool ResizableLayout::Anchor::operator>= (const Anchor& rhs) const
  { return component >= rhs.component; }

ResizableLayout::State::State (Component* component_)
: component (component_)
{
  jassert (component);
}

bool ResizableLayout::State::operator== (const State& rhs) const
  { return component == rhs.component; }

bool ResizableLayout::State::operator>= (const State& rhs) const
  { return component >= rhs.component; }

//----

ResizableLayout::ResizableLayout (Component* owner)
: m_owner (owner)
{
  m_bFirstTime = true;
	m_bActive = false;

  m_owner->addComponentListener (this);
}


ResizableLayout::~ResizableLayout()
{
}

void ResizableLayout::addToLayout (Component* component,
                                   const Point<int> &topLeft,
                                   const Point<int> &bottomRight,
                                   Style style )
{
	jassert (topLeft!=anchorNone);

  Anchor anchor (component);
	anchor.style = style;
  anchor.topLeft = topLeft;
	anchor.bottomRight = bottomRight;

  m_anchors.add (anchor);

  component->addComponentListener (this);
}

void ResizableLayout::removeFromLayout (Component* component)
{
  m_anchors.removeValue (component);
  m_states.removeValue (component);
}

void ResizableLayout::activateLayout( bool bActive )
{
	if( m_bActive!=bActive )
	{
		if( bActive && m_bFirstTime )
		{
			updateLayout();
			m_bFirstTime=false;
		}

		m_bActive=bActive;
	}
}

void ResizableLayout::updateLayout ()
{
  m_states.clearQuick();
	for( int i=0; i<m_anchors.size(); i++ )
		addStateFor (m_anchors[i]);
}

void ResizableLayout::updateLayoutFor (Component *component)
{
  m_states.removeValue (component);
  addStateFor (m_anchors[m_anchors.indexOf (Anchor(component))]);
}

void ResizableLayout::addStateFor (const Anchor& anchor)
{
	Rect rBounds = anchor.component->getBounds();

  // owner size
	Point<int> ptSize( int(m_owner->getWidth()), int(m_owner->getHeight()) );

  State state (anchor.component);

  // secret sauce
	state.margin.top = rBounds.top - (ptSize.getY() * anchor.topLeft.getY()) / anchorUnit;
	state.margin.left = rBounds.left - (ptSize.getX() * anchor.topLeft.getX()) / anchorUnit;
	state.margin.bottom = rBounds.bottom - (ptSize.getY() * anchor.bottomRight.getY()) / anchorUnit;
	state.margin.right = rBounds.right - (ptSize.getX() * anchor.bottomRight.getX()) / anchorUnit;

  state.aspect = double (rBounds.Width()) / rBounds.Height();

  m_states.add (state);
}

// Recalculate the position and size of all the controls
// in the layout, based on the owner Component size.
void ResizableLayout::recalculateLayout()
{
	if( m_bActive )
	{
		Rect rParent = m_owner->getBounds();
		
    for( int i=0; i<m_states.size(); i++ )
		{
      Anchor anchor = m_anchors[i];
      State state = m_states[i];
      jassert (anchor.component == state.component);

      Rect rBounds;

      // secret sauce
			rBounds.top = state.margin.top + (rParent.Height() * anchor.topLeft.getY()) / anchorUnit;
			rBounds.left = state.margin.left + (rParent.Width() * anchor.topLeft.getX()) / anchorUnit;
			if( anchor.bottomRight != anchorNone )
			{
				rBounds.bottom= state.margin.bottom + (rParent.Height() * anchor.bottomRight.getY()) / anchorUnit;
				rBounds.right = state.margin.right + (rParent.Width() * anchor.bottomRight.getX()) / anchorUnit;
			}
			else
			{
				rBounds.bottom = rBounds.top + anchor.component->getHeight();
				rBounds.right = rBounds.left + anchor.component->getWidth();
			}

			if (anchor.style == styleStretch)
			{
				anchor.component->setBounds (rBounds);
			}
			else if (anchor.style==styleFixedAspect)
			{
				Rect rItem;
				double aspect = double (rBounds.Width()) / rBounds.Height();

				if( aspect > state.aspect )
				{
					rItem.top = rBounds.top;
					rItem.bottom = rBounds.bottom;
					int width = int (state.aspect * rItem.Height());
					rItem.left = rBounds.left + (rBounds.Width()-width)/2;
					rItem.right = rItem.left + width;
				}
				else
				{
					rItem.left = rBounds.left;
					rItem.right = rBounds.right;
					int height = int (1. / state.aspect * rItem.Width());
					rItem.top = rBounds.top + (rBounds.Height() - height) / 2;
					rItem.bottom = rItem.top + height;
				}

				anchor.component->setBounds( rItem );
			}
		}
	}
}

void ResizableLayout::componentMovedOrResized (Component& component,
                                               bool wasMoved,
                                               bool wasResized)
{
  if( &component == m_owner )
  {
    if (wasResized)
    {
      recalculateLayout ();
    }
  }
  else
  {
    updateLayoutFor (&component);
  }
}

void ResizableLayout::componentBeingDeleted (Component& component)
{
  if( &component != m_owner )
  {
    m_anchors.removeValue (&component);
    m_states.removeValue (&component);
  }
}

Rectangle<int> ResizableLayout::calcBoundsOfChildren (Component* parent)
{
  Rectangle<int> r;

  for( int i=0; i<parent->getNumChildComponents(); i++ )
    r = r.getUnion( parent->getChildComponent(i)->getBounds() );

  return r;
}

Here’s a goofy example program that shows you how to use it

#include "juce.h"
#include "ResizableLayout.h"
struct ContentComponent : Component, ResizableLayout
{
  ContentComponent() : ResizableLayout (this)
  {
    // create the initial layout

    // must have a defined initial size
    setSize(512, 384);

    TextButton* b;

    b = new TextButton ("Juce");
    b->setBounds (32, 32, 512-64, 32);
    addToLayout (b, anchorTopLeft, anchorTopRight);
    addAndMakeVisible (b);

    b = new TextButton ("One");
    b->setBounds (32, 96, 128, 32);
    addToLayout (b, Point<int>(0,0), Point<int>(33,0));
    addAndMakeVisible (b);

    b = new TextButton ("Two");
    b->setBounds (32+128+32, 96, 128, 32);
    addToLayout (b, Point<int>(33,0), Point<int>(66,0));
    addAndMakeVisible (b);

    b = new TextButton ("Three");
    b->setBounds (32+128+32+128+32, 96, 128, 32);
    addToLayout (b, Point<int>(66,0), Point<int>(100,0));
    addAndMakeVisible (b);

    b = new TextButton ("BR");
    b->setBounds (512-96, 384-64, 64, 32);
    addToLayout (b, anchorBottomRight);
    addAndMakeVisible (b);

    b = new TextButton ("Aspect");
    b->setBounds (32, 160, 100, 100);
    b->setConnectedEdges (Button::ConnectedOnLeft|Button::ConnectedOnTop|Button::ConnectedOnRight|Button::ConnectedOnBottom);
    addToLayout (b, anchorTopLeft, anchorBottomCenter, styleFixedAspect);
    addAndMakeVisible (b);

    b = new TextButton ("Stretchy");
    b->setBounds (256, 160, 200, 96);
    b->setConnectedEdges (Button::ConnectedOnLeft|Button::ConnectedOnTop|Button::ConnectedOnRight|Button::ConnectedOnBottom);
    addToLayout (b, anchorTopCenter, anchorBottomRight);
    addAndMakeVisible (b);

    // turn it on
    activateLayout ();
  }
  ~ContentComponent() { deleteAllChildren(); }
  void paint (Graphics& g)
  {
    g.setColour (Colours::grey);
    g.fillAll();
  }
};

struct MainWindow : DocumentWindow
{
  MainWindow()
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
  {
    ContentComponent* p = new ContentComponent;
    setResizable (true, false);
    setContentComponent (p, true, true);
    centreWithSize (getWidth(), getHeight());
    setVisible( true );
  }
  ~MainWindow() {}

  void closeButtonPressed() { JUCEApplication::quit(); }
};

struct MainApp : JUCEApplication
{
  MainApp() : mainWindow(0) { s_app=this; }
  ~MainApp() { s_app=0; }
  static MainApp& GetInstance() { return *s_app; }
  const String getApplicationName() { return JUCE_T("JuceTest"); }
  const String getApplicationVersion() { return JUCE_T("0.1.0"); }
  bool moreThanOneInstanceAllowed() { return true; }
  void anotherInstanceStarted (const String& commandLine) {}

  void initialise (const String& commandLine)
  {
    mainWindow = new MainWindow;
  }

  void shutdown()
  {
    delete mainWindow;
  }

  static MainApp* s_app;
  MainWindow* mainWindow;
};

MainApp* MainApp::s_app = 0;

START_JUCE_APPLICATION (MainApp)

#2

Very good!
Thanks for sharing.


#3

Did anyone try it? Comments?


#4

Not tried yet, but I will as soon as possible. It looks very promising, and the MIT licence is a really nice thing


#5

I tried it and it works fine on Windows 7, vs2008. I’m a total Juce neophyte and all I did was
copy the Helloworld project, added the source files, removed the old sources, modified
a copy of properties to find juce.h and the libs and all built and ran fine. I take it that the aspect
button is supposed to disappear when the window is shrunk but should it come back when
the window is enlarged?

I’ve been using wxWidgets for years and had never seen or heard of Juce.

Why wasn’t I told? :smiley: It’s a great piece of work

cheers,

David


#6

If the resizable layout is made so small that it would cause a child component’s width or height to go negative, everything blows up.

Normally you would put a minimum size on your window to prevent this from happening, but I also happen to have written some companion classes that put a ComponentBoundsConstrainer on your content component that is ResizableLayout - aware. These classes not only prevent the component from going to a negative width or height, but also allow you to set size limits on a per-item basis. The results of the size limits are recursively propagated upwards to the constrainer and your window size is automagically limited.


#7

Very nice and useful, saving me a buncha typing! Tnx TheVinn


#8

It’s worth looking at the new Component::setBounds (const RelativeRectangle&) method - that’s going to be the way juce layout happens in the future…


#9

Ugh…way to go Jules, make all my code useless! :cry:

What about calculating a Component’s minimum and maximum dimensions based on recursively iterating through children and picking up their size limits? Once you implement that, I can throw away everything I wrote. And then proceed to commit hari-kari (Japanese ritual suicide).


#10

That’s a different problem to the one I’m trying to solve with the RelativeRectangle stuff, which is currently just concerned with letting components position themselves relatively to other things.

But, the expressions in these RelativeRectangles can use functions (it currently just supports simple things like min() and max()), so in the future there’s no reason why they couldn’t use functions that are based on layout algorithms.