File cifedit.tcl

This file contains the Tcl/Tk source code for the CIFEDIT program. These routines also call routines in file browsecif.tcl.
#!/bin/sh
# the next line restarts this script using wish found in the path\
exec wish "$0" "$@"
# If this does not work, change the #!/usr/bin/wish line below
# to reflect the actual wish location and delete all preceeding lines
#
# (delete here and above)
#!/usr/bin/wish
#
# $Id: cifedit.tcl,v 1.9 2003/07/25 00:32:11 toby Exp toby $
# A routine for editing CIFs 
# 
# Prerequisites:
#  1) BWidget routines be available (tested with version 1.2.1)
#      if the BWidget routines are not in a subdirectory of where this
#      file is located & is not in the normal tcl auto_path, add the 
#      path below by uncommenting & updating the next line:
#      lappend auto_path /usr/local/tcltk/
#      e.g. use /usr/local/tcltk/ if the package is in /usr/local/tcltk/BWidget-1.2.1
#
#  2) file browsecif.tcl must be in the same directory as this file
#
#  3) file CIF_index must be in the same directory as this file
#      This file is an index to the CIF dictionaries in use and is generated
#      using routines in indexCIFdict.tcl
#     note that variable CIF(cif_path) dictates where these dictionary 
#     files will be found. The path where these dictionaries were read
#     in indexCIFdict.tcl is saved in the CIF_index file. The 
#     directory where this file is located, as well asl a directory
#     below this named data (../data/) is also searched.

# Note the Maximum CIF size is set by this variable:
set CIF(maxvalues) 100000
# where CIF(maxvalues) is the number of data items (each value in a loop
# is counted). 
# 100,000 => 25Mb task size (Linux) & ~0.5Mb CIF
# use "set CIF(maxvalues) 0" to defeat the limit

# Note the maximum number of "rows" that can be displayed by selecting a row
# is set by this variable:
set CIF(maxRows) 100

# the maximum number of characters/line in CIF size is set by this variable:
set CIF(maxlinelength) 80

if {[llength $argv] != 1 || ![file exists [lindex $argv 0]]} {
    set file [tk_getOpenFile -title "Select CIF" -parent . \
	    -defaultextension cif -filetypes {{"CIF data" ".cif .CIF"}}]
    if {$file == ""} {exit}
    set argv $file
} else {
   set argv [lindex $argv 0]
}

# where is this file running from?
set script [info script]
# translate links -- go six levels deep
foreach i {1 2 3 4 5 6} {
    if {[file type $script] == "link"} {
	set link [file readlink $script]
	if { [file  pathtype  $link] == "absolute" } {
	    set script $link
	} {
	    set script [file dirname $script]/$link
	}
    } else {
	break
    }
}
# fixup relative paths
if {[file pathtype $script] == "relative"} {
    set script [file join [pwd] $script]
}
set scriptdir [file dirname $script ]

# where to find the BWidget program
lappend auto_path $scriptdir

source [file join $scriptdir browsecif.tcl]

if {$tcl_version < 8.2} {
    tk_dialog .error {Old Tcl/Tk} \
	    "Sorry, the CIF Editor requires version 8.2 or later of the Tcl/Tk package. This is $tcl_version" \
	    warning 0 Sorry
    exit
}

if [catch {package require BWidget}] {
    tk_dialog .error {No BWidget} \
	    "Sorry, the CIF Editor requires the BWidget package" \
	    warning 0 Sorry
    exit
}

if {![file exists [file join $scriptdir CIF_index]]} {
    MyMessageBox -parent . -title "No CIF index" \
	    -message "File CIF_index was not found in directory $scriptdir. Without this file, CIF definitions can not be read and editing is not recommended. See routine indexCIFdict.tcl for info on creating CIF_index" \
	    -icon error -type {"Oh darn"} -default "oh darn"
} elseif [catch {
    source  [file join $scriptdir CIF_index]
} errmsg] {
    MyMessageBox -parent . -title "CIF index error" \
	    -message "An error occured reading file CIF_index (directory $scriptdir). Without this file, CIF definitions can not be read and editing is not recommended. See routine indexCIFdict.tcl for info on creating CIF_index. Error: $errmsg" \
	    -icon error -type {"Oh darn"} -default "oh darn"
}

# add the standard location of the dictionary files to the dictionary search path
lappend CIF(cif_path) [file join $scriptdir dict]

wm withdraw .
# create window/text widget for CIF file
catch {destroy [set filew .cif]}
toplevel $filew
wm title $filew "CIF file contents"
set CIF(txt) $filew.t
# shrink file viewer on small screens & add a scroll bar
set w 80 
if {[winfo screenwidth .] < 700} {set w 60}
grid [text $CIF(txt) -height 10 -width $w -yscrollcommand "$filew.s set" \
	-wrap none] -column 0 -row 0 -sticky news
grid [scrollbar $filew.s -command "$CIF(txt) yview"] \
	-column 1 -row 0 -sticky ns
if {[winfo screenwidth .] < 700} {
    grid [scrollbar $filew.sx -command "$CIF(txt) xview" \
	    -orient horizontal] -column 0 -row 1 -sticky ew
    $CIF(txt) config -xscrollcommand "$filew.sx set" 
}
grid columnconfig $filew 0 -weight 1
grid rowconfig $filew 0 -weight 1
grid [frame $filew.f] -column 0 -columnspan 2 -row 2 -sticky news
grid columnconfig $filew.f 4 -weight 1
grid [button $filew.f.edit -text "Open for Editing" \
	-command "EditCIFBox $filew.f.edit"] -column 0 -row 0
grid [button $filew.f.b -text "go to line:" \
	-command {MarkGotoLine $CIF(goto)}] -column 5 -row 0
grid [entry $filew.f.e -textvariable CIF(goto) -width 6] -column 6 -row 0
set CIF(goto) 1
bind $filew.f.e  "$filew.f.b invoke"

# create window/text widget for the CIF definition
catch {destroy [set defw .def]}
toplevel $defw
wm title $defw "CIF definitions"
set CIF(defBox) $defw.t
# shrink definition viewer on small screens
set w 45
set h 18
if {[winfo screenwidth .] <= 800} {set w 35; set h 12}
grid [text $CIF(defBox) -width $w -height $h -xscrollcommand "$defw.x set" \
	-yscrollcommand "$defw.y set" -wrap word] -column 0 -row 0 -sticky news
grid [scrollbar $defw.y -command "$CIF(defBox) yview"] -column 1 -row 0 -sticky ns
grid [scrollbar $defw.x -command "$CIF(defBox) xview" \
	-orient horizontal] -column 0 -row 1 -sticky ew
grid columnconfig $defw 0 -weight 1
grid rowconfig $defw 0 -weight 1
# hide it
wm withdraw $defw
update

set CIF(showCIF) 0
set CIF(showDefs) 0
# make window for the CIF Editor
set CIF(browser) .browser
catch {destroy $CIF(browser)}
toplevel $CIF(browser) 
wm title $CIF(browser) "CIF Editor"
# put box in upper left
wm withdraw $CIF(browser)
wm geometry $CIF(browser) +0+0
update
wm deiconify $CIF(browser)
grid [frame $CIF(browser).box] -column 0 -row 2 -sticky ew
set col 0
grid [button $CIF(browser).box.c -text Close \
	-command "ConfirmDestroy $CIF(browser) [list $argv.new]"] \
	-column $col -row 1 -sticky w
grid columnconfig $CIF(browser).box $col -weight 1
incr col
grid [button $CIF(browser).box.f -text "Show CIF Contents" \
	-command "ShowCIFWindow $CIF(browser).box.f $filew $CIF(browser)"] \
	-column $col -row 1
incr col
grid [button $CIF(browser).box.d -text "Show CIF Definitions" \
	-command "ShowDefWindow $CIF(browser).box.d $defw $CIF(browser) $filew"] \
	-column $col -row 1 -sticky w
grid columnconfig $CIF(browser).box $col -weight 1

incr col
grid [button $CIF(browser).box.v -text "Validate CIF" \
	-command "ValidateAllItems $CIF(txt)"] -column $col -row 1 -sticky w
incr col
grid [button $CIF(browser).box.u -text "Undo" -command UndoChanges \
	-state disabled] \
	-column $col -row 1 -sticky w
incr col
grid [button $CIF(browser).box.r -text "Redo" -command RedoChanges \
	-state disabled] \
	-column $col -row 1 -sticky w

incr col
grid [label $CIF(browser).box.3 -text "Mode:"] -column $col -row 1
incr col
grid [radiobutton $CIF(browser).box.4 -text "browse" \
	-variable CIF(editmode) -value 0 -command RepeatLastshowCIFvalue \
	] -column $col -row 1
incr col
grid [radiobutton $CIF(browser).box.5 -text "edit" \
	-variable CIF(editmode) -value 1 -command RepeatLastshowCIFvalue \
	] -column $col -row 1
incr col
grid [button $CIF(browser).box.6 -text "Save" \
	-command "SaveCIFtoFile [list $argv]" -state disabled] -column $col -row 1
set CIF(editmode) 0

wm protocol $filew WM_DELETE_WINDOW \
	"ShowCIFWindow $CIF(browser).box.f $filew $CIF(browser)"
wm protocol $defw WM_DELETE_WINDOW \
	"ShowDefWindow $CIF(browser).box.d $defw $CIF(browser) $filew"
wm protocol $CIF(browser) WM_DELETE_WINDOW \
	"ConfirmDestroy $CIF(browser) [list $argv.new]"

trace variable CIF(changes) w "EnableSaveEdits $CIF(browser).box.6"
proc EnableSaveEdits {w args} {
    global CIF
    if {$CIF(changes)} {
	$w config -state normal
    } else {
	$w config -state disabled
    }
}
trace variable CIF(undolist) w "EnableUndo $CIF(browser).box.u undolist"
trace variable CIF(redolist) w "EnableUndo $CIF(browser).box.r redolist"
proc EnableUndo {w var args} {
    global CIF
    if {[llength $CIF($var)] > 0} {
	$w config -state normal
    } else {
	$w config -state disabled
    }
}


proc SaveCIFtoFile {file} {
    global CIF
    set CIF(changes) 0
    set CIF(undolist) {}
    set CIF(redolist) {}
    # at least for the moment, keep the previous version
    file rename -force $file ${file}.old
    set fp [open $file w]
    puts -nonewline $fp [$CIF(txt) get 1.0 end]
    close $fp
}

proc ConfirmDestroy {frame file} {
    global CIF
    if {$CIF(changes) != 0} {
	set ans [MyMessageBox -parent . -title "Discard Changes?" \
		-message "You have changed this CIF. Do you want to save or discard your changes?" \
		-icon question -type {Save Discard Cancel} -default Save]
	if {$ans == "save"} {
	    SaveCIFtoFile $file
	    destroy $frame
	} elseif {$ans == "discard"} {
	    destroy $frame
	}
    } else {
	destroy $frame
    }
    exit
}

proc ShowDefWindow {button window master cifw} {
    global CIF tcl_platform
    if {[lindex [$button cget -text] 0] == "Show"} {
	$button config -text "Hide CIF Definitions"
	if {$CIF(showDefs) == 0} {
	    # approximate size of border
	    if {$tcl_platform(platform) == "windows"} {
		# on windows border is included in computations
		set border 0
	    } else {
		set border [expr [winfo rooty $master] - [winfo vrooty $master]]
	    }
	    # put the window under the browser/file window on the first call
	    if {[winfo ismapped $cifw]} {
		# next to the cif contents window
		set x [expr 5 + [winfo x $cifw] + [winfo width $cifw]]
		set y [expr [winfo rooty $master] + [winfo height $master] + \
			   $border]
	    } else {
		# under the browser
		set x [winfo x $master]
		set y [expr [winfo rooty $master] + [winfo height $master] + \
			   $border]
	    }
	    wm geometry $window +$x+$y
	    update
	    set CIF(showDefs) 1
	}
	wm deiconify $window
    } else {
	$button config -text "Show CIF Definitions"
	wm withdraw $window
    }
}

proc ShowCIFWindow {button window master} {
    global CIF tcl_platform
    if {[lindex [$button cget -text] 0] == "Show"} {
	$button config -text "Hide CIF Contents"
	# approximate size of border
	if {$tcl_platform(platform) == "windows"} {
	    # on windows border is included in computations
	    set border 0
	} else {
	    set border [expr [winfo rooty $master] - [winfo vrooty $master]]
	}
	if {$CIF(showCIF) == 0} {
	    # put the window under the browser on the first call
	    set x [winfo x $master]
	    set y [expr [winfo rooty $master] + [winfo height $master] + \
			   $border]
	    wm geometry $window +$x+$y
	    update
	    set CIF(showCIF) 1
	    set CIF(showDefs) 0
	}
	wm deiconify $window
    } else {
	$button config -text "Show CIF Contents"
	wm withdraw $window
    }
}

proc EditCIFBox {button} {
    global CIF
    if {[$button cget -text] == "Open for Editing"} {
	if {[CheckForCIFEdits]} return
	$button config -text "Close Editing"
	# save the current mode
	set CIF(oldeditmode) $CIF(editmode)
	set CIF(editmode) 0
	RepeatLastshowCIFvalue
	$CIF(browser).box.3 config -fg gray
	# disable most of the editor to avoid conflicts
	foreach w {4 5 6 c f u r} {
	    $CIF(browser).box.$w config -state disabled
	}
	$CIF(txt) config -state normal
	# prevent other windows from functioning
	#grab $CIF(txt)
    } else {
	$button config -text "Open for Editing"
	# reenable the other windows
	#grab release $CIF(txt)
	$CIF(browser).box.3 config -fg black
	foreach w {4 5 6 c f} {
	    $CIF(browser).box.$w config -state normal
	}
	incr CIF(changes)
	set CIF(editmode) $CIF(oldeditmode)
	$CIF(txt) config -state disabled
	RepeatLastshowCIFvalue
	# need to parse the revised CIF
	foreach i $CIF(blocklist) {
	    global block$i
	    unset block$i
	}

	pleasewait "Parsing CIF"  CIF(status)

	set CIF(maxblocks) [ParseCIF $CIF(txt)]

	# update the blocklist & display them
	set CIF(blocklist) {}
	if {[array names block0] != ""} {
	    set i 0
	} else {
	    set i 1
	}
	global argv
	set errors ""
	for {} {$i <= $CIF(maxblocks)} {incr i} {
	    lappend CIF(blocklist) $i
	    if {![catch {set block${i}(errors)}]} {
		if {$errors ==""} {set errors "Errors in file $argv\n\n"}
		append errors "Data block #$i [set block${i}(data_)]\n"
		append errors "======================================\n\n"
		append errors "[set block${i}(errors)]\n"
	    }
	}
	
	if {$CIF(blocklist) != ""} {
	    CIFBrowser $CIF(txt) $CIF(blocklist) "" $CIF(browser)
	}
	catch {donewait}
	if {$errors != ""} {
	    if {[MyMessageBox -parent $CIF(txt) -title "CIF errors" \
		    -message "Note: $errors" \
		    -icon error -type {Continue "Save Errors"} \
		    -default continue] == "continue"} {return}
	    set file [tk_getSaveFile -parent $CIF(txt) \
		    -filetypes {{"text file" .txt}} -defaultextension .txt]
	    if {$file == ""} return
	    set fp [open $file w]
	    puts $fp $error
	    close $fp
	}
    }
}

# validate the fields in a CIF
proc ValidateAllItems {txt} {
    global CIF argv
    set errors 0
    set blocklist $CIF(blocklist)
    foreach n $blocklist {
	set block block$n
	global block$n
	# make a list of data names in loops
	set looplist {}
	foreach loop [array names block$n loop_*] {
	    eval lappend looplist [set block${n}($loop)]
	}
	# clear out the block error list
	set ${block}(validate) {}
	# loop over data names
	foreach dataname [lsort [array names block$n _*]] {
	    if {[lsearch $looplist $dataname] == -1} {
		set mark [set ${block}($dataname)]
		set item [$CIF(txt) get $mark.l $mark.r]
		set err [ValidateCIFName $dataname]
		if {$err != ""} {
		    append ${block}(validate) "$err\n"
		    incr errors
		} else {
		    set err [ValidateCIFItem $dataname $item]
		    if {$err != ""} {
			append ${block}(validate) "$err\n"
			incr errors
		    }
		}
	    } else {
		# looped names
		set err [ValidateCIFName $dataname]
		if {$err != ""} {
		    append ${block}(validate) "$err\n"
		    incr errors
		    continue
		}
		set index 0
		foreach mark [set ${block}($dataname)] {
		    incr index
		    set item [$CIF(txt) get $mark.l $mark.r]
		    set err [ValidateCIFItem $dataname $item]
		    if {$err != ""} {
			append ${block}(validate) "$err (loop index #$index)\n"
			incr errors
		    }
		}
	    }
	}
	if {[catch {set ${block}(validateicon)} err]} {
	    if {[set ${block}(validate)] != ""} {
		# insert validation error pointer
		$CIF(tree) insert 0 block$n [incr CIF(tree_lastindex)] \
			-text "Validation-errors" -image [Bitmap::get undo] \
			-data block$n
		set ${block}(validateicon) $CIF(tree_lastindex)
	    }
	}
    }
    if {$errors > 0} {
	set i 0
	set msg "$errors validation errors were found in $argv.\n\n"
	foreach n $blocklist {
	    incr i
	    set block block$n
	    global block$n
	    # get the block name
	    if {[set err [set block${n}(validate)]] != ""} {
		append msg "Data block #$i [set block${n}(data_)]\n"
		append msg "======================================\n\n"
		append msg "[set block${n}(validate)]\n"
	    }
	}
	if {[MyMessageBox -parent $CIF(tree) -title "Validation errors" \
		-message $msg -icon error -type {OK "Save Errors"} -default "ok"] \
		== "ok"} return
	set file [tk_getSaveFile -parent $CIF(tree) \
		-filetypes {{"text file" .txt}} -defaultextension .txt]
	if {$file == ""} return
	set fp [open $file w]
	puts $fp $msg
	close $fp
    }
}

# hide the browser window
wm iconify $CIF(browser)
# center the CIF text window
wm withdraw $filew
set x [expr {[winfo screenwidth $filew]/2 - [winfo reqwidth $filew]/2 \
            - [winfo vrootx [winfo parent $filew]]}]
set y [expr {[winfo screenheight $filew]/2 - [winfo reqheight $filew]/2 \
	- [winfo vrooty [winfo parent $filew]]}]
wm geometry $filew +$x+$y
update
wm deiconify $filew

pleasewait "while loading CIF file" CIF(status) $filew {Quit exit}

set CIF(maxblocks) [ParseCIF $CIF(txt) $argv]
set CIF(blocklist) {}
if {[array names block0] != ""} {
    set i 0
} else {
    set i 1
}
set errors ""
for {} {$i <= $CIF(maxblocks)} {incr i} {
    lappend CIF(blocklist) $i
    if {![catch {set block${i}(errors)}]} {
	if {$errors ==""} {set errors "Errors in file $argv\n\n"}
	append errors "Data block #$i [set block${i}(data_)]\n"
	append errors "======================================\n\n"
	append errors "[set block${i}(errors)]\n"
    }
}

if {$CIF(blocklist) != ""} {
    CIFOpenBrowser $CIF(browser)
    wm title $CIF(browser) "CIF Browser: file $argv"
    CIFBrowser $CIF(txt) $CIF(blocklist) "" $CIF(browser)
    wm deiconify $CIF(browser)
    # hide the CIF window
    wm withdraw $filew
}
donewait
if {$errors != ""} {
    if {[MyMessageBox -parent $CIF(txt) -title "CIF errors" \
	    -message "Note: this CIF has errors.\n\n$errors" \
	    -icon error -type {Continue "Save Errors"} \
	    -default continue] != "continue"} {
	set file [tk_getSaveFile -parent $CIF(txt) \
		-filetypes {{"text file" .txt}} -defaultextension .txt]
	if {$file != ""} {
	    set fp [open $file w]
	    puts $fp $errors
	    close $fp
	}
    }
}