GUI framework design

Archived bsnes development news, feature requests and bug reports. Forum is now located at http://board.byuu.org/
Locked
byuu

GUI framework design

Post by byuu »

Okay, this is intended for developers only.

So one thing bsnes needs is my cross-platform GUI wrapper, which I also use for other things, eg the UPS patching application and some personal tools. My goal is to make the most easy to use general purpose library possible.

A problem I've run into recently was with setting the state of GUI objects. For instance, I have six settings for speed regulation in the menu, and there should be a check next to the active setting.

You can control this setting both by selecting the menu option, or pressing a keyboard binding that changes it. Either way, the radiobox has to move to the currently active option.

So for the first, the user clicks a menu option. Do we make the UI automatically select that radio item? Tough not to, GTK+ does this internally. Next, we pretty much must invoke a callback function, as it's the only way you can know that an action has occurred.

For the second, let's say the codepath decides to change a setting on its end. Perhaps to initialize the startup GUI state, or in reaction to some other action. The problem is, what to do about the callback? When you call menuitem.check(), should it trigger the callback function (eg on_check()) to indicate that the state has changed?

Here's where it gets interesting ... there are basically three tasks involved with all of this:

1) updating the UI state to set the correct checks and such.
2) invoking a callback to know when a user checked something.
3) executing the internal code that should happen, eg adjusting speed regulation.

The ultimate goal is to avoid code repetition, so the first thought is to make a generalized function, eg update_speed_regulation(unsigned newsetting); and have both the user-initiated callback function and the internal code call this function.

But if it were an internal action, you still have to update the checkbox status. But the code to do that may rely upon the internal code, so you can't just put that code before calling update_speed_regulation().

So you think, okay, have update_speed_regulation() set the check state. But if it does that, it could end up invoking the callback routine, which calls USR(), which will quickly get you stuck in an infinite loop.

So basically, I want to find the optimal solution that gets everything working a ideally, simply and cleanly as possible.

One idea is to make a special silent_check(bool state = true) function (and parallel silent_uncheck()), which won't invoke the callback routine. But that could get annoying to look at in code, having two functions for every kind of state updater.

Another idea is to make check(), etc not invoke the callback function, but then when you want that to happen (say if all logic was inside the callback function), it would be difficult to do so. My functor implementation obviously doesn't support default parameters like normal functions, so you'd have to call object.on_tick(objects_parent, Event::MessageType, (uintptr_t)parameter); which is annoying.

It also doesn't help that GTK+ itself invokes the attached callback automatically when you call the code functions to set UI state, but that's easier to block with a simple "lock = true; gtk_set_check(); lock = false;" inside the GUI wrapper library.

So given the update_speed_regulation() example, any ideas for an optimal implementation?
creaothceann
Seen it all
Posts: 2302
Joined: Mon Jan 03, 2005 5:04 pm
Location: Germany
Contact:

Post by creaothceann »

byuu wrote:When you call menuitem.check(), should it trigger the callback function (eg on_check()) to indicate that the state has changed?
Yes. "menuitem.check()" should be the function to set the menuitem's status from code.
The item status should be updated automatically, IMO.

(EDIT: Scratch that, this is outdated now.)

---

When my code changes the status of a control which fires an unwanted event by itself, I set a variable first (e.g. "Form.Enabled := False;") and check that variable in the event handler of the control (e.g. "If (not Form.Enabled) then Exit;"). So a lot of event handlers have this line at the beginning (the "Form." part is not required).

Since you're writing your own framework you could for example provide a window's internal "LockCount" variable that prevents callbacks when non-zero, and matching "Lock"/"Unlock" functions.
Last edited by creaothceann on Thu Apr 24, 2008 3:57 pm, edited 1 time in total.
vSNES | Delphi 10 BPLs
bsnes launcher with recent files list
byuu

Post by byuu »

When my code changes the status of a control which fires an unwanted event by itself, I set a variable first (e.g. "Form.Enabled := False;") and check that variable in the event handler of the control (e.g. "If (not Form.Enabled) then Exit;").
Right. I can skip this by making API functions not trigger events. But you have a good idea about using an internal lock count. I was just using caching before, eg:
bool locked = form.locked;
form.locked = true;
...
form.locked = locked;

But a lock count simplifies that.

So, it's basically a choice between an API doesn't trigger events, eg:

Code: Select all

void speedreg3_tick() {
  update_speedreg(3);
}

void key_pressed(key_t key) {
  if(key == keyboard::tab) update_speedreg(3);
}

void update_speedreg(int reg) {
  if(reg == 3) {
    //... internal code ...
    speedreg3.check();
  }
}
... and an API that has lock counters, eg:

Code: Select all

void speedreg3_tick() {
  if(locked()) return;
  update_speedreg(3);
}

void key_pressed(key_t key) {
  if(key == keyboard::tab) update_speedreg(3);
}

void update_speedreg(int reg) {
  speedreg3.lock();
  if(reg == 3) {
    //... internal code ...
    speedreg3.check();
  }
  speedreg3.unlock();
}
I definitely like that the former is less code in this case. The only time the former would be a problem is when you put internal logic inside the callback functions. In that instance, you'd have to make your API-based calls also invoke the callback by hand.

So, what makes more sense? Use the callback as nothing more than a stub that says "the user clicked this button" and stick all the underlying code in a separate function, or put the logic inside the callback so that the API just has to call object.check() and be done with it?

The former seems better for abstraction and doing more complex things, but the latter seems better for simplicity. Both have the problem that you're basically calling speedreg3.check(), which is superfluous when the user ticked the option and the checkbox was already set there anyway, but there's no getting around that one to make it manual due to GTK+.
augnober
Rookie
Posts: 15
Joined: Fri Apr 18, 2008 5:29 am

Post by augnober »

Rather than thinking of the UI as something that triggers non-UI code directly, or that acts directly upon external variables, perhaps you can make it more maintainable by thinking of it as something that interacts externally only when necessary (the specifics of how this is actually implemented isn't the important part). This would tend toward code sharing and modularity. You may find that you don't really need to tell the app/engine/game to make an update to a single value, since you could get practically the same performance out of updating a structure/list of related properties in a single go - which will probably be easier to wrap your head around and be easier to maintain (ie. the code that handles the changes to the properties is what decides what actions are appropriate to bring them into effect, rather than the UI code which simply submits the new values). Similar thinking can be used when updating the UI to reflect current state (yes, I think it's totally fine if the UI operates upon a separate copy/structure of some properties until it's time to submit - even if you're deciding to submit it every frame as a user slides a slider or whatever).

So, not knowing much about your UI framework, I'm thinking that there's not necessarily anything wrong with it. Like a lot of things, the application developer just has to use it wisely to avoid getting in a bind.

(the main point is that I like submitting a chunk of values and having non-UI code figure out what to do about it. it's sort of a natural division of labor like submitting a proposal for a new state, and having the experts/grunts work out the details. makes it easy)
creaothceann
Seen it all
Posts: 2302
Joined: Mon Jan 03, 2005 5:04 pm
Location: Germany
Contact:

Post by creaothceann »

byuu:
Putting too much logic into event handlers is more of a quick & dirty solution, in my experience. It tends to get rewritten anyway into the "separate function" solution as development progresses.

Simply accessing the control (e.g. "Button1.Click") from somewhere else in the program certainly has its appeal, but without automatic events that's no longer possible... I guess.


augnober:
One "design pattern" I've used occassionally is having the event handlers operate on the internal state only, and then calling one function ("Update_GUI") to update all the controls on the window. Is that it what you meant?
vSNES | Delphi 10 BPLs
bsnes launcher with recent files list
augnober
Rookie
Posts: 15
Joined: Fri Apr 18, 2008 5:29 am

Post by augnober »

creaothceann wrote:augnober:
One "design pattern" I've used occassionally is having the event handlers operate on the internal state only, and then calling one function ("Update_GUI") to update all the controls on the window. Is that it what you meant?
I think that's not exactly what I meant. What I'm trying to say is that you can maintain a set of properties that determine operation of the app (I consider these the "back end" values), and a corresponding set(s) of properties that are modifiable by the GUI (I consider these the "front end" values). There is then a function that applies front end values to the back end. In many cases it will turn out that a value has not been changed and/or that it is not necessary to take any action -- and so no action will be taken. The GUI never makes it explicit that a particular change has or has not occurred. Rather, changes are deduced by comparing the new values against the existing back end values. If there has been a change, the code takes the course of action necessary to bring the app in line. And you write the new values into the back end so that you can figure out the changes next time too. This way, all action that must be taken due to changes in these properties is consolidated to the place where the front end values are applied to the back end (which is conveniently outside of the GUI code). The GUI just takes care of updating front end values, and you need to be sure to call the function to apply the set of values at the appropriate time.
(and of course you can subdivide the modifiable properties into sets that make sense for your app -- since comparisons take time and you don't want to make too many unnecessary comparisons particularly if you want changes to sliders to be applied in realtime)

There is also a separate method(s) for getting the back end values into the front end for display and modification. If you're doing the rest, then this part isn't any harder. You just have to make sure you fill up the front end values with fresh values whenever necessary.

That's mainly talking about changes to persistent properties that affect the app. In other cases, the above approach makes less sense. For example, if you're trying to reset the system, then that is more like a command than a value. You're going to have to make a function call to make it happen, and you're probably going to just want to go ahead and do it rather than setting up an unintuitive set of values associated with these actions (though that could be a workable strategy too - I never tried it).

As for hotkeys that modify properties.. To keep things consistent, I suppose it would be good to have it refresh the appropriate front end values struct (with values from the backend), modify the value within that struct (in the same way the GUI would, if applicable), and then apply the changes to the back end. Keypresses are rare events where the time spent doing the extra work isn't significant.

I suppose I should try out bsnes to see what the GUI makes available.. :)
creaothceann
Seen it all
Posts: 2302
Joined: Mon Jan 03, 2005 5:04 pm
Location: Germany
Contact:

Post by creaothceann »

I'd guess that bsnes doesn't have that many data to make this explicit distinction between frontend and backend data. Most of the work is probably just calling some functions, similar to the "system reset" you mentioned.
vSNES | Delphi 10 BPLs
bsnes launcher with recent files list
Locked