bind: remove one command

HaO 2019-04-12

Scope

The Tk bind command allows to add commands by the syntax:

bind win tag +cmd

There is no buildin way to remove the added command.

Warning

(Christian Gollwitzer on clt): The "+cmd" syntax is considered as broken. Bindtags (which are lists) or tcllib uevent should be used instead.

HaO: Nevertheless, I face this issue for important global events like androwish "<<WillEnterBackground>>" command, which is potentially important for a lot of modules in common.

Christian: This is the classic case of "misusing" Tk events - an event which has multiple listeners cannot be cleanly expressed in Tk.

If you have all the modules under your control. Refire the event as a uevent in the main setup:

bind . <<WillEnterBackground>> { uevent::generate AndroidWish GoesDown }

then, in each module, you do:

set myid [uevent::bind AndroidWish GoesDown mymethod]

and when you unload the module

uevent::unbind $myid

We use uevent in aRTist precisely to do this - signalling general events that are not connected to a widget.

And Mike Griffin about bindtags:

You can still use bindtags:

% bindtags
. Wish all
% bindtags . [linsert [bindtags .] 1 CustomFoo]
% bindtags .
. CustomFoo Wish all
% bind CustomFoo <<WillEnterBackground>> [list ScannerUnregister]

(Output above is on Wish on Windows; default bindtags may be different on AndroWish, or already altered by other libraries, but by using linsert as above you can inject your new bindtag safely regardless)

Observation

The command "bind win tag +cmd" does the following:

  • define "cmd" if no binding exists: "bind win tag cmd"
  • add a newline ("\n") and "cmd", if a binding exists

Pseudocode:

proc BindAdd {w t c} {
    set ccur [bind $w $t]
    if {$ccur eq ""} {
        bind $w $t $c
    } else {
        bind $w $t $c $ccur\n$c
    }
}

Removal function

Here is a proposition to remove my own registration.

# Remove first Cmd from bind pattern.
# @return true if removed
proc BindRemove {Win Pattern CmdIn} {
    set CmdCur [bind $Win $Pattern]
    set Pos 0
    while {-1 != [set Pos [string first $CmdIn $CmdCur $Pos]]} {
        # Check if found string starts at the beginning of after a new-line
        if { $Pos == 0 || [string index $CmdCur $Pos-1] eq "\n" } {
            # Check if found string ends at end or before a new-line
            set PosEnd [expr {$Pos + [string length $CmdIn]}]
            if {    $PosEnd == [string length $CmdCur]
                    || [string index $CmdCur $PosEnd] eq "\n"
            } {
                # Binding found
                if {$Pos != 0} {
                    set CmdNew [string range $CmdCur 0 $Pos-2]
                } else {
                    set CmdNew ""
                }
                if {$PosEnd!= [string length $CmdCur]} {
                    if {$CmdNew ne ""} {
                        append CmdNew \n
                    }
                    append CmdNew [string range $CmdCur $PosEnd+1 end]
                }
                bind $Win $Pattern $CmdNew
                return 1
            }
        }
        incr Pos
    }
    return 0
}

Usage:

proc mymodule::init {} {
    bind . a +[namespace code MyModuleFunction]
}
proc mymodule::delete {} {
    bindRemove . a [namespace code MyModuleFunction]
}