tablelist as tree with dict

Use tablelist as tree together with a dict.

tbltreedict

  • Recursively converts a tablelist tree into a dictionary.
    • It iterates over each child node, checks for further children, and builds the dictionary accordingly.
  • Recursively populates a tablelist tree from a given dictionary.
    • It inserts each dictionary key-value pair into the tree, handling nested dictionaries recursively.

Additionally, there is an issue when a key or value consists of multiple words.

#! /usr/bin/env tclsh

#tbltreedict.tcl
# 20240825

# tablelist as tree
# tbl::dict2tbltree $tbl root $data
# set data [tbl::tbltree2dict $tbl root]

# needs dict isdict from dicttool

namespace eval tbl {
   # Function to check whether the first elements of the lists are equal
   # use in proc dict2tbltree
   proc checkFirstElementsEqual {listOfLists} {
      if {[llength $listOfLists] < "2"} {
         return 0
      }
      set firstElement ""
      foreach sublist $listOfLists {
         if {[string is list $sublist]} {
            lassign $sublist first _
         } else {
            set first $sublist
         }
         if {$firstElement eq ""} {
            set firstElement $first
         } elseif {$firstElement ne $first} {
            return 0
         }
      }
      return 1
   }

   # Function to recursively convert a tree into a dictionary
   # dict with same keys
   proc tbltree2dict {tbl node} {
      set result {}
      # Get the children of the current node
      set children [$tbl childkeys $node]
      foreach child $children {
         # Get the text (key and value) of the current child
         set item [$tbl rowcget $child -text]
         set key [lindex $item 0]
         set value [lindex $item 1]
         # Check if the child itself has children
         if {[$tbl childcount $child] > 0} {
            set childDict [tbltree2dict $tbl $child]
            dict set result $key $childDict
         } else {
            if {[dict is_dict $result]} {
               # a dict with same keys
               if {[dict exists $result $key]} {
                  set dicttmp [list [list $key [dict get $result $key]]]
                  lappend dicttmp  [list $key $value]
                  set result $dicttmp
               } elseif {[checkFirstElementsEqual  $result]} {
                  lappend result [list $key $value]
               } else {
                  dict set result $key $value
               }
            }
         }
      }
      return $result
   }


 
   proc dict2tbltree {widget parent dict} {
      if {[dict is_dict $dict]} {
         set keys [dict keys $dict]
         foreach key $keys  {
            set child [$widget insertchild $parent end $key]
            set childdict [dict get $dict $key]
            if {[llength $childdict] eq "1"} {
               dict2tbltree $widget $child $childdict
            } elseif {[checkFirstElementsEqual $childdict]}  {
               foreach {k v } [concat {*}$childdict] {
                  $widget insertchild $child end [list $k $v]
               }
            } elseif {[llength $childdict] eq "2" && ![dict is_dict [lindex $childdict 1]]} {
               $widget cellconfigure $child,value -text $childdict
            } else {
               dict2tbltree $widget $child $childdict
            }
         }
      } else {
         $widget cellconfigure $parent,value -text $dict
      }
   }
}


Example

With this example, I experimented with the Tree mode of the Tablelist widget to display hierarchical data stored in dictionaries. The script showcases:

  • Data Insertion: Populating a Tablelist widget with hierarchical data from a dictionary.
  • Data Conversion: Extracting the displayed data back into a dictionary format.
  • Display in Multiple Widgets: The data is then displayed in another Tablelist widget, allowing comparison or further manipulation.
  • User Interaction: A Text widget is used to display detailed information when an item in the Tablelist is selected via double-click, Button-3 or the spacebar key.

The script also handles different datasets, each representing variations in the structure and content, such as the number of employees.

 Example
#! /usr/bin/env tclsh

#tbltreedict-example.tcl
#20240825

# delete and insert with popup
# https://www.nemethi.de/tablelist/tablelistWidget.html#local_drag_and_drop
#
package require tablelist_tile
package require ctext
package require dicttool

set dirname [file dirname [info script]]
source [file join $dirname tbltreedict.tcl]

# callback for tbl, Double 1 or space
proc cbtree {input t W x y args} {
   set tbl [tablelist::getTablelistPath $W]
   set treecolumn [$tbl cget -treecolumn]
   switch $input {
      m {
         foreach {tbl x y} [tablelist::convEventFields $W $x $y] {}
         set row [$tbl containing  $y]
         set cell [$tbl cellcget $row,$treecolumn -text]
         set data [tbl::tbltree2dict $tbl $row]
         $t insert end "\n#############################################n"
         $t insert end "\ncbtree $input :\n"
         $t insert end "$W $x $y :: $tbl $row \n"
         $t insert end "value: [$tbl cellcget $row,value -text] \n"
         $t insert end "dict data row $row $cell\n"
         $t insert end "[dict print $data]\n"
         $t insert end "[infoRow $tbl $row $t]\n"
         $t see end
      }
      k {
         set k $x
         set K $y
         if { $K eq "space" } {
            set row [$tbl curselection]
            set cell [$tbl cellcget $row,$treecolumn -text]
            set data [tbl::tbltree2dict $tbl $row]
            $t insert end "\n#############################################n"
            $t insert end "\ncbtree $input:\n"
            $t insert end "$W $x $y :: $tbl $row\n"
            $t insert end "value: [$tbl cellcget $row,value -text] \n"
            $t insert end "dict data row $row $cell\n"
            $t insert end "[dict print $data]\n"
            $t insert end "[infoRow $tbl $row $t]\n"
            $t see end
         }
      }
   }
}

# manages extra infos for text window
proc infoRow {tbl row t} {
   lappend  parentsRoot root [$tbl childkeys root]
   set parentkey [$tbl parentkey $row]
   set childcount [$tbl childcount $row]
   set childindex [$tbl childindex $row]
   set descendantcount [$tbl  descendantcount $row]
   set childkeys  [$tbl childkeys $row]
   set depth [$tbl depth $row]
   set childcountpk [$tbl childcount $parentkey]
   if {$parentkey eq "root"} {
      set childindexpk [$tbl childindex k0]
   } else {
      set childindexpk [$tbl childindex $parentkey]
   }
   set childkeyspk  [$tbl childkeys $parentkey]
   set depthpk [$tbl depth $parentkey]
   set noderow [$tbl noderow $parentkey $childindex]
   set childKindex [lindex $childkeys $childindex]
   set toplevelkey [$tbl toplevelkey $row]
   $t insert end "\ninfoRrow $tbl row: $row:"
   $t insert end "\npR: $parentsRoot :: pk: $parentkey :: cc: $childcount :: ci: $childindex :: da: $descendantcount \
    :: cks: $childkeys :: d: $depth" 
   $t insert end "\nccpk: $childcountpk :: cipk: $childindexpk :: ckpk: $childkeyspk :: \
    dpk: $depthpk :: noderow: $noderow :: cki: $childKindex :: tlk: $toplevelkey"
   $t see end
}

# https://www.nemethi.de/tablelist/tablelistWidget.html#local_drag_and_drop
proc acceptChildCmd {tbl targetParentNodeIdx sourceRow} {
   # Debugging output
   #puts "acceptChildCmd called with: $tbl, targetParentNodeIdx: $targetParentNodeIdx, sourceRow: $sourceRow"
   return 1  ;# For simplicity, allow all moves
}

proc acceptDropCmd {tbl targetRow sourceRow} {
   # Check if the operation stays within the same parent node
   # return [expr {$sourceRow != $rowCount - 1 && $targetRow < $rowCount}]
   return 1
}

# Create the Tablelist widget with tree configuration and local drag_and_drop
proc createTree {w t args} {
   set frt [ttk::frame $w.frt]
   set tbl [tablelist::tablelist $frt.tbl -columns {0 "Key" 40 "Value"} -height 20 -width 0 \
    -stretch all -treecolumn 0 -treestyle classic -stripebackground #f0f0f0 \
    -movablerows true -acceptchildcommand "acceptChildCmd" -acceptdropcommand "acceptDropCmd" -selectmode single]
   $tbl columnconfigure 0 -name key
   $tbl columnconfigure 1 -name value
   set vsb [scrollbar $frt.vsb -orient vertical -command [list $tbl yview]]
   set hsb [scrollbar $frt.hsb -orient horizontal -command [list $tbl xview]]
   $tbl configure -yscroll [list $vsb set] -xscroll [list $hsb set]

   bind [$tbl bodytag] <Double-1> [list cbtree m $t %W %x %y ]
   bind [$tbl bodytag] <KeyRelease> [list cbtree k $t %W %k %K ]

   bind [$tbl bodytag] <<Button3>> +[list cbtk_popup %W  %x %y %X %Y $t]
   bind [$tbl bodytag] <Button-1> +[list cbtk_popupExists  %W  %x %y %X %Y $t]

   pack $vsb -side right -fill y
   pack $hsb -side bottom -fill x
   pack $tbl -expand yes -fill both

   pack $frt -expand yes -fill both
   return $tbl
}


# button1 selection for popup only if popup already exists
proc cbtk_popupExists {W x y X Y t} {
   if {[winfo exists .cbtk_popup]} {
      cbtk_popup  $W  $x $y $X $Y $t
   }
}

# popup for infos
proc cbtk_popup {W x y X Y t} {
   if {[winfo exists .cbtk_popup]} {
      set geometry [wm geometry .cbtk_popup]
      destroy .cbtk_popup
   }
   set tbl [tablelist::getTablelistPath $W]
   foreach {tbl x y} [tablelist::convEventFields $W $x $y] {}
   set row [$tbl containing  $y]
   # if outside the table
   if {$row == "-1" } {
      set row last
   }
   set key [$tbl cellcget $row,key -text]
   set value [$tbl cellcget $row,value -text]
   set top [toplevel .cbtk_popup ]

   if {[info exists geometry]}  {
      wm geometry $top $geometry
   } else {
      wm geometry $top +$X+[expr {$Y+50}]
   }
   wm transient $top $tbl

   $tbl selection clear 0 end
   $tbl selection anchor $row
   $tbl selection set $row
   $tbl activate $row

   set krow [$tbl getfullkey $row]
   set pk [$tbl parentkey $row]
   set cix [$tbl childindex $row]
   set cc [$tbl childcount $row]
   set dc [$tbl  descendantcount $row]
   set nr [$tbl noderow $pk $cix]

   ttk::label $top.labinfo1 -text "row: $row krow: $krow nr: $nr" -background white
   ttk::label $top.labinfo2 -text "pk: $pk cix: $cix cc: $cc dc: $dc" -background white
   ttk::button $top.btninfo -text "Info row $row"  -command [list infoRow $tbl $row $t]
   ttk::button $top.btndump -text "dumptostring " -command [list cbtkpm $tbl $row dumptostring $top.entkey $top.entvalue $t]
   ttk::button $top.btntree2dict -text tbltree2dict -command [list cbtkpm $tbl $row tbltree2dict $top.entkey $top.entvalue $t]
   ttk::button $top.btndel -text "Delete row $row" -command [list $tbl delete $row]
   ttk::button $top.btnupt -text "Update row $row" -command [list cbtkpm $tbl $row update $top.entkey $top.entvalue $t]
   ttk::button $top.btnins -text "Insert after row $row" -command [list cbtkpm $tbl $row insert $top.entkey $top.entvalue $t]

   ttk::entry $top.entkey
   ttk::entry $top.entvalue

   $top.entkey insert 0 $key
   $top.entvalue insert 0 $value

   pack {*}[winfo children $top] -fill x -pady 2 -padx 2
}

# callbacks for popup
proc cbtkpm {tbl row cmd entkey entval t} {
   set key [$entkey get]
   set value [$entval get]

   switch $cmd {
      update {
         $tbl cellconfigure $row,key -text $key
         $tbl cellconfigure $row,value -text $value
      }
      insert {
         set parentkey [$tbl parentkey $row]
         set childindex [$tbl childindex $row]
         $tbl insertchild $parentkey [incr childindex] [list $key $value]
      }
      dumptostring {
         set data [$tbl dumptostring]
         $t insert end "\n$data"
      }
      tbltree2dict {
         set data [tbl::tbltree2dict $tbl root]
         $t insert end "\n$data"
      }
   }
   $t see end
}

# text window for information
proc createText {w} {
   set frt [ttk::frame $w.frt]
   set t [ctext $frt.t -setgrid true -wrap word -width 120 \
    -yscrollcommand "$frt.vsb set" -xscrollcommand "$frt.hsb set"]
   set vsb [scrollbar $frt.vsb -orient vertical -command "$t yview"]
   set hsb [scrollbar $frt.hsb -orient horizontal -command "$t xview"]
   pack $hsb -side bottom -fill x
   pack $vsb -side right -fill y
   pack $t -side left -fill both -expand true
   pack $frt -expand yes -fill both
   return $t
}

# cb for selection example data and info window for tablelist options and commands
proc createButton {w tbl1 tbl2 data t} {
   set dataList [dict keys $data]
   set frt [ttk::frame $w.frt]
   # combobox
   set cbselection [ttk::combobox $frt.cbselection -values $dataList -exportselection 0 -width 15]
   $cbselection current 2

   bind $cbselection <<ComboboxSelected>> [namespace code [list cbComboSelected %W $tbl1 $tbl2 $data $t]]
   cbComboSelected $cbselection $tbl1 $tbl2 $data $t

   set infotext [string map {:: \n} {pR: root [$tbl childkeys root] :: pk: [$tbl parentkey $row] :: cc: [$tbl childcount $row] :: ci: [$tbl childindex $row] :: da: [$tbl  descendantcount $row]  :: cks: [$tbl childkeys $row] :: d: [$tbl depth $row] :: ccpk: [$tbl childcount $parentkey] :: cipk: $childindexpk :: ckpk: [$tbl childkeys $parentkey] :: dpk: [$tbl depth $parentkey] :: noderow:[$tbl noderow $parentkey $childindex] :: cki: [lindex $childkeys $childindex] :: tlk: [$tbl toplevelkey $row]}]

   set infobtn [ttk::button $frt.infobtn -text "infoRow" -command [list tk_messageBox -detail $infotext]]

   pack $cbselection $infobtn -side left
   pack $frt -side top -expand 0 -fill x

   return $cbselection
}

# Insert the data into the Tablelist widget, starting at the root node
proc dataTotbl {tbl data t} {
   tbl::dict2tbltree $tbl root $data
   # output in text widget
   $t insert end "data $tbl:\n"
   $t insert end $data
   $t insert end "\n\n"
   $t insert end "data $tbl:"
   $t insert end [dict print $data]
   $t insert end "\n\n"
}

proc cbComboSelected {w tbl1 tbl2 data t} {
   set data1 [dict get $data [$w get]]
   if {[$w get] eq "all" }  {
      set data1 $data
   }
   $tbl1 delete 0 end
   $tbl2 delete 0 end
   $t delete 1.0 end
   dataTotbl $tbl1 $data1 $t
   set data2 [tbl::tbltree2dict $tbl1 root]
   dataTotbl $tbl2 $data2 $t
   $t see end
}

###############
# Example datas
###############

#Example datas in dict data, 2-4 differences in number of employees
dict set data all {}
dict set data Example1 {person {name "John Doe" age 30 address {street "123 Main St" city "Anytown"}} job {title "Developer" company "Works"}}
dict set data Example2 {person  {name "John Doe" age 30 address {street "123 Main St" city "Anytown"}  employees {{name "Alice Smith"} {name "Bob Smith"} {name "John Good"} {name "Jane Good"}}} job {title "Developer" company "Works"}}
dict set data Example3 {a1 {b11 {a11 {b1111 c1 b1112 c1}} b12 {a12 {b1211 c1 b1212 c1}}} a2 {b21 {a21 {b2111 c1 b2112 c1}} b22 {a22 {b2211 c1 b2212 c1}}}}

set employeeInfo {
   12345-A {forenames "Joe" surname "Schmoe" street "147 Short Street" city "Springfield" phone "555-1234"}
   98372-J {forenames "Anne" surname "Other" street "32995 Oakdale Way" city "Springfield" phone "555-8765"}
}
dict set data employeeInfo $employeeInfo

#####
#main
#####

# create two Tablelist and a text widget
ttk::frame .fr1
ttk::frame .fr2
ttk::frame .frt
ttk::frame .frb

pack .frt -side right -expand 1 -fill both
pack .frb .fr1  .fr2 -side top -expand 1 -fill both

set t    [createText .frt]
set tbl1 [createTree .fr1 $t]
set tbl2 [createTree .fr2 $t]
set btn  [createButton .frb $tbl1 $tbl2 $data $t]

Links

 Links