Version 1 of ttk::treeview mixins

Updated 2020-04-06 05:49:56 by DDG

Introduction

DDG 2020-04-06: If you write mega widgets your widgets become fast to specialized. You add features and behaviours to your widget and some time later you observe that you have to create a similar widget and you are extracting those specializations from your previous widgets. After reading Designing SNIT widgets as mixins I decided to do a sample implementation for the ttk::treeview widget to check the concept. The widget adaptor are just adding a small set of functionality and can be added by nesting at object creation. Here an hypothetical example which creates a filebrowser with letter search automatic stripe adding:

set filebrowser [filebrowser [filtersearch [bandtable [ttk::treeview .tv]] -directory .]] 

The nice thing is, that you can use the bandtable or filtersearch widget adaptors for any other treeview widget. Mixins are an additional concept to inheritiance and delegation which allows you to add behaviour on the fly to existing objects.

Implementation Examples

Below a few sample mixins for the ttk::treeview widget.

package require Tk
package require snit

namespace eval dgw {} 
package provide dgw::tvfilebrowser

# widget adaptor which does a banding of the ttk::treeview 
# widget automatically after each insert command
snit::widgetadaptor ::dgw::tvband {
    delegate option * to hull 
    delegate method * to hull
    # problem:
    # can't avoid delegating insert as if it is 
    # overerwritten parent insert can't be called
    # solved using trace
    constructor {args} {
        installhull $win
        $win tag configure band0 -background #FFFFFF
        $win tag configure band1 -background #DDEEFF
        trace add execution $win leave [mymethod wintrace]
        $self configurelist $args
    }
    method wintrace {args} {
        set path [lindex [lindex $args 0] 0]
        set meth [lindex [lindex $args 0] 1]
        if {$meth eq "insert"} {
            set parent [lindex [lindex $args 0] 2]
            set index [lindex [lindex $args 0] 3]
            set item [lindex [$path children $parent] $index]
            if {$index eq "end"} {
                set i [llength [$path children $parent]]
            } else {
                set i $index
            }
            set t [expr { $i % 2 }]
            $path tag remove band0 $item 
            $path tag remove band1 $item
            $path tag add band$t $item
        }
    }
}

# widget adaptor which allows forward searching in a ttk::treeview 
# with typing beginning letters of entries matching first column text
# further has bindings of Home and End key
snit::widgetadaptor ::dgw::tvksearch {
    delegate option * to hull 
    delegate method * to hull
    variable LastKeyTime [clock seconds]
    variable LastKey ""
    constructor {args} {
        installhull $win
        bind $win <Key-Home>   [mymethod setSelection 0]
        bind $win <Key-End>   [mymethod setSelection end]
        bind $win <Any-Key> [mymethod ListMatch %A]
        $self configurelist $args
                
    }
    method setSelection {index} {
        $self focus [lindex [$self children {}] $index]
        $self selection set  [lindex [$self children {}] $index]
        focus -force $self
        $self see [lindex [$self selection] 0]
    }
    method  ListMatch {key} {
        if [regexp {[-A-Za-z0-9]} $key] {
            set ActualTime [clock seconds]
            if {[expr {$ActualTime-$LastKeyTime}] < 3} {
                set ActualKey "$LastKey$key"
            } else {
                set ActualKey $key
            }

            set n 0
            foreach i [$win children {}] {
                set name [lindex [$win item $i -value] 0]
                if [string match $ActualKey* $name] {
                    $win selection remove [$win selection]
                    $win focus $i 
                    $win selection set  $i
                    focus -force $win
                    $win see $i
                    set LastKeyTime [clock seconds]
                    set LastKey $ActualKey
                    break
                } else {
                    incr n
                }
            }
        } 
            
    }
}

# a file browser widget as widget adaptor
# could may be better a snit::widget
# as it is already quite specialized
# however writing it as a adaptor allows nesting
# so banding widget adaptor can go intern
# this is required as within the constructor
# $self browseDir is called
# the banding must be installed before this is called
snit::widgetadaptor ::dgw::tvfilebrowser {
    option -dummy ""
    option -filepattern ".+"
    option -directory "."
    option -browsecmd ""
    option -fileimage fileImg
    delegate option * to hull 
    delegate method * to hull except browseDir
    variable LastKeyTime [clock seconds]
    variable LastKey ""
    constructor {args} {
        ttk::style configure Treeview.Item -padding {1 1 1 1}
        installhull $win ;# using ttk::treeview
        $win configure -columns [list Name Size] -show [list tree headings]
        $win heading Name -text Name
        $win heading Size -text Size
        $win column Name -width 60
        $win column Size -width 30
        $win column #0 -width 35 -anchor w -stretch false
        bind $win <Double-1> [mymethod fbOnClick %W %x %y]
        bind $win <Return> [mymethod fbReturn %W]
        bind $win <Key-BackSpace> [mymethod browseDir ..]
        $win tag configure hilight -foreground blue
        $self configurelist $args
        $self browseDir $options(-directory)
    }   
    typeconstructor {
        image create photo movie -data {
            R0lGODlhEAAQAIIAAPwCBARCRAQCBASChATCxATCBASCBAAAACH5BAEAAAAA
            LAAAAAAQABAAAANHCLrc/izISauYI5NduvlXMIjEQBSnUYCYxnmsSJrouhqh
            6J4wLo0mWuqWy5heN58seBrGdEdeMgQsNW0ggXbL7Qog4HDDnwAAIf5oQ3Jl
            YXRlZCBieSBCTVBUb0dJRiBQcm8gdmVyc2lvbiAyLjUNCqkgRGV2ZWxDb3Ig
            MTk5NywxOTk4LiBBbGwgcmlnaHRzIHJlc2VydmVkLg0KaHR0cDovL3d3dy5k
            ZXZlbGNvci5jb20AOw==
        }
        image create photo fileImg -data {
            R0lGODlhEAAOAPcAAAAAADVJYzZKZJOit5WkuZalupqpvpyrwJ6uw6OyyKSzyae2zKm5z6u70a6+
            1K+/1bLC2LrF1L3K4cTP5MnT5svV59HZ6tPb69Xd7Njf7drh7tzj79/l8OHn8ePp8ubr9Ont9evv
            9u7x9/Dz+PL1+fX3+vf4+/n6/Pv8/fz9/v7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAQAA4A
            AAh7AP/9g0CwoAMGCgQqFAhhhcOHKw4IWCjwAcSHBCJMXNjgosMBAkIuXOBxBYoBIBcm8KiiBIgB
            ARYi8HhCRAeYCw1cTEHigwacCgtcNBGCwwWgAgdARDHCQ4YKSP8pddgSxAYLE6JOXVGzAwYKErSi
            HEs2aoCzaNOeFRgQADs=}
        image create photo clsdFolderImg -data {
            R0lGODlhEAAOAPcAAAAAAJycAM7OY//OnP//nP//zvf39wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAQAA4A
            AAhjAP8JHEiw4MAACBECMHjQQIECBAgEWGgwgICLGAUkTCgwwMOPIB8SELDQY8STKAkMIPnPZEqV
            MFm6fDlApUyIKGvqHFkSZ06YK3ue3KkzaMsCRIEOMGoxo1OMFAFInUqV6r+AADs=}
        
    }
    method fbReturn {w} {
        set row [$win selection]
        $win tag remove hilight
        $win tag add hilight $row 
        set fname [lindex [$win item $row -values] 0]
           
        if {[file isdirectory $fname]} {
            $self browseDir $fname
        }  else {
            if {$options(-browsecmd) ne ""} {
                $options(-browsecmd) $fname
            }
        }
    }
    method fbOnClick {w x y} {
        set row [$win identify item $x $y]
        $win tag remove hilight
        $win tag add hilight $row 
        set fname [lindex [$win item $row -values] 0]
        if {[file isdirectory $fname]} {
            $self browseDir $fname
        }  else {
            if {$options(-browsecmd) ne ""} {
                $options(-browsecmd) $fname
            }
        }
    }
    onconfigure -directory value {
        $self browseDir $value
        set options(-directory) $value
    }
    method browseDir {{dir "."}} {
        if {[llength [$win children {}]] > 0} {
            $win delete [$win children {}]
        }
        if {$dir ne "."} {
            cd $dir
            set options(-directory) [pwd]
        }
        $win insert {} end -values [list ".."  " "] -image clsdFolderImg
        foreach dir [lsort [glob -types d -nocomplain [file join $options(-directory) *]]] {
            $win insert {} end -values [list [file tail $dir]  " "] -image clsdFolderImg
        }
        
        foreach file [lsort [glob -types f -nocomplain [file join $options(-directory) *]]] {
            if {[regexp $options(-filepattern) $file]} {
                $win insert {} end -values [list [file tail $file]  [format "%3.1fMb" [expr {([file size $file] /1024.0)/1024.0}]]] -image $options(-fileimage)
            }
        }
        $win focus [lindex [$win children {}] 0]
        $win selection set  [lindex [$win children {}] 0]
        focus -force $win
    }

}

You can now nest the widget adaptors:

# Example usage code

set fb [dgw::tvksearch [dgw::tvfilebrowser [dgw::tvband [ttk::treeview .fp]] -directory . -fileimage fileImg]]
pack $fb -side top -fill both -expand yes
# less specialized but still a file browser
set fb2 [dgw::tvfilebrowser [ttk::treeview .fp2] -directory . -fileimage movie -filepattern {\.(3gp|mp4|avi|mkv|mp3|ogg)$}]
pack $fb2 -side top -fill both -expand yes

See below for a screenshot:

treeview-mixin-image


Discussion

Please discuss here.