tkMenuMgr is a module that greatly simplifies the drudgery of properly handling menus and menubars. It provides an easy way to handle enabling and disabling menu items according to your program's state, and keeps torn-off menus in sync with those on the menu bar. It can also enable or disable arbitrary widgets not part of the menu hierarchy.
Here's the module:
# Create an entire menu hierachy from a description. It can # control all functions in the hierachy, enabling them and # disabling them as your program changes state. Keeps torn-off # menus in sync with home menu. # ################################################################# # # # __ _ _ _ _ # # / _\ |_ __ _ ___| | _____ /\/\ ___ __| |_ _| | ___ # # \ \| __/ _` |/ __| |/ / __| / \ / _ \ / _` | | | | |/ _ \ # # _\ \ || (_| | (__| <\__ \ / /\/\ \ (_) | (_| | |_| | | __/ # # \__/\__\__,_|\___|_|\_\___/ \/ \/\___/ \__,_|\__,_|_|\___| # # # ################################################################# proc height { stack } { upvar $stack s return [ llength $s ] } proc push { stack str } { upvar $stack s lappend s $str } proc pull { stack } { upvar $stack s if { $s == "" } return "" set result [ lindex $s end ] set s [ lreplace $s end end ] return $result } proc peek { stack } { upvar $stack s if { $s == "" } return "" return [ lindex $s end ] } # returns the entire stack as a pathname using the # given separator. The last argument can be "prefix", # "suffix" or both, and indicates whether the separator # will precede the pathname, follow the pathname, or # both, resulting in .a.b.c, a.b.c. or .a.b.c. proc pathname { stack { separator "." } { how prefix } } { upvar $stack s set result "" if { "$how" != "suffix" } { foreach n $s { append result $separator $n } } else { foreach n $s { append result $n $separator } } if { "$how" == "both" } { append result $separator } return $result } proc pushpath { stack pathname { separator "." } } { upvar $stack s set s [ split $pathname $separator ] if { [ lindex $s 0 ] == "" } { set s [ lreplace $s 0 0 ] } } ################################################################# # # # _ _ # # /\/\ ___ _ __ _ _ /\/\ ___ __| |_ _| | ___ # # / \ / _ \ '_ \| | | | / \ / _ \ / _` | | | | |/ _ \ # # / /\/\ \ __/ | | | |_| | / /\/\ \ (_) | (_| | |_| | | __/ # # \/ \/\___|_| |_|\__,_| \/ \/\___/ \__,_|\__,_|_|\___| # # # ################################################################# set stack "" set funclist "" set menulist "" set indxlist "" set nextwidget 0 # returns a string for the next widget name proc getname { } { global nextwidget set result w$nextwidget incr nextwidget return $result } # scans for -foo "str" pairs and converts them # into variable/value pairs in the surrounding # scope - i.e. -foo "str" becomes "foo" with a # value of "str" in the calling routine. proc do_switches { args } { upvar $args arglist set max [ llength $arglist ] if { $max == 1 } { # braced set of args eval set arglist $arglist set max [ llength $arglist ] } for { set i 0 } { $i <= $max } { } { set s [ lindex $arglist $i ] if { [ string index $s 0 ] == "-" } { set var [ string range $s 1 end ] incr i if { $i < $max } { set val [ lindex $arglist $i ] if { [ string index $val 0 ] != "-" } { uplevel 1 set $var \{$val\} continue } } uplevel 1 set $var 1 } incr i } } # Removes and returns the 1st element of a list proc first { args } { upvar $args arglist set rtn [ lindex $arglist 0 ] set arglist [ lreplace $arglist 0 0 ] return $rtn } # called when a menu is torn off, saves the name # of the torn-off menu so entries on it are con- # trolled like regular menu entries. proc tearoffctrl { parent newwidget } { global torn if { [ info exists torn($parent) ] == 0 } { set torn($parent) "" } push torn($parent) $newwidget } # returns list of menus torn off of this main one. proc get_tearoffs { parent } { global torn if { [ info exists torn($parent) ] == 1 } { return $torn($parent) } else { return "" } } # removes a torn-off menu that no longer exists. proc del_tearoffs { parent w } { global torn set i [ lsearch -exact $torn($parent) $w ] # RBR 2002-11-19: added missing "set torn()..." to fix buglet set torn($parent) [lreplace torn($parent) $i $i] } proc setstate { active widget index } { if { $active } { $widget entryconfigure $index -state normal } else { $widget entryconfigure $index -state disabled } } proc savectrl { widget when index } { global menulist funclist indxlist push menulist $widget push funclist $when push indxlist $index } # the menu mgr proper proc mm { keyword args } { global stack menulist funclist indxlist if { "$keyword" == "menubar" } { return ".w0" # mm menu - defines a new menu } elseif { "$keyword" == "menu" } { set label [ first args ] # check to see if menu is on menubar or is cascade # from pulldown and create owner accordingly set name [ getname ] if { [ height stack ] == 0 } { push stack $name frame [ pathname stack ] -relief raised -borderwidth 3 -height 30 -width 300 pack [ pathname stack ] -side left -fill x -side top } else { if { [ height stack ] == 1 } { push stack $name menubutton [ pathname stack ] -menu [ pathname stack ].menu -text "$label" pack [ pathname stack ] -side left -fill x push stack menu menu [ pathname stack ] -tearoffcommand { tearoffctrl } } else { menu [ pathname stack ].$name -tearoffcommand { tearoffctrl } [ pathname stack ] add cascade -label $label -menu [ pathname stack ].$name push stack $name } } eval set body $args set body [ string range $body 1 [ expr [ string length $body ] - 1 ] ] eval $body pull stack if { [ height stack ] == 2 } { pull stack } if { [ height stack ] == 0 } { mm update } # mm func - defines a function a menu can refer to } elseif { "$keyword" == "func" } { if { [ height stack ] < 3 } { puts "***FATAL: func must occur within menu" exit } set when "" set cmd "" do_switches args [ pathname stack ] add command -label $label -command $cmd savectrl [ pathname stack ] $when [ [ pathname stack ] index end ] # mm toggle - insert a settable boolean in menu } elseif { "$keyword" == "toggle" } { set when "" set var "" set cmd "" set init 0 do_switches args [ pathname stack ] add checkbutton -label $label -variable \ $var -command $cmd -onvalue 1 -offvalue 0 -selectcolor black uplevel #0 set $var $init savectrl [ pathname stack ] $when [ [ pathname stack ] index end ] # mm check - insert a radio selector in menu } elseif { "$keyword" == "check" } { set when "" set var "" set cmd "" set init 0 do_switches args [ pathname stack ] add radiobutton -label $label -variable \ $var -command $cmd -value $label -selectcolor black if { $init } { uplevel #0 set $var $label } savectrl [ pathname stack ] $when [ [ pathname stack ] index end ] # mm separator - inserts a horizontal rule in menu } elseif { "$keyword" == "separator" } { [ pathname stack ] add separator # mm control - puts a non-menu widget under mm state control } elseif { "$keyword" == "control" } { set widget [ first args ] set when "" set cmd "" do_switches args savectrl $widget $when "" mm update # mm update - updates all controlled widgets according to # state control expressions current values. } elseif { "$keyword" == "update" } { set max [ height funclist ] for { set i 0 } { $i < $max } { incr i } { set this_menu [ lindex $menulist $i ] set ctrl [ lindex $funclist $i ] set index [ lindex $indxlist $i ] set active 1 if { "$ctrl" != "" } { set active [ uplevel #0 expr $ctrl ] } if { "$index" == "" } { if { $active } { $this_menu configure -state normal } else { $this_menu configure -state disabled } } else { foreach widget $this_menu { setstate $active $widget $index set torn [ get_tearoffs $widget ] if { "$torn" != "" } { foreach w $torn { set result [ catch { setstate $active $w [ expr $index - 1 ] } ] if { $result != 0 } { del_tearoffs $widget $w } } } } } } } update }
and here's a test program to show how it works.
source tkMenuMgr.tcl set fileopen 0 set filemod 0 mm menu Top { mm menu File { mm func { -label "New" -cmd { set fileopen 1; mm update } -when { !$fileopen } } mm func { -label "Open..." -cmd { set fileopen 1; mm update } -when { !$fileopen } } mm separator mm func { -label "Save" -when { $fileopen && $filemod } -cmd { set filemod 0; mm update } } mm func { -label "Save As..." -when { $fileopen && $filemod } -cmd { set filemod 0; mm update } } mm func { -label "Close" -when { $fileopen } -cmd { set fileopen 0; mm update } } mm separator mm func -label "Quit" -cmd { exit } } mm menu Test { mm func { -label "Modify State" -cmd { set filemod 1; mm update } } mm separator mm check { -label "First" -var selection } mm check { -label "Second" -var selection -init 1 } mm check { -label "Third" -var selection } } mm menu Options { mm toggle { -label "Flag 1" -var flag1 -init 1 } mm toggle { -label "Flag 2" -var flag2 -init 0 } } } while 1 { vwait selection puts "selection is now: $selection" }
RBR - For a version of this that uses namespaces, see Menus Even Easier Redux