Non-modal dialogs and multi-step transactions

How do you handle the execution and unwinding of multi-step interactive transactions with non-modal dialogs?

Let’s define a transaction as multiple changes in a data model that make sense only as a whole. For example, scripted stuff like adding tracks to a DAW based on predefined templates, or dropping MIDI files and letting the user pick target tracks and sounds. Let’s assume they implement a flow diagram with user interaction (e.g. select something, yes, no, cancel).

With modal dialogs, one can simply use the stack (RAII) and show any dialog along the way when a decision is needed. Easy coding and maintenance, but blocks the UI.

A non-modal alternative is emulating a transaction as a state machine that branches and continues only after user interaction. It needs to maintain its own stack (akin to Undo). If you make sure only a single such transaction can run at a time, your screen won’t fill up with dialogs of half-completed transactions (and conflicting states). Just make the screen look and feel as if it was modal, although it actually isn’t. The cumbersomeness here is that you need to disable all commands while the transaction is running (widgets, menus, toolbars). A smart UI framework can handle this transparently, but w/o such a framework you are in for a forced labor experience.

Another approach is to gather all decision-relevant information from the user in advance, by running a pre-flight or something, and start the transaction based on a single dialog (which essentially predetermines the path through the flow diagram). Logic for example, does this “mission briefing” thing when you add a new track. I think it’s a beautiful solution.

And then there’s keyboard modifiers (alt, shift, ctrl, command, …) that could be used to branch along different paths after dropping an object, but these are notoriously difficult to remember.

I get that the days of just popping up dialogs along the way as you need them are gone. But coding every transaction as a state machine, or building “mission briefing dialogs” with pre-flights seems overkill if you have many transactions of this kind.

How do you (would you) handle this?

I have recently considered whether coroutines introduced in C++20 would be up to the task of solving exactly the problem you describe: C++ coroutines instead of modal loops and async callbacks for UI?

Unfortunately my suggestion didn’t seem to gain much traction and the main answer was “don’t try to do that, use callback lambdas instead”. But your description of the problem above is far better and shows why callback lambdas don’t scale well for the case you describe.
In a complex flowchart, each branching would need to provide handling code for all further branching downhill from there, with possible joining paths afterwards, causing an unnecessary combinatory explosion of possibilities, duplication of code that makes it difficult to maintain, etc.

I don’t have time at the moment to dig further into the direction of coroutines, but it’s on my list to try and experiment with them for user interaction with JUCE UI