Fabricio Rocha - 15 Jul 2011 - After the idea of creating eDictor, it soon became evident that showing and editing nested dicts and lists visually would not be a so simple task. Then I thought that it would be a better thing to isolate this part of the editor in a self-contained part, and this could perfectly fit in the concept of a megawidget.
The valuepanel megawidget presented here takes advantage of the "everything is a string" characteristic of Tcl data handling, and allows one to create, inspect and modify data which can be represented as strings, lists and/or dicts. It is specially useful for large and nested data structures. Each nesting level is shown in a separate text or a treeview widget, and the user can create, remove and move list items and dict keys/values using interface buttons, without having to worry about quoting and bracing.
In a valuepanel megawidget, data is shown in one or more sequential nodes or panels (the difference between these names is important only internally). The nodes are created in a ttk::panedwindow widget, so they can have their width changed as needed. In the top of each node, there is a mode selector which, according to the data contained in the node, allows the user to choose the way the data is displayed and edited:
In Tcl, a dict is a list with an even number of items, and a list is always a string; but not all strings are valid lists, and a list with an odd number of items can not be a dict. These basic principles are observed as much as possible by each node of a valuepanel, and everytime the data is changed the mode selector is updated for showing only the valid modes for the underlying data.
In its initial version, the valuepanel megawidget has very few commands and options:
::ValuePanel::valuepanel pathname ?option value? - Creates a new valuepanel megawidget. Returns path on success. The only possible option at the moment is -data, which must be followed by the value which will be loaded to the widget (as in -data $datavar).
pathname getdata - Returns the data currently stored in the megawidget, just as it may be viewed in the widget's first node.
pathname setdata data - Clears the previously existant data in the widget, closes any children nodes, and loads the new data to the widget's first node.
VIRTUAL EVENT - Whenever the data in a valuepanel widget is changed, the widget generates a <<Modified>> virtual event, which can be used for triggering a callback procedure for reading the widget's data.
Feel free to add questions!
Oops, I mistyped a dict key! How to correct it? You can change the panel's view to "string" and correct it right there, or you can change to "list" view, double-click the mistyped key and edit it in "string" mode in the panel which opens.
What about supporting arrays? Arrays are not a "data format" in Tcl, unlike lists and dicts. But the valuepanel megawidget can work with the contents of an array element.
If you find other applications for the valuepanel megawidget, please add them here.
Data files edition - This was the original purpose of this megawidget, as it was created for being specially used by eDictor.
Variables inspection in debuggers - In a debugger, one could use valuepanels for checking the contents of variables in breakpoints and alikes.
Debugging with tkcon - Source the valuepanel code into tkcon, then an application you are testing, and you have a way to watch how your program changes (or respond to changes in) a certain data structure.
Application configuration - If your application stores preferences and configuration data in a list or dict, this megawidget can be used as an interface for easy changes, more or less similar to the Windows registry editor or GConf editor. Of course, as anything can be changed, removed or renamed, this interface can be recommended only if the application user surely knows what he will be messing around...
Simple databases - If you want to create databases which have only textual contents, the megawidget can help in editing and organizing them.
Here are some ideas for making the megawidget fancier or more useful. If you have a suggestion, please add it here. I can't really promise I'll implement it, nor when I could do it, but "public interest" may be a motivation, and the ideas can also inspire someone else to implement the requests.
Refactoring - I must admit that the whole concept of this beast turned out to be much more complicated than I thought, and I am pretty sure that some things could be done simpler and more clear. It works as it is (at least as far as I tested it), but some third-party look and thinking is always helpful.
Buttons-based mode selector - I was really tempted to do this before the release, in spite of some added complexity, but I still haven't had good ideas for representing a list, a dict and a string in 16x16 iconic ways (suggestions are welcome!). It would be neat to see the buttons become disabled/enabled as the contents are changed. Also, it would allow the user to change the view mode with one single click, which is always a good thing.
Visual differentiation between list items and dict keys - Maybe dict keys should be shown in bold, or in a different color. I would like to receive suggestions about that.
Make a Tcl module of it - It's all in one file, all in one separate namespace, so it won't be too hard a task... or will it?
Finally, here is the code. Each new version or revision will be in a separate "discussion" section, along with a brief explanation of changes. If you want to correct a bug or change something, be welcome; just create a copy of the section and put your new version of the code there, so it will be easier to follow the evolutions and regressions in the code.
The initial release. Save it to a file like valuepanel.tcl and source it.
# Fabricio Rocha, June 2011 # valuepanel.tcl # # A megawidget for showing and editing nested data structures based on Tcl's # dicts, lists and strings. package require Tk 8.5 package require msgcat namespace eval ::ValuePanel { variable WdgsData } # ############################################################################# # Widget-as-a-whole procedures - layout, configuration, control data # ############################################################################# # ValuePanel::valuepanel # Creates a new ValuePanel widget along with its internal data structures. # ARGUMENTS: "w" is the path of the widget. "args" are an optional option/value # sequence for configuration. # RESULTS: The path to the new widget is returned. A "nodes list" is created for # each widget instance. proc ::ValuePanel::valuepanel {w args} { variable WdgsData # Create the base frame where the widget will be built. It's "catched" # because the given path may be invalid. set wfail [catch {ttk::frame $w} rv ro] if { $wfail } { set errormsg "(ValuePanel) - $rv" return -code error $errormsg } # The widget consists of a frame containing a canvas scrolled horizontally. # In the canvas there is a panedwindow, containing one or more panels. set cvs [canvas $w.c -xscrollcommand [list ValuePanel::ScrollConfig $w.scr]] set scr [ttk::scrollbar $w.scr -orient horizontal -command [list $w.c xview]] grid $w.c -column 0 -row 0 -sticky news grid $w.scr -column 0 -row 1 -sticky ew grid columnconfigure $w 0 -weight 1 grid rowconfigure $w 0 -weight 1 grid rowconfigure $w 1 -weight 0 set pnw [ttk::panedwindow $w.c.pnw -orient horizontal] set pnwhnd [$w.c create window 0 0 -anchor nw -window $pnw ] # Define a binding which will allow us to clear the data structures related # to the panel when it is destroyed... bind $w <Destroy> [list ::ValuePanel::Wdg_Destroy %W] # ... and bindings for the <Configure> event, which will allow the widgets # inside the frame to be resized when the widget gets more available space # from its container bind $w <Configure> [list ::ValuePanel::Wdg_Resized %W] # bind $pnw <Configure> [list ::ValuePanel::Wdg_Resized %W] # Create a namespace for icons, if it still doesn't exist if { ![namespace exists Icons] } { ::ValuePanel::Wdg_CreateIcons } # Create the new widget's entry in the WdgsData array, with some default # coniguration options which may change during widget's configuration. set WdgsData($w) [dict create \ Canvas $cvs \ Scrollbar $scr \ Pnw $pnw \ Pnwhnd $pnwhnd \ Nodes {} \ OnChange {} \ ] # Now create the first panel inside the widget Node_Add $w # Now the "megawidget magic": create the widget command, after renaming the # original base frame so its path will not be overwritten by the new proc rename $w orig_$w proc ::$w {subcmd args} { # This line makes the procedure discover its own name (and the name of # the widget in which it will operate). set wdg [lindex [info level 0] 0] # Now define the procs to be called according to the passed subcommand switch -- $subcmd { setdata { ::ValuePanel::Wdg_SetData $wdg [lindex $args 0] } getdata { return [::ValuePanel::Wdg_GetData $wdg] } default { set errormsg "ValuePanel: Unknown subcommand $subcmd" return -code error $errormsg } } } # Widget instance and commands created. Proceed to configure, using the # passed options. Wdg_Config $w $args return $w } # Wdg_Config # Configures the widget as a whole. # ARGUMENTS: "w" is the path to the widget, "opts" is a list of options and # values. proc ::ValuePanel::Wdg_Config {w opts} { # Check the number of arguments. if { [expr {[llength $opts] % 2}] } { set errormsg "ValuePanel: wrong number of options/values" return -code error $errormsg } dict for {opt val} $opts { switch -- $opt { -data { Wdg_SetData $w $val } default { set errormsg "ValuePanel: unknown option $opt" return -code error $errormsg } }; # end of switch } return } # Wdg_SetData # Clears the current data of a ValuePanel widget and sets a new value for it. # ARGUMENTS: "w" is the widget which will receive a new value. "data", well, is # the new value. # RESULTS: The values which are currently stored in the widget, if any, are # replaced and only the first node of the widget is shown. proc ::ValuePanel::Wdg_SetData {w data} { variable WdgsData # If there are nodes in the widget, remove them. if { [llength [dict get $WdgsData($w) Nodes]] } { Node_Remove $w 0 } # Create a new node 0 Node_Add $w $data return } # Wdg_GetData # Retrieves the value currently stored in the widget (specifically, in the # widget's node 0). # ARGUMENTS: "w" is the widget whose value is required. # RESULTS: proc ::ValuePanel::Wdg_GetData {w} { variable WdgsData if { ![llength [dict get $WdgsData($w) Nodes]] } { set retdata {} } else { set retdata [Node_GetNodeInfo $w 0 Data] } return $retdata } # Wdg_Resized # This procedure, called as a callback for the <Configure> event when a # ValuePanel widget is resized, reevaluates the visible area of the canvas and # the internal panedwindow, so the horizontal scrollbars can be reconfigured. # It also resizes the panedwindow vertically to reflect height change in the # ValuePanel. proc ::ValuePanel::Wdg_Resized { w } { variable WdgsData set cvs [dict get $WdgsData($w) Canvas] set scr [dict get $WdgsData($w) Scrollbar] set pnd [dict get $WdgsData($w) Pnw] set phnd [dict get $WdgsData($w) Pnwhnd] # If we don't update idletasks, the main scrollbar will not appear until # the widget is resized externally, even if the panedwindow has grown update idletasks # Reconfigure the canvas scrollregion; it will be the size of the # internal panedwindow. set pndcoords [$cvs bbox $phnd] $cvs configure -scrollregion $pndcoords # Try to keep the right side of the panedwindow always visible set pndx1 [lindex $pndcoords 0] set pndx2 [lindex $pndcoords 2] set pndwidth [expr {$pndx2 - $pndx1}] set visiblewidth [winfo width $cvs] if { $visiblewidth < $pndwidth } { # The panedwindow is wider than the canvas window. set unseenpixels [expr {$pndwidth - $visiblewidth}] set fraction [expr {double($unseenpixels) / $pndwidth}] $cvs xview moveto $fraction } # Make the scrollbar know about the resizing... ScrollConfig $scr {*}[$cvs xview] # ScrollConfig $scr $newfirst 1 # Resize the panedwindow vertically, if the whole widget has gained height if { [winfo ismapped $cvs] } { set newheight [winfo height $cvs] } else { set newheight [winfo reqheight $cvs] } $cvs itemconfigure $phnd -height $newheight return } # Wdg_Destroy # Called as callback for the <Destroy> event, this procedure removes all the # data related to the widget from memory. # ARGUMENTS: "w" is the widget's path. # RESULTS: The widget's entry in WdgsData is removed. proc ::ValuePanel::Wdg_Destroy {w} { variable WdgsData # Destroy the internal widgets dict with WdgsData($w) { destroy $Canvas $Scrollbar $Pnw } # Destroy the once renamed baseframe for the megawidget destroy orig_$w {} # Delete the widget's entry in WdgsData array unset WdgsData $w # If there are no other widgets left, we can remove the images from memory. if { ![array size WdgsData] } { namespace delete ::ValuePanel::Icons } return } # Wdg_CreateIcons # Creates a sub-namespace and images on it, for being used by the widget # panels. # ARGUMENTS: # RESULTS: proc ::ValuePanel::Wdg_CreateIcons {} { namespace eval Icons {} image create photo ::ValuePanel::Icons::Add -format gif -data { R0lGODlhEAAQAOMMAACIADTDAEDHC0vLFVfOIGPSKm/WNXraP4beSpLhVJ3lX6npaf ///////////////yH+EUNyZWF0ZWQgd2l0aCBHSU1QACH5BAEKAA8ALAAAAAAQABAAAARJ 8EkJqp14gsUXyNnWfaDWeaWmrAqpWVUiJ7CF3HieV0fv/7+KYUgsFiuFpHK5rFUIUIIzNK gOXCWAYCvAggCBcMAbEo9TlFomAgA7 } image create photo ::ValuePanel::Icons::Remove -format gif -data { R0lGODlhEAAQAMIFAGYAALMAAMwAAOYAAP8AAP///////////yH+EUNyZWF0ZWQgd2l0aCB HSU1QACH5BAEKAAcALAAAAAAQABAAAAMkeLrc/jDKSRW4OOtLuv/fNYxkWV5Cqq7rFbxwHG 90Vt14rjcJADs= } image create photo ::ValuePanel::Icons::Up -format gif -data { R0lGODlhEAAQAKUlAC40Njk/QT5CRTxDQz9CRj1DRT5DRT9DRj5ERj9FR0BGSEJISYSI iLq9tsjExsnGxsjLxcvKyc3My8zOydbY09bY1O7t7vPs8u/v7vDx8fHx8fLy8/r4+vn5 +Pn5+fr6+vv7+vv7+/39/P39/f7+/v////////////////////////////////////// //////////////////////////////////////////////////////////////////// /yH+EUNyZWF0ZWQgd2l0aCBHSU1QACH5BAEKAD8ALAAAAAAQABAAAAZYwJ9wSCwaj8gh IFksYAxMIcIikgiYCQ3HM4lcj4pM6RJqTB6EImOxKY1HDUjFcWAoAYDAmNSgDPBLRQBj Ig0VgUeDFyCGiEaKHY1JgyUfFYeTgIBRnJ0/QQA7 } image create photo ::ValuePanel::Icons::Down -format gif -data { R0lGODlhEAAQAKUjAC40Njk/QT5ERz5FRj9FSD9GSEFHSUFISYaIioiKjKCipKOkprq9 tsnFxsnHx8vKycnMxs3My8zOydbX09bY1Ozr7fPs8u7u7vDw8PHy8vv4+vn5+Pn5+fr6 +vv7+vv7+/39/P39/f7+/v////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// /yH+EUNyZWF0ZWQgd2l0aCBHSU1QACH5BAEKAD8ALAAAAAAQABAAAAZdwJ9wSCwaj0iA cglA/gCjUYdCaSZHlg2j6oRaPFvr0QsKG5cBrIgxGSyHCUMmagkxIJSGAEFcGDBYHwwS DgQKRn4YGhwSDwWHR34VIBEGkEh+FweXTgsAnE6hokVBADs= } return } # ScrollConfig # Evaluates the need of a scrollbar for viewing the whole contents of a # widget, gridding or ungridding it as needed, and reconfiguring its view # ARGUMENTS: "scr" is the scrollbar widget; "first" and "last" are # the arguments automatically passed by -xscrollcommand and -yscrollcommand. proc ::ValuePanel::ScrollConfig {scr first last} { if { $first == 0 && $last == 1} { grid remove $scr } else { grid $scr } # Now the regular scrollbar's "set" command is called $scr set $first $last return } # ############################################################################ # Nodes-related procedures # ############################################################################ # Node_EvalDataType # Evaluates the data and returns a string to tell if the data is (or can be # seen, or treated) as a "dict", a "list" or a "string" only. Because Tcl is so # weakly typed, this is a far from precise and general evaluation, but is # sufficient for our needs. # ARGUMENTS: "datavar" is the name of the variable containing the data to be # evaluated. # RESULTS: Returns "dict", "list" or "string" according to the evaluation. proc ::ValuePanel::Node_EvalDataType { datavar } { upvar 1 $datavar data set invalidlist [catch {set listitems [llength $data]} rv ro] if { $invalidlist } { set datatype "string" } else { if { [expr {$listitems % 2}] } { # Odd number of items. Must be only a list. set datatype "list" } else { set datatype "dict" } } return $datatype } # Node_Add # Creates a node in the widget. The node's data is a dict, appended to the # Nodes list of the widget. Also, this procedure creates a new panel which is # appended to the widget's internal panedwindow. # ARGUMENTS: "wdg" is the widget which will gain a new node and panel. "data" is # optional: it is the value to be shown in the node, and it is an empty string # by default. # RESULTS: The widget's "Node" list gets the new node's data appended to it, and # the new respective panel is added to the widget's panedwindow. proc ::ValuePanel::Node_Add {wdg {data {}} } { variable WdgsData # Get the current nodes list size. It will be used as an index. set nodescount [llength [dict get $WdgsData($wdg) Nodes]] # Create the node's panel (the procedure already puts the panel in the # widget's panedwindow!) set nodeinfo [Panel_Create $wdg $nodescount] # Evaluate passed data for defining a mode set showmode [Node_EvalDataType data] # Add node information to the returned dict, and store it in the widget's # Nodes list. dict set nodeinfo Mode $showmode dict set nodeinfo OpenItem {} dict set nodeinfo Data $data dict lappend WdgsData($wdg) Nodes $nodeinfo # Call the procedure which updates the available viewing modes, and # configure the modes selector Panel_UpdateAvailableModes $wdg $nodescount set cbb [dict get $nodeinfo Combobox] $cbb set $showmode # Get the data shown in a proper panel Panel_ChangeMode $wdg $nodescount $showmode Panel_UpdateFromData $wdg $nodescount Panel_EditButtonsConfigure $wdg $nodescount # We need to force the widget's scrollbar reconfiguration here. There is # no way to use a binding on the panedwindow for this... Wdg_Resized $wdg return } # Node_Remove # Removes a certain node, along with any "children" nodes, from a widget's # Nodes list. Panels are destroyed. # ARGUMENTS: "wdg" is the widget to be altered, "nodeidx" is the node to be # destroyed # RESULTS: The node and its children are removed, panels are destroyed. proc ::ValuePanel::Node_Remove {wdg nodeidx} { variable WdgsData # Retrieve all the Nodes list; it will be replaced soon. set nodeslist [dict get $WdgsData($wdg) Nodes] # Get the current list's size set totalnodes [llength $nodeslist] # Remove items from the new Nodes list set deletednode $nodeidx set lastnodeidx [expr {$totalnodes -1}] while { $deletednode <= $lastnodeidx } { set nodepanel [dict get [lindex $nodeslist $deletednode] Panel] destroy $nodepanel incr deletednode } set lastremainingnode [expr {$nodeidx -1}] set nodeslist [lrange $nodeslist 0 $lastremainingnode] Wdg_Resized $wdg # Replace the old Nodes list with the new one dict set WdgsData($wdg) Nodes $nodeslist return } # Node_GetNodeInfo # Convenience proc for retrieving all or specific configuration data of a # specific node. # ARGUMENTS: "wdg" is the widget containing the node we are querying about; # "nodeidx" is the node's index within the widget; the optional "info" is one # of the configuration options of a node's description (see Node_Add) # RESULTS: If the "info" argument is ommited, all the information of the node is # returned as a dict. If "info" is passed and valid, only the value for this # particular configuration option is returned. If "info" is invalid, an error # is triggered. proc ::ValuePanel::Node_GetNodeInfo {wdg nodeidx {info {}} } { variable WdgsData set nodeinfo [lindex [dict get $WdgsData($wdg) Nodes] $nodeidx] if { $info eq {} } { return $nodeinfo } if { ![dict exists $nodeinfo $info] } { set errormsg "Invalid node option: $info" return -code error $errormsg } return [dict get $nodeinfo $info] } # Node_SetNodeInfo # Convenience proc for easier update of a node's configuration option. The # changes are reflected to the node's panel as needed. This proc is NOT to be # called for setting a node's Data directly: instead, use Node_UpdateAllData. # ARGUMENTS: "wdg" is the widget the node belongs to. "nodeidx" is the node's # index in the widget's Nodes list. "opt" is the key to be updated (see # Node_Add). "newval" is the new value for option "opt". # RESULTS: proc ::ValuePanel::Node_SetNodeInfo {wdg nodeidx opt newval} { variable WdgsData dict with WdgsData($wdg) { set nodeinfo [lindex $Nodes $nodeidx] if { ![dict exists $nodeinfo $opt] } { set errormsg "Invalid node option: $info" return -code error $errormsg } dict set nodeinfo $opt $newval # Store the changed node information set Nodes [lreplace $Nodes $nodeidx $nodeidx $nodeinfo] # Now check for special cases. # A change in Data might also affect the panel. if { $opt eq "Data"} { # Check the possible view/edit modes for the new data set posmodes [Panel_UpdateAvailableModes $wdg $nodeidx] } }; # end of "dict with" return } # Node_UpdateAllData # Called whenever the last node of a widget has its value changed somehow. It # uses the data of the last node as new value for the previous node's OpenItem, # and so on up to the first node of a widget. # ARGUMENTS: "wdg" is the widget in which we're gonna to operate. # RESULTS: All the nodes/panels of the widget have their data and panels # updated. proc ::ValuePanel::Node_UpdateAllData {wdg} { variable WdgsData # Discover which is the last node set lastnodeidx [expr {[llength [dict get $WdgsData($wdg) Nodes]] - 1} ] # Reversal loop through the Nodes list set curnodeidx $lastnodeidx while { $curnodeidx >= 0 } { set nodeinfo [Node_GetNodeInfo $wdg $curnodeidx] set openitem [dict get $nodeinfo OpenItem] set curnodedata [dict get $nodeinfo Data] set mode [dict get $nodeinfo Mode] if { $openitem ne {} } { # Node has a child node. Its mode will tell us how to store the # child data. if { $mode eq "list" } { # In this case, openitem is a list index. lset curnodedata $openitem $nextnodedata } if { $mode eq "dict" } { # In this case, openitem is a dict key. dict set curnodedata $openitem $nextnodedata } # Store the node's new data Node_SetNodeInfo $wdg $curnodeidx Data $curnodedata # Now the visual stuff, which requires that the node's new data is # already stored. # If the node's mode is "list", it shows items which might have been # updated. We need to update the view as well. if { $mode eq "list" } { Panel_UpdateFromData $wdg $curnodeidx } } set nextnodedata $curnodedata incr curnodeidx -1 } # Generate a virtual event event generate $wdg <<Modified>> return } # ############################################################################# # Panels-related procedures # ############################################################################# # Panel_Create # Creates a panel for a new node. # ARGUMENTS: "wdg" is the widget which will contain a new node and panel. # "idx" is the node's number, which will be used for naming the base # frame of the new panel and will be passed to the widgets bindings. # RESULTS: Creates the Tk widgets of a panel and returns the paths of the # important ones in a dict. proc ::ValuePanel::Panel_Create {wdg idx} { variable WdgsData # Create the base frame, as child of the widget's panedwindow set wpnd [dict get $WdgsData($wdg) Pnw] set base [ttk::frame $wpnd.panel$idx -relief sunken -border 1] # The mode combobox and its label set cbbwdg [ttk::combobox $base.cbb -state readonly -justify right] ttk::label $base.lbl -text [::msgcat::mc "Mode"] -anchor w # Setup the combobox binding bind $cbbwdg <<ComboboxSelected>> \ [list ::ValuePanel::Panel_ModeSelected $wdg $idx %W] # The "frmView subframe": text, treeview and scrollbar (scrollbar's command # must be reconfigured whenever the treeview is replaced by the text # widget and vice-versa). # 12 Jun 2011 - These widgets are create as children of its layout subframe, # because the "grid remove" command used for the auto-scrollbar effects # forgets the "-in" option used for grid later in the layout stage (known # bug in Tk 8.5.9). ttk::frame $base.frmView set txtwdg [text $base.frmView.txt -width 30 \ -yscrollcommand [list ::ValuePanel::ScrollConfig $base.frmView.scrY]] set tvwwdg [ttk::treeview $base.frmView.tvw -selectmode browse -show {tree}\ -yscrollcommand [list ::ValuePanel::ScrollConfig $base.frmView.scrY]] set scrwdg [ttk::scrollbar $base.frmView.scrY -command [list $txtwdg yview]] # Setup bindings for the treeview and text widgets bind $tvwwdg <Button-1> +[list \ ::ValuePanel::Panel_TreeviewClicked $wdg $idx %W %x %y] bind $tvwwdg <Double-Button-1> +[list \ ::ValuePanel::Panel_TreeviewDoubleClicked $wdg $idx %W %x %y] bind $txtwdg <<Modified>> +[list \ ::ValuePanel::Panel_TextEdited $wdg $idx %W] # The "edit buttons frame" that go along the treeview set btnfrm [ttk::frame $base.frmView.frmButtons] ttk::button $btnfrm.btnAdd -image {::ValuePanel::Icons::Add} \ -command [list ::ValuePanel::Panel_TreeviewItemAdd $wdg $idx] ttk::button $btnfrm.btnRemove -image {::ValuePanel::Icons::Remove} \ -command [list ::ValuePanel::Panel_TreeviewItemRemove $wdg $idx] ttk::button $btnfrm.btnUp -image {::ValuePanel::Icons::Up} \ -command [list ::ValuePanel::Panel_TreeviewItemMove $wdg $idx "up"] ttk::button $btnfrm.btnDown -image {::ValuePanel::Icons::Down} \ -command [list ::ValuePanel::Panel_TreeviewItemMove $wdg $idx "down"] grid $btnfrm.btnAdd -column 0 -row 0 -padx {1 1} grid $btnfrm.btnRemove -column 1 -row 0 -padx {1 1} grid $btnfrm.btnUp -column 2 -row 0 -padx {1 1} grid $btnfrm.btnDown -column 3 -row 0 -padx {1 1} grid rowconfigure $btnfrm 0 -weight 0 # Put everything in place, with the help of some intermediate frames ttk::frame $base.frmMode grid $base.lbl -in $base.frmMode -column 0 -row 0 -padx {2 5} -sticky news grid $cbbwdg -in $base.frmMode -column 1 -row 0 -sticky news lower $base.frmMode grid columnconfigure $base.frmMode 0 -weight 0 grid columnconfigure $base.frmMode 1 -weight 1 grid rowconfigure $base.frmMode 0 -weight 0 grid configure $tvwwdg -column 0 -row 0 -sticky news grid $txtwdg -column 0 -row 0 -sticky news grid $scrwdg -column 1 -row 0 -sticky ns grid $btnfrm -column 0 -row 1 -columnspan 2 -sticky ns grid columnconfigure $base.frmView 0 -weight 1 grid columnconfigure $base.frmView 1 -weight 0 grid rowconfigure $base.frmView 0 -weight 1 grid rowconfigure $base.frmView 1 -weight 0 grid $base.frmMode -column 0 -row 0 -sticky ew grid $base.frmView -column 0 -row 1 -sticky news grid columnconfigure $base 0 -weight 1 grid rowconfigure $base 0 -weight 0 grid rowconfigure $base 1 -weight 1 $base configure -width 200 # Finally, add the new panel to the widget's panedwindow... $wpnd add $base Wdg_Resized $wdg # Create the dict with widgets paths set nodeinfo [dict create \ Panel $base \ Combobox $cbbwdg \ Treeview $tvwwdg \ Text $txtwdg \ Scrollbar $scrwdg \ EditButtons $btnfrm ] return $nodeinfo } # Panel_UpdateAvailableModes # Updates the mode combobox items, according to the possible modes for # a node's data. If the currently selected mode becomes unavailable, a new mode # is automatically chosen (and the "programmatical" choice should reconfigure # the panel accordingly). # ARGUMENTS: "wdg" is the widget in which the panel is. "nodeidx" is the node's # index. # RESULTS: The combobox showing possible view/edition modes of the data is # updated. The new list of possible options is returned. proc ::ValuePanel::Panel_UpdateAvailableModes {wdg nodeidx} { set nodeinfo [Node_GetNodeInfo $wdg $nodeidx] dict with nodeinfo { # Evaluate the node's data, so we will know which showing modes are # possible. set maxmode [Node_EvalDataType Data] # Create a list of possible modes. A dict can be shown as a list; a # list can be shown as a string; but the other way is not always # possible. set possiblemodes {string} if { $maxmode ne "string" } { set possiblemodes [linsert $possiblemodes 0 "list"] if { $maxmode eq "dict" } { set possiblemodes [linsert $possiblemodes 0 "dict"] } } # Get the current view mode set curviewmode [$Combobox get] # Update the combobox items $Combobox configure -values $possiblemodes }; # end of "dict with" return $possiblemodes } # Panel_ModeSelected # Callback for the mode combobox in a node. Calls the procedures which # reconfigure the node's panel and sets the node's Mode. Because the combobox # should only have been showing valid modes for the node's data, no checking is # done on the node's data. # ARGUMENTS: "w" is the widget, "nodeidx" is the node's position in the widget's # "Nodes" list; "nodecbb" is the combobox which had its value changed. These # arguments are passed by the binding assigned to each panel's combobox when # it is created by Panel_Create. # RESULTS: Node_ChangeMode is called with appropriate arguments for really # proceeding to the requested change. proc ::ValuePanel::Panel_ModeSelected {w nodeidx nodecbb} { set newchoice [$nodecbb get] Panel_ChangeMode $w $nodeidx $newchoice Panel_UpdateFromData $w $nodeidx return } # Panel_ChangeMode # Reconfigures a node's panel to a given mode. The widgets put in place are # NOT filled with data. # ARGUMENTS: "wdg" is the widget; "nodeidx" is the index of the node's dict in # the widget "Nodes" list; "newmode" is the mode to be used for showing the # data. # RESULTS: proc ::ValuePanel::Panel_ChangeMode {wdg nodeidx newmode} { variable WdgsData set nodeinfo [Node_GetNodeInfo $wdg $nodeidx] dict with nodeinfo { # The "newmode" could only be passed if it was appropriate for the type # of the node's current "Data". We don't need to check the data again. # Replace the current panel widgets, if needed, and reload them with the # node's data. if { $newmode eq "string" } { grid remove $Treeview grid remove $EditButtons grid $Text $Scrollbar configure -command [list $Text yview] focus $Text } else { grid remove $Text grid $Treeview grid $EditButtons $Scrollbar configure -command [list $Treeview yview] }; # end of "else" $Combobox set $newmode }; # end of "dict with" update idletasks # Finally, update the node's Mode information. Node_SetNodeInfo $wdg $nodeidx Mode $newmode return } # Panel_UpdateFromData # Reads the node's Data and puts it in the widgets, according to the node's # Mode. # ARGUMENTS: "wdg" is the widget containing the node; "nodeidx" is the node # index # RESULTS: The widgets in the node's panel have their contents updated. proc ::ValuePanel::Panel_UpdateFromData {wdg nodeidx} { set nodeinfo [Node_GetNodeInfo $wdg $nodeidx] set nodedata [dict get $nodeinfo Data] set nodemode [dict get $nodeinfo Mode] if { $nodemode eq "string" } { set txt [dict get $nodeinfo Text] $txt replace 1.0 end $nodedata } else { set tvw [dict get $nodeinfo Treeview] # Keep the previous selection, if any set prevselect [$tvw selection] $tvw delete [$tvw children {}] if { $nodemode eq "list"} { set tvwitems $nodedata } else { set tvwitems [dict keys $nodedata] } # Create the treeview's items, each item having as ID its position # in the treeview. set itempos 0 foreach item $tvwitems { $tvw insert {} $itempos -id $itempos -text $item incr itempos } # Recreate the previous selection, if possible catch {$tvw selection set $prevselect} } update idletasks return } # Panel_TextEdited # Callback for any edition happened in the node panel's text widget in # "string" mode. # ARGUMENTS: "wdg" is the ValuePanel megawidget in which the text widget is. # "nodeidx" is the index of the node which contains the text widget. # "txt" is the text widget itself. # RESULTS: The contents of the text widget become the new value of this node. proc ::ValuePanel::Panel_TextEdited {wdg nodeidx txt} { set txtdata [$txt get 1.0 end-1chars] # Reset the widget's "modified" flag, so we can catch other changes later. $txt edit modified 0 # Store the new data in the node info Node_SetNodeInfo $wdg $nodeidx Data $txtdata # Reflect the data change to the ancestor nodes Node_UpdateAllData $wdg return } # Panel_EditButtonsConfigure # If there's no item selected in the node's treeview: in "list" and "dict" # mode, only "add" is enabled. # If an item is selected in the treeview: in "list" mode, enable add, remove, # up and down. In "dict" mode, enable add and remove # "up" and "down" must be enabled/disabled if the item is first or last item # in the treeview # # ARGUMENTS: # RESULTS: proc ::ValuePanel::Panel_EditButtonsConfigure {wdg nodeidx} { set buttons [Node_GetNodeInfo $wdg $nodeidx EditButtons] # Get the node's treeview, get the selected item set tvw [Node_GetNodeInfo $wdg $nodeidx Treeview] set selitem [$tvw selection] if { [llength $selitem] == 0 } { # No item selected! Disable buttons remove, up and down $buttons.btnRemove configure -state disabled $buttons.btnUp configure -state disabled $buttons.btnDown configure -state disabled return } # If any item is selected, it can be removed $buttons.btnRemove configure -state normal # The up and down buttons: enabled or disabled according to the selected # item's position in the treeview set selitempos [$tvw index $selitem] set lastitempos [expr {[llength [$tvw children {}]] - 1}] set up normal set down normal if { $selitempos == 0} { set up disabled } if { $selitempos == $lastitempos} { set down disabled } $buttons.btnUp configure -state $up $buttons.btnDown configure -state $down return } # Panel_TreeviewClicked # Callback for a single mouse click on a treeview. If the click does not # happen over an item, the selection is cleared (why isn't this already part # of the widget?). If an item is selected, the edit buttons are reconfigured. # ARGUMENTS: "wdg" and "nodeidx" refer to the widget and respective node # to which the treeview belongs. "tvw" is the treeview itself. "x" and "y" # are the coordinates of the mouse pointer where the click happened. proc ::ValuePanel::Panel_TreeviewClicked {wdg nodeidx tvw x y} { set buttons [Node_GetNodeInfo $wdg $nodeidx EditButtons] set clickeditem [$tvw identify item $x $y] if { $clickeditem eq ""} { # The click was not over an item. De-select, disable buttons, close # child nodes $tvw selection remove [$tvw selection] } else { # Set the treeview's selection manually. If we don't set it this way, # God knows why, the selection is shown but it is not changed # internally in the treeview (bug in Tk 8.5.9?), and procedures which # try to read the selection will get {} until the item is clicked # once more. $tvw selection set $clickeditem } # On an item or not, the click must close any child node, reconfigure # the EditButtons of the panel and enable the mode selector. Node_Remove $wdg [expr {$nodeidx + 1}] Node_SetNodeInfo $wdg $nodeidx OpenItem {} set nodecbb [Node_GetNodeInfo $wdg $nodeidx Combobox] $nodecbb configure -state readonly Panel_EditButtonsConfigure $wdg $nodeidx return } # Panel_TreeviewDoubleClicked # Callback procedure for a double click in a node's treeview. If an item was # double-clicked, a new node is created and the value of the clicked item is # shown on it. Other nodes which might exist after the one which received the # double-click are deleted. # ARGUMENTS: "wdg" is the widget; "nodeidx" is the index of the node which # contains the treeview which was clicked (this is defined when the binding # is created), "tvw" is the clicked treeview. "x" and "y", passed by the # binding, are the mouse position where the double-click happened. # RESULTS: A node is created and filled with the data of the clicked item. The # node which had its panel clicked has its OpenItem reconfigured (to a list # index or a dict key, accordingly to the node's mode). proc ::ValuePanel::Panel_TreeviewDoubleClicked {wdg nodeidx tvw x y} { # Retrieve the item which was double-clicked set clickeditem [$tvw identify item $x $y] set nodecbb [Node_GetNodeInfo $wdg $nodeidx Combobox] # Over a widget or not the click was, children nodes must be removed Node_Remove $wdg [expr {$nodeidx + 1}] Node_SetNodeInfo $wdg $nodeidx OpenItem {} if { $clickeditem eq ""} { # The click was not over an item. De-select, enable the mode selector, # reconfigure the EditButtons $tvw selection remove [$tvw selection] $nodecbb configure -state readonly } else { # Item clicked. Open a new node. # Set the treeview's selection manually $tvw selection set $clickeditem # Create the child node Panel_CreateNodeFromItem $wdg $nodeidx } # Reconfigure the panel's EditButtons Panel_EditButtonsConfigure $wdg $nodeidx return } # Panel_CreateNodeFromItem # Gets the value for the currently selected item in a node's treeview, and # creates a new node using this value # ARGUMENTS: "wdg" is the widget in which the node is, "nodeidx" is the node's # position in the widget's Nodes list. proc ::ValuePanel::Panel_CreateNodeFromItem {wdg nodeidx} { set nodeinfo [Node_GetNodeInfo $wdg $nodeidx] set tvw [dict get $nodeinfo Treeview] # Get the treeview's selected item set selitem [$tvw selection] # Disable the mode selector set nodecbb [dict get $nodeinfo Combobox] $nodecbb configure -state disabled # Get the treeview/node mode, then get the clicked data accordingly in # the variable newnodedata, and reconfigure the node's OpenItem. set nodedata [Node_GetNodeInfo $wdg $nodeidx Data] set mode [Node_GetNodeInfo $wdg $nodeidx Mode] if { $mode eq "list" } { # Get the position of the selected item set dataindex [$tvw index $selitem] set newnodedata [lindex $nodedata $dataindex] Node_SetNodeInfo $wdg $nodeidx OpenItem $dataindex } if { $mode eq "dict" } { # The text of the selected item should be the dict's key. set key [$tvw item $selitem -text] set newnodedata [dict get $nodedata $key] Node_SetNodeInfo $wdg $nodeidx OpenItem $key } # Create the new node Node_Add $wdg $newnodedata } # Panel_TreeviewItemMove # Moves the selected item in a list or dict treeview. This procedure should # only be called after a move button was clicked (and this can only happen # when an item is selected). # ARGUMENTS: "wdg" is the widget where the treeview is; "nodeidx" is the # index of the node/panel within the widget; "direction" is a string: # "up" or "down" according to the desired movement. # RESULTS: proc ::ValuePanel::Panel_TreeviewItemMove {wdg nodeidx direction} { set nodeinfo [Node_GetNodeInfo $wdg $nodeidx] # Get the node/treeview mode set tvw [dict get $nodeinfo Treeview] set mode [dict get $nodeinfo Mode] # Get the selected item position in the treeview set selitem [$tvw selection] set selitempos [$tvw index $selitem] # Close children nodes if { [dict get $nodeinfo OpenItem] ne {} } { Node_Remove $wdg [expr {$nodeidx + 1}] Node_SetNodeInfo $wdg $nodeidx OpenItem {} } # Move treeview item, according to the direction if { $direction eq "up" } { set newpos [expr {$selitempos - 1} ] } if { $direction eq "down" } { set newpos [expr {$selitempos + 1} ] } # Now move the list item itself set olddata [Node_GetNodeInfo $wdg $nodeidx Data] if { $mode eq "list"} { # Get the moved item set item [lindex $olddata $selitempos] # Create a list without the moved item set newdata [concat [lrange $olddata 0 [expr {$selitempos - 1}]]\ [lrange $olddata [expr {$selitempos + 1}] end]] # Insert the moved item in its new position set newdata [linsert $newdata $newpos $item] } if { $mode eq "dict" } { # Create a new dict by reading the keys from the old dict in the # order they are in the treeview. It does not look very efficient, # it's an improvement target for someday... foreach tvitem [$tvw children {}] { set key [$tvw item $tvitem -text] set val [dict get $olddata $key] dict set newdata $key $val } } # Update the node's data Node_SetNodeInfo $wdg $nodeidx Data $newdata # Use UpdateFromData here, because the item's indexes in the treeview are # not in sync with how they are displayed after the move Panel_UpdateFromData $wdg $nodeidx # Re-select the moved item $tvw selection set $newpos Panel_EditButtonsConfigure $wdg $nodeidx # Reflect the change in the data to parent nodes Node_UpdateAllData $wdg } # Panel_TreeviewItemRemove # Removes an item both from the treeview and from the underlying data of its # node. This proc is called from the Remove button, and we presume that this # button is enabled only if there's a treeview item selected. # ARGUMENTS: "wdg" is the widget where the node belongs, "nodeidx" is the node's # index in the widget's Nodes list. proc ::ValuePanel::Panel_TreeviewItemRemove {wdg nodeidx} { set nodeinfo [Node_GetNodeInfo $wdg $nodeidx] # Get the selected item set tvw [dict get $nodeinfo Treeview] set tvwitem [$tvw selection] # If this node has children, close them if { [dict get $nodeinfo OpenItem] ne ""} { Node_Remove $wdg [expr {$nodeidx + 1}] Node_SetNodeInfo $wdg $nodeidx OpenItem {} } # Get the node's mode set mode [dict get $nodeinfo Mode] # Remove the data's item, according to the mode set olddata [dict get $nodeinfo Data] if { $mode eq "list" } { # Get the selected item's position in the treeview: it is also an index # for the underlying list set tvwitempos [$tvw index $tvwitem] # Create a new list without the item set previtempos [expr {$tvwitempos - 1}] set nextitempos [expr { $tvwitempos + 1}] set newdata [concat [lrange $olddata 0 $previtempos]\ [lrange $olddata $nextitempos end]] } if { $mode eq "dict" } { # Get the treeview item's text: it's the key to be removed from the # previous dict set remkey [$tvw item $tvwitem -text] set newdata [dict remove $olddata $remkey] } Node_SetNodeInfo $wdg $nodeidx Data $newdata # The visual stuff now: remove the item, redefine the treeview selection, # update the EditButtons according to the new selection set nextsel [$tvw next $tvwitem] if { $nextsel eq {} } { set nextsel [$tvw prev $tvwitem] } $tvw selection set $nextsel $tvw delete $tvwitem Panel_EditButtonsConfigure $wdg $nodeidx # Reflect the change in the data through the widget Node_UpdateAllData $wdg } # Panel_TreeviewItemAdd # Callback for the "add" EditButton of a panel. proc ::ValuePanel::Panel_TreeviewItemAdd {wdg nodeidx} { set nodeinfo [Node_GetNodeInfo $wdg $nodeidx] set insertcancelled false # If this node has children, close them if { [dict get $nodeinfo OpenItem] ne ""} { Node_Remove $wdg [expr {$nodeidx + 1}] Node_SetNodeInfo $wdg $nodeidx OpenItem {} } # If there is an item selected, the new item will be created right after it; # if not, it will be created as the last item set tvw [dict get $nodeinfo Treeview] set tvwitem [$tvw selection] if { [llength $tvwitem] } { set curitempos [$tvw index $tvwitem] set newitempos [expr {$curitempos + 1}] } else { set newitempos [llength [$tvw children {}] ] } set mode [dict get $nodeinfo Mode] set olddata [dict get $nodeinfo Data] if { $mode eq "list" } { # If the node is in "list" mode, the new item will be an empty list and # its contents can be edited in a child panel created later. set newdata [linsert $olddata $newitempos {}] } if { $mode eq "dict"} { # If the node is in "dict" mode, the user will be requested to type in # the new dict key. set newkey [Panel_DictKeyInput $wdg $nodeidx] if { $newkey ne {} && ![string is space $newkey] } { # Users should not rely in key/value positions in a dict, but let's # create the new treeview item in the requested order. set curkeys [dict keys $olddata] set newkeys [linsert $curkeys $newitempos $newkey] foreach newkey $newkeys { if { [dict exists $olddata $newkey] } { dict set newdata $newkey [dict get $olddata $newkey] } else { dict set newdata $newkey {} } } } else { # If nothing was typed as the new key, nothing will be created. set insertcancelled true } } if { !$insertcancelled } { # Substitute the node's data by the new one Node_SetNodeInfo $wdg $nodeidx Data $newdata # Update the panel/treeview from the new node's data Panel_UpdateFromData $wdg $nodeidx # Make the new item selected. Because the items IDs are their positions # in the treeview, we can use newitempos here. $tvw selection set $newitempos Panel_EditButtonsConfigure $wdg $nodeidx # Create a child node using the data from the selected item Panel_CreateNodeFromItem $wdg $nodeidx # Update the whole widget's data Node_UpdateAllData $wdg # It seems to be more natural for the interface to show the new node in # "string" mode for immediate input... Panel_ChangeMode $wdg [expr {$nodeidx + 1}] "string" } return } # Panel_DictKeyInput # Used only by Panel_TreeviewItemAdd, this procedure shows an entry widget in # which the user can type in the key which will be created on a "dict"-mode # panel. # ARGUMENTS: # RESULT: If the user types in something and presses "Enter", the typed text # is returned. If the user presses the "Esc" key or clicks out of the entry, # the empty string is returned. proc ::ValuePanel::Panel_DictKeyInput {wdg nodeidx} { # Create a temporary namespace-variable which will allow the retrieval # of the entry's value variable input {} # Ungrid the EditButtons set btnfrm [Node_GetNodeInfo $wdg $nodeidx EditButtons] grid remove $btnfrm # Create the entry widget and put it in the place of the EditButtons set entwdg [winfo parent $btnfrm].ent ttk::entry $entwdg grid $entwdg -column 0 -row 1 -sticky ew focus $entwdg # Create the bindings for the entry bind $entwdg <Return> [list ::ValuePanel::Panel_DictKeyInput_Return %W] bind $entwdg <KP_Enter> [list ::ValuePanel::Panel_DictKeyInput_Return %W] bind $entwdg <Escape> [list ::ValuePanel::Panel_DictKeyInput_Esc %W] bind $entwdg <FocusOut> [list ::ValuePanel::Panel_DictKeyInput_Esc %W] # Wait until the entry is destroyed by its binded procs... tkwait window $entwdg # Grid back the EditButtons grid $btnfrm set ret $input unset input return $ret } # Panel_DictKeyInput_Esc proc ::ValuePanel::Panel_DictKeyInput_Esc {entwdg} { set ::ValuePanel::input {} grid remove $entwdg destroy $entwdg return } # Panel_DictKeyInput_Return proc ::ValuePanel::Panel_DictKeyInput_Return {entwdg} { set ::ValuePanel::input [$entwdg get] grid remove $entwdg destroy $entwdg return }