NEM 17 Oct 2005: Here's an experiment with combining Tk widgets with namespaces as another attempt at a megawidget package. Instead of being based on an
OO design, this one is based on a simple compositional syntax: you create nested hierarchies of widgets by simply nesting them inside one another. It's not quite as compositional as I'd like it yet, but it illustrates the general idea. Also,
beware: widget path-names are currently tied to the namespace hierarchy, so you should either fully-qualify widget names (as you have to in Tk), or always create widgets from the global namespace. I intend to fix that behaviour at some point. Without further ado, some code:
# comptk.tcl --
#
# Compositional Tk: an experiment on making Tk widget composition
# simpler.
#
# Copyright (c) 2005 by Neil Madden ([email protected]).
#
# License: Same as Tcl
package require Tcl 8.4
package require Tk 8.4
package provide comptk 0.1
namespace eval ::comptk {
proc Dispatch {fqname args} {
if {[llength $args] == 0} {
return [string map {:: .} $fqname]
} else {
uplevel 1 [list namespace eval $fqname $args]
}
}
proc Values {arrName} {
upvar 1 $arrName array
set ret [list]
foreach item [array names array] {
lappend ret $array($item)
}
return $ret
}
# Option parsing
proc CheckOpts {opts arglist} {
if {[lindex $opts end] eq "args"} {
set have_args 1
set opts [lrange $opts 0 end-1]
} else {
set have_args 0
}
array set options { }
# Defaults
foreach item $opts {
if {[llength $item] == 2} {
set options([lindex $item 0]) [lindex $item 1]
}
}
set pass [list]
foreach {name value} $arglist {
# Remove leading -
set sname [string range $name 1 end]
if {[lsearch -exact $opts $sname] > -1} {
set options($sname) $value
} elseif {$have_args} {
lappend pass $name $value
} else {
error "unknown option \"$sname\""
}
}
set ret [list]
foreach item $opts {
if {[info exists options($item)]} {
lappend ret $options($item)
} else {
error "no value specified for option \"$item\""
}
}
if {$have_args} {
return [concat $ret $pass]
} else {
return $ret
}
}
proc Resolve {name} {
if {[string index $name 0] eq "."} {
# Fully qualified
return [string map {. ::} $name]
}
# Note! uplevel _2_...
set ns [uplevel 2 [list namespace current]]
if {$ns eq "::"} {
set fqname ::[string map {. ::} $name]
} else {
set fqname ${ns}::[string map {. ::} $name]
}
}
proc In {ns params args body} {
set env [CheckOpts $params $args]
set tmp ${ns}::InTmp[clock clicks]
proc $tmp $params $body
uplevel #0 [linsert $env 0 $tmp]
rename $tmp {}
}
proc self {args} {
set ns [uplevel 1 [list namespace current]]
if {[llength $args]} {
set name [lindex $args 0]
return [lreplace $args 0 0 [string map {:: .} $ns].$name]
} else {
return [string map {:: .} $ns]
}
}
proc widget {name params body} {
set ns [uplevel 1 { namespace current }]
interp alias {} ${ns}::$name {} \
[namespace current]::Create $params $body
return $name
}
proc Create {opts script name args} {
if {([llength $args] %2) == 1} {
set body [lindex $args end]
set args [lrange $args 0 end-1]
} else { set body "" }
set fqname [Resolve $name]
namespace eval $fqname { namespace import -force ::comptk::* }
In $fqname $opts $args $script
namespace eval $fqname $body
interp alias {} $fqname {} ::comptk::Dispatch $fqname
return $name
}
proc widgetadaptor {name {target {}}} {
if {![string length $target]} { set target ::$name }
uplevel 1 [list ::comptk::widget $name args [format {
eval [linsert $args 0 %s [self]]
} [list $target]]]
}
widget window {title args} {
eval toplevel [self] $args
wm title [self] $title
}
widgetadaptor text
widgetadaptor scrollbar
widgetadaptor entry
# Other widgets...
namespace export {[a-z]*}
}
Now that we have the basic package, here is some demo code that creates a new megawidget that is the beginnings of a simple text editor:
# Withdraw root window
wm withdraw .
# A simple test
comptk::widget texteditor {title} {
comptk::window [self] -title $title {
text t -yscrollcommand [self vsb set] -xscrollcommand [self hsb set]
scrollbar vsb -orient vertical -command [self t yview]
scrollbar hsb -orient horizontal -command [self t xview]
entry status -textvariable [namespace current]::status
}
grid [t] -row 0 -column 0 -sticky nsew
grid [vsb] -row 0 -column 1 -sticky ns
grid [hsb] -row 1 -column 0 -sticky ew
grid [status] -row 2 -column 0 -sticky ew -columnspan 2
grid rowconfigure [self] 0 -weight 1
grid columnconfigure [self] 0 -weight 1
}
texteditor foo -title "CompTk Test"
# Set the status to something nice:
foo set status "Hello, World!"
bind [foo] <Destroy> { exit }
Note little touches like that [foo set status ...] stuff. Making widgets into namespaces comes along with some bonuses like that. Making a new text editor is as simple as just:
texteditor bar -title "Another editor!"
bar set status "More status messages..."
Obviously there is a lot of polish to add before this is as industrial-quality as, say
snit. It might be nice to combine this with
Traits for adding orthogonal behaviours to widgets (e.g., undo etc). Widget hierarchies are ensembles too, so you can do stuff like:
[bar status] configure -text
Ideally, that would be just [bar status configure -text], but I need to add some magic for that to work...