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...