Revert -Providing 'Revert' and Save Prompt Functionality to Text Editors

Bezoar (03/30/08) Made minor changes to make program portable.

WJG (04/11/06) One of the frustrating things about editing an existing text file is not making a security backup before the edit job begins. Sure, we can make back-ups as we go along, but a typical application may only perform 2 or 3 saves which will still include the most recent changes. Some editing applications (text or graphics) provide a handy revert to original function, and here's a Tcl/Tk version of this that I've just cooked up. Also included is a simple checking function to test whether the current file has been saved or not. If a file has been saved, then the associated button and menu items will be deactivated with the corresponding changes to the icons giving visual feedback. If a file has been modified and not saved before exit, then the user will be prompted to save or discard changes. For the simplicity's sake, the demo program merely loads/saves a file named 'test.txt'.

 #---------------
 # revert.tcl
 #---------------
 # William J Giddings, 2006
 #
 # The following routines:
 #   1) gives the the user the option to save open text documents that have not been yet saved.
 #   2) modifies menu and toolbar buttons to indicate if the current data has been saved or not.
 #   3) provides 'revert' option for files opened for re-editing, deletes text undo/redo history.
 #
 # Notes:
 #   To implement the revert function, a copy of any opened document is made in the system applications data
 #   directory for the calling programme. The actual location of this file may vary from system to system.
 #   The file is delete on exit.
 #
 #---------------

 #---------------
 # initialise autosave
 # args:
 # w  pathName of text widget associated with file i/o operations
 # b  pathName of associated save button
 # m  pathName of menu containing save entry
 # i  index of save entry in associated menu
 #---------------
 proc save:init {w b m i} {

  # create package specific namespace
  namespace eval ::save {}
  set ::save::save 0        ;# saved condition of the open document 
  set ::save::button $b     ;# pathName of associated save button
  set ::save::menu $m       ;# pathName of menu containing save entry
  set ::save::menuIndex $i  ;# index of save entry in associated menu

  # create text bindings, reset button and menu item
  bind $w <Key> "
      $b configure -state normal
      $m entryconfigure $i -state normal
      set ::save::save 1
      "
 }

 #---------------
 # reset condition flag and associated button/menu item
 #---------------
 proc save:reset {} {
  set ::save::save 1
  $::save::button configure -state normal
  $::save::menu entryconfigure $::save::menuIndex -state normal
 }

 #---------------
 # set flags to incicate that current document is saved
 #---------------
 proc save:me {} {
    $::save::button configure -state disabled
    $::save::menu entryconfigure $::save::menuIndex -state disabled    
    set ::save::save 0    
 }

 #---------------
 # save a revert copy of the most recenty opened document
 # call from within file loading proceedures
 # args:
 # fname  name of file most recently loaded
 #---------------
 proc revert:save {fname} {
  file copy -force $fname [file join $::revert::targetDir revert.tmp ]
  set ::revert::firstTime 0
  # enable 'revert' option in the associated menu 
  $::revert::menu entryconfigure $::revert::index -state normal
 }

 #---------------
 # delete revert copy file, called prior to program exit
 #---------------
 proc revert:exit {} {
  file delete -force [file join $::revert::targetDir revert.tmp ]
 }

 #---------------
 # the content here will depend upon application data file parsing
 #---------------
 proc revert:load {} {
  # will cause error if edited file was not loaded from disk
  if {!$::revert::firstTime} {
    set fp [open [file join $::revert::targetDir revert.tmp ] "r"]
    $::revert::text delete 1.0 end
    $::revert::text insert end [gets $fp]
    close $fp
  }
 }

 #---------------
 # initialise revert package
 # args:
 # w  pathName of associated text widget
 # m  pathName of menu containing the 'revert' option
 # i  index of 'revert' option in the associated menu 
 #---------------
 proc revert:init {w m i} {
  namespace eval ::revert {}
  set ::revert::text $w      ;# pathName of associated text widget
  set ::revert::firstTime 1  ;# flag to monitor if a 'revert' copy exists
  set ::revert::menu $m      ;# pathName of menu containing the 'revert' option
  set ::revert::index $i     ;# index of 'revert' option in the associated menu 

  # build pathname to directory in which to save application temporary data 
    if { [ string equal $::tcl_platform(platform) "unix" ] } {
         set ::revert::targetDir  [file join $::env(HOME) .[file rootname [ file tail $::argv0 ] ] ]  
    } else {
         set ::revert::targetDir [file join $::env(HOME) "Application Data" [file rootname [file tail $::argv0] ] ]
    }
    # create application data directory if necessary
    file mkdir $::revert::targetDir
    # add revert item to specified menu
    $m insert $i command -label Revert -command revert:load -state disabled
  }

 #---------------
 # reset revert flags and menu items
 #---------------
 proc revert:reset {} {
  set ::revert::firstTime 1  ;# flag to monitor if a 'revert' copy exists
  # disable 'revert' option in the associated menu until a file is loaded
  $::revert::menu entryconfigure $::revert::index -state disabled
 }

 #---------------
 # test application main proc
 #---------------
 proc main {} {

  # button graphics
  image create photo save -data {R0lGODlhEAAQAMQAAP////7+/fnv7+/39+/v9+rr69jY2NTQyM7e3re3t7bP0am1taioqI+xtoyYmIyUiXulrXuMjHOcrWZmZGNzc1Jze1Jrc0pjY0pSWkVGREJaYzVCSgAAAAAAAAAAAAAAACwAAAAAEAAQAAAFauAhjiPHkWjJSWeqKkphuiasBAOL1nfg+zMRp4H4GQOyUmRw/BGCB06hOShIdMIG01e9WixQKfeqwVSurWiDYNUw2BN0eiWxDKqFxUWzmTfqGxAGBQkUaSVXAgsGDxmHJRcSBg5QNCaPIiEAOw==}

  # create menus
  menu .menubar -type menubar 
  . configure -menu .menubar

  # add file menu
  menu .menubar.file -tearoff 0
  .menubar add cascade -label File -menu .menubar.file

  # add edit menu
  menu .menubar.edit -tearoff 0
  .menubar add cascade -label Edit -menu .menubar.edit

  # file menu options
  .menubar.file add command -label New -command file:new
  .menubar.file add command -label Open -command file:open 
  .menubar.file add command -label Save -compound left -image save -command file:save
  .menubar.file add separator
  .menubar.file add command -label Quit -command file:quit

  # edit menu options
  .menubar.edit add command -label Undo -command edit:undo 
  .menubar.edit add command -label Redo -command edit:redo
  .menubar.edit add separator
  .menubar.edit add command -label Cut -command edit:cut
  .menubar.edit add command -label Copy -command edit:copy
  .menubar.edit add command -label Paste -command edit:paste
  .menubar.edit add command -label Delete -command edit:delete

  pack [frame .fr] -side top -anchor nw -fill x
  pack [button .fr.but1 -image save -command file:save -borderwidth 0] -side left 

  pack [text .txt] -side top -anchor nw -fill both

  revert:init .txt .menubar.file 3
  save:init .txt .fr.but1 .menubar.file 2

  # exit prooperly..
  wm protocol . WM_DELETE_WINDOW { file:quit }
 }

 #---------------
 # proceedures called from menus and buttons
 #---------------
 proc file:new {} {
  save:reset
  revert:reset
  .txt delete 1.0 end
 }

 proc file:open {} {
  save:reset
  revert:save test.txt
  .txt delete 1.0 end
  set fp [open test.txt r]
  .txt insert end [read $fp]
  close $fp
 }

 proc file:save {} {
  save:me
  # test save block..
  set fp [open test.txt w]
  puts $fp [.txt get 1.0 end]
  close $fp 
 }

 proc file:revert {} {
  revert:load
 }

 proc file:quit {} {
  if {$::save::save} {
      if { [tk_dialog .foo "Save File.." "Save document changes?" \
        questhead 0 "  Yes  " "   No   "]} {
        # save file
        set fp [open test.txt "w"]
        puts $fp [.txt get 1.0 end]
        close $fp
      }
    }
  revert:exit
  exit 
 }

 # run test application
 main

[Category Example