Event Handling in the YaST2 UI

Author: Stefan Hundhammer <sh@suse.de>

Introduction

Contents

The YaST2 Event Model

Classic GUI Event Loops

Classic graphical user interface (GUI) programming is almost always event-driven: The application initializes, creates its dialog(s) and then spends most of its time in one central event loop.

When the user clicks on a button or enters text in an input field, he generates events. The underlying GUI toolkit provides mechanisms so the application can react to those events - perform an action upon button click, store the characters the user typed etc.; all this is done from callback functions of one kind or the other (whatever they may be called in the respective GUI toolkit).

In any case, it all comes down to one single event loop in the application from where small functions (let's call them callbacks for the sake of simplicity) are called when events occur. Those callbacks each contain a small amount of the application's GUI logic to do whatever is to be done when the respective event occurs. The overall application logic is scattered among them all.

This approach is called event-driven. Most GUI toolkits have adopted it.

Depending on the primary goal of a GUI application, this event-driven approach may or may not be appropriate. It is perfectly suitable for example for word processor applications, for web browsers or for most other GUI applications that have one central main window the user works with most of his time: The user is the driving force behind those kinds of applications; only he knows what he next wishes to do. The application has no workflow in itself.

Thus the event-driven application model fits perfectly here: The callbacks can easily be self-contained; there is little context information, and there are limited application-wide data.

The YaST2 Approach

Applications like YaST2 with all its installation and configuration workflows, however, are radically different. The driving force here is the application workflow, the sequence of dialogs the user is presented with.

Of course this can be modeled with a traditional event loop, but doing that considerably adds to the complexity of the application: Either the application needs a lot more callbacks, or the callbacks need to keep track of a lot of status information (workflow step etc.) - or both.

For the YaST2 UI, a different approach was chosen: Rather than having one central event loop and lots of callbacks, the flow control remains in the interpreted YCP code. User input is requested on demand - very much like in simplistic programming languages like the first versions of BASIC.

This of course means that there is no single one central "waiting point" in the program (like the event loop in the event-driven model), but rather lots of such waiting points spread all over the YCP code within each UserInput() or WaitForEvent() statement.

Side note: Of course a graphical UI like the YaST2 Qt UI still has to be prepared to perform screen redraws whenever the underlying window system requires that - i.e. whenever X11 sends an Expose (or similar) event. For this purpose the Qt UI is multi-threaded: One thread takes care of X event handling, one thread is the actual YCP UI interpreter. This instant screen redraw is what you lose when you invoke y2base with the "--nothreads" command line option.

YCP was meant to be an easy-to-understand programming language for developers who specialize in a particular aspect of system configuration or installation, not in GUI programming.

Practical experience with all the YaST2 modules developed so far has shown that application developers tend to adopt this concept of UserInput() very easily. On the other hand it is a widely known fact that event-driven GUI programming means a steep learning curve because (as mentioned before) it requires splitting up the application logic into tiny pieces for all the callbacks.

Thus, this design decision of YaST2 seems to have proven right much more often than there are problems with its downsides (which of course also exist).

Simplicity vs. Features

The basic idea of YaST2 UI programming is to create a dialog asking the user for some data and then continue with the next such dialog - meaning that most of those dialogs are basically forms to be filled in with an "OK" (or "Next") and a "Cancel" (or "Back") button. The YCP application is usually interested only in those button presses, not in each individual keystroke the user performs.

This is why by default UserInput() and related functions react to little more than button presses - i.e. they ignore all other events, in particular low-level events the widgets handle all by themselves like keystrokes (this is the input fields' job) or selecting items in selection boxes, tables or similar. Most YCP applications simply don't need or even want to know anything about that.

This makes YCP UI programming pretty simple. The basic principle looks like this:

UI::OpenDialog(
               `VBox(
                     ... // Some input fields etc.
                     `HBox(
                           `PushButton(`id(`back ), "Back" ),
                           `PushButton(`id(`next ), "Next" )
                          )
                     )
              );

symbol button_id = UI::UserInput();

if ( button_id == `next )
{
    // Handle "Next" button
}
else if ( button_id == `back )
{
    // Handle "Back" button
}

UI::CloseDialog();

Strictly spoken, you don't even require a loop around that - even though this is very useful and thus strongly advised.

All that can make UserInput() return in this example are the two buttons. Other widgets like input fields ( TextEntry), selection boxes etc. by do not do anything that makes UserInput() return - unless explicitly requested.

The notify Option

If a YCP application is interested in events that occur in a widget other than a button, the notify widget option can be used when creating it with UI::OpenDialog().

Example:

UI::OpenDialog(...
               `SelectionBox(`id(`pizza ), `opt(`notify ), ... ),
               ...
               `Table(`id(`toppings), `opt(`notify, `immediate ), ... ),
               ...
               )

In general, the notify options makes UserInput() return when something "important" happens to that widget. The immediate option (always in combination with notify!) makes the widget even more "verbose".

Note: UserInput() always returns the ID of the widget that caused an event. You cannot tell the difference when many different types of event could have occured. This is why there are different levels of verbosity with `opt(`notify ) or `opt(`notify, `immediate ) and the new WaitForEvent() UI builtin function which returns more detailed information. A Table widget for example can generate both Activated and SelectionChanged WidgetEvents.

Exactly what makes UserInput() return for each widget class is described in full detail in the YaST2 event reference.

Downsides and Discussions

The YaST2 event handling model has been (and will probably always remain) a subject of neverending discussions. Each and every new team member and everybody who casually writes a YaST2 module (to configure the subsystem that is his real responsibility) feels compelled to restart this discussion.

The idea of having a function called UserInput() seems to conjure up ghastly memories of horrible times that we hoped to have overcome: The days of home-computer era BASIC programming or university Pascal lectures (remember Pascal's readln()?) or even low-tech primitive C programs (gets() or scanf() are not better, either).

But it's not quite like that. Even though the function name is similar, the concept is radically different: It is not just one single value that is being read, it is a whole dialog full of whatever widgets you see fit to put there. All the widgets take care of themselves; they all handle their values automatically. You just have to ask them (UI::QueryWidget()) for the values when you need them (leave them alone as long as you don't).

The similarity with computing stone age remains, however, in that you have to explicitly call UserInput() or related when you need user input. If you don't, you open your dialog, and a moment later when you continue in your code it closes again - with little chance for the user to enter anything.

Thus, the YaST2 approach has its intrinsic formalisms in that sequence:

OpenDialog(...);

UserInput();
QueryWidget(...);
QueryWidget(...);
QueryWidget(...);
...

CloseDialog();

This is the price to pay for this level of simplicity.

Design Alternatives

In the course of those discussions some design alternatives began to emerge:

  1. Use the single-event-loop and callback model like most other toolkits.
  2. Keep multiple event loops (like UserInput()), but add callbacks to individual widget events when needed so the YCP application can do some more fine-grained control of individual events.
  3. Keep multiple event loops, but return more information than this simplistic UserInput() that can return no more than one single ID.

Having just a single event loop would not really solve any problem, but create a lot of new ones: A sequence of wizard style dialogs would be really hard to program. Switching back and forth between individual wizard dialogs would have to be moved into some callbacks, and a lot of status data for them all to share (which dialog, widget status etc.) would have to be made global.

What a mess. We certainly don't want that.

All the callback-driven models have one thing in common: Most of the application logic would have to be split up and moved into the callbacks. The sequence of operations would be pretty much invisible to the application developer, thus the logical workflow would be pretty much lost.

Most who discussed that agreed that we don't want that, too.

Add to that the formalisms that would be required for having callbacks: Either add a piece of callback code (at least a function name) to UI::OpenDialog() for each widget that should get callbacks or provide a new UI builtin function like, say, UI::SetCallback() or UI::AddCallback() that gets a YCP map that specifies at least the widget to add the callback to, the event to react to and the code (or at least a function name) to execute and some transparent client data where the application can pass arbitrary data to the callback to keep the amount of required global data down.

While we are at it, add some logical counterparts like UI::RemoveCallback().

It might look about like this:


define void selectionChanged( any widgetID, map event, any clientData ) ``{
    ...
    // Handle SelectionChanged event
    ...
};

define void activated( any widgetID, map event, any clientData ) ``{
    ...
    // Handle Activated event
    ...
};

...
UI::OpenDialog(
               ...
               `Table(`id(`devices ), ... ),
               ...
              )
...
UI::AddCallback(`id(`devices ), `SelectionChanged, nil );
UI::AddCallback(`id(`devices ), `Activated, nil );

If you think "oh, that doesn't look all too bad", think twice. This example is trivial, yet there are already three separate places that address similar things:

A lot of GUI toolkits do it very much this way - most Xt based toolkits for example (OSF/Motif, Athena widgets, ...). But this used to be a source of constant trouble: Change a few things here and be sure that revenge will come upon you shortly. It simply adds to the overall complexity of something that is already complex enough - way enough.

Bottom line: Having callbacks is not really an improvement.


What remains is to stick to the general model of YaST2 but return more information - of course while remaining compatible with existing YCP code. We don't want (neither can we economically afford to) break all existing YCP code. So the existing UI builtin functions like UserInput() or PollInput() have to remain exactly the same. But of course we can easily add a completely new UI builtin function that does return more information.

This is what we did. This is how WaitForEvent() came into existence. It behaves like UserInput(), but it returns more information about what really happened - in the form of an event map rather than just a single ID. That map contains that ID (of course) plus additional data depending on the event that occured.

One charming advantage of just adding another UI builtin is that existing code does not need to be touched at all. Only if you want to take advantage of the additional information returned by WaitForEvent() you need to do anything at all.

So let's all hope with this approach we found a compromise we all can live with. While that probably will not prevent those discussions by new team members, maybe it will calm down the current team members' discussion a bit. ;-)

Event Delivery

Event Queues vs. One Single Pending Event

Since the YaST2 UI doesn't have a single event loop where the program spends most of its time, an indefinite period of time may pass between causing an event (e.g., the user clicks on a widget) and event delivery - the time where the (YCP) application actually receives the event and begins processing it. That time gap depends on exactly when the YCP code executes the next UserInput() etc. statement.

This of course means that events that occured in the mean time need to be stored somewhere for the YCP code to pick them up with UserInput() etc.

The first approach that automatically comes to mind is "use a queue and deliver them first-in, first-out". But this brings along its own problems:

Events are only useful in the context of the dialog they belong to. When an event's dialog is closed or when a new dialog is opened on top of that event's dialog (a popup for example) it doesn't make any more sense to handle that event. Even worse, it will usually lead to utter confusion, maybe even damage.

Imagine this situation: The user opens a YaST2 partitioning module just to have a look at his current partitioning scheme.

Side note: This scenario is fictious. The real YaST2 partitioning module is not like that. Any similarities with present or past partitioning modules or present or past YaST2 hackers or users is pure coincidence and not intended. Ah yes, and no animals were harmed in the process of making that scenario. ;-)

Argh. What a mess.

Yes, this example is contrived. But it shows the general problem: Events belong to one specific dialog. It never makes any sense to deliver events to other dialogs.

But this isn't all. Even if the internal UI engine (the libyui) could make sure that events are only delivered to the dialog they belong to (maybe with a separate queue for each dialog), events may never blindly be taken from any queue. If the user typed (or clicked) a lot ahead, disaster scenarios similar to the one described above might occur just as well.

Events are context specific. The dialog they belong to is not their only context; they also depend on the application logic (i.e. on YCP code). This is another byproduct of the YaST2 event handling approach.

It has been suggested to use (per-dialog) event queues, but to flush their contents when the dialog context changes:

Exactly when and how this should happen is unclear. Every imaginable way has its downsides or some pathologic scenarios. You just can't do this right. And YCP application developers would have to know when and how this happens - which is clearly nothing they should be troubled with.

This is why all current YaST2 UIs have onle one single pending event and not a queue of events. When a new event occurs, it usually overwrites any event that may still be pending - i.e. events get lost if there are too many of them (more than the YCP application can and wants to handle).

Event Reliability

While it may sound critical to have only one single pending event, on this works out just as everybody expects:

As described above, events can and do get lost if there are too many of them. This is not a problem for button clicks (the most common type of event), and it should not be a problem for any other events if the YCP application is written defensively.

Defensive Programming

Don't take anything for granted. Never rely on any specific event to always occur to make the application work allright.

In particular, never rely on individual SelectionChanged WidgetEvents to keep several widgets in sync with each other. If the user clicks faster than the application can handle, don't simply count those events to find out what to do. Always treat that as a hint to find out what exactly happened: Ask the widgets about their current status. They know best. They are what the user sees on the screen. Don't surprise the user with other values than what he can see on-screen.

In the past, some widgets that accepted initially selected items upon creation had sometimes triggered events for that initial selection, sometimes not. Even though it is a performance optimization goal of the UI to suppress such program-generated events, it cannot be taken for granted if they occur or not. But it's easy not to rely on that. Instead of writing code like this:

{
    // Example how NOT to do things

    UI::OpenDialog(
                   ...
		   `SelectionBox(`id(`colors ),
                                 [
				  `item(`id("FF0000"), "Red" ),
				  `item(`id("00FF00"), "Blue",true),  // Initially selected
				  `item(`id("0000FF"), "Green" )
                                 ] ),
                  );

    // Intentionally NOT setting the initial color:
    //
    // Selecting an item in the SelectionBox upon creation will trigger a
    // SelectionChanged event right upon entering the event loop.
    // The SelectionChanged handler code will take care of setting the initial color.
    // THIS IS A STUPID IDEA!

    map event = $[];

    repeat
    {
        event = UI::WaitForEvent();

        if ( event["ID"]:nil == `colors )
        {
            if ( event["EventReason"]:nil == "SelectionChanged" )
            {
                // Handle color change
		setColor( UI::QueryWidget(`id(`colors ), `SelectedItem ) );
            }
        }
        ...
    } until ( event["ID"]:nil == `close );
}

Write code like that:
{
    // Fixed the broken logic in the example above

    UI::OpenDialog(
                   ...
		   `SelectionBox(`id(`colors ),
                                 [
				  `item(`id("FF0000"), "Red" ),
				  `item(`id("00FF00"), "Blue",true),  // Initially selected
				  `item(`id("0000FF"), "Green" )
                                 ] ),
                  );

    // Set initial color                                       
    setColor( UI::QueryWidget(`id(`colors ), `SelectedItem ) );

    map event = $[];

    repeat
    {
        event = UI::WaitForEvent();

        if ( event["ID"]:nil == `colors )
        {
            if ( event["EventReason"]:nil == "SelectionChanged" )
            {
                // Handle color change
		setColor( UI::QueryWidget(`id(`colors ), `SelectedItem ) );
            }
        }
        ...
    } until ( event["ID"]:nil == `close );
}

It's that easy. This small change can make code reliable or subject to failure on minor outside changes - like a version of the Qt lib that handles things differently and sends another SelectionChanged Qt signal that might be mapped to a SelectionChanged WidgetEvents - or does not send that signal any more like previous versions might have done.

Being sceptical and not believing anything, much less taking anything for granted is an attitude that most programmers adopt as they gain more an more programming experience.

Keep it that way. It's a healthy attitude. It helps to avoid a lot of problems in the first place that might become hard-to-find bugs after a while.


$Id$