Component Layout Editor


#1

An old snippet dug out of my personal archive, thought I might share it here…

It’s a Component you can use as an overlay to allow the user to edit the layout of children of another Component. The only prerequisite is that the editor component is a sibling of the component being edited.

Just add the editor to the same component as your ‘editee’, and call setTargetComponent(). It will automatically take the bounds of the target, and create overlay alias components (each draggable/resizable) for each child of the target. You can toggle the editor using setEnabled().

Pretty simple, really.

ComponentLayoutEditor.h

#ifndef _COMPONENTLAYOUTEDITOR_H_
#define _COMPONENTLAYOUTEDITOR_H_

#include "juce.h"

//=============================================================================
class ChildAlias	:	public Component
{
public:
	ChildAlias (Component* targetChild);
	~ChildAlias ();

	void resized ();
	void paint (Graphics& g);

	const Component* getTargetChild ();

	void updateFromTarget ();
	void applyToTarget ();

	virtual void userChangedBounds ();
	virtual void userStartedChangingBounds ();
	virtual void userStoppedChangingBounds ();

	bool boundsChangedSinceStart ();

	void mouseEnter (const MouseEvent& e);
	void mouseExit (const MouseEvent& e);
	void mouseDown (const MouseEvent& e);
	void mouseUp (const MouseEvent& e);
	void mouseDrag (const MouseEvent& e);

private:

	CriticalSection bounds;

	ComponentDragger dragger;
	ComponentDeletionWatcher target;
	bool interest;
	bool userAdjusting;
	Rectangle startBounds;
	ResizableBorderComponent* resizer;
};

//=============================================================================
class ComponentLayoutEditor	:	public Component
{
public:

	enum ColourIds
	{
		aliasIdleColour,
		aliasHoverColour
	};

	ComponentLayoutEditor ();
	~ComponentLayoutEditor ();

	void resized ();
	void paint (Graphics& g);

	void setTargetComponent (Component* target);

	void bindWithTarget ();
	void updateFrames ();

	void enablementChanged ();
	const Component* getTarget ();
private:

	virtual ChildAlias* createAlias (Component* child);

	ComponentDeletionWatcher* target;
	OwnedArray<ChildAlias> frames;
};

#endif//_COMPONENTLAYOUTEDITOR_H_

ComponentLayoutEditor.cpp

#include "ComponentLayoutEditor.h"

ChildAlias::ChildAlias (Component* targetChild)
:	target (targetChild)
{
	resizer = new ResizableBorderComponent (this,0);
	addAndMakeVisible (resizer);
	resizer->addMouseListener (this,false);

	interest = false;
	userAdjusting = false;

	updateFromTarget ();
	setRepaintsOnMouseActivity (true);
}

ChildAlias::~ChildAlias ()
{
	delete resizer;
}

void ChildAlias::resized ()
{
	resizer->setBounds (0,0,getWidth(),getHeight());

	if (resizer->isMouseButtonDown ())
	{
		applyToTarget ();
	}
}

void ChildAlias::paint (Graphics& g)
{
	Colour c;
	if (interest)
		c = findColour (ComponentLayoutEditor::aliasHoverColour,true);
	else c = findColour (ComponentLayoutEditor::aliasIdleColour,true);

	g.setColour (c.withMultipliedAlpha (0.3f));
	g.fillAll ();
	g.setColour (c);
	g.drawRect (0,0,getWidth(),getHeight(),3);
}

const Component* ChildAlias::getTargetChild ()
{
	return target.getComponent ();
}

void ChildAlias::updateFromTarget ()
{
	if (!target.hasBeenDeleted ())
	{
		setBounds ( target.getComponent ()->getBounds () );
	}
}

void ChildAlias::applyToTarget ()
{
	if (!target.hasBeenDeleted ())
	{
		Component* c = (Component*) target.getComponent ();
		c->setBounds (getBounds ());
		userChangedBounds ();
	}
}

void ChildAlias::userChangedBounds ()
{
}

void ChildAlias::userStartedChangingBounds ()
{
}

void ChildAlias::userStoppedChangingBounds ()
{
}

bool ChildAlias::boundsChangedSinceStart ()
{
	return startBounds != getBounds ();
}


void ChildAlias::mouseDown (const MouseEvent& e)
{
	toFront (true);
	if (e.eventComponent == resizer)
	{
	}
	else
	{
		dragger.startDraggingComponent (this,0);
	}
	userAdjusting = true;
	startBounds = getBounds ();
	userStartedChangingBounds ();
}

void ChildAlias::mouseUp (const MouseEvent& e)
{
	if (e.eventComponent == resizer)
	{
	}
	else
	{
	}
	if (userAdjusting) userStoppedChangingBounds ();
	userAdjusting = false;
}

void ChildAlias::mouseDrag (const MouseEvent& e)
{
	if (e.eventComponent == resizer)
	{
	}
	else
	{
		if (!e.mouseWasClicked ())
		{
			dragger.dragComponent (this,e);
			applyToTarget ();
		}
	}
}

void ChildAlias::mouseEnter (const MouseEvent& e)
{
	interest = true;
	repaint ();
}

void ChildAlias::mouseExit (const MouseEvent& e)
{
	interest = false;
	repaint ();
}

//=============================================================================
ComponentLayoutEditor::ComponentLayoutEditor ()
:	target (0)
{
	setColour (ComponentLayoutEditor::aliasIdleColour,Colours::lightgrey.withAlpha(0.2f));
	setColour (ComponentLayoutEditor::aliasHoverColour,Colours::white.withAlpha(0.5f));
}

ComponentLayoutEditor::~ComponentLayoutEditor ()
{
	if (target) { deleteAndZero (target); }
}

void ComponentLayoutEditor::resized ()
{
	for (int i=0; i<frames.size(); i++)
	{
		frames.getUnchecked(i)->updateFromTarget ();
	}
}

void ComponentLayoutEditor::paint (Graphics& g)
{
}

void ComponentLayoutEditor::setTargetComponent (Component* targetComp)
{
	jassert (targetComp);
	jassert (targetComp->getParentComponent() == getParentComponent());

	if (target)
	{
		if (target->getComponent() == targetComp) return;
		deleteAndZero (target);
	}

	target = new ComponentDeletionWatcher (targetComp);
	bindWithTarget ();
}

void ComponentLayoutEditor::bindWithTarget ()
{
	if (target && !target->hasBeenDeleted ())
	{
		Component* t = (Component*) target->getComponent ();
		Component* p = t->getParentComponent ();

		p->addAndMakeVisible (this);
		setBounds (t->getBounds ());

		updateFrames ();
	}
}

void ComponentLayoutEditor::updateFrames ()
{
	frames.clear ();

	if (target && !target->hasBeenDeleted ())
	{
		Component* t = (Component*) target->getComponent ();

		int n = t->getNumChildComponents ();
		for (int i=0; i<n; i++)
		{
			Component* c = t->getChildComponent (i);
			if (c)
			{
				ChildAlias* alias = createAlias (c);
				if (alias)
				{
					frames.add (alias);
					addAndMakeVisible (alias);
				}
			}
		}
	}
}

void ComponentLayoutEditor::enablementChanged ()
{
	if (isEnabled ())
	{
		setVisible (true);
	}
	else
	{
		setVisible (false);
	}
}

const Component* ComponentLayoutEditor::getTarget ()
{
	if (target) return target->getComponent ();
	return 0;
}

ChildAlias* ComponentLayoutEditor::createAlias (Component* child)
{
	return new ChildAlias (child);
}

It’s been dug out as I’m rebuilding handxl from the ground up in my free time, so maybe it might one day see a release (and in the plugin form it was always meant to be in!). I was planning on expanding this to be a little more customisable [e.g. subclassing the alias overlay to act on the user editing the position - bypassing awkward resized() issues when components are mapped to scaled back-end bounds, etc], but this is the form it’s been sat about doing nothing in. Have fun!


#2

gr8 haydxn ! was about to write one myself this afternoon !
(it seems people read in my brain these days :D)

thanx’n’cheers !


#3

:slight_smile: i suppose it’s less people reading your brain, and more your brain summoning people to task - i just felt compelled to post it today!

I’ve just updated the code above, you’ll probably want to use that instead. It now has differentiating between user-resize and incidental resize built in, and can be subclassed;

subclassing ChildAlias gives you access to

virtual void userChangedBounds ();
virtual void userStartedChangingBounds ();
virtual void userStoppedChangingBounds ();

so you can for example trigger an undoable data ammendment once the adjustment has been finished. You can check ‘boundsChangedSinceStart ()’ in the stopped callback too, to save doing anything when you don’t need to.

If you subclass ComponentLayoutEditor, you can then instantiate your own ChildAlias in the ‘createAlias’ function. [you could also dynamic_cast the component passed in to create a different type of alias, or even ignore, based on the child type].

all it really needs now is the obvious ComponentBoundsConstrainer.
I hope it’s useful!


#4

cool :slight_smile:
i would only suggest making this patch:

[code]ComponentLayoutEditor::ComponentLayoutEditor ()
: target (0)
{
setColour (ComponentLayoutEditor::aliasIdleColour,Colours::lightgrey.withAlpha(0.2f));
setColour (ComponentLayoutEditor::aliasHoverColour,Colours::white.withAlpha(0.5f));

setInterceptsMouseClicks (false, true);
}[/code]

this way you can intercept the mouse events on the underlying component and show a regular popup menu if you want to (the easy way to add components to edit) :slight_smile:


#5

ahh, of course, excellent idea.

i remember being really surprised at how easy it was to do this - the last time i’d tried to make such an editable interface, it involved confusing per-component overlays, and was really fiddly. I hope it saves you plenty of effort :slight_smile:


#6

well the idea behind it is very clear, the same i had in mind. you saved me some time testing it :slight_smile:

in less than 10 minutes of real coding i’ve added it to jost and introduced the first steps of the surface editor for controlling the parameters you want of the plugins modularized on your patch :smiley:

thanx !!


#7

that’s excellent news! thanks for the feedback :slight_smile:


#8

Have been using this in an app for a while, and after updating to the latest Git tip recently, it had stopped working because ComponentDeletionWatcher is no longer around. I updated the class using SafePointers instead which seems to work nicely. Basically just had to switch if(!target.hasBeenDeleted()) to if (target != NULL)-- unless you see a more correct way?

Also, for my purposes I had added a ComponentBoundsConstrainer a while back to prevent dragging objects offscreen, another to limit how small a component could be resized, and a few other little things (any small additions should be commented), although, besides those things, functionality should be pretty similar to the original version. Here you go…

ComponentLayoutEditor.h

/*
 *  ComponentLayoutEditor.h
 *  
 *  Original written by Haydxn
 *  Modified by Jordan Hochenbaum on 10/25/10.
 *  http://www.rawmaterialsoftware.com/viewtopic.php?f=6&t=2635
 *
 */

#ifndef _COMPONENTLAYOUTEDITOR_H_
#define _COMPONENTLAYOUTEDITOR_H_

#include "JuceHeader.h"

//=============================================================================
class ChildAlias   :   public Component
    {
    public:
		ChildAlias (Component* targetChild);
		~ChildAlias ();
		
		void resized ();
		void paint (Graphics& g);
		
		const Component* getTargetChild ();
		
		void updateFromTarget ();
		void applyToTarget ();
		
		virtual void userChangedBounds ();
		virtual void userStartedChangingBounds ();
		virtual void userStoppedChangingBounds ();
		
		bool boundsChangedSinceStart ();
		
		void mouseEnter (const MouseEvent& e);
		void mouseExit (const MouseEvent& e);
		void mouseDown (const MouseEvent& e);
		void mouseUp (const MouseEvent& e);
		void mouseDrag (const MouseEvent& e);
		
    private:
		
		CriticalSection bounds;
		ComponentBoundsConstrainer*  constrainer;

		ComponentDragger dragger;
		SafePointer<Component> target;
		bool interest;
		bool userAdjusting;
		Rectangle<int> startBounds;
		ComponentBoundsConstrainer* resizeContainer; //added resizeContainer to limit resizing sizes
		ResizableBorderComponent* resizer;
    };

//=============================================================================
class ComponentLayoutEditor   :   public Component
    {
    public:
		
		enum ColourIds
		{
			aliasIdleColour,
			aliasHoverColour
		};
		
		ComponentLayoutEditor ();
		~ComponentLayoutEditor ();
		
		void resized ();
		void paint (Graphics& g);
		
		void setTargetComponent (Component* target);
		
		void bindWithTarget ();
		void updateFrames ();

		void enablementChanged ();
		const Component* getTarget ();
		
    private:
		
		virtual ChildAlias* createAlias (Component* child);
		
		SafePointer<Component> target;
		OwnedArray<ChildAlias> frames;
		
    };

#endif//_COMPONENTLAYOUTEDITOR_H_

ComponentLayoutEditor.cpp


/*
 *  ComponentLayoutEditor.cpp
 *  
 *  Original written by Haydxn
 *  Modified by Jordan Hochenbaum on 10/25/10.
 *  http://www.rawmaterialsoftware.com/viewtopic.php?f=6&t=2635
 *
 */
#include "ComponentLayoutEditor.h"

ChildAlias::ChildAlias (Component* targetChild)
:   target (targetChild)
{	
	resizeContainer = new ComponentBoundsConstrainer();
	resizeContainer->setMinimumSize(target.getComponent()->getWidth()/2, target.getComponent()->getHeight()/2); //set minimum size so objects cant be resized too small
	resizer = new ResizableBorderComponent (this,resizeContainer);
	addAndMakeVisible (resizer);
	resizer->addMouseListener (this,false);
	constrainer = new ComponentBoundsConstrainer();
	
	interest = false;
	userAdjusting = false;
	
	updateFromTarget ();
	setRepaintsOnMouseActivity (true);
}

ChildAlias::~ChildAlias ()
{
	delete resizer;
}

void ChildAlias::resized ()
{
	resizer->setBounds (0,0,getWidth(),getHeight());
	
	if (resizer->isMouseButtonDown ())
	{
		applyToTarget ();
	}
}

void ChildAlias::paint (Graphics& g)
{
	Colour c;
	if (interest)
		c = findColour (ComponentLayoutEditor::aliasHoverColour,true);
	else c = findColour (ComponentLayoutEditor::aliasIdleColour,true);
	
	g.setColour (c.withMultipliedAlpha (0.3f));
	g.fillAll ();
	g.setColour (c);
	g.drawRect (0,0,getWidth(),getHeight(),1);
}

const Component* ChildAlias::getTargetChild ()
{
	return target.getComponent ();
}

void ChildAlias::updateFromTarget ()
{
	if (target != NULL)
		//if (!target.hasBeenDeleted ())
	{
		setBounds ( target.getComponent ()->getBounds () );
	}
}

void ChildAlias::applyToTarget ()
{
	if (target != NULL)
		//!target.hasBeenDeleted ())
	{
		Component* c = (Component*) target.getComponent ();
		c->toFront(false); //added this to bring the the component to the front
		c->setBounds (getBounds ());
		userChangedBounds ();
	}
}

void ChildAlias::userChangedBounds ()
{
	//update minimum onscreen amounts so that object can't be resized past the screen area
	resizeContainer->setMinimumOnscreenAmounts(getHeight()+target.getComponent()->getHeight(), getWidth()+target.getComponent()->getWidth(), 
											   getHeight()+target.getComponent()->getHeight(), getWidth()+target.getComponent()->getWidth());
	
}

void ChildAlias::userStartedChangingBounds ()
{
}

void ChildAlias::userStoppedChangingBounds ()
{
}

bool ChildAlias::boundsChangedSinceStart ()
{
	return startBounds != getBounds ();
}


void ChildAlias::mouseDown (const MouseEvent& e)
{
	toFront (true);
	if (e.eventComponent == resizer)
	{
	}
	else
	{
		//added a constrainer so that components can't be dragged off-screen
		constrainer->setMinimumOnscreenAmounts(getHeight(), getWidth(), getHeight(), getWidth());
		dragger.startDraggingComponent (this,constrainer);
	}
	userAdjusting = true;
	startBounds = getBounds ();
	userStartedChangingBounds ();
}

void ChildAlias::mouseUp (const MouseEvent& e)
{	
	if (e.eventComponent == resizer)
	{
	}
	else
	{
		//add this to reset MainComponent to have keyboard focus so that keyboard shortcuts (eg. lock/unlock) still work / intercept the messages
		getTopLevelComponent()->getChildComponent(0)->grabKeyboardFocus(); 
	}
	if (userAdjusting) userStoppedChangingBounds ();
	userAdjusting = false;
	
}

void ChildAlias::mouseDrag (const MouseEvent& e)
{
	if (e.eventComponent == resizer)
	{
	}
	else
	{
		if (!e.mouseWasClicked ())
		{
			dragger.dragComponent (this,e);
			applyToTarget ();
		}
	}
}

void ChildAlias::mouseEnter (const MouseEvent& e)
{
	interest = true;
	repaint ();
}

void ChildAlias::mouseExit (const MouseEvent& e)
{
	interest = false;
	repaint ();
}

//=============================================================================
ComponentLayoutEditor::ComponentLayoutEditor ()
:   target (0)
{
	setColour (ComponentLayoutEditor::aliasIdleColour,Colours::lightgrey.withAlpha(0.2f));
	setColour (ComponentLayoutEditor::aliasHoverColour,Colours::white.withAlpha(0.5f));
    setInterceptsMouseClicks (false, true);	
}

ComponentLayoutEditor::~ComponentLayoutEditor ()
{
	if (target != getTopLevelComponent()->getChildComponent(0) ){deleteAndZero(target);} //added this to make sure we dont remove our background component
	//if (target) { deleteAndZero (target); } //original
}

void ComponentLayoutEditor::resized ()
{
	for (int i=0; i<frames.size(); i++)
	{
		frames.getUnchecked(i)->updateFromTarget ();
	}
}

void ComponentLayoutEditor::paint (Graphics& g)
{
}

void ComponentLayoutEditor::setTargetComponent (Component* targetComp)
{
	jassert (targetComp);
	jassert (targetComp->getParentComponent() == getParentComponent());
	
	if (target)
	{
		if (target.getComponent() == targetComp) return;
		deleteAndZero (target);
	}
	
	target = targetComp;
	bindWithTarget ();
}

void ComponentLayoutEditor::bindWithTarget ()
{
	if (target != NULL)
		//if (target && !target->hasBeenDeleted ())
	{
		Component* t = (Component*) target.getComponent ();
		Component* p = t->getParentComponent ();		
		p->addAndMakeVisible (this);
		setBounds (t->getBounds ());
		
		updateFrames ();
	}
}

void ComponentLayoutEditor::updateFrames ()
{
	frames.clear ();
	
	if (target != NULL)
		//if (target && !target->hasBeenDeleted ())
	{
		Component* t = (Component*) target.getComponent ();
		
		int n = t->getNumChildComponents ();
		for (int i=0; i<n; i++)
		{
			Component* c = t->getChildComponent (i);
			if (c)
			{
                ChildAlias* alias = createAlias (c);
                if (alias)
                {
					frames.add (alias);
					addAndMakeVisible (alias);
                }
			}
		}
	}
}

void ComponentLayoutEditor::enablementChanged ()
{
	if (isEnabled ())
	{
		setVisible (true);
	}
	else
	{
		setVisible (false);
	}
}


const Component* ComponentLayoutEditor::getTarget ()
{
	if (target) return target.getComponent ();
	return 0;
}

ChildAlias* ComponentLayoutEditor::createAlias (Component* child)
{
	return new ChildAlias (child);
}