Version 0 of TclOO widget/object framework

Updated 2013-11-13 23:20:40 by RZ

TclOO extension

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

Everything is on top of TclOO. The class ::zz::class contains the ::oo::class commands and the additional features. The ::zz::define command contains the ::oo::define commands and the additional features. New classes should use ::zz::object as superclass. All classes will be created with the createWithNamespace function of TclOO. New Object will be created without the new function. The new object name is the first parameter.

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 -privateappend -privateclear and -privateset switches

New class commands

option <name> <value> <body>

Define new option. The <body> will on optionsetting in the current class context evaluated.

option delete <name> ..

Remove previously defined option.

component <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 delete <name> ..

Remove previously defined component's.

New object commands

cget <option> Get option values.

configure ?option value ..? Get and set options.

'component ?name?' Return all component names or the command of the given name.

Commands inside methods

_zz_method Access to private variables

_zz_component ?-private? name createcommand ?optionlist? Create components and private components.

Variables

The public array variable 'zz' is used to store options zz(-*) and components objects zz(:*) and component widgets zz(.*). The private array variable _zz is used to store private component object _zz(:*) and private component widgets _zz(.*).

Examples

Extending widgets

::zz::class create togglelabel {
  superclass zz::object
  component . {label %W -text test} {keep -*}
  constructor {args} {my configure {*}$args}
  method toggle {} {
    set myBg $zz(-background)
    set myFg $zz(-foreground)
    array set zz [list -foreground $myBg -background $myFg]
  }
  togglelabel .l -foreground black -background white]
  .l toggle

Private variables, components

::zz::class create zz1 {
  superclass zz::object
  option -xyz z1xyz {puts zz1-xyz=$zz(-xyz)}
  option -abc abc {puts zz1-abc=$zz(-abc)}
  component . {toplevel %W}
  component .l1 {label %W.l1 -text extern} {keep -text -bd -bd ignore -bd}
  constructor args {
    lappend zz(a) zz1
    lappend _zz(my) zz1
    my _zz_component -private .l2 {label $zz(.).l2 -text inside} {-text -text}
    grid $zz(.l1) $_zz(.l2)
    my configure {*}$args
  }
  destructor {}
  method parray {name} {puts zz1>;::parray $name}
}
::zz::class create zz2 {
  superclass zz1
  option -xyz z2xyz {puts zz2-xyz=$zz(-xyz)}
  component .l2 {label $zz(.).l3 -text outside} {-text -text}
  destructor {}
  constructor args {
    lappend zz(a) zz2
    lappend _zz(my) zz2
    grid $zz(.l2)
  }
  method parray {name} {puts zz2>;::parray $name;next $name}
}
zz2 .z
.z parray zz
.z parray _zz

Code

# Helper functions.
interp alias {} ::? {} ::msgcat::mc

#===============================================================================

namespace eval ::zz {
  ##    Customized ::oo::define command.
  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 {
        switch -- [lindex $args 1] {
          -private - -privateappend {
            foreach myVar [lrange $args 2 end] {
              if {[lsearch [set ${class}::(vars)] $myVar] == -1} {
                lappend ${class}::(vars) $myVar $myVar
              }
            }
          }
          -privateclear {
            set ${class}::(vars) {_zz _zz}
          }
          -privateset {
            set ${class}::(vars) {}
            foreach myVar [lrange $args 2 end] {
              lappend ${class}::(vars) $myVar $myVar
            }
          }
          default {::oo::define $class variable {*}$args}
        }
      }
      option {;# option value ?body? || delete option ..                         
        if {[lindex $args 1] eq {delete}} {                                      
          foreach myOpt [lrange $args 2 end] {                                   
            if {[string index $myOpt 0] ne {-}} {                                
              error [? {wrong option name: %1$s} $myOpt]                         
            }
            set myNr [lsearch [set ${class}::(optionsets)] $myName]
            if {$myNr == -1} {error [? {option not found: %1$s} $myOpt]}
            set ${class}::(optionsets) [lreplace [set ${class}::(optionsets)] $myNr $myNr]
            set ${class}::(optioninit) [lreplace [set ${class}::(optioninit)] $myNr $myNr]
          }
        }
        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 [set ${class}::(optionsets)] $myName]
        if {$myNr == -1} {
          lappend ${class}::(optionsets) $myName $myBody
          lappend ${class}::(optioninit) $myOpt $myVal
        } else {
          set myNr [expr {1+$myNr*2}]
          lset ${class}::(optionsets) $myNr $myBody
          lset ${class}::(optioninit) $myNr $myVal
        }
      }
      component {;# name createcmd ?optionlist? || delete name ..
        if {[lindex $args 1] eq {delete}} {
          foreach myName [lrange $args 2 end] {
            set myNr [lsearch -index 0 [set ${class}::(complist)] $myName]
            if {$myNr == -1} {error [? {component not found: %1$s} $myName]}
            set ${class}::(complist) [lreplace [set ${class}::(complist)] $myNr $myNr]
          }
        }
        lassign $args x myName myCmd myOpts
        if {[string index $myName 0] ni {. :}} {
          default {error [? {wrong comp name %1$s} $myName]}
        }
        if {[lsearch -index 0 [set ${class}::(complist)] $myName] != -1} {
          error [? {comp name exists: %1$s} $myName]
        }
        lappend ${class}::(complist) [list $myName $myCmd $myOpts]
      }
      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 to handle option setting.
    #   Defined in each class to support access to private class parts.
    ::oo::define $myCls method _zz_trace {array field op} {
      if {$op eq {}} {;# internal call to eval body in class context
        eval $array
        return
      }
      # array write call
      if {[string index $field 0] ne {-}} return
      set myC [self class]
      # Ensure the option setting body of . comes last, TODO optimization
      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) {} {}
        }
      }
    }
    # Internal class informations. Define private variable array _zz.
    array set ${myCls}:: [list vars {_zz _zz} optionsets {} optioninit {} complist {}]
    # Define internally used zz array variable.
    my eval variable zz
    # 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 {
  foreach myComp [array names zz {[.,]*}] {
    if {![info exists zz($myComp)]} continue;# may be already destroyed
    if {[string index $myComp 0] eq {:}} {
      $zz($myComp) destroy
      continue
    }
    destroy $zz($myComp)
  }
}

##      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} {
    return [array get zz -*]
  } 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} {
    set myArgs {}
    if {[catch {
      foreach {o v} $args {
        if {[string index $o 0] ne {-} || ![info exists zz($o)]} {
          error [? {unknown option %1$s} $o]
        }
        lappend myArgs $o $zz($o)
        set zz($o) $v
      }
    } myMsg]} {
      my configure {*}$myArgs
      error [? {error in configure: %1$s} $myMsg]
    }
  } else {
    error [? {wrong configure: %s} $args]
  }
}

##      Return component command.
::oo::define ::zz::object method component {{comp {}}} {
  if {$comp eq {}} {return [array names zz {[.,]*}]}
  if {[string index $comp 0] ni {. ,} || ![info exists zz($comp)]} {
    error [? {unknown comp %1$s} $comp]
  }
  return $zz($comp)
}

##      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 _zz_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
  foreach myComp [array names $myVar {[.,]*}] {
    if {[string index $myComp 0] eq {:}} {
      catch {[set ${myVar}($myComp)] destroy}
      continue
    }
    catch {destroy [set ${myVar}($myComp)]}
  }
}

##      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)]]
}

##      Work with components.
# - add new widget
#       _zz_component ?-private? .* createcmd ?optionlist?
# - add new object
#       _zz_component ?-private? ,* createcmd ?optionlist?
# - delete widget/object
#       _zz_component ?-private? delete ..
::oo::define ::zz::object method _zz_component {args} {
  set myCls     [uplevel 1 self class]
  if {[lindex $args 0] eq {-private}} {
    set args [lrange $args 1 end]
    set myVar   [my varname { }]${myCls}::_zz
  } else {
    set myVar   [my varname zz]
  }
  # Delete existing component
  if {[lindex $args 0] eq {delete}} {
    foreach myComp $args {
      set myVar1 ${myVar}($myComp)
      if {![info exists $myVar1]} return
      # Remove option info
      foreach myName [array names $myVar " $myCls $myComp -*"] {
        unset ${myVar}($myName)
      }
      unset $myVar1
      # Remove widget/object
      if {[string index $myComp 0] eq {:}} {
        catch {[set $myVar1] destroy}
        continue
      }
      set w [set $myVar1]
      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
      }
    }
    return
  }
  # Add new component
  lassign $args myComp myCmd myOpts
  set myVar ${myVar}($myComp)
  if {[info exists $myVar]} {
    error [? {comp %1$s already exists} $myComp]
  }
  set myCopts   {}
  set myCvals   {}
  switch -- [string index $myComp 0] {
    . {
      set myW [lindex $myCmd 1]
      if {[string range $myW 0 1] eq {%W}} {
        lset myCmd 1 [namespace tail [self]][string range $myW 2 end]
      }
      if {$myComp eq {.}} {
        set mySelf [self]
        rename $mySelf ::zz::self
        set w [uplevel 1 $myCmd]
        foreach myList [$w configure] {
          lappend myCopts [lindex  $myList 0]
          lappend myCvals [lindex $myList end]
        }
        set myCmd [list $w destroy]
        rename $w ::${w}__zz__
        set $myVar ${w}
        set myW ::${w}__zz__
        rename ::zz::self $mySelf
      } else {
        set w [uplevel 1 $myCmd]
        set $myVar $w
        set myCmd "array unset [my varname zz] \{ $myCls $myComp -*\} \; unset \{$myVar\}"
        foreach myList [$w configure] {
          lappend myCopts [lindex  $myList 0]
          lappend myCvals [lindex $myList end]
        }
        set myW [set $myVar]
      }
      bindtags $w [list ::zz::$w {*}[bindtags $w]]
      bind ::zz::$w <Destroy> $myCmd
    }
    : {
      set $myVar [uplevel 1 $myCmd]
      foreach myList [$myCmd configure] {
        lappend myCopts [lindex  $myList 0]
        lappend myCvals [lindex $myList end]
      }
      set myW   [set $myVar]
    }
    default {error [? {wrong comp name %1$s} $myComp]}
  }
  # 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 $myVar]
}

#===============================================================================