tko

tko -- oo class like widgets

Date21 Aug 2020
Current stateno release yet
Sources<https://chiselapp.com/user/rene/repository/tko >
Binaries<https://sourceforge.net/projects/kbskit/files/zipkit/ >
AuthorRené Zaumseil
LicenseBSD

The tko package implements oo class widgets. It provides the following commands:

  • ::tko procedure to create oo widgets and deal with options.
  • _tko class method to deal with options.
  • ::tko::toplevel oo class version of ::toplevel widget.
  • ::tko::frame oo class version of ::frame widget.
  • ::tko::labelframe oo class version of ::labelframe widget.
  • ::tko::graph oo class version of RBC graph widget.
    • ::tko::vector vector command used in graph.
  • ::tko::path oo class version of tkpath widget.
    • ::tko::matrix matrix transformations in path
    • ::tko::style styles in path
    • ::tko::gradient gradients in path
    • ::tko::surface surface drawing in path

The package was developed with data from tip 556. It can be build with tcl/tk 8.7. Currently it is using tcl/tk internals!

Binaries are build using zipkit. There is also the patched version of pdf4tcl needed to work with tko.

New script only version

File tko.tcl with implementation:

##
# \file tko.tcl
#
# proc ::tko::init mode args
#   Initialize class.
# proc ::tko::option class args
#   Define options.
#
# Mixin class ::tko
#   Provide cget/configure functionality.
#
#   classvar Tko
#     Contain class option definitions
#   variable tko
#     Array variable containing object option values
#   method configure args
#     Public configure method.
#   method cget -option
#     Public cget method.
#   method _tko_constructor arglist ?widget? ?widgetargs?
#     Setup call in constructor.
#   method _tko_destructor
#     Cleaup call for destructor
#   method _tko_init class
#     Call in constructor to call all -option methods of current class.
#   method -?option?
#     One method for each option to check and set value.
#   method __tko_trace_tko array field op
#     Internal array trace method.
#
namespace eval ::tko {}
set ::tko(mixinclass) {
    mixin ::tko
    variable tko
    set [info object namespace [self]]::Tko {}
}
set ::tko(mixinwidget) {
    mixin ::tko
    variable tko
    set [info object namespace [self]]::Tko {}
    self unexport createWithNamespace
    self unexport new
    self unexport create
    self method unknown {args} {
        string trimleft [my create {*}$args] :
    }
}
set ::tko(superclass) {
    superclass -append %C
    variable tko
    set [info object namespace [self]]::Tko [set [info object namespace %C]::Tko]
}
set ::tko(superwidget) {
    superclass -append %C
    variable tko
    set [info object namespace [self]]::Tko [set [info object namespace %C]::Tko]
    self unexport createWithNamespace
    self unexport new
    self unexport create
    self method unknown {args} {
        string trimleft [my create {*}$args] :
    }
}
set ::tko(wrapwidget) {
    mixin ::tko
    variable tko
    set [info object namespace [self]]::Tko {}
    self unexport createWithNamespace
    self unexport new
    self unexport create
    self method unknown {args} {
        string trimleft [my createWithNamespace [lindex $args 0] {*}$args] :
    }
    constructor {args} {my _tko_constructor $args %W %A}
    destructor {my _tko_destructor}
    method unknown {args} {$tko(..) {*}$args}
}
#-------------------------------------------------------------------------------
## Initialize tko class functionality
# \param mode
#   mixinclass
#   mixinwidget
#   superclass class
#   superwidget class
#   wrap widget
proc ::tko::init {mode args} {
    switch -- $mode {
        mixinclass {
            uplevel 1 $::tko(mixinclass)
        }
        mixinwidget {
            uplevel 1 $::tko(mixinwidget)
        }
        superclass {
            uplevel 1 [string map [list %C $args] $::tko(superclass)]
        }
        superwidget {
            uplevel 1 [string map [list %C $args] $::tko(superwidget)]
        }
        wrapwidget {
            set myClass [uplevel 1 self]
            lassign $args myWidget myIgnore
            # Get options from widget
            catch {destroy .__tko__}
            set myConf [[$myWidget .__tko__] configure]
            destroy .__tko__
            # Add options to class
            set myArgs {}
            foreach myList $myConf {
                lassign $myList o n c d v
                if {$o in $myIgnore} continue
                switch [llength $myList] {
                    2 {;# synonym options
                        ::tko::option $myClass $o $n {}
                    }
                    5 {;# normal options
                        if {[catch {.__tko__ configure $o $v}]} {
                            ::tko::option $myClass $o $n $c $d ::tko::readonly
                        } else {
                            ::tko::option $myClass $o $n $c $d\
"\$tko(..) configure $o \$tko($o) ; set tko($o) \[\$tko(..) cget $o\]"
                        }
                        append myArgs "$o \$tko($o) "
                    }
                }
            }
            # build new widget class
            uplevel 1 [string map [list %W $arg1 %A [list $myArgs]] $::tko(wrapwidget)]
        }
        default {
return -code error [::msgcat::mc {unknown mode: %s} $mode]
        }
    }
}
#-------------------------------------------------------------------------------
## ::tko::option -option dbclass dbname default mode body
#   mode=   - set in constructor
#   mode=c  - set and call in constructor
#   mode=cr - set, call and make readonly in constructor
#   mode=i  - set in constructor and call in init
#   mode=ir - set in constructor and call and make readonly in init
#   mode=r  - set and make readonly in constructor
#
# \param args
#   class
#   class -option
#   class -option -synonym
#   class -option dbname dbclass default body
proc ::tko::option {class args} {
    upvar [info object namespace $class]::Tko Tko
    switch -- [llength $args] {
        0 {;# return all definitions
            return $Tko
        }
        1 {;# return definition of given option
            lassign $args myOpt
            if {[string index $myOpt 0] ne {-}} {
return -code error [::msgcat::mc {option not starting with "-": %s} $myOpt] 
            }
            if {![dict exists $Tko $myOpt]} {
return -code error [::msgcat::mc {option not existing: %s} $myOpt] 
            }
            return [dict get $Tko $myOpt]
        }
        2 {;# add {-option -synonym}
            lassign $args myOpt myVal
            if {[string index $myOpt 0] ne {-}} {
return -code error [::msgcat::mc {option not starting with "-": %s} $args] 
            }
            if {[string index $myVal 0] ne {-}} {
return -code error [::msgcat::mc {alias not starting with "-": %s} $args] 
            }
            if {[dict exists $Tko $myOpt] && $myVal ne [dict get $Tko $myOpt]} {
return -code error [::msgcat::mc {could not change existing option: %s} $args] 
            }
        }
        5 - 6 {;# add {-option dbname dbclass default mode body}
            lassign $args myOpt myDbname myDbclass myDef myMode myBody
            if {[string index $myOpt 0] ne {-}} {
return -code error [::msgcat::mc {option not starting with "-": %s} $args] 
            }
            if {$myMode ni {{} c cr i ir r}} {
return -code error [::msgcat::mc {wrong mode "%s"in: %s} $myMode $args] 
            }
            set myVal [list $myDbname $myDbclass $myDef $myMode]
            if {[dict exists $Tko $myOpt] && $myVal ne [dict get $Tko $myOpt]} {
return -code error [::msgcat::mc {could not change existing option: %s} $args] 
            }
            ::oo::define $class method $myOpt {} $myBody
        }
        default {
return -code error [::msgcat::mc {wrong option args: %s} $args] 
        }
    }
    dict set Tko $myOpt $myVal
}
#-------------------------------------------------------------------------------
## Mixin ::tko
# classvar Tko
# variable tko (-option)=value (.)=widget (..)=hidden widget
# method _tko_constructor arglist ?widget? ?widgetargs?
# method _tko_destructor
# method _tko_init class
# method __tko_trace_tko array field op
# method -?option?
# method configure args
# method cget -option
catch {::tko destroy}
oo::class create ::tko {}
#-------------------------------------------------------------------------------
proc ::tko::readonly {} {error readonly}
## Should be first command in class constructor.
# \arglist  List of {?option vlaue? ..} pairs
oo::define ::tko method _tko_constructor {arglist {widget {}} {widgetargs {}}} {
    variable tko
    upvar [info object namespace [info object class [self]]]::Tko Tko
    # create dummy widget needed for "::option get" calls
    if {$widget ne {}} {
        set mySelf [self]
        rename $mySelf ::tko::self
        set myW [namespace tail $mySelf]
        ::frame $myW
    } else {
        set myW .
    }
    # initialize all options
    array set myOpts $arglist
    set tko(init) {}
    set myInit {}
    dict for {myOpt myDeflist} $Tko {
        switch -- [llength $myDeflist] {
            1 {;# {-synonym}
                if {[info exists myOpts($myOpt)]} {
                    set tko($myDeflist) $myOpts($myOpt)
                    unset myOpts($myOpt)
                }
            }
            4 {;# {dbname dbclass default mode}
                lassign $myDeflist myDbname myDbclass myDefault myMode
                if {[info exists myOpts($myOpt)]} {
                    set tko($myOpt) $myOpts($myOpt)
                    unset myOpts($myOpt)
                } elseif {$myDbname eq {}} {
                    set tko($myOpt) $myDefault
                } else {
                    set myVal [::option get $myW $myDbname $myDbclass]
                    if {$myVal eq {}} {
                        set tko($myOpt) $myDefault
                    } else {
                        set tko($myOpt) $myVal
                    }
                }
                switch -- $myMode {
                    c {lappend myInit $myOpt 0}
                    cr {lappend myInit $myOpt 1}
                    i {lappend tko(init) $myOpt 0}
                    ir {lappend tko(init) $myOpt 1}
                    r {::oo::objdefine [self] method $myOpt {} {error readonly}}
                }
            }
        }
    }
    # create widget if needed
    if {$widget ne {}} {
        destroy $myW
        set myW [uplevel 1 $widget $myW {*}$widgetargs]
        set myBind "catch {$myW destroy}"
        rename $myW ::${myW}__tko__
        rename ::tko::self ::$myW
        set tko(.) $myW
        set tko(..) ${myW}__tko__
        bindtags $myW [list tko$myW {*}[bindtags $myW]]
        bind tko$myW <Destroy> $myBind
    }
    foreach {myOpt myRo} $myInit {
        my $myOpt
        if {$myRo} {::oo::objdefine [self] method $myOpt {} {error readonly}}
    }
    # error on unsupported options
    if {[array size myOpts]} {
return -code error [::msgcat::mc {unknown options: %s} [array names myOpts]]
    }
    # start option value tracing
    trace add variable [my varname tko] write [list [namespace which my] __tko_trace_tko]
}
#-------------------------------------------------------------------------------
## Call -option functions.
oo::define ::tko method _tko_init {class} {
    variable tko
    if {![info exists tko(init)]} return
    upvar [info object namespace $class]::Tko Tko
    if {![info exists Tko]} {return $tko(init)}
    set myInit {}
    foreach {myOpt myRo} $tko(init) {
        if {[dict exists $Tko $myOpt]} {
            my $myOpt
            if {$myRo} {::oo::objdefine [self] method $myOpt {} {error readonly}}
        } else {
            lappend myInit $myOpt $myRo
        }
    }
    if {$myInit eq {}} {
        unset tko(init)
    } else {
        set tko(init) $myInit
    }
    return $myInit
}
#-------------------------------------------------------------------------------
## Remove internal tko related data.
oo::define ::tko method _tko_destructor {} {
    variable tko
    trace remove variable [my varname tko] write [list [namespace which my] __tko_trace_tko]
    catch {destroy $tko(.)}
}
#-------------------------------------------------------------------------------
## Internal command called from trace of tko(-*) variables.
oo::define ::tko method __tko_trace_tko {array field op} {
    if {[string index $field 0] eq {-}} {my $field}
}
#-------------------------------------------------------------------------------
## 
oo::define ::tko method _tko_configure {} {}
#-------------------------------------------------------------------------------
## Configuration of defined options.
# \param args
#   {}  Return list of options descriptions.
#   -*  Return value of given option
#   {?-* value? ..} Set given options to new values.
oo::define ::tko method configure {args} {
    variable tko
    upvar [info object namespace [info object class [self]]]::Tko Tko
    set myLength [llength $args]
    # get all configuration options
    if {$myLength == 0} {
        set myRet {}
        dict for {myKey myDef} $Tko {
            if {[string index $myKey 0] eq {-}} {
                switch -- [llength $myDef] {
                    1 {;# {-synonym}
                        lappend myRet [list $myKey $myDef]
                    }
                    4 {;# {dbname dbclass default mode}
                        lappend myRet [list $myKey {*}[lrange $myDef 0 2] $tko($myKey)]
                    }
                }
            }
        }
        return [lsort $myRet]
    }
    # get value of given option
    if {$myLength == 1} {
        set myOpt [lindex $args 0]
        if {[string index $myOpt 0] ne {-}} {
return -code error [::msgcat::mc {unknown option "%s"} $myOpt]
        }
        # try real option
        if {[info exist tko($myOpt)]} {
            return $tko($myOpt)
        }
        # try synonym option
        if {[dict exists $Tko $myOpt]} {
            set myDeflist [dict get $Tko $myOpt]
            if {[info exist tko($myDeflist)]} {
                return $tko($myDeflist)
            }
        }
return -code error [::msgcat::mc {unknown option "%s"} $myOpt]
    }
    # set one or more option values
    if {[expr {$myLength%2}]==1} {
return -code error [::msgcat::mc {value for "%s" missing} [lindex $args end]]
    }
    # get old values
    set myOld {}
    foreach {o v} $args {
        # check option existance
        if {[string index $o 0] ne {-} || ![dict exists $Tko $o]} {
return -code error [::msgcat::mc {unknown option "%s"} $o]
        }
        # apply synonym option
        set myOpt [dict get $Tko $o]
        if {[llength $myOpt] == 1} {set o $myOpt}
        lappend myOld $o $tko($o)
        lappend myNew $o $v
    }
    # try to set new values
    if {[catch {array set tko $myNew} m]} {
        catch {array set tko $myOld}
return -code error $m
    }
}
#-------------------------------------------------------------------------------
## Return current value of given option.
# \param option Name of option to get value from.
oo::define ::tko method cget {option} {
    variable tko
    if {[string index $option 0] eq {-}} {
        if {[info exist tko($option)]} {
            return $tko($option)
        }
        upvar [info object namespace [info object class [self]]]::Tko Tko
        if {[dict exists $Tko $option]} {
            set myOpt [dict get $Tko $option]
            if {[dict exists $Tko $myOpt]} {
                return $tko($myOpt)
            }
        }
    }
    return -code error [::msgcat::mc {Unknown option "%s"} $option]
}
#-------------------------------------------------------------------------------
## vim: set ts=4 sw=4 sts=4 et :
# uncomment the following lines to run tests from tko.test file:
#package req tcltest
#::tcltest::configure -testdir [file dirname [info script]] -file tko.test -singleproc 1 -match init-*
#::tcltest::runAllTests

File tko.test with test commands (currently only simple tests):

package require tcltest
namespace import ::tcltest::*

proc cleanup {} {
    destroy {*}[winfo children .]
    catch {namespace delete ::TEST}
}
proc setup {args} {
    cleanup
    foreach x $args {
        setup_$x
    }
}
proc setup_C1 {} {
    oo::class create ::TEST::C1 {
        ::tko::init mixinclass
        ::tko::option [self] -o1 o1 O1 "o1" i {}
        ::tko::option [self] -o2 {} {} "o2" i {}
        ::tko::option [self] -r1 r1 R1 "r1" cr
        ::tko::option [self] -s1 -o1
        constructor {args} {
            my _tko_constructor $args
            my _tko_init ::TEST::C1
        }
        destructor {my _tko_destructor}
    }
}
proc setup_C2 {} {
    oo::class create ::TEST::C2 {
        ::tko::init superclass ::TEST::C1
        ::tko::option [self] -o3 o3 O3 "o3" i {}
        constructor {args} {
            next {*}$args
            my _tko_init ::TEST::C2
        }
        destructor {my _tko_destructor}
    }
}
proc setup_W1 {} {
    oo::class create ::TEST::W1 {
        ::tko::init mixinwidget
        ::tko::option [self] -o1 o1 O1 "o1" i {}
        ::tko::option [self] -o2 {} {} "o2" i {}
        ::tko::option [self] -class class Class "myWidget" ir
        ::tko::option [self] -s1 -o1
        constructor {args} {
            my _tko_constructor $args ::frame {-borderwidth 2 -class $tko(-class)}
            my _tko_init ::TEST::W1
        }
        destructor {my _tko_destructor}
    }
}
proc setup_W2 {} {
    oo::class create ::TEST::W2 {
        ::tko::init superwidget ::TEST::W1
        ::tko::option [self] -o3 o3 O3 "o3" i {}
        constructor {args} {
            next {*}$args
            my _tko_init ::TEST::W2
        }
        destructor {next}
    }
}

#
# Test "::zz .."
#
test init-1.1 {class init wrong args} -cleanup cleanup\
-setup {setup C1}\
-body {
    ::TEST::C1 xx
}\
-match glob -returnCodes error\
-result {unknown method "xx": must be create, destroy or new}

test init-1.2 {widget init wrong args} -cleanup cleanup\
-setup {setup W1}\
-body {
    ::TEST::W1 x
}\
-match glob -returnCodes error\
-result {bad window path name "x"}

test init-2.1 {widget configure} -cleanup cleanup\
-setup {setup W1}\
-body {
    ::TEST::W1 .x -class Xx -o1 oo1 -o2 oo2
    .x configure
}\
-result {{-class class Class myWidget Xx} {-o1 o1 O1 o1 oo1} {-o2 {} {} o2 oo2} {-s1 -o1}}

test init-2.2 {widget configure} -cleanup cleanup\
-setup {setup W1}\
-body {
    ::TEST::W1 .x -class Xx -o1 oo1 -o2 oo2 -s1 ss1
    .x configure
}\
-result {{-class class Class myWidget Xx} {-o1 o1 O1 o1 ss1} {-o2 {} {} o2 oo2} {-s1 -o1}}

test init-2.3 {widget configure} -cleanup cleanup\
-setup {setup W1}\
-body {
    ::TEST::W1 .x -o1 xxx
    set myRet [.x configure -s1]
    .x configure -s1 yyy
    lappend myRet [.x configure -o1]
    lappend myRet [.x configure -s1]
}\
-result {xxx yyy yyy}

test init-2.4 {widget configure} -cleanup cleanup\
-setup {setup W1 W2}\
-body {
    ::TEST::W2 .x -class x -o1 1 -o2 2 -o3 3
    .x configure
}\
-result {{-class class Class myWidget x} {-o1 o1 O1 o1 1} {-o2 {} {} o2 2} {-o3 o3 O3 o3 3} {-s1 -o1}}

test init-2.5 {widget configure} -cleanup cleanup\
-setup {setup W1 W2}\
-body {
    ::TEST::W2 .x -class x -o1 1 -o2 2 -o3 3
    catch {.x configure -o1 11 -x dummy -o2 22} myRet
    lappend myRet [.x configure]
}\
-result {unknown option -x {{-class class Class myWidget x} {-o1 o1 O1 o1 1} {-o2 {} {} o2 2} {-o3 o3 O3 o3 3} {-s1 -o1}}}

test init-2.6 {widget configure} -cleanup cleanup\
-setup {setup W1 W2}\
-body {
    ::TEST::W2 .x -class x -o1 1 -o2 2 -o3 3
    .x configure -o1 11 -o2 22 -s1 111 -o3 33
    .x configure
}\
-result {{-class class Class myWidget x} {-o1 o1 O1 o1 111} {-o2 {} {} o2 22} {-o3 o3 O3 o3 33} {-s1 -o1}}


# vim: set ts=4 sw=4 sts=4 ff=unix et :