Widget tags

RS 2003-02-17: Tags - strings that represent groups of objects - are a powerful concept, used in Tk for text portions, canvas items, and for bindings in bindtags. Another use for tags, and their Tcl implementation, has been proposed by Bryan Oakley in comp.lang.tcl.

The posting was in response to the question "is there an easy way to enable or disable related widgets" (or something like that)

It's fairly easy to code up yourself, and makes for a nice little programming exercise.

A variation of what I do looks roughly like this (off of the top of my head, not tested, YMMV, void where prohibited by law, do not operate heavy machinery while using, wait two hours before going swimming, not suitable for young children, etc)

# some typical widgets...
.menubar.editMenu add command -label Paste ...
.popupMenu add command -label Paste ...
button .toolbar.paste -text Paste ...

# associate them with a symbolic name
associate paste .menubar.editMenu Paste
associate paste .toolbar.paste
associate paste .popupMenu Paste

....

# enable only if there's something on the clipboard
if {there is nothing on the clipboard} {
    disable paste
} else {
    enable paste
}

All that's left is to write the procs associate, enable and disable.

Associate is simple:

proc associate {what args} {
    global widgets
    lappend widgets($what) $args
}

enable and disable are also pretty simple:

proc enable {what} {
    global widgets
    foreach item $widgets($what) {
        set widget [lindex $item 0]
        if {[winfo exists $widget]} {
            if {[winfo class $widget] == "Menu"} {
                set index [lindex $item 1]
                $widget entryconfigure $index -state normal
            } else {
                $widget configure -state normal
            }
        }
    }
}

I've used a variation of this for years and it works great. It's a pain to have to associate widgets with a symbolic name, but it's worth the effort. Besides the obvious benefits, it makes the code easier to maintain in that if you change widget paths or add or remove widgets you only have to change the related "associate" commands rather than hunting down hard-coded widget paths throughout your code.

There are lots of things you can do to make the code even more fancy. For example, you can support accelerators, build your own widget wrappers that do the "associate" step automatically, add a disassociate step, etc.


After reading this, I was inspired to write a little library to really add this functionality. Here it is.

Damon Courtney: If anyone is interested, I could package it up and actually distribute it as a package. Maybe add it to Tklib?

##
## TkTags - An interface for associating symbolic names with Tk widgets.
##
## tag add       tagName <widget ?menuItem?> ??widget ?menuItem?? ... ?
## tag addmenu   tagName menu menuItem ?menuItem ...?
## tag addtag    tagName searchSpec ?arg arg ... ?
##
## tag addtag    tagName ?recursive? ?class widgetClass? ?type menuItemType?
##                       ?children widgetName? ?menuentries menuName? ?arg ... ?
##
## tag cget      widgetOrTagName option
## tag configure widgetOrTagName ?option? ?value? ?option value ... ?
## tag delete    tagName ?tagName ... ?
## tag names     ?widget ?menuItem??
## tag remove    tagName <widget ?menuItem?> ??widget ?menuItem?? ... ?
## tag widgets   tagName ?tagName ... ?
##

package require Tk
package provide "TkTags" "1.0"

namespace eval ::TkTags {
    variable tags
    variable widgets

    proc ::TkTags::Lempty {list} {
        if {[catch {llength $list} len]} { return 0 }
        return [expr $len == 0]
    }

    proc ::TkTags::Lremove {list args} {
        foreach elem $args {
            set x [lsearch $list $elem]
            if {$x < 0} { continue }
            set list [lreplace $list $x $x]
        }
        return $list
    }

    proc ::TkTags::OptionErrorString {word type value list} {
        set msg "Error: $word $type \"$value\": must be "
        set last [lindex $list end]
        set list [lrange $list 0 end-1]
        append msg [join $list ", "] ", or $last"
        return $msg
    }

    proc ::TkTags::add {tagName args} {
        variable tags
        variable widgets
        foreach arg $args {
            if {![info exists tags($tagName)]
                || [lsearch $tags($tagName) $arg] < 0} {
                lappend tags($tagName) $arg
            }

            if {![info exists widgets($arg)]
                || [lsearch $widgets($arg) $tagName] < 0} {
                lappend widgets($arg)  $tagName
            }
        }
    }
    
    proc ::TkTags::addmenu {tagName menu args} {
        set list [list]
        foreach index $args {
            lappend list [list $menu $index]
        }
        eval add $tagName $list
    }

    proc ::TkTags::addtag {tagName args} {
        set recursive 0
        set pass      [list]
        set widgets   [list]
        for {set i 0} {$i < [llength $args]} {incr i} {
            set arg [lindex $args $i]

            switch -glob -- $arg {
                "cl*" { ## class
                    set class [lindex $args [incr i]]
                    lappend classes $class
                    lappend pass class $class
                }

                "r*" { ## recursive
                    set recursive 1
                    lappend pass recursive
                }

                "t*" { ## type
                    set type [lindex $args [incr i]]
                    lappend types $type
                    lappend pass type $type
                }

                default {
                    lappend search $arg
                }
            }
        }

        set args $search

        for {set i 0} {$i < [llength $args]} {incr i} {
            set arg [lindex $args $i]

            switch -glob -- $arg {
                "ch*" { ## children
                    set widget [lindex $args [incr i]]
                    if {![winfo exists $widget]} { continue }
                    foreach widget [winfo children $widget] {
                        if {[string match "*.#*" $widget]} { continue }
                        if {![info exists classes]
                            || [lsearch $classes [winfo class $widget]] > -1} {
                            lappend widgets $widget
                        }
                        if {$recursive} {
                            eval lappend widgets \
                                    [eval addtag $tagName $pass children $widget]
                        }
                    }
                }

                "m*" { ## menuentries
                    set widget [lindex $args [incr i]]
                    if {![winfo exists $widget]} { continue }
                    if {[string match "*.#*" $widget]} { continue }
                    if {[winfo class $widget] != "Menu"} {
                        return -code error "Error: $widget is not a menu"
                    }
                    for {set j 0} {1} {incr j} {
                        if {[$widget index $j] != $j} { break }
                        set type [$widget type $j]
                        if {![info exists types]
                            || [lsearch $types $type] > -1} {
                            lappend widgets [list $widget $j]
                        }

                        if {$type == "cascade" && $recursive} {
                            set menu [$widget entrycget $j -menu]
                            eval lappend widgets \
                                    [eval addtag $tagName $pass menuentries $menu]
                        }
                    }
                }

                default {
                    if {[winfo exists $arg]} { lappend widgets $arg }
                }
            }
        }
        eval add $tagName $widgets
        return $widgets
    }

    proc ::TkTags::cget {tagName option} {
        variable tags
        if {![info exists tags($tagName)]} {
            set widget [lindex $tagName 0]
            set index  [lindex $tagName 1]
            if {![winfo exists $widget]} { return }
        } else {
            if {[llength $tags($tagName)] > 1} { return }
            set item   [lindex $tags($tagName) 0]
            set widget [lindex $item 0]
            set index  [lindex $item 1]
        }

        if {[Lempty $index]} { return [$widget cget $option] }

        if {[winfo class $widget] == "Menu"} {
            return [$widget entrycget $index $option]
        } else {
            return [$widget itemcget $index $option]
        }
    }

    proc ::TkTags::configure {tagName args} {
        variable tags
        if {![info exists tags($tagName)]} {
            set widget [lindex $tagName 0]
            if {![winfo exists $widget]} { return }
            set items [list $tagName]
        } else {
            set items $tags($tagName)
        }

        if {[llength $args] < 2} {
            if {[llength $items] > 1} { return }
            set item   [lindex $items 0]
            set widget [lindex $item 0]
            set index  [lindex $item 1]
            if {![winfo exists $widget]} { return }

            if {[Lempty $index]} { return [eval $widget configure $args] }

            if {[winfo class $widget] == "Menu"} {
                return [eval $widget entryconfigure $index $args]
            } else {
                return [eval $widget itemconfigure $index $args]
            }
        }

        foreach {opt val} $args {
            foreach item $items {
                set widget [lindex $item 0]
                set index  [lindex $item 1]
                if {![winfo exists $widget]} { continue }
                if {![Lempty $index]} {
                    if {[winfo class $widget] == "Menu"} {
                        catch { $widget entryconfigure $index $opt $val }
                    } else {
                        catch { $widget itemconfigure $index $opt $val }
                    }
                } else {
                    catch { $widget configure $opt $val }
                }
            }
        }
    }

    proc ::TkTags::delete {args} {
        variable tags
        variable widgets

        foreach tagName $args {
            if {![info exists tags($tagName)]} { continue }
            foreach widget $tags($tagName) {
                if {![info exists widgets($widget)]} { continue }
                set widgets($widget) [Lremove $widgets($widget) $tagName]
                if {[Lempty $widgets($widget)]} { unset widgets($widget) }
            }
            unset tags($tagName)
        }
    }

    proc ::TkTags::names {{widget ""}} {
        variable tags
        variable widgets
        if {[Lempty $widget]} { return [lsort [array names tags]] }
        if {![info exists widgets($widget)]} { return }
        return $widgets($widget)
    }

    proc ::TkTags::remove {tagName args} {
        variable tags
        variable widgets

        if {![info exists tags($tagName)]} { return }

        set tags($tagName) [eval Lremove [list $tags($tagName)] $args]
        if {[Lempty $tags($tagName)]} { unset tags($tagName) }

        foreach widget $args {
            if {![info exists widgets($widget)]} { continue }
            set widgets($widget) [Lremove $widgets($widget) $tagName]
            if {[Lempty $widgets($widget)]} { unset widgets($widget) }
        }
    }

    proc ::TkTags::widgets {args} {
        variable tags
        set widgets [list]
        foreach tagName $args {
            if {![info exists tags($tagName)]} { continue }
            foreach widget $tags($tagName) {
                if {[lsearch $widgets $widget] > -1} { continue }
                lappend widgets $widget
            }
        }
        return $widgets
    }
}

proc ::tag {args} {
    if {[::TkTags::Lempty $args]} {
        set msg "Error: wrong # args: should be tag option arg ?arg ...?"
        return -code error $msg
    }

    set cmd [lindex $args 0]

    set list    [list add configure delete names remove widgets]
    set command [info commands ::TkTags::$cmd]
    if {[::TkTags::Lempty $command]} {
        set cmds [info commands ::TkTags::$cmd*]
        if {[::TkTags::Lempty $cmds]} {
            set msg [::TkTags::OptionErrorString bad option $cmd $list]
            return -code error $msg
        }
        if {[llength $cmds] > 1} {
            set msg [::TkTags::OptionErrorString ambiguous option $cmd $list]
            return -code error $msg
        }
        set command [lindex $cmds 0]
    }

    return [eval $command [lrange $args 1 end]]
}