TclOO extension edit
RZ I'm missing some features in plain TclOO. So I added these features on top of it. Feel free to comment or use it.- private variables (see also TclOO private variables)
- options with cget/configure function
- components (object and widgets) with integration in cget/configure methods
- private components
Enhanced commands
- constructor
- Access to private variables, setup internal structures and calling next
- destructor
- Access to private variables, deleting components and calling next
- method
- Access to private variables
- variable
- Additional -private and -privateclear switches
New class commands
option <name> <value> <body>- Define new option. The <body> will on optionsetting in the current class context evaluated.
- Remove previously defined option.
component addprivate <name> createcommand ?optionlist?
- Define new component. If the name starts with '.' (dot) it is a widget. If name is '.' (only a dot) it will make the current object act as a widget. If the name starts with '::' (double colon) it is a object.
- The createcommand will be evaluated to create the component. It should return the component command. Component commands should also have cget/configure methods to access options. If the second word inside the createcomand start with %W then %W is replaced with the current object widget '$zz(.)'
- The optionlist is a "key value" list.
- If key is keep then value is used as an option list. All component options matching one of these entries will be added to the object options.
- If key is ignore then value is used as an option list. All already defined component options matching one of these entries will be deleted.
- If key and value is starting with '-' (minus sign) then component option names key is mapped to object option value.
component deleteprivate <name> ..
- Remove previously defined component's.
New object commands
- cget <option>
- Get option values.
- configure ?option value ..?
- Get and set options.
- component
- Return all component names.
- component <name>
- Return command of the given name.
- component args
- See above for the add* and del* command syntax.
Commands inside methods
- _zz_constructor
- Setup internal variables
- _zz_destructor
- Internal cleanup
- _zz_method
- Access to private variables
Variables
The public array variable {} is used to store options (-*) and components objects (:*) and component widgets (.*). The private array variable _ is used to store private component object _(:*) and private component widgets _(.*).Examples edit
Extending widgets
::zz::class create togglelabel {
superclass zz::object
component . {label %W -text test} {keep -*}
constructor {args} {my configure {*}$args}
method toggle {} {
set myBg $(-background)
set myFg $(-foreground)
array set {} [list -foreground $myBg -background $myFg]
}
}
togglelabel .l -foreground black -background white
.l togglePrivate variables, components
::zz::class create zz1 {
superclass zz::object
option -xyz z1xyz {puts zz1-xyz=$(-xyz)}
option -abc abc {puts zz1-abc=$(-abc)}
component add . {toplevel %W}
component add .l1 {label %W.l1 -text extern} {keep -text -bd -bd ignore -bd}
constructor args {
lappend (a) zz1
lappend _(my) zz1
my component addprivate .l2 {label $(.).l2 -text inside} {-text -text}
grid $(.l1) $_(.l2)
my configure {*}$args
}
destructor {}
method parray {name} {puts zz1>;::parray $name}
}
::zz::class create zz2 {
superclass zz1
option -xyz z2xyz {puts zz2-xyz=$(-xyz)}
component add .l2 {label $(.).l3 -text outside} {-text -text}
destructor {}
constructor args {
lappend (a) zz2
lappend _(my) zz2
grid $(.l2)
}
method parray {name} {puts zz2>;::parray $name;next $name}
}
zz2 .z
.z parray ""
.z parray _Code edit
catch {rename ::? {}}
## Helper function for msgcat::mc command inside classes.
proc ::? {args} {
if {[catch {set myNs [uplevel 1 self class]}]} {
set myNs [uplevel 1 namespace current]
}
namespace eval $myNs ::msgcat::mc $args
}
#===============================================================================
namespace eval ::zz {
## Customized ::oo::define command.
#
# Constructor with private variables, next and initialization:
# constructor arglist body
#
# Destructor with private variables, next and internal clean up:
# destructor body
#
# Method with private variables:
# method arglist body
#
# Definition of additional private variables:
# variable -private <name> ..
# Remove all private variables:
# variable -privateclear
#
# New definition or overwrite of options:
# option <-name> value ?body?
# Remove of existing options:
# option delete <-name> ..
#
# Handling of components. See method component for documentation.
# component add <component> createcmd ?optionlist?
# component addprivate <component> createcmd ?optionlist?
# component delete <component> ..
# component deleteprivate <component> ..
#
proc define {class args} {
switch -- [lindex $args 0] {
constructor {::oo::define $class {*}[lrange $args 0 1]\
"my _zz_method;next;my _zz_constructor\n[lindex $args 2]"
}
destructor {::oo::define $class [lindex $args 0]\
"my _zz_method\n[lindex $args 1] \nmy _zz_destructor;next"
}
method {::oo::define $class {*}[lrange $args 0 2]\
"my _zz_method\n[lindex $args 3]"
}
variable {
upvar 0 ${class}::(vars) _
switch -- [lindex $args 1] {
-private {
foreach myVar [lrange $args 2 end] {
if {[lsearch $_ $myVar] == -1} {lappend _ $myVar $myVar}
}
}
-privateclear {set _ {_zz _zz}}
default {::oo::define $class variable {*}$args}
}
}
option {
upvar 0 ${class}:: _
if {[lindex $args 1] eq {delete}} {
set myName " $class\ -\ "
foreach myOpt [lrange $args 2 end] {
if {[string index $myOpt 0] ne {-}} {
error [? {wrong option name: %1$s} $myOpt]
}
set myNr [lsearch $_(optionsets) $myName$myOpt]
if {$myNr == -1} {error [? {option not found: %1$s} $myOpt]}
set _(optionsets) [lreplace $_(optionsets) $myNr $myNr]
set _(optioninit) [lreplace $_(optioninit) $myNr $myNr]
}
} else {
lassign $args x myOpt myVal myBody
if {[string index $myOpt 0] ne {-}} {
error [? {wrong option name: %1$s} $myOpt]
}
set myName " $class\ -\ $myOpt"
set myNr [lsearch $_(optionsets) $myName]
if {$myNr == -1} {
lappend _(optionsets) $myName $myBody
lappend _(optioninit) $myOpt $myVal
} else {
lset _(optionsets) [incr myNr] $myBody
lset _(optioninit) $myNr $myVal
}
}
}
component {
upvar 0 ${class}::(complist) _
switch -- [lindex $args 1] {
add - addprivate {
lassign $args x myMode myName myCmd myOpts
if {[string index $myName 0] ni {. :}} {
default {error [? {wrong comp name %1$s} $myName]}
}
foreach myL $_ {
if {[lindex $myL 1] eq $myName && [lindex $myL 0] eq $myMode} {
error [? {comp name exists%1$s} $myName]
}
}
lappend _ [list $myMode $myName $myCmd $myOpts]
}
delete - deleteprivate {
if {[lindex $args 1] eq {delete}} {
set myMode add
} else {
set myMode addprivate
}
foreach myName [lrange $args 2 end] {
set myNr 0
foreach myL $_ {
if {[lindex $myL 1] eq $myName && [lindex $myL 0] eq $myMode} {
set _ [lreplace $_ $myNr $myNr]
set myNr -1
break
}
incr myNr
}
if {$myNr != -1} {error [? {component not found: %1$s} $myName]}
}
}
default {[? {wrong component command '%1$s', should be one of %2$s}\
[lindex $args 1] {add addprivate delete deleteprivate}]
}
}
}
default {tailcall ::oo::define $class {*}$args}
}
}
}
#-------------------------------------------------------------------------------
## Customized ::oo::class command.
::oo::class create ::zz::class {
superclass ::oo::class
self export createWithNamespace
self unexport new
## Always create new classes with namespace.
# See "oo::class create" command.
self method create {args} {
return [uplevel 1 [list [self] createWithNamespace [lindex $args 0] {*}$args]]
}
## Build new class using ::zz::class with additional commands.
constructor {args} {
# Current class name.
set myCls [self object]
# Make ::zz::* methods in class definition available.
foreach myName {constructor destructor method variable option component} {
interp alias {} [self namespace]::$myName {} ::zz::define $myCls $myName
}
# Make ::oo::define methods available.
foreach myName {renamemethod deletemethod forward unexport mixin superclass export filter} {
interp alias {} [self namespace]::$myName {} ::oo::define $myCls $myName
}
## Internal method \c _zz_trace to handle option setting.
# Defined in each class to support access to private class parts.
# If op is empty then eval command given in array (internal usage only!)
# Otherwise call all option related bodies.
set myBody "namespace upvar \[my varname { }\]$myCls {*}\$${myCls}::(vars)"
append myBody {
if {$op eq {}} {eval $array ; return};# eval body
if {[string index $field 0] ne {-}} return;# no option
# Ensure the option setting body of . comes last, TODO optimization
set myC [self class]
foreach myList [lsort -decreasing [array names $array *\ $field]] {
lassign $myList myCls myCmp myOpt
if {$myCls eq $myC} {
my _zz_trace $zz($myList) {} {}
} else {
nextto $myCls $zz($myList) {} {}
}
}
}
::oo::define $myCls method _zz_trace {array field op} $myBody
# Internal class informations. Define class definition variables.
array set ${myCls}:: [list vars {_zz _zz} optionsets {} optioninit {} complist {}]
# Define internally used array variable.
::oo::define $myCls variable zz
# Add ::zz::object to list of superclasses
if {$myCls ne {::zz::object}} {
::oo::define $myCls {superclass ::zz::object}
}
# Define default constructor
::zz::define $myCls constructor args {}
# Define default destructor.
::zz::define $myCls destructor {}
# Read and evaluate the class definition.
my eval {*}$args
}
## Enable object creation with namespace and without "new" word.
method unknown {args} {
my createWithNamespace ::[lindex $args 0] {*}$args
}
}
#-------------------------------------------------------------------------------
## Class to create objects. Define class methods with ::oo::define!
::zz::class create ::zz::object {
## Array variable to hold internal informations.
# (-*) Value of option.
# (.*) Component widget command.
# (:*) Component object command.
# ( <class> <comp> <option>) Used body when setting options.
variable zz
}
#-------------------------------------------------------------------------------
##
::oo::define ::zz::object constructor {args} { }
#-------------------------------------------------------------------------------
##
::oo::define ::zz::object destructor {
# object
foreach {myN myV} [array get zz :*] {$myV destroy}
# widget
if {[info exists zz(.)]} {
if {[winfo exists $zz(.)]} {destroy $zz(.)}
} else {
foreach {myN myV} [array get zz .*] {
if {[winfo exists $myV]} {destroy $myV}
}
}
}
## Return value of configuration option.
::oo::define ::zz::object method cget {option} {
if {[string index $option 0] ne {-} || ![info exists zz($option)]} {
error [? {unknown option %1$s} $option]
}
return $zz($option)
}
#-------------------------------------------------------------------------------
## Work with configuration options.
::oo::define ::zz::object method configure {args} {
set l [llength $args]
if {$l == 0} {
set myRet {}
foreach myOpt [lsort [array names zz -*]] {
lappend myRet $myOpt $zz($myOpt)
}
return $myRet
} elseif {$l == 1} {;# same as cget() function
if {[string index $args 0] ne {-} || ![info exists zz($args)]} {
error [? {unknown option %1$s} $args]
}
return $zz($args)
} elseif {$l%2 == 0} {
foreach {o v} $args {
if {[string index $o 0] ne {-} || ![info exists zz($o)]} {
error [? {unknown option %1$s} $o]
}
set myOld $zz($o)
if {[catch {set zz($o) $v} myMsg]} {
catch {set zz($o) $myOld}
error [? {error in configure %1$s: %2$s} $o $myMsg]
}
}
} else {
error [? {wrong configure: %s} $args]
}
}
#-------------------------------------------------------------------------------
## Component command.
# <component> names starting with . are treated as widgets.
# <component> names starting with : are treated as objects.
#
# Get list of available public components:
# component
# Get command of available public component:
# component <component>
# Add new public component:
# component add <component> createcmd ?optionlist?
# Add new private component:
# component addprivate <component> createcmd ?optionlist?
# Delete existing public component:
# component delete <component> ..
# Delete existing private component:
# component deleteprivate <component> ..
#
# \note Defined with ::zz::define to access private variable _zz.
::zz::define ::zz::object method component {args} {
# Return public component names
if {$args eq {}} {return [array names zz {[.:]*}]}
set myMode [lindex $args 0]
# Return public component command
if {[string index $myMode 0] in {. :}} {
if {[info exists zz($myMode)]} {
return $zz($myMode)
}
error [? {unknown component %1$s} $myMode]
}
# Add and delete components
set myCls [uplevel 1 self class]
set args [lrange $args 1 end]
switch -- $myMode {
add - addprivate {;# Add new component
if {$myMode eq {add}} {
set myVar [my varname zz]
} else {
set myVar [my varname { }]${myCls}::_zz
}
lassign $args myComp myCmd myOpts
set myCompvar ${myVar}($myComp)
if {[info exists $myCompvar]} {
error [? {comp %1$s already exists} $myComp]
}
set myCopts {}
set myCvals {}
switch -- [string index $myComp 0] {
. {
set myCmd [string map [list %W [namespace tail [self]]] $myCmd]
if {$myComp eq {.}} {
set mySelf [self]
rename $mySelf ::zz::self
set w [uplevel 1 $myCmd]
set myW ::${w}__zz__
set myBind [list $w destroy]
rename $w $myW
rename ::zz::self $mySelf
} else {
set w [uplevel 1 $myCmd]
set myW $w
set myBind "array unset \{$myVar\} \{ $myCls $myComp -*\} \; unset -nocomplain \{$myCompvar\}"
}
set w [string trimleft $w :]
bindtags $w [list zz$myW {*}[bindtags $w]]
bind zz$myW <Destroy> $myBind
set $myCompvar $w
}
: {
set $myCompvar [uplevel 1 $myCmd]
foreach myList [$myCmd configure] {
lappend myCopts [lindex $myList 0]
lappend myCvals [lindex $myList end]
}
set myW [set $myCompvar]
}
default {error [? {wrong comp name %1$s} $myComp]}
}
foreach myList [$myW configure] {
lappend myCopts [lindex $myList 0]
lappend myCvals [lindex $myList end]
}
# Get all component options
array set myFound {}
foreach {myFrom myTo} $myOpts {
if {[string index $myFrom 0] eq {-}} {;# -copt -opt
if {[string index $myTo 0] ne {-}} {
error [? {wrong option name: %1$s} $myTo]
}
set myNr [lsearch $myCopts $myFrom]
if {$myNr == -1} {
error [? {option not found: %1$s} $myFrom]
}
append myFound($myTo) "\n$myW configure $myFrom \$zz($myTo)"
if {[lsearch $myCopts $myTo] == -1} {
lappend myCopts $myTo
lappend myCvals [lindex $myCvals $myNr]
}
} elseif {$myFrom eq {keep}} {;# keep -*
foreach myT $myTo {
foreach myO [lsearch -inline -glob -all $myCopts $myT] {
append myFound($myO) "\n$myW configure $myO \$zz($myO)"
}
}
} elseif {$myFrom eq {ignore}} {;# ignore -*
foreach myT $myTo {
foreach myO [array names myFound $myT] {unset myFound($myO)}
}
} else {
error [? {wrong from part name: %1$s} $myFrom]
}
}
# Set options
foreach myOpt [array names myFound] {
set zz(\ $myCls\ $myComp\ $myOpt) $myFound($myOpt)
if {![info exists zz($myOpt)]} {
set zz($myOpt) [lindex $myCvals [lsearch $myCopts $myOpt]]
}
}
return [set $myCompvar]
}
delete - deleteprivate {;# Delete existing component
if {$myMode eq {delete}} {
set myVar [my varname zz]
} else {
set myVar [my varname { }]${myCls}::_zz
}
foreach myComp $args {
set myCompvar ${myVar}($myComp)
if {![info exists $myCompvar]} return
# Remove option info
array unset $myVar " $myCls $myComp -*"
unset $myCompvar
# Remove widget/object
if {[string index $myComp 0] eq {:}} {
catch {[set $myCompvar] destroy}
continue
}
set w [set $myCompvar]
if {[winfo exists $w]} {
set myTags [bindtags $w]
set i [lsearch $myTags "::zz::$w"]
if {$i >= 0} {
bindtags $w [lreplace $myTags $i $i]
}
bind ::zz::$w <Destroy> {}
destroy $w
}
}
}
default {[? {wrong command '%1$s', should be one of %2$s}\
[lindex $args 1] {add addprivate delete deleteprivate}]
}
}
}
#-------------------------------------------------------------------------------
## Function for use in constructor.
::oo::define ::zz::object method _zz_constructor {} {
set myCls [uplevel 1 self class]
array set zz [set ${myCls}::(optionsets)]
array set zz [set ${myCls}::(optioninit)]
foreach myList [set ${myCls}::(complist)] {
uplevel 1 [list my component {*}$myList]
}
# Start option variable trace in outermost class
if {[info object class [self object]] eq $myCls} {
trace add var [my varname zz] write [list [namespace which my] _zz_trace]
}
}
#-------------------------------------------------------------------------------
## Function for use in destructor.
::oo::define ::zz::object method _zz_destructor {} {
set myCls [uplevel 1 self class]
set myVar [my varname { }]${myCls}::_zz
# object
foreach {myN myV} [array get $myVar :*] {$myV destroy}
# widget
if {[info exists ${myVar}(.)]} {
set myV [set ${myVar}(.)]
if {[winfo exists $myV]} {destroy $myV}
} else {
foreach {myN myV} [array get $myVar .*] {
if {[winfo exists $myV]} {destroy $myV}
}
}
}
#-------------------------------------------------------------------------------
## Function to access private variables.
::oo::define ::zz::object method _zz_method {} {
set myCls [uplevel 1 self class]
set myNs [my varname { }]$myCls
namespace eval $myNs {}
uplevel 1 [list namespace upvar $myNs {*}[set ${myCls}::(vars)]]
}
#-------------------------------------------------------------------------------
## Function to access private variables.
::oo::define ::zz::object method _zz_varname {name} {
return [my varname { }][uplevel 1 self class]::$name
}
#-------------------------------------------------------------------------------
Comments edit
DKF: My main comment is this: have you put this in a repository somewhere? It's much easier to develop when you've got proper history mechanisms available. If you prefer fossil, check out http://chiselapp.com
(run by Roy Keene), if you prefer git, there's github of course, and for subversion you're probably better with google code.Aside from that, a very useful technique for doing the configure is to evaluate the user's script in a namespace (that's what oo::define really is doing, with some small extra tricks). It's great, because it takes very little code to do right. I'd also commend using forwarded methods as a technique for exposing methods from underlying widgets; by putting the contained implementation widgets in the instance namespace, you get automatic cleanup and concealment and organisation for almost nothing.RZ This is so far just a proof of concept. If it is working I will put it into some fossil repository and remove the code from here. TclOO is still a great tool but I hope to get private variables directly in it in time ;) Options, cget/configure and components would be fine too. But this is more tricky and can be evaluated in scripted extensions.Do you mean by configure the option setting part? Here I have used the _zz_trace function to evaluate code in the correct namespace. This is necessary to access private variables. Is there a better solution for this task?I'm at loss with your hint to use forward. For which part should I use it?To make cleanup easy I have put all private variables on the same place as normal variables. But I have used here for each class a separate sub-namespace. This prevent collisions because normal variables could not contain the : sign.Component widgets and objects need still deletion by hand. Therefore the destructor and _zz_destructor functions.DKF: The little megawidget framework inside Tk (see library/megawidget.tcl) puts the real Tk widgets it wraps inside its instance namespace and forwards some methods on to them. For example, if you embedded a button and wanted to expose its flash method, you might do:oo::define megabuttonclass {
forward flash buttonWidget flash
}Where buttonWidget is what the button has been renamed to inside the instance. This is a class-level forwarding that forwards to something in an instance (technically, the forwarding target command is resolved with respect to the instance namespace); you can do a lot of clever stuff with this. TclOO is an extremely heavy user of Tcl's namespace and stack frame facilities; because of this, it required almost no core changes.RZ Thank you for the example.


