Inplace edit in ttk::treeview

Flame ttk::treeview widget lacks one important function for me - inplace editing of the contents. I've decided to try to implement it, though tktreectrl suits more for this task. The main problem is to track focus changes since there is no mechanism in ttk::treeview for this.

JE See also: <URL: http://www.flightlab.com/~joe/repos/themebag/inplace.tcl >.

DDG - 2020-04-14: See dgw::tvmixins for a snit::widget adaptor of the code below which simplifies addition to ttk::treeview widgets.

# BSD license
package require Tk 8.5

package provide xtreeview 1.2

namespace eval xtreeview {
  # intercept all the events changing focus
  bind XTreeview <<TreeviewSelect>> "+::xtreeview::checkFocus %W"
  bind XTreeview <ButtonRelease-1> "+::xtreeview::checkFocus %W %x %y"
  bind XTreeview <KeyRelease> "+::xtreeview::checkFocus %W"
  bind XTreeview <ButtonPress-4> "+after idle ::xtreeview::updateWnds %W"
  bind XTreeview <ButtonPress-5> "+after idle ::xtreeview::updateWnds %W"
  bind XTreeview <MouseWheel> "+after idle ::xtreeview::updateWnds %W"
  bind XTreeview <B1-Motion> {+if {$ttk::treeview::State(pressMode)=="resize"} { ::xtreeview::updateWnds %W }}
  bind XTreeview <Configure> "+after idle ::xtreeview::updateWnds %W"
  bind XTreeview <Home> {%W focus [lindex [%W children {}] 0]}
  bind XTreeview <End> {%W focus [lindex [%W children {}] end]}
  
  # images indicating sort order 
  image create bitmap ::xtreeview::arrow(0) -data {
     #define arrowUp_width 7
     #define arrowUp_height 4
     static char arrowUp_bits[] = {
        0x08, 0x1c, 0x3e, 0x7f
     };
   }
   image create bitmap ::xtreeview::arrow(1) -data {
     #define arrowDown_width 7
     #define arrowDown_height 4
     static char arrowDown_bits[] = {
        0x7f, 0x3e, 0x1c, 0x08
     };
   }
   image create bitmap ::xtreeview::arrowBlank -data {
     #define arrowBlank_width 7
     #define arrowBlank_height 4
     static char arrowBlank_bits[] = {
        0x00, 0x00, 0x00, 0x00
     };
   }

  
  variable curfocus
  
  # check, if focus has changed
  proc checkFocus {w {X {}} {Y {}} } {
    variable curfocus
    if {![info exists curfocus($w)]} {
      set changed 1
    } elseif {$curfocus($w)!=[$w focus]} {
      _clear $w $curfocus($w)
      set changed 1
    } else {
      set changed 0
    }
    set newfocus [$w focus]
    if {$changed} {
      if {$newfocus!=""} {
        _focus $w $newfocus
        if {$X!=""} {
          set col [$w identify column $X $Y]
          if {$col!=""} {
            if {$col!="#0"} {
              set col [$w column $col -id]
            }  
          }  
          catch {focus $w.$col}
        }  
      }        
      set curfocus($w) $newfocus
      updateWnds $w 
    }
  }
  # update inplace edit widgets positions
  proc updateWnds {w} {
    variable curfocus
    if {![info exists curfocus($w)]} { return }
    set item $curfocus($w)
    if {$item==""} { return }
    foreach col [concat [$w cget -columns] #0] {
      set wnd $w.$col
      if {[winfo exists $wnd]} {
        set bbox [$w bbox $item $col]
        if {$bbox==""} { 
          place forget $wnd
        } else {
          place $wnd -x [lindex $bbox 0] -y [lindex $bbox 1] -width [lindex $bbox 2] -height [lindex $bbox 3]
        }
      }
    }
  }
  # remove all inplace edit widgets
  proc _clear {w item} {
    foreach col [concat [$w cget -columns] #0] {
      set wnd $w.$col
      if {[winfo exists $wnd]} { 
        destroy $wnd
      }
    }
  }
  # called when focus item has changed
  proc _focus {w item} {
    set cols [$w cget -displaycolumns]
    if {$cols=="#all"} { 
      set cols [concat #0 [$w cget -columns]]
    }
    foreach col $cols {
      event generate $w <<TreeviewInplaceEdit>> -data [list $col $item]
      if {[winfo exists $w.$col]} {
        bind $w.$col <Key-Tab> {focus [tk_focusNext %W]}
        bind $w.$col <Shift-Key-Tab> {focus [tk_focusPrev %W]}
      }
    }
  }
  # hierarchical sorting procedure
  proc _sorttree {tree col direction {isroot 1} {root {}} } {
    if {$isroot} {
      if {$col!="#0"} {
        set col [$tree column $col -id]
      }
      set selection [$tree selection]
      $tree selection remove $selection
      set focus [$tree focus]
      $tree focus {}
      checkFocus $tree
    }
    # Build something we can sort
    set data {}
    if {$col=="#0"} {
      foreach row [$tree children $root] {
          lappend data [list [$tree item $row -text] $row]
      }
    } else {
      foreach row [$tree children $root] {
          lappend data [list [$tree set $row $col] $row]
      }
    }  
    if {$data!=""} {
      set dir [expr {$direction ? "-decreasing" : "-increasing"}]
      set r -1
      # Now reshuffle the rows into the sorted order
      foreach info [lsort -dictionary -index 0 $dir $data] {
          $tree move [lindex $info 1] $root [incr r]
          if {[$tree item [lindex $info 1] -open]} {
            _sorttree $tree $col $direction 0 [lindex $info 1]
          }  
      }
    }  
    if {$isroot} {
       # Switch the heading so that it will sort in the opposite direction
      variable curfocus
      catch {
        eval [lindex [after info $curfocus($tree,sorticon)] 0]
        after cancel $curfocus($tree,sorticon)
      }
      set curfocus($tree,sorticon) [after 3000 [list catch [list $tree heading $col -image ::xtreeview::arrowEmpty]]]
      $tree heading $col -command [namespace code [list _sorttree $tree $col [expr {1-$direction}]]] -image ::xtreeview::arrow($direction)
      $tree selection set $selection
      $tree focus $focus
      checkFocus $tree
    }  
  }
  # installs in-place edit bindings, adjusts tree header columns width, assigns column names, installs sorting handlers  
  proc _treeheaders {path {sort true} {treecolumnname {}} } {
    set tags [bindtags $path]
    if {[lsearch -exact $tags XTreeview]<0} {
      bindtags $path [linsert $tags [lsearch -exact $tags Treeview]+1 XTreeview]
    }
    set font [::ttk::style lookup [$path cget -style] -font]
    if {$font==""} {
      set font TkTextFont
    }
    foreach col [$path cget -columns] {
      if {$col!=""} {
        if {$sort} {
          $path heading $col -text $col -command [namespace code [list _sorttree $path $col 0]] 
        } else {
          $path heading $col -text $col 
        }
        $path column $col -width [font measure $font @@@@$col]
      }
    }
    if {$treecolumnname!=""} {
      $path heading #0 -text $treecolumnname
      $path column  #0 -width [font measure $font @@@@$treecolumnname]
      if {$sort} {
        $path heading #0 -command [namespace code [list _sorttree $path #0 0]] 
      }
    }
  }
  # helper functions for inplace edit
  proc _get_value {w column item} {
    if {$column=="#0"} {
      return [$w item $item -text]
    } else {
      return [$w set $item $column]
    }
  }
  proc _set_value {w column item value} {
    if {$column=="#0"} {
      $w item $item -text $value
    } else {
      $w set $item $column $value
    }
  }
  proc _update_value {w column item} {
    variable curfocus
    set value [_get_value $w $column $item]
    set newvalue $curfocus($w,$column)
    if {$value!=$newvalue} {
      _set_value $w $column $item $newvalue
    }
  }
  # these functions create widgets for in-place edit, use them in your in-place edit handler
  proc _inplaceEntry {w column item} {
    variable curfocus
    set wnd $w.$column 
    ttk::entry $wnd -textvariable [namespace current]::curfocus($w,$column) -width 3
    set curfocus($w,$column) [_get_value $w $column $item]
    bind $wnd <Destroy> [namespace code [list _update_value $w $column $item]]
  }
  proc _inplaceEntryButton {w column item script} {
    variable curfocus
    set wnd $w.$column
    ttk::frame $wnd
    pack [ttk::entry $wnd.e -width 3 -textvariable [namespace current]::curfocus($w,$column)] -side left -fill x -expand true
    pack [ttk::button $wnd.b -style Toolbutton -text "..." -command [string map [list %v [namespace current]::curfocus($w,$column)] $script]] -side left -fill x 
    set curfocus($w,$column) [_get_value $w $column $item]
    bind $wnd <Destroy> [namespace code [list _update_value $w $column $item]]
  }
  proc _inplaceCheckbutton {w column item {onvalue 1} {offvalue 0} } {
    variable curfocus
    set wnd $w.$column 
    ttk::checkbutton $wnd -variable [namespace current]::curfocus($w,$column) -onvalue $onvalue -offvalue $offvalue
    set curfocus($w,$column) [_get_value $w $column $item]
    bind $wnd <Destroy> [namespace code [list _update_value $w $column $item]]
  }
  proc _inplaceList {w column item values} {
    variable curfocus
    set wnd $w.$column 
    ttk::combobox $wnd -textvariable [namespace current]::curfocus($w,$column) -values $values -state readonly 
    set curfocus($w,$column) [_get_value $w $column $item]
    bind $wnd <Destroy> [namespace code [list _update_value $w $column $item]]
  }
  proc _inplaceSpinbox {w column item min max step} {
    variable curfocus
    set wnd $w.$column 
    spinbox $wnd -textvariable [namespace current]::curfocus($w,$column) -from $min -to $max -increment $step
    set curfocus($w,$column) [_get_value $w $column $item]
    bind $wnd <Destroy> [namespace code [list _update_value $w $column $item]]
  }
}

if { $argv0 eq [info script] } {
  catch {console show}
  pack [ttk::treeview .tv -columns {bool int list} -show {tree headings} -selectmode extended -yscrollcommand {.sb set}] -fill both -expand true -side left
  pack [ttk::scrollbar .sb -orient v -command {after idle ::xtreeview::updateWnds .tv;.tv yview}] -fill y -side left
  xtreeview::_treeheaders .tv true text

  .tv insert {} end -text {Sample text} -values {true 15 {Letter B}}
  set i0 [.tv insert {} end -text {Sample text} -values {false 25 {Letter C}}]
  .tv insert {} end -text {Sample text} -values {true 35 {Letter D}}
  .tv insert $i0 end -text {Sample subitem} -values {true 45 {Letter A}}
  for {set i 0} {$i<50} {incr i} {
    .tv insert $i0 end -text "Subitem $i" -values [list true $i {Letter B}]
  }
 
  bind .tv <<TreeviewInplaceEdit>> {
    if {[%W children [lindex %d 1]]==""} {
      switch [lindex %d 0] {
        {#0} { xtreeview::_inplaceEntry %W {*}%d }
        {bool} { xtreeview::_inplaceCheckbutton %W {*}%d true false}
        {int} { xtreeview::_inplaceSpinbox %W {*}%d 0 100 1 }
        {list} { xtreeview::_inplaceList %W {*}%d {"Letter A" "Letter B" "Letter C" "Letter D"} }
      }
    } elseif {[lindex %d 0]=="list"} {
      xtreeview::_inplaceEntryButton %W {*}%d {
        set %%v "tree: %W, column,item=%d"
      }
    }
  }
}