eDictor

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.

https://wiki.tcl-lang.org/_repo/images/eDictor-0.1-3.png

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:

  • - Keyboard shortcuts for the common operations (new, open, save, quit)
  • - You can open more than one file at once: just select the files you want in the "Open file" dialog.
  • - The editor checks if a page's data has not been saved since the last change, and asks if you want to cancel closing, save or discard the changes.

Possible improvements

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.


Versions

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.

  Version 0.1.1 - 19 Jul 2011

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
# #############################################################################

Init

  Version 0.1 - 15 Jul 2011

The 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