JOB 2016-07-29
Another approach for a megawidget in TclOO. Also noticed recently that there is an undocumented ::tk::Megawidget interface which comes along with Tk? Nevertheless, the following code follows the rules of TclOO - which seems to be a good opportunity as well. For the animation effect a coroutine is in place.
# ----------------------------------------------------------------------------- # accordion.tcl --- # ----------------------------------------------------------------------------- # Credits: # Source code was carried over from the original accordion.tm file # and slightly modified to go together with TclOO. # Thanks to: # Copyright (c) 2014, Schelte Bron <[email protected]> # ----------------------------------------------------------------------------- # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # ----------------------------------------------------------------------------- # Modified by: # 2016, Johann Oberdorfer - Engineering Support | CAD | Software # johann.oberdorfer [at] gmail.com, www.johann-oberdorfer.eu # ----------------------------------------------------------------------------- package require Tk package require TclOO package require tile package provide accordion 0.2 namespace eval accordion { namespace export accordion variable cnt 0 # this is a tk-like wrapper around my... class so that # object creation works like other tk widgets proc accordion {path args} { variable cnt set obj [AccordionClass create tmp${cnt} $path {*}$args] incr cnt # rename oldName newName rename $obj ::$path return $path } ttk::style configure Higlighted.TButton \ -background [ttk::style configure . -selectbackground] } # ----------------------------------------------------------------------------- # widget interface declaration # ----------------------------------------------------------------------------- oo::class create AccordionClass { constructor {path args} { my variable thisframe my variable widgetOptions my variable row my variable panes my variable buttons my variable coroid my variable coro set row 0 set panes {} set buttons {} set coroid "" set coro "" # declaration of all additional widget options array set widgetOptions { -width 10 -height 300 -speed 20 } array set widgetOptions $args # we use a frame for this specific widget class ttk::frame $path -class accordion # we must rename the widget command # since it clashes with the object being created set widget ${path}_ rename $path $widget my Build $path my configure {*}$args } destructor { # clean up once the widget get's destroyed set w [namespace tail [self]] catch {bind $w <Destroy> {}} catch {destroy $w} } method cget { {opt "" } } { my variable widgetOptions if { [string length $opt] == 0 } { return [array get widgetOptions] } if { [info exists widgetOptions($opt) ] } { return $widgetOptions($opt) } return -code error "option \"$opt\" is not available" } method configure { args } { my variable thisframe my variable widgetOptions if {[llength $args] == 0} { # as well as all custom options foreach xopt [array get widgetOptions] { lappend opt_list $xopt } return $opt_list } elseif {[llength $args] == 1} { # return configuration value for this option set opt $args if { [info exists widgetOptions($opt) ] } { return $widgetOptions($opt) } return -code error "value for \"[lindex $args end]\" is not declared" } # error checking # if {[expr {[llength $args]%2}] == 1} { # return -code error "value for \"[lindex $args end]\" missing" #} # process the new configuration options... array set opts $args foreach opt_name [array names opts] { set opt_value $opts($opt_name) # overwrite with new value if { [info exists widgetOptions($opt_name)] } { puts "--> $opt_name : $opt_value" set widgetOptions($opt_name) $opt_value } # some options need action from the widgets side switch -- $opt_name { -width { $thisframe configure -width $opt_value } -height { $thisframe configure -height $opt_value } -speed {} default { return -code error "option \"$opt_name\" is not valid" } } } } method unknown {method args} { return -code error "method \"$method\" is unknown" } } # ----------------------------------------------------------------------------- # class method declarations # ----------------------------------------------------------------------------- oo::define AccordionClass { # -------------- # public methods # -------------- method add {win args} { # Add is just an alias for inserting a pane at the end tailcall my InsertItem end $win {*}$args } method getframe {} { my variable thisframe return $thisframe } method showpane {idx} { my variable panes my variable buttons if {[expr {$idx -1}] >= [llength $panes]} { return } my SelectCmd [lindex $buttons $idx] "" $idx } # --------------- # private methods # --------------- method Build {w} { my variable thisframe set thisframe [ttk::frame $w.frm] pack $thisframe -padx 2 -pady 2 -fill both -expand true # The widget should not resize based on its content grid propagate $thisframe 0 # All panes fill the available horizontal space grid columnconfigure $thisframe 0 -weight 1 # The first pane is initially open, once it will be created grid rowconfigure $thisframe 0 -weight 1000 } method CreateFrame {num} { my variable thisframe return $thisframe.__$num } method GetArg {args keystr arg} { upvar $arg cvalue set cvalue "" while {[set i [lsearch -exact $args $keystr]] >= 0} { set j [expr $i + 1] set cvalue [lindex $args $j] set args [lreplace $args $i $j] } return $args } method InsertItem {pos win args} { my variable thisframe my variable row my variable panes my variable buttons my variable coro # Translate pos to an integer, if necessary if {[catch {my GetPaneIndex $pos} pos info]} { # Rethrow the error to get a clean stack trace return -code error -errorcode [dict get $info -errorcode] $pos } # Check that an even number of args was provided if {[llength $args] % 2 == 1} { set msg [format {value for "%s" missing} [lindex $args end]] return -code error -errorcode {TK VALUE_MISSING} $msg } # callback command given as an argument ? set args [my GetArg $args "-accordioncmd" accordioncmd] # We can't handle adding panes while an animation is playing if {$coro ne ""} {rename $coro ""} # If win is already managed by the accordion, delete the old pane set old [lsearch -exact $panes $win] if {$old >= 0} { my ForgetCmd $old if {$old < $pos} {incr pos -1} } # Add a new pane (containing a button and a frame) at the end set num [llength $panes] set f [ttk::frame [my CreateFrame $num]] set callback [list [namespace which my] SelectCmd $f.button $accordioncmd $num] set b [ttk::button $f.button -command $callback] lappend buttons $b set a [ttk::frame $f.frame] grid $b -in $f -sticky ew grid $a -in $f -sticky snew grid columnconfigure $f $b -weight 1 grid rowconfigure $f $a -weight 1 grid $f -in $thisframe -sticky snew -row $num grid remove $a # Insert the new pane in the list of panes set panes [linsert $panes $pos $win] # Shift existing panes after the new one down for {set i $num} {$i > $pos} {} { set p [lindex $panes $i] set f1 [my CreateFrame $i] set f2 [my CreateFrame [incr i -1]] $f1.button configure -text [$f2.button cget -text] raise $p $f1 } if {$num == 0} { # This is the first pane, open it grid $a place $win -in $a -relwidth 1 -relheight 1 my SelectCmd [lindex $buttons 0] $accordioncmd } elseif {$row >= $pos} { # Shift down the opened pane my OpenCmd [expr {$row + 1}] } # Make sure the helper frames don't obscure their contents raise $win [my CreateFrame $pos] # Apply any additional configuration settings if {[llength $args] > 0} { tailcall my CreatePane $pos {*}$args } return } method CreatePane {pos args} { my variable thisframe my variable paneopts if {[catch {my CreateFrame [my GetPaneIndex $pos]} f info]} { # Rethrow the error to get a clean stack trace return -code error -errorcode [dict get $info -errorcode] $f } set paneopts {-compound -image -text -textvariable -underline -style} set argc [llength $args] if {$argc == 0} { foreach opt $paneopts {lappend rc $opt [$f.button cget $opt]} return $rc } elseif {$argc == 1} { set opt [lindex $args 0] if {$opt in $paneopts} {return [$f.button cget $opt]} # Fall through to error message } elseif {$argc % 2 == 1} { set opt [lindex $args end] if {$opt in $paneopts} { return -code error -errorcode {TK VALUE_MISSING} \ set msg [format {value for "%s" missing} $opt] } # Fall through to error message } else { set opt [apply { {thisframe valid opts} { foreach {opt val} $opts { if {$opt ni $valid} {return $opt} catch { $thisframe configure $opt $val } } } } $f.button $paneopts $args] if {$opt eq ""} return # Fall through to error message } set valid [lreplace $paneopts end end "or [lindex $paneopts end]"] return -code error -errorcode [list TK LOOKUP OPTION $opt] \ "bad option \"$opt\": must be [join $valid {, }]" } method GetPaneIndex {paneid} { my variable row my variable panes # Integer paneid's are straight-forward if {[string is integer -strict $paneid]} { # No conversion needed set pos $paneid } elseif {$paneid in {end last}} { set pos [llength $panes] } elseif {$paneid eq "current"} { set pos $row } else { # Position of the named pane set pos [lsearch -exact $panes $paneid] } if {$pos < 0 || $pos > [llength $panes]} { return -code error -errorcode {TK BAD_VALUE} \ [format {invalid pane "%s"} $paneid] } return $pos } method SelectCmd {wbttn cmd {id ""}} { my variable row my variable panes my variable buttons # reset button styles foreach b $buttons { $b configure -style "" -state normal} # and set button style to indicate selected button $wbttn configure -style Higlighted.TButton -state disabled if {$id eq ""} { return [lindex $panes $row] } # execute user command... if {[string length $cmd] != 0} { uplevel $cmd $wbttn } # Use a coroutine for the animation coroutine coro my SlideCmd $id } method SlideCmd {id} { my variable thisframe my variable widgetOptions my variable row my variable panes # Check if the requested pane isn't already selected set new [my GetPaneIndex $id] if {$new == $row} return variable coro [info coroutine] # Always switch to the new row when the coroutine terminates, in # whatever way (run to completion, error, redefined). trace add command $coro delete [list [namespace which my] OpenCmd $new] # Determine the final height of the new pane (same as the current) set height [winfo height [lindex $panes $row]] # Prepare the new frame set f [my CreateFrame $new] set p [lindex $panes $new] grid $f.frame place $p -in $f.frame -relwidth 1 -height $height -relheight 0 # Switch the old pane from relative- to absolute height to # prevent continuous resizing during the slide animation set f [my CreateFrame $row] set p [lindex $panes $row] place $p -in $f.frame -relwidth 1 -height $height -relheight 0 # Calculate a stepsize based on the configured speed setting and # the distance to travel set incr [expr {max($widgetOptions(-speed) * 4000 / $height, 1)}] # Manipulate the weight of the two rows involved to produce the # animation effect while {[incr step $incr] < 1000} { grid rowconfigure $thisframe $row -weight [expr {1000 - $step}] grid rowconfigure $thisframe $new -weight $step variable coroid [after 25 $coro] yield } # The command trace will take care of completing the row change } method ForgetCmd {id} { my variable row my variable panes my variable coro if {[catch {my GetPaneIndex $id} pos info]} { # Rethrow the error to get a clean stack trace return -code error -errorcode [dict get $info -errorcode] $pos } # We can't handle deleting panes while an animation is playing if {$coro ne ""} {rename $coro ""} # Unmap the contents of the pane that will be deleted place forget [lindex $panes $pos] # Remove the pane from the list of panes set panes [lreplace $panes $pos $pos] # Shift existing panes after the new one up set num [llength $panes] for {set i $pos} {$i < $num} {} { set p [lindex $panes $i] set f1 [my CreateFrame $i] set f2 [my CreateFrame [incr i]] $f1.button configure -text [$f2.button cget -text] raise $p $f1 } # Delete the last helper frame destroy [my CreateFrame $num] # Make sure the correct pane is shown if {$row > $pos || $row == $num && $row != 0} { my OpenCmd [expr {$row - 1}] } else { my OpenCmd $row } return } method OpenCmd {num args} { my variable thisframe my variable row my variable panes my variable coroid # Kill any pending attempts to resume the coroutine after cancel $coroid variable coro "" if {$num != $row} { # Close the currently opened row if {$row < [llength $panes]} { set f [my CreateFrame $row] grid remove $f.frame } grid rowconfigure $thisframe $row -weight 0 } if {$num < [llength $panes]} { # (Re-)Open the new row set f [my CreateFrame $num] grid $f.frame set p [lindex $panes $num] place $p -in $f.frame -relwidth 1 -relheight 1 -height 0 # Keep the contents just above their helper frame raise $p $f } grid rowconfigure $thisframe $num -weight 1000 set row $num } }
#!/usr/bin/tclsh set dir [file dirname [info script]] lappend auto_path [file join $dir "."] package require Tk package require accordion # demo code ... catch {console show} accordion::accordion \ .a -width 600 -height 500 -speed 15 pack .a -fill both -expand 1 proc addSpaces {str} { set maxlen 40 while {[string length $str] < $maxlen} { append str " " } return $str } set accframe [.a getframe] # create some arbitrary data... set flist [lsort -dictionary [glob -dir $dir "*.tcl"]] foreach n $flist { set fnew [ttk::frame $accframe.f[incr f]] .a add $fnew \ -text [addSpaces "[file tail $n]"] text $fnew.t \ -yscrollcommand [list $fnew.vs set] \ -background white \ -highlightthickness 0 \ -relief flat \ -bd 4 ttk::scrollbar $fnew.vs -command [list $fnew.t yview] pack $fnew.vs -side right -fill y pack $fnew.t -fill both -expand 1 if {[catch {open $n} fd]} { $fnew.t insert end "$fd" } else { $fnew.t insert end [read -nonewline $fd] close $fd } } # show specific pane ... .a showpane 3
#!/usr/bin/tclsh set dir [file dirname [info script]] lappend auto_path [file join $dir "."] package require Tk package require accordion proc setspeed {value} { global speed set speed [expr {round($value)}] .a configure -speed $speed # .scale.s set $speed } ttk::frame .scale pack .scale -side top -fill x ttk::label .scale.l1 -text Speed: ttk::scale .scale.s -from 1 -to 20 -command setspeed ttk::label .scale.l2 -textvariable speed -width 4 -anchor center pack .scale.l1 -side left -padx 4 -pady 4 pack .scale.l2 -side right -padx 4 -pady 4 pack .scale.s -side left -fill x -expand 1 accordion::accordion .a -width 600 -height 500 pack .a -fill both -expand 1 .scale.s set [.a cget -speed] foreach n {green blue red yellow cyan purple} { set w [frame [.a getframe].f[incr f] -bg $n] .a add $w -text [string totitle $n] }
Have fun.