1
votes

I have a GUI, in it I have a properties window that opens in a new window. In some of the times, (randomly and not deterministicly reproducible) when I open the window it gives the fallowing error:

grab failed: window not viewable

It doesn't interfere with the normal function of the program nor doesn't seem to have any affect on anything besides printing that message.

The code for creating the new window is:

proc _prop_menu_make_top  {{elem {}}}  {
    toplevel .prop_menu
    #...initialize some variables...

    wm title .prop_menu "Properties for $_prop_attr(name)"

    #...create and display the window widgets...

    bind    .prop_menu  <Key-KP_Enter>  {_prop_menu_ok_button}
    bind    .prop_menu  <Return>        {_prop_menu_ok_button}
    bind    .prop_menu  <Escape>        {_prop_menu_cancel_button}

    # catch presses on the window's `x` button
    wm protocol .prop_menu WM_DELETE_WINDOW {
        _prop_menu_cancel_button
    }

    # make the top window unusable
    center_the_toplevel .prop_menu

    focus .prop_menu.main_frame.model_name.entry
    grab release .
    grab set .prop_menu
}

proc center_the_toplevel { window } {    
    if { [string equal $window [winfo toplevel $window]] } {
        set width   [winfo reqwidth $window]
        set height  [winfo reqheight $window]
        set x       [expr {([winfo vrootwidth  $window] - $width) / 2}]
        set y       [expr {([winfo vrootheight $window] - $height) / 2 }]

        wm geometry $window +${x}+${y}
    }
    return
}

proc _prop_menu_ok_button {} {
   #....saving the needed data...
   _prop_menu_cancel_button
}

proc _prop_menu_cancel_button {} {
    destroy .prop_menu
    # make the top window usable again
    grab set .
    # redraw the canvas
    nlv_draw
}

Does anyone has any idea as to what may be causing this problem? Does anyone has any advice as to how make the bug easier to reproduse?

EDIT: running Tcl version 8.4.6 for 64bit, don't know which tk version.

1
Which version of Tcl and Tk are you running?Jackson
Maybe try some 'tkwait visibility' to make sure the window is visible ? wiki.tcl.tk/10013schlenk
Tk version should match the Tcl version exactly through the 8.4 series.Donal Fellows
It's not a bug FWIW; it's definitely deliberately designed that way. And when using grabs, be careful also with keyboard focus management as that ignores grabs completely.Donal Fellows

1 Answers

5
votes

Explanation

For various reasons (some technical, some design principles), Tk only permits grabs to be set on windows that are mapped onto the screen. This is almost certainly what you want; mouse clicks should be going to a window you can see after all.

The problem you've got is that you're trying to do the grab too early. In particular, Tk postpones the creation of the underlying X11/OS window (depending on platform) for each widget until it has finished deciding what the configuration of that widget will be, which is taken to be when Tk becomes “idle”. Idle is defined to be when the event loop is entered and there are no pending events to be serviced. At that point, Tk tells the basic system graphics engine to allocate a rectangular chunk of screen estate (the window) and to put it on the screen. That in turn triggers a whole cascade of events and processing (there's a lot going on at that point) that ends up with the window being shown to you; it's only once the window is shown that you can set a grab on it.

So how do you know when you can set a grab? Well, you've got to wait for the window to turn up. That means waiting for an event: the key ones that you might care about for this task are <Map>, <Visibility> and <Expose>. They indicate respectively when the window is logically present within the root window, when there is a change in what is actually viewable, and when there is something to redraw. (There are equivalents on Windows to the first and last, which Tk remaps internally, but Windows doesn't tell you about actual visibility changes at all. Ever.)

Solution

The simplest way to wait for a widget to become grabbable (and then do the grabbing) is to use:

tkwait visibility $theWidget
grab set $theWidget

That just waits in the event loop for a <Visibility> event to turn up on that window. (It doesn't work on Windows though because of the absence of the event type on that platform.) You could rewrite the above as this:

bind $theWidget <Visibility> [list set waiting($theWidget) gotit]
vwait waiting($theWidget)
bind $theWidget <Visibility> {}    ;# remove the binding again!
grab set $theWidget

If you're on Windows[*], you'll have to use that second technique, but replacing <Visibility> with <Map>.

bind $theWidget <Map> [list set waiting($theWidget) gotit]
vwait waiting($theWidget)
bind $theWidget <Map> {}           ;# remove the binding again!
grab set $theWidget

I can't remember if <Expose> is available to scripts in Tk 8.4; that's an event that Tk handles for you normally. In any case, <Map> works and <Visibility> is perfect on X11.

You should also be aware that both tkwait and vwait can cause problems with reentrant event handling — you don't want it if you can help it! — so take care. You can deal with the problems by rewriting it all into continuation-passing style, which happens to be fairly easy in this case:

bind $theWidget <Map> {
    bind %W <Map> {}
    grab set %W
}

But if you're doing anything more than setting a grab, it can become really complex. (Tcl 8.6's coroutines can help untangle this mess, but they're definitely not portable to 8.4.)


[*] I forget whether there are issues round here with OSX with a non-X11 build of Tk. Check for yourself if you care.