Is it possible to group multiple ValueTree.setProperty into a single action to be undoable


#1

I’m new to both ValueTree’s and the UndoMangager, but thanks to David Rowland’s ADC17 talk, I am making great progress with ValueTree’s. But as I start to add in the UndoManger, I ran into the case where an operation updates several of the ValueTree properties as part of it’s work, and they need to be undone together. Is there a standard way to handle this?


#2

Every property that’s been updated since last undoManager.beginTransaction() should be undone when you do undoManager.undo(). Guess that’s the standard way.


#3

I assume you mean beginNewTransaction. So, when performing the group of property updates, do I call beginNewTransaction before, after, or both? And since the code has a bunch of places where individual calls to setProperty are made, do I need to be calling beginNewTransaction at any other time? If I don’t will they all just be undone at once?


#4

Before, no and yes. All updates since last beginnewtransaction will be undone when you issue undo. The whole purpose of beginNewtransaction is to group valutree updates such as property changes, child additions and -removals into undo/redo packets i.e transactions…


#5

Got it! So, generally speaking, it’s common to call it prior to setProperty calls? ie. a user moves a clip on a track, code call beginNewTransaction and then call setProperty when setting the new position?


#6

An easy trick is to have a timer call it every couple of seconds, as long as the user isn’t in the middle of an operation e.g. dragging the mouse or something. (It’s OK to call it multiple times in succession, the calls will be ignored if nothing has changed in between)


#7

Yeaps. And if you also make use of CachedValues it might look something like this

class AudioTrack
	: public Component
	, public ValueTree::Listener
{

public:
	AudioTrack()
		: Component("AudioClip")
	{
		//listen to changes in valuetree, esp clipPosition
		vt.addListener(this);	

		//updates to clipPosition will update valuetree, which will cause valueTreePropertyChanged to be called 
		clipPosition.referTo(vt, IDposition, &undomanager);	
	}

	void mouseDrag(const MouseEvent& e) noexcept override
	{
		//move audioClip acc to mousevent, but DON'T update clipPosition
	}

	void mouseUp(const MouseEvent& e) noexcept override
	{
		undomanager.beginNewTransaction("Move audio clip");

		//will trigger a call to valueTreePropertyChanged 
		//which is prob unneccassry while the clip is already dragged to it's new position
		//but it will create an action entry in the undomanager for clipPosition i.e record its new value 
		// so it can be undone
		clipPosition = audioClip->getX();	
	}

	//when you do undomanager.undo() this will be called by the undomanager and reposition your audio clip to 
	//previous value of clipPosition. And redo will redo it (surprisingly, eh...)
	void valueTreePropertyChanged(ValueTree& changedValueTree, const Identifier& property) noexcept override
	{
		if (property == IDposition)
		{
			audioClip->setTopLeftPosition(changedValueTree[property], getY());
		}
	}

	void valueTreeParentChanged(ValueTree& treeWhoseParentHasChanged) {}
	void valueTreeChildRemoved(ValueTree& parentTree, ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved) {}
	void valueTreeChildAdded(ValueTree& parentTree, ValueTree& childWhichHasBeenAdded) {}
	void valueTreeChildOrderChanged(ValueTree& parentTreeWhoseChildrenHaveMoved, int oldIndex, int newIndex) {}

	
private:
	MyAudioClip *audioClip;
	CachedValue<int>clipPosition;
	ValueTree vt;
};

#8

@jules this advice makes me a bit worried, since timer events come whenever they want, and I don’t think they are blocked during a drag. Also what is a reasonable interval?

Wouldn’t it be better to create a global mouse listener and call beginNewTransaction on each mouseDown?
That way the user doesn’t have to check, if there is an user interaction going on…

Possible caveat: the mouseListener should probably be defined as first thing, so it receives the mouseDown before each component?


#9

I think what Jules means is to check Component::isMouseButtonDownAnywhere and simply don’t start a new transaction if it is.

The point of starting transactions after doing some actions is that you’ll always be sitting on the start of a new transaction.

The other thing we have in Waveform is an UndoTransactionInhibitor which basically increments/decrements a counter in an RAII way. If the count is non-zero, new transactions aren’t started. This can be useful in long operations or when the mouse might not be being held down (e.g. when rendering files to new clips but you want all the new clips to be undone in one go).


One other important thing to note is that if you’re doing complex modifications to properties, you might want to call UndoManager::undoCurrentTransactionOnly before each new property set to get a ‘clean’ transaction which represents the start -> end states only.


#10

Thanks, that sounds easy enough.

Does this make a difference when I call undo? I assume if there was an empty beginNewTransaction, it would simply skip that?
So that idea simply avoids the situation having forgotten to call a new transaction. Could be helpful in some situations.

With my mouseDown approach I get the benefit, that I already know, what action is about to happen, and I can name the transaction appropriately.

Also I only put user supplied values into the ValueTree, so a ValueTree::Listener restores everything calculated, when something is undone.


#11

Yes, it will skip it.

Yes, but you also have setCurrentTransactionName you could use before starting the next one.


#12

@jules would this timer be the only place where beginNewTransaction is called, or would it make sense to call it prior to specifically grouped ValueTree mutations? Or are you saying that the timer is sufficient in all cases?


#13

Yeah, you’d probably have a few other places where you’d want to call it too, e.g. if you were doing a big operation that you definitely don’t want to be grouped with any other actions.