Fabricio Rocha - 14 Jul 2011 - XML may be the current universal format for data files, but it's often way too much for simpler needs. As strings can be easily read and written to files, the "everything is a string" Tcl concept makes it possible to output to a simple text file very complex data structures made of lists and dicts, which can later be read to scalar or array variables and promptly used by the built-in language commands. The files built from Tcl variables contents can be open and edited in any Unicode-capable text editor, but they may become difficult to understand and edit if the data is large and nested.
This problem pushed me to create eDictor, the simple data-files editor presented here. It could be fancier, indeed, but I made it as a development tool for helping me in my TKtEasy project, and I thought it could be useful for other people. It allows you to open, create and edit files created from the output of Tcl variables, and it is also useful for planning data structures which will be used in an application.

Data is shown in a small panel which contain a "mode selector" in which you can choose how you want to view and edit the data. In the "string" mode, data is presented in a text widget; in "list" mode, each list item is a line of a ttk::treeview widget. Using the buttons below the treeview, you can move the list items up or down, remove or add elements, and edit an item in another similar panel by double-clicking it. In "dict" mode, the treeview shows keys, and by double-clicking them you can view and edit their respective values in the new panel that appears. The "nodes" which represent each nesting level of the data are placed in a ttk::panedwindow widget, so you can change their width for showing all the content of a line.
eDictor is built around the valuepanel megawidget and has the following features in its initial version:
The initial version of eDictor is really really simple, it barely works as the development tool I wanted and is also a simple demo of the valuepanel megawidget. I have some ideas of "nice to haves" which may be implemented someday. If you have a suggestion or want to "vote" for an idea, please tell; and if you can implement a suggestion, take a look at the "Code" section.
AMW suggest using a ttk::treeview to present the dict data and created the dictree widget to achieve this.
Translation files and better support - You'll notice that the interface strings are using msgcat, and message catalog files are expected to be in a langs subdirectory below the script's directory. But this is a very basic implementation and I did not write any translation files yet.
Save window size and position
Keep track on the last used directory for open/save as
Open files passed from the command line
Toolbar
Configurable keyboard shortcuts
Support other common data formats - No easy task here, but as INI files can be converted to/from dicts in some conditions, its support is possible. I think a fork of eDictor for XML edition with a similar interface might be interesting.
Here is the code for eDictor. Please remember to save the valuepanel code in a file at the same directory. If you want to correct a bug or change something, be welcome. Please, create a new "version/code" section like the original one, and place the new code there, along with an explanation of what you have changed. Use the common sense for version numbers: major, minor, revision.
Idiot bug: if you saved untitled data, the notebook tab did not have its title changed to the new file name. Corrected. By the way, I discovered that under Tk 8.5.9 (in Debian GNU-Linux) I could not select the contents of the text widget in a "string" node: the selection "flickered" and apparently made the mode selector go nuts. This did not happen after an upgrade to Tk 8.5.10.
#!/bin/sh
# -*- tcl -*-
# The next line is executed by /bin/sh, but not tcl \
exec tclsh "$0" ${1+"$@"}
# eDictor - A Tcl viewer/editor for Tcl lists and dicts
# Fabricio Rocha, June 2011
package require Tcl 8.5
package require Tk 8.5
package require msgcat
source valuepanel.tcl
# #############################################################################
# Initialization and business-logic
# #############################################################################
# Init
proc Init {} {
variable pagecount 0
variable Pages
set scriptdir [file dirname [file normalize [info script]]]
# Configure msgcat
catch {::msgcat::mcload [file join $scriptdir "langs"]}
# Create the main window
MainWindow_Create
# Create the data for the first page (still empty)
set ptitle [::msgcat::mc "Untitled%s" $pagecount]
Page_New $ptitle {}
}
# Page_New
# Create and configure the data structure which represents a page
# ARGUMENTS: "title" is the title of the new page. "filepath" is optional.
# RESULTS: A new page is created in the notebook, with an empty valuepanel.
# Returns the Tk path to the new page frame.
proc Page_New { title {filepath {}} } {
variable pagecount
variable Pages
set page [ttk::frame .nbk.frm$pagecount]
.nbk add $page -text $title -sticky news -padding 2
::ValuePanel::valuepanel $page.vp
bind $page.vp <<Modified>> [list Page_Modified $page true]
ttk::button $page.btnClose -text "X" -width 0 \
-command [list MainWindow_OnPageClose $page]
grid $page.btnClose -column 0 -row 0 -sticky e
grid $page.vp -column 0 -row 1 -sticky news
grid columnconfigure $page 0 -weight 1
grid rowconfigure $page 0 -weight 0
grid rowconfigure $page 1 -weight 1
dict set Pages($page) filepath $filepath
dict set Pages($page) unsaved false
dict set Pages($page) title $title
.nbk select $page
incr pagecount
return $page
}
# Page_Modified
# Called as a binding for the <<Modified>> event of a page's valuepanel.
# ARGUMENTS: "pagenum" is the index of the page; "modified" is boolean
proc Page_Modified {page modified} {
variable Pages
dict set Pages($page) unsaved $modified
set status [expr bool($modified)]
if { $status == 1 } {
set newtitle "[dict get $Pages($page) title]*"
} else {
set newtitle [dict get $Pages($page) title]
}
.nbk tab $page -text $newtitle
return
}
# #############################################################################
# MAIN WINDOW AND DIALOGS
# #############################################################################
# MainWindow_Create
# Creates the main window's widgets.
# ARGUMENTS:
# RESULTS:
proc MainWindow_Create {} {
variable pagecount
wm withdraw .
wm protocol . WM_DELETE_WINDOW MainWindow_OnQuit
# Setup the window size, title, etc
wm title . "eDictor"
# Create the menus
option add *tearOff 0
menu .menubar
. configure -menu .menubar
menu .menubar.file
.menubar add cascade -menu .menubar.file -label [::msgcat::mc "File"]
.menubar.file add command -label [::msgcat::mc "New"] \
-command {Page_New [::msgcat::mc "Untitled%s" $pagecount] {}}\
-accelerator "Ctrl+N"
.menubar.file add command -label [::msgcat::mc "Open..."] \
-command MainWindow_OnOpen -accelerator "Ctrl+O"
.menubar.file add command -label [::msgcat::mc "Save"] \
-command MainWindow_OnSave -accelerator "Ctrl+S"
.menubar.file add command -label [::msgcat::mc "Save As..."] \
-command MainWindow_OnSaveAs
.menubar.file add separator
.menubar.file add command -label [::msgcat::mc "Quit"] \
-command MainWindow_OnQuit -accelerator "Ctrl+Q"
menu .menubar.help
.menubar add cascade -menu .menubar.help -label [::msgcat::mc "Help"]
.menubar.help add command -label [::msgcat::mc "About..."]\
-command MainWindow_OnAbout
# The main notebook
ttk::notebook .nbk -padding {2 2}
# Layout all the stuff
grid .nbk -column 0 -row 0 -sticky news
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
# Configure global bindings. Does someone know a way to avoid this silly
# duplication due to upper/lower case?
bind . <Control-N> {Page_New [::msgcat::mc "Untitled%%s" $pagecount] {}}
bind . <Control-n> {Page_New [::msgcat::mc "Untitled%%s" $pagecount] {}}
bind . <Control-O> MainWindow_OnOpen
bind . <Control-o> MainWindow_OnOpen
bind . <Control-S> MainWindow_OnSave
bind . <Control-s> MainWindow_OnSave
bind . <Control-Q> MainWindow_OnQuit
bind . <Control-q> MainWindow_OnQuit
# Restore the main window's visibility
wm deiconify .
}
# MainWindow_OnPageClose
# Called for closing a datatree currently shown by a page. Checks if the
# data structure is saved before proceeding. Destroys the page and removes the
# data structure from memory.
# ARGUMENTS: "page" is the Tk path to the page to be closed.
proc MainWindow_OnPageClose {page} {
variable Pages
# If the data of this page is not saved, select it and ask the user to save
if { [dict get $Pages($page) unsaved] } {
.nbk select $page
set det [::msgcat::mc "Unsaved data in %s" \
[dict get $Pages($page) title]]
set dlg [tk_messageBox -icon question -type yesnocancel \
-message [::msgcat::mc "Save changes before closing?"] \
-title "eDictor" \
-detail $det -default yes]
switch -- $dlg {
cancel {
return -code return
}
yes {
MainWindow_OnSave
}
}; # end of switch
}
.nbk forget $page
destroy $page
array unset Pages $page
return
}
# MainWindow_OnOpen
# Callback for an "open file" action from the interface (menu, button, etc).
# Shows a file dialog and, if the reading of the file succeeds, a new page is
# created, the file contents are written to the page's value panel
proc MainWindow_OnOpen {} {
variable Pages
variable pagecount
variable env
# Show the Open dialog
set paths [tk_getOpenFile \
-initialdir $env(HOME) \
-multiple true \
-title [::msgcat::mc "Open data file..."] \
-parent . \
]
foreach path $paths {
if { [catch {File_LoadRaw $path} rv ro] } {
set errormsg $rv
tk_messageBox -icon error -title "eDictor" -type ok \
-message $errormsg
} else {
# No error: "rv" contains the successful result of File_LoadRaw
set title [file tail $path]
set newpage [Page_New $title $path]
$newpage.vp setdata $rv
}
}
return
}
# MainWindow_OnSaveAs
# Called in the event of a "Save As..." action by the user.
# ARGUMENTS:
# RESULTS: Shows a Save As... dialog, asking for a filename. This filename is
# then stored in the datatree's properties, and a Save action is called.
proc MainWindow_OnSaveAs {} {
variable Pages
# Get the currently active tab
set page [.nbk select]
# Check if there is already a filepath for this page
set curpath [dict get $Pages($page) filepath]
if { $curpath ne {} } {
set initdir [file dirname $curpath]
set initfile [file tail $curpath]
} else {
variable env
set initdir $env(HOME)
set initfile [dict get $Pages($page) title]
}
# Open the "Save As" file dialog
set newpath [tk_getSaveFile\
-initialdir $initdir \
-initialfile $initfile \
-parent . \
-title [::msgcat::mc "Save as..."] \
]
if { $newpath ne {} } {
# Get the page's valuepanel data
set data [$page.vp getdata]
if { [catch {File_SaveRaw $newpath $data} rv ro] } {
set errormsg [::msgcat::mc "Can't write to file %s" $newpath]
tk_messageBox -icon error -title "eDictor" -type ok \
-message $errormsg -detail $rv
# Call MainWindow_OnSaveAs for trying another path
MainWindow_OnSaveAs
} else {
dict set Pages($page) filepath $newpath
# v.0.1.1, 10 Jul 2011 - Update the page's title to the new filename
dict set Pages($page) title [file tail $newpath]
Page_Modified $page false
}
}
return
}
# MainWindow_OnSave
# Called in the event of a "Save" action by the user. Discover which page is
# currently shown in the notebook, then uses the filepath stored in the
# respective datatree for the proper Save procedure.
proc MainWindow_OnSave {} {
variable Pages
# Get the currently active tab
set page [.nbk select]
set path [dict get $Pages($page) filepath]
if { $path eq "" } {
MainWindow_OnSaveAs
return
}
# Get the page valuepanel's data
set data [$page.vp getdata]
if { [catch {File_SaveRaw $path $data} rv ro] } {
set errormsg [::msgcat::mc "Can't write to file" $path]
tk_messageBox -icon error -title "eDictor" -type ok -detail $path\
-message $errormsg
# Call MainWindow_OnSaveAs for trying another path
MainWindow_OnSaveAs
} else {
Page_Modified $page false
}
}
# MainWindow_OnQuit
# Checks if there are shown and unsaved data structures before proceeding.
# Destroys the main window and exits the program.
proc MainWindow_OnQuit {} {
variable Pages
# Browse thru the pages data: if there's any unsaved, ask user
foreach page [array names Pages] {
MainWindow_OnPageClose $page
}
destroy .
exit
}
# MainWindow_OnAbout
# Displays the "About" dialog.
proc MainWindow_OnAbout {} {
set msg "eDictor 0.1"
set d 14
set m Jul
set y 2011
set det [::msgcat::mc "Date: %s/%s/%s\n" $d $m $y]
set det "$det[::msgcat::mc "GPL license\nFabricio Rocha"]"
tk_messageBox -icon info -type ok -default ok -message $msg -detail $det
return
}
# #############################################################################
# FILE I/O
# #############################################################################
# File_Load_Raw
# Loads a file as a raw string.
# ARGUMENTS: "path" is the file to be read
# RESULTS: Returns the data read from the file.
proc File_LoadRaw {path} {
set data {}
if { [catch {open $path r} rv ro] } {
set errormsg [::msgcat::mc "Can't read from file %s" $path]
return -code error $errormsg
} else {
set filehandler $rv
set data [read -nonewline $filehandler]
close $filehandler
}
return $data
}
# File_Save_Raw
# Outputs to a file the passed data without any formatting (i.e., simply as it
# is stored in a valuepanel)
proc File_SaveRaw {path data} {
if { [catch {open $path w} rv ro] } {
set errormsg [::msgcat::mc "Can't write to %s" $path]
return -code error $errormsg
} else {
set filehandler $rv
puts -nonewline $filehandler $data
close $filehandler
}
return
}
# #############################################################################
# Application startup at "script root" level
# #############################################################################
InitThe initial, quick-and-dirty release. It needs the valuepanel code saved to a file called "valuepanel.tcl" in the same directory.
#!/bin/sh
# -*- tcl -*-
# The next line is executed by /bin/sh, but not tcl \
exec tclsh "$0" ${1+"$@"}
# eDictor - A Tcl viewer/editor for Tcl lists and dicts
# Fabricio Rocha, June 2011
package require Tcl 8.5
package require Tk 8.5
package require msgcat
source valuepanel.tcl
# #############################################################################
#
# #############################################################################
# Init
proc Init {} {
variable pagecount 0
variable Pages
set scriptdir [file dirname [file normalize [info script]]]
# Configure msgcat
catch {::msgcat::mcload [file join $scriptdir "langs"]}
# Create the main window
MainWindow_Create
# Create the data for the first page (still empty)
set ptitle [::msgcat::mc "Untitled%s" $pagecount]
Page_New $ptitle {}
}
# Page_New
# Create and configure the data structure which represents a page
# ARGUMENTS: "title" is the title of the new page. "filepath" is optional.
# RESULTS: A new page is created in the notebook, with an empty valuepanel.
# Returns the Tk path to the new page frame.
proc Page_New { title {filepath {}} } {
variable pagecount
variable Pages
set page [ttk::frame .nbk.frm$pagecount]
.nbk add $page -text $title -sticky news -padding 2
::ValuePanel::valuepanel $page.vp
bind $page.vp <<Modified>> [list Page_Modified $page true]
ttk::button $page.btnClose -text "X" -width 0 \
-command [list MainWindow_OnPageClose $page]
grid $page.btnClose -column 0 -row 0 -sticky e
grid $page.vp -column 0 -row 1 -sticky news
grid columnconfigure $page 0 -weight 1
grid rowconfigure $page 0 -weight 0
grid rowconfigure $page 1 -weight 1
dict set Pages($page) filepath $filepath
dict set Pages($page) unsaved false
dict set Pages($page) title $title
.nbk select $page
incr pagecount
return $page
}
# Page_Modified
# Called as a binding for the <<Modified>> event of a page's valuepanel.
# ARGUMENTS: "pagenum" is the index of the page; "modified" is boolean
proc Page_Modified {page modified} {
variable Pages
dict set Pages($page) unsaved $modified
set status [expr bool($modified)]
if { $status == 1 } {
set newtitle "[dict get $Pages($page) title]*"
} else {
set newtitle [dict get $Pages($page) title]
}
.nbk tab $page -text $newtitle
return
}
# #############################################################################
# MAIN WINDOW AND DIALOGS
# #############################################################################
# MainWindow_Create
# Creates the main window's widgets.
# ARGUMENTS:
# RESULTS:
proc MainWindow_Create {} {
variable pagecount
wm withdraw .
wm protocol . WM_DELETE_WINDOW MainWindow_OnQuit
# Setup the window size, title, etc
wm title . "eDictor"
# Create the menus
option add *tearOff 0
menu .menubar
. configure -menu .menubar
menu .menubar.file
.menubar add cascade -menu .menubar.file -label [::msgcat::mc "File"]
.menubar.file add command -label [::msgcat::mc "New"] \
-command {Page_New [::msgcat::mc "Untitled%s" $pagecount] {}}\
-accelerator "Ctrl+N"
.menubar.file add command -label [::msgcat::mc "Open..."] \
-command MainWindow_OnOpen -accelerator "Ctrl+O"
.menubar.file add command -label [::msgcat::mc "Save"] \
-command MainWindow_OnSave -accelerator "Ctrl+S"
.menubar.file add command -label [::msgcat::mc "Save As..."] \
-command MainWindow_OnSaveAs
.menubar.file add separator
.menubar.file add command -label [::msgcat::mc "Quit"] \
-command MainWindow_OnQuit -accelerator "Ctrl+Q"
menu .menubar.help
.menubar add cascade -menu .menubar.help -label [::msgcat::mc "Help"]
.menubar.help add command -label [::msgcat::mc "About..."]\
-command MainWindow_OnAbout
# The main notebook
ttk::notebook .nbk -padding {2 2}
# Layout all the stuff
grid .nbk -column 0 -row 0 -sticky news
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
# Configure global bindings. Does someone know a way to avoid this silly
# duplication due to upper/lower case?
bind . <Control-N> {Page_New [::msgcat::mc "Untitled%%s" $pagecount] {}}
bind . <Control-n> {Page_New [::msgcat::mc "Untitled%%s" $pagecount] {}}
bind . <Control-O> MainWindow_OnOpen
bind . <Control-o> MainWindow_OnOpen
bind . <Control-S> MainWindow_OnSave
bind . <Control-s> MainWindow_OnSave
bind . <Control-Q> MainWindow_OnQuit
bind . <Control-q> MainWindow_OnQuit
# Restore the main window's visibility
wm deiconify .
}
# MainWindow_OnPageClose
# Called for closing a datatree currently shown by a page. Checks if the
# data structure is saved before proceeding. Destroys the page and removes the
# data structure from memory.
# ARGUMENTS: "page" is the Tk path to the page to be closed.
proc MainWindow_OnPageClose {page} {
variable Pages
# If the data of this page is not saved, select it and ask the user to save
if { [dict get $Pages($page) unsaved] } {
.nbk select $page
set det [::msgcat::mc "Unsaved data in %s" \
[dict get $Pages($page) title]]
set dlg [tk_messageBox -icon question -type yesnocancel \
-message [::msgcat::mc "Save changes before closing?"] \
-title "eDictor" \
-detail $det -default yes]
switch -- $dlg {
cancel {
return -code return
}
yes {
MainWindow_OnSave
}
}; # end of switch
}
.nbk forget $page
destroy $page
array unset Pages $page
return
}
# MainWindow_OnOpen
# Callback for an "open file" action from the interface (menu, button, etc).
# Shows a file dialog and, if the reading of the file succeeds, a new page is
# created, the file contents are written to the page's value panel
proc MainWindow_OnOpen {} {
variable Pages
variable pagecount
variable env
# Show the Open dialog
set paths [tk_getOpenFile \
-initialdir $env(HOME) \
-multiple true \
-title [::msgcat::mc "Open data file..."] \
-parent . \
]
foreach path $paths {
if { [catch {File_LoadRaw $path} rv ro] } {
set errormsg $rv
tk_messageBox -icon error -title "eDictor" -type ok \
-message $errormsg
} else {
# No error: "rv" contains the successful result of File_LoadRaw
set title [file tail $path]
set newpage [Page_New $title $path]
$newpage.vp setdata $rv
}
}
return
}
# MainWindow_OnSaveAs
# Called in the event of a "Save As..." action by the user.
# ARGUMENTS:
# RESULTS: Shows a Save As... dialog, asking for a filename. This filename is
# then stored in the datatree's properties, and a Save action is called.
proc MainWindow_OnSaveAs {} {
variable Pages
# Get the currently active tab
set page [.nbk select]
# Check if there is already a filepath for this page
set curpath [dict get $Pages($page) filepath]
if { $curpath ne {} } {
set initdir [file dirname $curpath]
set initfile [file tail $curpath]
} else {
variable env
set initdir $env(HOME)
set initfile [dict get $Pages($page) title]
}
# Open the "Save As" file dialog
set newpath [tk_getSaveFile\
-initialdir $initdir \
-initialfile $initfile \
-parent . \
-title [::msgcat::mc "Save as..."] \
]
if { $newpath ne {} } {
# Get the page's valuepanel data
set data [$page.vp getdata]
if { [catch {File_SaveRaw $newpath $data} rv ro] } {
set errormsg [::msgcat::mc "Can't write to file %s" $newpath]
tk_messageBox -icon error -title "eDictor" -type ok \
-message $errormsg -detail $rv
# Call MainWindow_OnSaveAs for trying another path
MainWindow_OnSaveAs
} else {
dict set Pages($page) filepath $newpath
Page_Modified $page false
}
}
return
}
# MainWindow_OnSave
# Called in the event of a "Save" action by the user. Discover which page is
# currently shown in the notebook, then uses the filepath stored in the
# respective datatree for the proper Save procedure.
proc MainWindow_OnSave {} {
variable Pages
# Get the currently active tab
set page [.nbk select]
set path [dict get $Pages($page) filepath]
if { $path eq "" } {
MainWindow_OnSaveAs
return
}
# Get the page valuepanel's data
set data [$page.vp getdata]
if { [catch {File_SaveRaw $path $data} rv ro] } {
set errormsg [::msgcat::mc "Can't write to file" $path]
tk_messageBox -icon error -title "eDictor" -type ok -detail $path\
-message $errormsg
# Call MainWindow_OnSaveAs for trying another path
MainWindow_OnSaveAs
} else {
Page_Modified $page false
}
}
# MainWindow_OnQuit
# Checks if there are shown and unsaved data structures before proceeding.
# Destroys the main window and exits the program.
proc MainWindow_OnQuit {} {
variable Pages
# Browse thru the pages data: if there's any unsaved, ask user
foreach page [array names Pages] {
MainWindow_OnPageClose $page
}
destroy .
exit
}
# MainWindow_OnAbout
# Displays the "About" dialog.
proc MainWindow_OnAbout {} {
set msg "eDictor 0.1"
set d 14
set m Jul
set y 2011
set det [::msgcat::mc "Date: %s/%s/%s\n" $d $m $y]
set det "$det[::msgcat::mc "GPL license\nFabricio Rocha"]"
tk_messageBox -icon info -type ok -default ok -message $msg -detail $det
return
}
# #############################################################################
# FILE I/O
# #############################################################################
# File_Load_Raw
# Loads a file as a raw string.
# ARGUMENTS: "path" is the file to be read
# RESULTS: Returns the data read from the file.
proc File_LoadRaw {path} {
set data {}
if { [catch {open $path r} rv ro] } {
set errormsg [::msgcat::mc "Can't read from file %s" $path]
return -code error $errormsg
} else {
set filehandler $rv
set data [read -nonewline $filehandler]
close $filehandler
}
return $data
}
# File_Save_Raw
# Outputs to a file the passed data without any formatting (i.e., simply as it
# is stored in a valuepanel)
proc File_SaveRaw {path data} {
if { [catch {open $path w} rv ro] } {
set errormsg [::msgcat::mc "Can't write to %s" $path]
return -code error $errormsg
} else {
set filehandler $rv
puts -nonewline $filehandler $data
close $filehandler
}
return
}
# #############################################################################
# Application startup at "script root" level
# #############################################################################
Init