Best strategies for dealing with change feedback a GUI?

Hello, all and sundry. Sorry for no word recently but I’m still on the last phases on this programming project, I probably shan’t have time to get relaxed here for a month or more.

I keep creating the same sort of bug in different ways. If you think of my data as a “model” and my Juce components being the “view”, the issue is that I get a feedback loop between the model and the view, or between the model and itself, and one that’s often hidden from me by the MessageQueue.

My application is a simple data driven system. Many different things can change the data, which then broadcasts those changes to all its consumers. Setting the data without sending out updates is not possible nor a good strategy - there’s a guarantee that all clients are always aware of the almost-current state of their data. The data can’t contain cycles so feedback loops just aren’t possible.

I want my Juce View to be the “slave” of the data… so whenever possible I use the non-propagating version of Juce calls - except a lot of the time (particularly in the Tables) there aren’t such calls. I have various mechanisms to filter… but…

I also am careful not to trigger my updates unless the variable has “really changed” but that’s work, I initially had troubles the other way where I wasn’t getting updates at all, and another one where there was a numerical instability so a GUI component’s location “wobbled” (the layout operation wasn’t actually idempotent).

When I run into a problem more than once, I always look for a systematic solution to it. But nothing has come to mind here. Your ideas are welcome!

This is the solution that I took:

  • When the user manipulates a Juce control, it tells the model.

  • The model broadcasts a message to all the views (of which the Juce control is one)

  • When a view receives a broadcast, it updates its appearance to reflect the new value

So, a Juce control is never allowed to change its own value, it can only request from the model that the value be changed, and then it has to listen for the broadcast in order to actually change. This works with sliders, knobs, check boxes, pretty much everything.

I have this working with an expanded version of the Juce Listener concept. Specifically, not only can a view register itself as a Listener for a particular object, but it can also specify upon which thread it wants to receive its callback on. This eliminates the need for locks on data (at the expense of every thread having to keep its own copy of the values).

Using this approach, it is possible to have uniform behavior for all controls, in a multithreaded application, where each thread can both receive notifications and set the value on program parameters, in a completely safe way, without the need for locking on data. Additionally, it handles the case where two different threads try to manipulate the same parameter.

So, a Juce control is never allowed to change its own value, it can only request from the model that the value be changed, and then it has to listen for the broadcast in order to actually change. This works with sliders, knobs, check boxes, pretty much everything.

Very good clear advice!

This what I settled on, in essence, but unfortunately the ListBox and TableListBox components don’t quite work that way - they set themselves and then tell you that this has happened, and there is no way to set their selection state without triggering a new callback.

Working on this last bug, I realized in fact that it’s only selections that have this issue so I have a special case that looks to see if my supposed “data update” is in fact a non-update from the selection and kill the feedback.

Perhaps a mode in these two where you can set the selection but not trigger a selection update callback?

And I guess this leads to a general rule for all setters on all GUI components - whenever possible, have a version of each setter which does not trigger listener callbacks and use it in the right place.

Handle the selection yourself in the listboxes.

The Juce ListBox selection code doesn’t scale well to hundreds of thousands of rows.

I don’t even have a dozen rows!! :smiley:

I’m actually still having issues even with that selection feedback loop choked, it seems, so I’m now wondering if TableListBox::updateContent is generating callbacks that I’m not filtering out…

Little update for future spelunkers - it turns out there are other ways to set selections in tables where you can turn side effects on and off, only the one method I was using (selectRange()) doesn’t have a non-propagating version.

I also discovered a subtle but gross :smiley: fault in my code where I had a poorly named method onChange() (unrelated to Juce’s “onChange()” methods) that was being used for subtly different purposes in different parts of the program - mostly I called it when my persistent data changed in the “database” (there’s no database, but you can think of it that way) and the GUI and local data needed to be updated - but occasionally I was calling it when the local data has been changed by the GUI and I need to update the “database” - in other words, these were in some sense callbacks in reverse directions!

I fixed this by renaming the methods to onPersistentDataChange() and updatePersistentData() and suddenly it was obvious what I had done.

Had I been using my generic Listener/Broadcasters, I might not have made this error, because there I distinguish things by the signature of their callbacks, and it would have been clear which one was “coming” and which one “going”.

I’ve run into this issue in my code and other people’s before, it’s very easy to do in untyped languages like Python.

Takeaway is “you need long, clear names for methods that represents an event happening to make dam’ good and sure you know exactly what event that is.”

Juce generally doesn’t have this issue - for example, Juce’s onChange actually receives the object that has been changed as a parameter, so it’s unambiguous (meaning exactly "this object changed’). Jules occasionally gets a little too verbose, but that’s 100000x better than the reverse.