# 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

