Toplevel widgets in a tree hierarchy

Marco Maggi - The code in this page is derived from UWP, a package licensed under the terms of the GNU Lesser General Public License, but I relicense here under the same terms of the TCL core source code.

This module provides the ability to organise toplevel widgets in a forest hierarchy and to define groups in it. A forest is a set of trees. Groups are used to map/unmap sets of windows together and to configure windows to give the focus to other selected windows.

Marco Maggi (Sep 26, 2003) - Updated with the [wm transient] workaround.


OVERVIEW

To give away the focus is useful whenever a logical child of a window, like a dialog or error window, has to force the user to complete an operation before allowing him to interact with its parent window. By "logical child" we don't mean "child in the TK sense", but child from the logical point of view in an application. All the toplevel windows may be TK-children of the main window ..

The typical classes of windows that must steal the focus of their parent are: error windows, confirmation dialogs and "local context" dialogs.

*** The first case is obvious: if we (users of the application) send a command request that raises an error, the error window pops up and we have to explicitly dismiss it before going on; this is to make sure that we know that the command failed.

*** The second case is similar to the first: we send a command request that needs explicit confirmation to prevent us from erroneously doing an unrecoverable (or very time-consuming) operation.

*** In the third case we open a dialog (like the font selection one) which requests an operation linked to the context, say, in which the keyboard insertion cursor is placed; the application forbids us to move the cursor before we have completed the operation; this is not technically mandatory, but it's simple to understand for the user.

Whenever a window is configured to steal the focus from its parent, the two windows are logically part of the same group. Every time we try to select the parent window (for example with a click of the mouse), the child window will be focused in its place. If we iconify the child window, the parent window must be iconified, too; this is because, from the logical point of view, the request of iconification is sent to the group of windows that we are using to carry out the same job. The same occurs when we deiconify one of the windows in a group: all the windows in the group will be deiconified, and the focus-thief one will be raised on top and focused.

This is a little complicated from the user's point of view. Probably the average user will think: "I can iconify this little error window to take a look at the underlying data window, so that I can understand why the operation failed; then I'll come back and dismiss the error window." This is because if the user doesn't understand what the error message is saying, he will want to read it again and again.

(This is not exactly true: the average user will hit the dismiss button on the error dialog without even reading the message, and then will ask to us what the heck is going on here!)

A solution is to make dialog and error windows "transient", that is: use the [wm transient <window>] command. That way (we hope) the dialog windows will be displayed without the iconify button, preventing the user from removing the toplevel from the desktop independently. The iconify button is still present on the parent data window, so the user may iconify the whole group. The transient windows will not be iconified, they will be withdrawn: that way the user will not be able to deiconify a transient toplevel independently, he will deiconify the parent data window and this operation will display also all the associated transient windows. This behaviour is guaranteed by TK itself.

However, it appears that we cannot trust [wm transient] on many platforms, so wtree uses a couple of procedures bound to the Map and Unmap events to do the iconification work. On the Fvwm2 window manager for the X Window System it works.

Let's look at some scenarios.


We are interacting with a window showing some data: an error occurs and the application pops up an error dialog; we want the error dialog to grab the focus and disallow us to send mouse and keyboard events to the data window until we push the dismiss button on the error dialog.

Every time we try to select the data window, the error window should receive the focus and be raised in its place.

Meanwhile we want to be able to send events to other data windows that the application has opened.


We are editing text in a data window. We want to select a new font for the paragraph in which the insertion cursor is placed: the application pops up the font selection dialog an we interact with it. Let's say that an error is raised when the system tries to access a font file on the disk: an error dialog will pop up to inform us.

The font dialog is a focus-thief with respect to the data window and the error dialog is a focus-thief with respect to the font dialog. If we try to select the data window, we want the data window, the font dialog and the error dialog to be raised in sequence, with the error dialog taking the focus in the end. The same should happen if we select the font dialog or the error dialog after having selected another window on the desktop; the concept is that when we select a window in a group, the group hierarchy should be made clearly evident to the user.

Meanwhile we want to be able to send events to the other data window in the application.


We are editing text in a data window. We begin a text search to look for a word: the application pops up a search dialog window; we want to be able to look at and edit the text while we skip from one found word to the next.

So the search dialog should stay on top of the data window, but we should be able to focus the data window and interact with it. This behaviour has been observed on Nestscape Communicator.


We are using a floating tool bar, say, to edit an image. Floating tool bars aren't associated to the context of a data window, instead they are associated to a specific view of a particular data type; so the tool bar is associated to the data window which happens to embed such a view. There may be more than one data window showing the same type of view, so we would like to have a single floating tool bar that links itself dynamically to the view we are using "now".

When we select the floating tool bar and deliver a command request, which data window will be affected among the ones that show the correct view? A good answer is: the last in time that we have focused. So the system has to register the sequence of selections of the data windows embedding a type of view, and whenever the floating tool bar is focused it must raise the last one to show us where its commands are directed to. We would like also to see the floating tool bar always on top of the view window it is acting upon.

For each data window some of the functionalities of the tool bar may be disabled or not available, it depends on the granularity of the definition of "type of view".

This relation between the floating tool bar and the view windows is a one-to-many one; it doesn't cleanly fit in the windows tree hierarchy proposed by this module, so it's not addressed. (Stay tuned for a new wiki page on this one. By the way: floating tool bar are best used if they, and only they, are configured with the "focus follows mouse" mode; this is possible with some window managers, like Fvwm2).


IMPLEMENTATION NOTES

This module defines a tag to be attached to the tag list of a registered toplevel window. Bindings to the FocusIn and Destroy events are attached to the module's tag. This module avoids completely the usage of the [grab] TK command. We should keep the freedom to not attach the tag to all the toplevels in the application.

Notes.

*** It is possible to modify this module to break the chain of evaluation of binding scripts attached to the events, and send virtual events that may be used by toplevels to really really take the focus. This needs a lot of testing because we don't know which operations are attached to the FocusIn events of other tags. Other tags would also be bound to known that they have to use our virtual event rather than the TK one.

*** If we are using the "focus follows mouse" method to select toplevels, this module will probably not work.

*** Probably the code in the [focus] procedure triggers multiple traverses of the tree; this may be a waste of resources, but usually an application has not many windows linked together. Note that the operations on a root window in the tree will never affect the behaviour of the other root windows.

The windows registered in the forest may not be aware of it: the tree doesn't assume that registered window will take special actions as a consequence of being part of it; it will just act on the windows with the following commands: [wm deiconify], [focus], [raise].

All the operations are carried out by inspecting the attributes of each registered window: binding actions to the hierarchy of the tree will complicate the code if later we want to add other attributes.


PROCEDURES

The whole module may be seen as a big wrapper around a single array. All the procedures are in the wtree namespace.

[register window ?parent?] - Registers a window as child of another one. Only toplevel windows are meant to be registered in the tree, and only registered windows are meant to be selected as parent of other registered windows.

It is not required for the widget hierarchy to be the same as the tree hierarchy: a window and its parent in this tree, may or may not be parent and child in the TK tree.

Arguments: window, window's pathname; parent, parent's pathname, may be empty or . if the new window is a root window.

Appends the new window's pathname to the list of children of the parent window; if the parent window is the empty string, the new window is appended to the list of root windows.

The tag UWPWTreeWindow is inserted in the tag list of the new window, in position 1.

[forget window] - Removes from the tree's array all the elements associated to the window. Removes the window's pathname from the list of children of its parent. If the window has children, they become children of the root window. The tag UWPWTreeWindow is removed from the list of tags of the window.

[exists window] - Returns true if the window is registered, false otherwise.

[get_root_windows] - Returns the list of root windows.

[get_window_parent window] - Returns the parent's pathname or . if the selected window is a root window.

[get_window_children window] - Return the list of children of a window.

[set_focus_mode window mode] - Selects a new focus mode for a window. Mode must be: keep or ontop.

[set_focus_window window tofocus] - Selects a window to take the focus in place of its parent.

[focus window] - Assigns the focus to window or to a child of window that has been registered to take the focus in its place.

If the selected window has no take-focus child, it keeps the focus and raises itself on top of all the other windows. In this case the return value is true.

In all the following cases the return value is false. In all the following cases the window is raised.

If a window has been registered to take the focus in place of the one to which the event was delivered, the focus-thief window is focused if and only if its focus mode is keep. If it's focus mode is ontop: the window is just raised. Then the tree is traversed following the path of "tofocus" windows, raising all of them: if a window with no focus-thief is found: return; if a window with keep mode is found: it is focused and then the procedure returns.

There is a special scenario: a data window is created and takes the focus; an ontop dialog window is created as child of the data window; the data window takes the focus; an error window is created as child of the data window and takes the focus; a request is sent to the dialog window to take the focus. In this case the dialog must give the focus to the data window, which in turn will give it to the error window.

The focus concept used in this function is not the keyboard focus: is the ensemble of two attributes: the keyboard focus and the "stay on top" behaviour.

It's possible for a window to stay on top and to allow it's parent window to have the keyboard focus. This may conflict with some window managers that use the "focus follows mouse" method to select windows.


Toplevel widgets in a tree hierarchy: the package

Toplevel widgets in a tree hierarchy: the test suite

Toplevel widgets in a tree hierarchy: a script to play