File browsecif.tcl

These routines are the basic CIF browser and editing routines that used in a variety of applications.
# $Id: browsecif.tcl,v 1.7 2003/07/25 00:29:10 toby Exp toby $

# possible future work:
#   implement adding a new data item to a CIF? Delete one?
#   can I bind to the tree window only? (.browser.pw.f0.frame.lf.tree)
#   clean up use of block arrays. Should the prefix be changable? Use
#    the same syntax throughout

#------------------------------------------------------------------------------
# Misc Tcl/Tk utility routines follow
#------------------------------------------------------------------------------
#	Message box code that centers the message box over the parent.
#          or along the edge, if too close, 
#          but leave a border along +x & +y for reasons I don't remember
#       It also allows the button names to be defined using 
#            -type $list  -- where $list has a list of button names
#       larger messages are placed in a scrolled text widget
#       capitalization is now ignored for -default
#       The command returns the name button in all lower case letters
#       otherwise see  tk_messageBox for a description
#
#       This is a modification of tkMessageBox (msgbox.tcl v1.5)
#
proc MyMessageBox {args} {
    global tkPriv tcl_platform

    set w tkPrivMsgBox
    upvar #0 $w data

    #
    # The default value of the title is space (" ") not the empty string
    # because for some window managers, a 
    #		wm title .foo ""
    # causes the window title to be "foo" instead of the empty string.
    #
    set specs {
	{-default "" "" ""}
        {-icon "" "" "info"}
        {-message "" "" ""}
        {-parent "" "" .}
        {-title "" "" " "}
        {-type "" "" "ok"}
        {-helplink "" "" ""}
    }

    tclParseConfigSpec $w $specs "" $args

    if {[lsearch {info warning error question} $data(-icon)] == -1} {
	error "bad -icon value \"$data(-icon)\": must be error, info, question, or warning"
    }
    if {![string compare $tcl_platform(platform) "macintosh"]} {
      switch -- $data(-icon) {
          "error"     {set data(-icon) "stop"}
          "warning"   {set data(-icon) "caution"}
          "info"      {set data(-icon) "note"}
	}
    }

    if {![winfo exists $data(-parent)]} {
	error "bad window path name \"$data(-parent)\""
    }

    switch -- $data(-type) {
	abortretryignore {
	    set buttons {
		{abort  -width 6 -text Abort -under 0}
		{retry  -width 6 -text Retry -under 0}
		{ignore -width 6 -text Ignore -under 0}
	    }
	}
	ok {
	    set buttons {
		{ok -width 6 -text OK -under 0}
	    }
          if {![string compare $data(-default) ""]} {
		set data(-default) "ok"
	    }
	}
	okcancel {
	    set buttons {
		{ok     -width 6 -text OK     -under 0}
		{cancel -width 6 -text Cancel -under 0}
	    }
	}
	retrycancel {
	    set buttons {
		{retry  -width 6 -text Retry  -under 0}
		{cancel -width 6 -text Cancel -under 0}
	    }
	}
	yesno {
	    set buttons {
		{yes    -width 6 -text Yes -under 0}
		{no     -width 6 -text No  -under 0}
	    }
	}
	yesnocancel {
	    set buttons {
		{yes    -width 6 -text Yes -under 0}
		{no     -width 6 -text No  -under 0}
		{cancel -width 6 -text Cancel -under 0}
	    }
	}
	default {
#	    error "bad -type value \"$data(-type)\": must be abortretryignore, ok, okcancel, retrycancel, yesno, or yesnocancel"
	    foreach item $data(-type) {
		lappend buttons [list [string tolower $item] -text $item -under 0]
	    }
	}
    }

    if {[string compare $data(-default) ""]} {
	set valid 0
	foreach btn $buttons {
	    if {![string compare [lindex $btn 0] [string tolower $data(-default)]]} {
		set valid 1
		break
	    }
	}
	if {!$valid} {
	    error "invalid default button \"$data(-default)\""
	}
    }

    # 2. Set the dialog to be a child window of $parent
    #
    #
    if {[string compare $data(-parent) .]} {
	set w $data(-parent).__tk__messagebox
    } else {
	set w .__tk__messagebox
    }

    # 3. Create the top-level window and divide it into top
    # and bottom parts.

    catch {destroy $w}
    toplevel $w -class Dialog
    wm title $w $data(-title)
    wm iconname $w Dialog
    wm protocol $w WM_DELETE_WINDOW { }
    wm transient $w $data(-parent)
    if {![string compare $tcl_platform(platform) "macintosh"]} {
	unsupported1 style $w dBoxProc
    }

    frame $w.bot
    pack $w.bot -side bottom -fill both
    frame $w.top
    pack $w.top -side top -fill both -expand 1
    if {$data(-helplink) != ""} {
#	frame $w.help
#	pack $w.help -side top -fill both
	pack [button $w.top.1 -text Help -bg yellow \
		-command "MakeWWWHelp $data(-helplink)"] \
		-side right -anchor ne
	bind $w  "MakeWWWHelp $data(-helplink)"
    }
    if {[string compare $tcl_platform(platform) "macintosh"]} {
	$w.bot configure -relief raised -bd 1
	$w.top configure -relief raised -bd 1
    }

    # 4. Fill the top part with bitmap and message (use the option
    # database for -wraplength and -font so that they can be
    # overridden by the caller).

    option add *Dialog.msg.wrapLength 6i widgetDefault

    if {[string length $data(-message)] > 300} {
	if {![string compare $tcl_platform(platform) "macintosh"]} {
	    option add *Dialog.msg.t.font system widgetDefault
	} else {
	    option add *Dialog.msg.t.font {Times 18} widgetDefault
	}
	frame $w.msg
	grid [text  $w.msg.t  \
		-height 20 -width 55 -relief flat -wrap word \
		-yscrollcommand "$w.msg.rscr set" \
		] -row 1 -column 0 -sticky news
	grid [scrollbar $w.msg.rscr  -command "$w.msg.t yview" \
		] -row 1 -column 1 -sticky ns
	# give extra space to the text box
	grid columnconfigure $w.msg 0 -weight 1
	grid rowconfigure $w.msg 1 -weight 1
	$w.msg.t insert end $data(-message)
    } else {
	if {![string compare $tcl_platform(platform) "macintosh"]} {
	    option add *Dialog.msg.font system widgetDefault
	} else {
	    option add *Dialog.msg.font {Times 18} widgetDefault
	}
	label $w.msg -justify left -text $data(-message)
    }
    pack $w.msg -in $w.top -side right -expand 1 -fill both -padx 3m -pady 3m
    if {[string compare $data(-icon) ""]} {
	label $w.bitmap -bitmap $data(-icon)
	pack $w.bitmap -in $w.top -side left -padx 3m -pady 3m
    }

    # 5. Create a row of buttons at the bottom of the dialog.

    set i 0
    foreach but $buttons {
	set name [lindex $but 0]
	set opts [lrange $but 1 end]
      if {![llength $opts]} {
	    # Capitalize the first letter of $name
          set capName [string toupper \
		    [string index $name 0]][string range $name 1 end]
	    set opts [list -text $capName]
	}

      eval button [list $w.$name] $opts [list -command [list set tkPriv(button) $name]]

	if {![string compare $name [string tolower $data(-default)]]} {
	    $w.$name configure -default active
	}
      pack $w.$name -in $w.bot -side left -expand 1 -padx 3m -pady 2m

	# create the binding for the key accelerator, based on the underline
	#
	set underIdx [$w.$name cget -under]
	if {$underIdx >= 0} {
	    set key [string index [$w.$name cget -text] $underIdx]
          bind $w   [list $w.$name invoke]
          bind $w   [list $w.$name invoke]
	}
	incr i
    }

    # 6. Create a binding for  on the dialog if there is a
    # default button.

    if {[string compare $data(-default) ""]} {
      bind $w  [list tkButtonInvoke $w.[string tolower $data(-default)]]
    }

    # 7. Withdraw the window, then update all the geometry information
    # so we know how big it wants to be, then center the window in the
    # display and de-iconify it.

    wm withdraw $w
    update idletasks
    set wp $data(-parent)
    # center the new window in the middle of the parent
    set x [expr [winfo x $wp] + [winfo width $wp]/2 - \
	    [winfo reqwidth $w]/2 - [winfo vrootx $wp]]
    set y [expr [winfo y $wp] + [winfo height $wp]/2 - \
	    [winfo reqheight $w]/2 - [winfo vrooty $wp]]
    # make sure that we can see the entire window
    set xborder 10
    set yborder 25
    if {$x < 0} {set x 0}
    if {$x+[winfo reqwidth $w] +$xborder > [winfo screenwidth $w]} {
	incr x [expr \
		[winfo screenwidth $w] - ($x+[winfo reqwidth $w] + $xborder)]
    }
    if {$y < 0} {set y 0}
    if {$y+[winfo reqheight $w] +$yborder > [winfo screenheight $w]} {
	incr y [expr \
		[winfo screenheight $w] - ($y+[winfo reqheight $w] + $yborder)]
    }
    wm geom $w +$x+$y
    update
    wm deiconify $w

    # 8. Set a grab and claim the focus too.

    catch {set oldFocus [focus]}
    catch {set oldGrab [grab current $w]}
    catch {
	grab $w
	if {[string compare $data(-default) ""]} {
	    focus $w.[string tolower $data(-default)]
	} else {
	    focus $w
	}
    }

    # 9. Wait for the user to respond, then restore the focus and
    # return the index of the selected button.  Restore the focus
    # before deleting the window, since otherwise the window manager
    # may take the focus away so we can't redirect it.  Finally,
    # restore any grab that was in effect.

    tkwait variable tkPriv(button)
    catch {focus $oldFocus}
    destroy $w
    catch {grab $oldGrab}
    return $tkPriv(button)
}

# tell'em what is happening
proc pleasewait {{message {}} {statusvar {}} {parent .} {button ""}} {
    catch {destroy .msg}
    toplevel .msg
    wm transient .msg [winfo toplevel .]
    pack [frame .msg.f -bd 4 -relief groove] -padx 5 -pady 5
    pack [message .msg.f.m -text "Please wait $message"] -side top
    if {$statusvar != ""} {
	pack [label .msg.f.status -textvariable $statusvar] -side top
    }
    if {$button != ""} {
	pack [button .msg.f.button -text [lindex $button 0] \
		-command [lindex $button 1]] -side top
    }
    wm withdraw .msg
    update idletasks
    # place the message on top of the parent window
    set x [expr [winfo x $parent] + [winfo width $parent]/2 - \
	    [winfo reqwidth .msg]/2 - [winfo vrootx $parent]]
    if {$x < 0} {set x 0}
    set y [expr [winfo y $parent] + [winfo height $parent]/2 - \
	    [winfo reqheight .msg]/2 - [winfo vrooty $parent]]
    if {$y < 0} {set y 0}
    wm geom .msg +$x+$y
    update
    wm deiconify .msg
    global makenew
    set makenew(OldGrab) ""
    set makenew(OldFocus) ""
    # save focus & grab
    catch {set makenew(OldFocus) [focus]}
    catch {set makenew(OldGrab) [grab current .msg]}
    catch {grab .msg}
    update
}

# clear the wait message
proc donewait {} {
    global makenew
    catch {destroy .msg}
    # reset focus & grab
    catch {
	if {$makenew(OldFocus) != ""} {
	    focus $makenew(OldFocus)
	}
    }
    catch {
	if {$makenew(OldGrab) != ""} {
	    grab $makenew(OldGrab)
	}
    }
}

# this routine is used to fix up tk_optionMenu widgets that have too many
# entries for a single list -- by using cascades
proc FixBigOptionMenu {widget enum "cmd {}"} {
    # max entries
    set max 12
    set menu [winfo children $widget]
    $menu delete 0 end
    eval destroy [winfo children $menu]
    set var [$widget cget -textvariable]
    # do we need a cascade?
    if {[set n [llength $enum]] <= $max} {
	# no
	foreach l $enum {
	    $menu add radiobutton -value $l -label $l -variable $var \
		    -command $cmd
	}
	return
    }
    # yes
    set nmenus [expr int(($max + $n - 1 )/ (1.*$max))]
    set nper [expr 1 + $n/$nmenus]
    if {$nper > $max} {set nper $max}
    for {set i 0} {$i < $n} {incr i $nper} {
	set j [expr $i + $nper -1]
	set sublist [lrange $enum $i $j]
	$menu add cascade -label "[lindex $sublist 0]-[lindex $sublist end]" \
		-menu $menu.$i
	menu $menu.$i
	foreach l $sublist {
	    $menu.$i add radiobutton -value $l -label $l -variable $var \
		    -command $cmd
	}
    }
}

# this routine is used to add . and ? in a cascade for enum lists
proc AddSpecialEnumOpts {widget "cmd {}"} {
    set menu [winfo children $widget]
    set var [$widget cget -textvariable]

    # add the cascade and entries to it
    $menu add cascade -label "(special values)" -menu $menu.special
    menu $menu.special
    $menu.special add radiobutton -value . -command $cmd \
	    -label "Inapplicable (.)" -variable $var
    $menu.special add radiobutton -value ? -command $cmd \
	    -label "Unknown (?)" -variable $var
}

#------------------------------------------------------------------------------
# end of Misc Tcl/Tk utility routines
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# ParseCIF reads and parses a CIF file putting the contents of
# each block into arrays block1, block2,... in the caller's level
#    the name of the block is saved as blockN(data_)
# data names items are saved as blockN(_data_name) = marker_number
#    where CIF data names are converted to lower case
#    and marker_number.l marker_number.r define the range of the value
# for looped data names, the data items are included in a list:
#    blockN(_cif_name) = {marker1 marker2 ...}
# the contents of each loop are saved as blockN(loop_M)
#
# if the filename is blank or not specified, the current contents
#    of the text widget, $txt, is parsed.
#
# The proc returns the number of blocks that have been read or a
#    null string if the file cannot be opened
#
# This parser does some error checking [errors are reported in blockN(error)]
#    but the parser could get confused if the CIF has invalid syntax
#
proc ParseCIF {txt "filename {}"} {
    global CIF tcl_version
    global CIF_dataname_index

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

    if {$filename != ""} {
	if [catch {
	    $txt configure -state normal
	    set fp [open $filename r]
	    $txt insert end [read $fp]
	    close $fp
	    $txt configure -state disabled
	}] {
	    return ""
	}
    }

    # maximum size of file
    set maxvalues 0
    catch {
	set maxvalues $CIF(maxvalues)
    }

    set CIF(undolist) {}
    set CIF(redolist) {}
    set pos 1.0
    set blocks 0
    set EOF 1
    set dataname {}
    set CIF(markcount) -1
    # this flags where we are w/r a loop_
    #    -1 not in a loop
    #     0 reading a loop header (data names)
    #     1 reading the data items in a loop
    set loopflag -1
    set loopnum -1
    # loop over tokens
    while {$EOF} {
	if {$CIF(markcount) % 1000 == 0} {
	    $txt see $pos
	    set CIF(status) "($CIF(markcount) values read.)"
	    update
	    # are we over the limit?
	    if {$maxvalues > 0 && $CIF(markcount) > $maxvalues} {
		donewait
		set msg "Too many data values to parse; stopping at $CIF(markcount), line [lindex [split $pos .] 0].\n\nIf your computer has sufficient memory to read more, increase CIF(maxvalues) in cifedit.tcl"
		set ans [MyMessageBox -parent . -title "CIF Too big" \
			-message $msg -icon error -type "{Stop Parsing}" \
			-default "stop parsing"]
		
		return $blocks
	    }
	}
	# skip forward to the first non-blank character
	set npos [$txt search -regexp {[^[:space:]]} $pos end]
	# is this the end?
	if {$npos == "" || \
		[lindex [split $npos .] 0] < [lindex [split $pos .] 0] } {
	    set EOF 0
	    break
	} else {
	    set pos $npos
	}

	# is this a comment, if so skip to next line
	if {[$txt get $pos] == "#"} {
	    set pos [$txt index "$pos + 1 line linestart"]
	    continue
	}

	# find end of token
	set epos [$txt search -regexp {[[:space:]]} $pos "$pos lineend"]
	if {$epos == ""} {
	    set epos [$txt index "$pos lineend"]
	}

	set token [$txt get $pos $epos]

	if {[string tolower [string range $token 0 4]] == "data_"} {
	    # this is the beginning of a data block
	    incr blocks
	    set blockname [string range $token 5 end]
	    global block$blocks
	    catch {unset block$blocks}
	    set block${blocks}(data_) $blockname
	    set loopnum -1
	    if {$dataname != ""} {
		# this is an error -- data_ block where a data item is expected
		append block${blocks}(errors) "No data item was found for $dataname near line [lindex [split $pos .] 0]\n"
		set dataname {}
	    }
	    # move forward past current token
	    set pos [$txt index "$epos +1c"]
	    continue
	}
	
	if {[$txt get $pos] == "_"} {
	    # this is a cif data name
	    if {$dataname != ""} {
		# this is an error -- data name where a data item is expected
		append block${blocks}(errors) "No data item was found for $dataname near line [lindex [split $pos .] 0]\n"
	    }
	    # convert it to lower case & save
	    set dataname [string tolower $token]

	    # are we in a loop header or loop body?
	    if {$loopflag == 0} {
		# in a loop header, save the names in the loop list
		lappend looplist $dataname
		# check the categories used in the loop
		set category {}
		catch {
		    set category [lindex \
			    [lindex $CIF_dataname_index($dataname) 1] 5]
		}
		# don't worry if we don't have a category
		if {$category != ""} {
		    if {$catlist == ""} {
			set catlist $category
		    } elseif {[lsearch $catlist $category] == -1} {
			# error two categories in a loop
			lappend catlist $category
			append block${blocks}(errors) \
				"Multiple categories ($catlist) in a loop_ for $dataname at line [lindex [split $pos .] 0]\n"
		    }
		}
		
		if {$blocks == 0} {
		    # an error -- a loop_ before a data_ block start
		    global block${blocks}
		    set block${blocks}(data_) undefined
		    append block${blocks}(errors) \
			    "A loop_ begins before a data_ block is defined (line [lindex [split $pos .] 0])\n"
		}
		set block${blocks}(loop_${loopnum}) $looplist
		# clear the array element for the data item 
		# -- should not be needed for a valid CIF but if a name is used
		# -- twice in the same block, want to wipe out the 1st data
		catch {
		    if {[set block${blocks}($dataname)] != ""} {
			# this is an error -- repeated data name
			append block${blocks}(errors) \
				"Data name $dataname is repeated near line [lindex [split $pos .] 0]\n"
		    }   
		    set block${blocks}($dataname) {}
		}
		set dataname {}
	    } elseif {$loopflag > 0} {
		# in a loop body, so the loop is over
		set loopflag -1
	    }
	    # move forward past current token
	    set pos [$txt index "$epos +1c"]
	    continue
	}
	
	if {[string tolower [string range $token 0 4]] == "loop_"} {
	    set loopflag 0
	    incr loopnum
	    set looplist {}
	    set catlist {}
	    set block${blocks}(loop_${loopnum}) {}
	    # move forward past current token
	    set pos [$txt index "$epos +1c"]
	    continue
	}

	# keywords not matched, must be some type of data item
	set item {}
	incr CIF(markcount)
	
	if {[$txt get "$pos linestart"] == ";" && \
		[$txt index $pos] == [$txt index "$pos linestart"]} {
	    # multiline entry with semicolon termination
	    set epos [$txt search -regexp {^;} "$pos + 1 line linestart"]
	    if {$epos == ""} {
		set epos end
		append block${blocks}(errors) \
			"Unmatched semicolon for $dataname starting at line [lindex [split $pos .] 0]\n"
	    }

	    $txt mark set $CIF(markcount).l "$pos linestart"
	    $txt mark set $CIF(markcount).r "$epos + 1c"
	    $txt mark gravity $CIF(markcount).l left
	    $txt mark gravity $CIF(markcount).r right
	    set item [$txt get "$pos linestart" "$epos +1c"]
	    # move forward past current token
	    set pos [$txt index "$epos + 1c"]
	} elseif {[$txt get $pos] == "\""} {
	    # a quoted string -- find next quote
	    set epos [$txt search "\"" "$pos +1c" "$pos lineend"]
	    # skip over quotes followed by a non-blank
	    while {$epos != "" && \
		    [regexp {[^[:space:]]} [$txt get "$epos +1c"]] == 1} {
		set epos [$txt search "\"" "$epos +1c" "$pos lineend"]
	    }
	    # did we hit the end of line?
	    if {$epos == ""} {
		set epos [$txt index "$pos lineend"]
		append block${blocks}(errors) "The quoted string on line [lindex [split $pos .] 0] does not have a close quote:\n\t[$txt get $pos $epos]\n"
	    }
	    $txt mark set $CIF(markcount).l "$pos"
	    $txt mark set $CIF(markcount).r "$epos + 1c"  
	    $txt mark gravity $CIF(markcount).l left
	    $txt mark gravity $CIF(markcount).r right
	    set item [$txt get  $pos "$epos +1c"]
	    # move forward past current token
	    set pos [$txt index "$epos +2c"]
	} elseif {[$txt get $pos] == {'}} {
	    # a quoted string -- find next quote
	    set epos [$txt search {'} "$pos +1c" "$pos lineend"]
	    # skip over quotes followed by a non-blank
	    while {$epos != "" && \
		    [regexp {[^[:space:]]} [$txt get "$epos +1c"]] == 1} {
		set epos [$txt search {'} "$epos +1c" "$pos lineend"]
	    }
	    # did we hit the end of line?
	    if {$epos == ""} {
		set epos [$txt index "$pos lineend"]
		append block${blocks}(errors) "The quoted string on line [lindex [split $pos .] 0] does not have a close quote:\n\t[$txt get $pos $epos]\n"
	    }
	    $txt mark set $CIF(markcount).l "$pos"        
	    $txt mark set $CIF(markcount).r "$epos + 1c"  
	    $txt mark gravity $CIF(markcount).l left
	    $txt mark gravity $CIF(markcount).r right
	    set item [$txt get $pos "$epos +1c"]
	    # move forward past current token
	    set pos [$txt index "$epos + 2 c"]
	} elseif {[$txt get $pos] == {[}} {
	    # CIF v1.1 square bracket quotes
	    set count 1
	    set epos $pos
	    while {$count != 0} {
		set epos [$txt search -regexp {[\]\[]} "$epos +1c"]
		if {$epos == ""} {
		    # unmatched open square bracket
		    append block${blocks}(errors) "No closing \] was found for open \] at line [lindex [split $pos .] 0]\n"
		    set count 0
		    set epos [$txt index end]
		} elseif {[$txt get $epos] == {]}} {
		    # close bracket -- decrement
		    incr count -1
		} else {
		    # open bracket -- increment
		    incr count
		}
	    }
	    $txt mark set $CIF(markcount).l "$pos"        
	    $txt mark set $CIF(markcount).r "$epos + 1c"  
	    $txt mark gravity $CIF(markcount).l left
	    $txt mark gravity $CIF(markcount).r right
	    set item [$txt get $pos "$epos +1c"]
	    # move forward past current token
	    set pos [$txt index "$epos + 2 c"]
	} else {
	    # must be a single space-delimited value
	    $txt mark set $CIF(markcount).l $pos
	    $txt mark set $CIF(markcount).r $epos
	    $txt mark gravity $CIF(markcount).l left
	    $txt mark gravity $CIF(markcount).r right
	    set item $token
	    set pos [$txt index "$epos + 1 c"]
	}
	# a data item has been read

	# store the data item
	if {$loopflag >= 0} {
	    # if in a loop, increment the loop element counter to select the
	    # appropriate array element
	    incr loopflag
	    set i [expr ($loopflag - 1) % [llength $looplist]]
	    lappend block${blocks}([lindex $looplist $i]) $CIF(markcount)
	} elseif {$dataname == ""} {
	    # this is an error -- a data item where we do not expect one
	    append block${blocks}(errors) "The string \"$item\" on line [lindex [split $pos .] 0] was unexpected\n"
	} else {
	    if {$blocks == 0} {
		# an error -- a data name before a data_ block start
		global block${blocks}
		set block${blocks}(data_) undefined
		append block${blocks}(errors) \
			    "Data name $dataname appears before a data_ block is defined (line [lindex [split $pos .] 0])\n"
	    }
	    catch {
		if {[set block${blocks}($dataname)] != ""} {
		    # this is an error -- repeated data name
		    append block${blocks}(errors) \
			    "Data name $dataname is repeated near line [lindex [split $pos .] 0]\n"
		}
	    }
	    set block${blocks}($dataname) $CIF(markcount)
	    set dataname ""
	}
    }
    $txt see 1.0
    return $blocks
}

#------------------------------------------------------------------------------
# Create a CIF browser/editor
#  $txt is a text widget with the entire CIF loaded
#  blocklist contains the list of defined blocks (by #)
#  selected is the list of blocks that will be expanded
#  frame gives the name of the toplevel window to hold the browser
proc BrowseCIF {txt blocklist "selected {}" "frame .cif"} {
    catch {destroy $frame}
    toplevel $frame 
    wm title $frame "CIF Browser"
    CIFOpenBrowser $frame
    CIFBrowser $txt $blocklist $selected $frame
    grid [button $frame.c -text Close -command "destroy $frame"] -column 0 -row 1
}

# Populate a hierarchical CIF browser
#    $txt is a text widget with the entire CIF loaded
#    blocklist contains the list of defined blocks (by #)
#    selected is the list of blocks that will be expanded
#    frame gives the name of the toplevel or frame to hold the browser
proc CIFBrowser {txt blocklist "selected {}" "frame .cif"} {
    global CIF CIFtreeindex CIF_dataname_index

    if {$selected == ""} {set selected $blocklist}

    # clear out old info, if any, from browser
    eval $CIF(tree) delete [$CIF(tree) nodes root]
    catch {unset CIFtreeindex}
    # remove the loop counter frame from window & edit buttons from that frame
    grid forget $CIF(LoopBar)
    pack forget $CIF(AddtoLoopButton) $CIF(DeleteLoopEntry)
    # delete old contents of frame
    set frame [$CIF(displayFrame) getframe]
    eval destroy [grid slaves $frame]
    set CIF(widgetlist) {}
    # reset the scrollbars
    $CIF(tree) see 0
    $CIF(displayFrame) xview moveto 0
    $CIF(displayFrame) yview moveto 0

    # Bwidget seems to have problems with the name "1", so avoid it
    set num 100
    foreach n $blocklist {
	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)]
	}
	# put the block name
	set blockname [set block${n}(data_)]
	set open 0
	if {[lsearch $selected $n] != -1} {set open 1}
	$CIF(tree) insert end root block$n -text "_data_$blockname" \
		-open $open -image [Bitmap::get folder]

	# show errors, if any
	foreach name [array names block$n errors] {
	    $CIF(tree) insert end block$n [incr num] -text "Parse-errors" \
		    -image [Bitmap::get undo] -data block$n
	}
	# loop over the names in each block 
	foreach name [lsort [array names block$n _*]] {
	    # don't include looped names
	    if {[lsearch $looplist $name] == -1} {
		$CIF(tree) insert end block$n [incr num] -text $name \
			-image [Bitmap::get file] -data block$n
		set CIFtreeindex(block${n}$name) $num
	    }
	}
	foreach loop [lsort [array names block$n loop_*]] {
	    # make a list of categories used in the loop
	    set catlist {}
	    foreach name [lsort [set block${n}($loop)]] {
		set category {}
		catch {
		    foreach {type range elist esd units category} \
			    [lindex $CIF_dataname_index($name) 1] {}
		}
		if {$category != "" && [lsearch $catlist $category] == -1} {
		    lappend catlist $category
		}
	    }

	    $CIF(tree) insert end block$n block${n}$loop \
		    -text "$loop ($catlist)" \
		    -image [Bitmap::get copy] -data "block$n loop"
	    set CIFtreeindex(block${n}$loop) block${n}$loop
	    foreach name [lsort [set block${n}($loop)]] {
		$CIF(tree) insert end block${n}$loop [incr num] -text $name \
			-image [Bitmap::get file] -data "block$n $loop"
		set CIFtreeindex(block${n}$name) $num
	    }
	}
    }
    $CIF(tree) bindImage <1> showCIFbyTreeID
    $CIF(tree) bindText <1>  showCIFbyTreeID
    set CIF(tree_lastindex) $num
}

# Create the widgets for a hierarchical CIF browser in $frame 
#   (where $frame is a frame or toplevel)
#   note that the BWidget package is required
proc CIFOpenBrowser {frame} {
    global CIF 
    if [catch {package require BWidget}] {
	tk_dialog .error {No BWidget} \
                "Sorry, the CIF Browser requires the BWidget package" \
                warning 0 Sorry
	return
    }

    set pw    [PanedWindow $frame.pw -side top]
    grid $pw -sticky news -column 0 -row 0 
    set CIF(LoopBar) [frame $frame.f]
    #grid $CIF(LoopBar) -sticky es -column 0 -row 1
    set width 900
    if {$width > [winfo screenwidth .]} {set width [winfo screenwidth .]}
    grid columnconfigure $frame 0 -weight 1 -minsize $width
    # shrink browser on small screens
    set h 250 
    if {[winfo screenheight .] < 500} {set h 180}
    grid rowconfigure $frame 0 -minsize $h -weight 1

    # create a left hand side pane for the hierarchical tree
    set pane  [$pw add -weight 1]
    set sw    [ScrolledWindow $pane.lf \
	    -relief sunken -borderwidth 2]
    set CIF(tree)  [Tree $sw.tree \
	    -relief flat -borderwidth 0 -width 15 -highlightthickness 0 \
	    -redraw 1]
    bind $frame  "$CIF(tree) yview scroll -1 page"
    bind $frame  "$CIF(tree) yview scroll 1 page"
#    bind $frame  "$CIF(tree) yview scroll -1 unit"
#    bind $frame  "$CIF(tree) yview scroll 1 unit"
    bind $frame  "$CIF(tree) yview moveto 0"
    #bind $frame  "$CIF(tree) yview moveto end" -- does not work
    bind $frame  "$CIF(tree) yview scroll 99999999 page"
    grid $sw
    grid $sw -sticky news -column 0 -row 0 
    grid columnconfigure $pane 0 -minsize 275 -weight 1
    grid rowconfigure $pane 0 -weight 1
    $sw setwidget $CIF(tree)
    
    # create a right hand side pane to show the value
    set pane [$pw add -weight 4]
    set sw   [ScrolledWindow $pane.sw \
	    -relief sunken -borderwidth 2]
    pack $sw -fill both -expand yes -side top

    set CIF(AddtoLoopButton) [button $CIF(LoopBar).l -text "Add to loop"]
    set CIF(DeleteLoopEntry) [button $CIF(LoopBar).d \
	    -text "Delete loop entry" -command DeleteCIFRow]
    label $CIF(LoopBar).1 -text "Loop\nelement #"
    set CIF(LoopSpinBox) [SpinBox $CIF(LoopBar).2 -range "1 1 1"  -width 5]
    pack $CIF(LoopBar).2 $CIF(LoopBar).1 -side right
    set CIF(displayFrame) $sw.lb
    set lb [ScrollableFrame::create $CIF(displayFrame) -width 400]
    $sw setwidget $lb
}

# Warn to save changes that are not saved in a file
proc CheckForCIFEdits {} {
    #puts "CheckForCIFEdits [info level [expr [info level]-1]]"
    global CIF
    set errorlist {}
    set errorflag 0
    set msg "The following edits cannot be saved due to errors:\n"
    foreach widget $CIF(widgetlist) {
	CheckChanges $widget 1
	if {$CIF(errormsg) != ""} {
	    set errorflag 1
	    foreach err $CIF(errormsg) {
		append msg "  " $err \n
	    }
	}

    }
    if {$errorflag} {
	append msg \n {Do you want to make corrections, or discard these edits?}
	set ans [MyMessageBox -parent . -title "Invalid edits" \
		-message $msg -icon error -type "Correct Discard" \
		-default correct]
	if {$ans == "correct"} {
	    # if not, don't allow the mode/loop value to change
	    set CIF(editmode) 1
	    catch {
		$CIF(LoopSpinBox) setvalue @$CIF(lastLoopIndex)
	    }
	    return 1
	}
    }
    return 0
}

# showCIFbyTreeID is used in BrowseCIF to response to clicking on a tree widget
#   shows the contents data name or a loop
proc showCIFbyTreeID {name} {
    if {[CheckForCIFEdits]} return

    global CIF
    # code to allow multiple selection within loops
    #set loopname [lindex [$CIF(tree) itemcget $name -data] 1]
    #set addtolist 1
    #if {$loopname == "" || $loopname == "loop"} {set addtolist 0}
    #foreach n $CIF(treeSelectedList) {
	#if {$loopname != [lindex [$CIF(tree) itemcget $n -data] 1]} {
	#    set addtolist 0
	#    break
	#}
    #}
    #if {$addtolist} {
	#catch {$CIF(tree) itemconfigure $name -fill red}
	#lappend CIF(treeSelectedList) $name
    #} else {
	foreach n $CIF(treeSelectedList) {
	    catch {$CIF(tree) itemconfigure $n -fill black}
	}
	set CIF(treeSelectedList) $name
	# for some reason, BWidget sometimes has problems doing this: 
	# (but ignore the error)
	catch {$CIF(tree) itemconfigure $name -fill red}
	set CIF(lastShownTreeID) $name
	set pointer [$CIF(tree) itemcget $name -data]
	set dataname [lindex [$CIF(tree) itemcget $name -text] 0]
	showCIFbyDataname $pointer $dataname
    #}
}

proc showCIFbyDataname {pointer dataname "loopindex {}"} {
    global CIF CIFtreeindex
    set CIF(lastShownItem) [list $pointer $dataname]
    # remove the loop counter frame from window & edit buttons from that frame
    grid forget $CIF(LoopBar)
    pack forget $CIF(AddtoLoopButton) $CIF(DeleteLoopEntry)

    # delete old contents of frame
    set frame [$CIF(displayFrame) getframe]
    eval destroy [grid slaves $frame]
    # reset the scrollbars
    $CIF(displayFrame) xview moveto 0
    $CIF(displayFrame) yview moveto 0
    # leave room for a scrollbar
    grid columnconfig $frame 0 -minsize [expr \
	    [winfo width [winfo parent $frame]] - 20]
    if {$pointer == ""} {
	return
    }
    # create list of widgets defined here
    set CIF(widgetlist) {}

    # is this a looped data item?
    set block [lindex $pointer 0]
    if {[llength $pointer] == 2} {
	global $block
	# display contents of a rows of the loop
	if {[lindex $pointer 1] == "loop"} {
	    if {$CIF(editmode)} {
		pack $CIF(DeleteLoopEntry) -side right
		pack $CIF(AddtoLoopButton) -side right
		$CIF(AddtoLoopButton) config -command "AddToCIFloop ${block} $dataname"
	    }
	    set looplist [set ${block}($dataname)]
	    # get number of elements for first name
	    set names [llength [set ${block}([lindex $looplist 0])]]
	    # can't delete the only entry
	    if {$names == 1 && $CIF(editmode)} {
		$CIF(DeleteLoopEntry) configure -state disabled
	    } else {
		$CIF(DeleteLoopEntry) configure -state normal
	    }
	    $CIF(LoopSpinBox) configure -range "1 $names 1" \
		    -command    "ShowLoopVar ${block} $dataname" \
		    -modifycmd  "ShowLoopVar ${block} $dataname"
	    set CIF(lastLoopIndex) {}
	    if {$loopindex == ""} {
		$CIF(LoopSpinBox) setvalue first
	    } else {
		$CIF(LoopSpinBox) setvalue @$loopindex
	    }
	    # show the loop counter frame
	    grid $CIF(LoopBar) -sticky es -column 0 -row 1
	    set row 0
	    set i 0
	    ShowDictionaryDefinition $looplist
	    foreach var $looplist {
		incr i
		grid [TitleFrame $frame.$i -text $var -side left] \
			-column 0 -row $i -sticky ew
		set row $i
		set frame0 [$frame.$i getframe]
		DisplayCIFvalue $frame0.l $var 1 "" ${block}
		grid columnconfig $frame0 2 -weight 1
	    }
	    ShowLoopVar ${block} $dataname
	} else {
	    # look at a single looped variable
	    ShowDictionaryDefinition $dataname
	    grid [TitleFrame $frame.0 -text $dataname -side left] \
		    -column 0 -row 0 -sticky ew
	    set row 0
	    set i 0
	    set frame0 [$frame.0 getframe]
	    grid columnconfig $frame0 2 -weight 1
	    # maximum number of entries
	    set maxcols 100
	    catch {
		set maxcols $CIF(maxRows)
	    }
	    if {[set l [llength [set ${block}($dataname)]]] > $maxcols} {
		grid [label $frame0.a$i -justify left \
			-text "$dataname has $l entries, too many to display by column" \
			] -sticky w -column 0 -row $i
		return
	    }
	    foreach mark [set ${block}($dataname)] {
		incr i
		if {$i == 1} {$CIF(txt) see $mark.l}
		set value [StripQuotes [$CIF(txt) get $mark.l $mark.r]]	    
		grid [label $frame0.a$i -justify left -text $i]\
			-sticky w -column 0 -row $i
		DisplayCIFvalue $frame0.b$i $dataname $i $value ${block} $i
		#grid $frame0.b$i -sticky new -column 1 -row $i
	    }
	}
    } else {
	# unlooped data name
	global ${block}
	ShowDictionaryDefinition $dataname
	grid [TitleFrame $frame.0 -text $dataname -side left] \
		-column 0 -row 0 -sticky ew
	set row 0
	if {$dataname == "Parse-errors"} {
	    set value [set ${block}(errors)]
	} elseif {$dataname == "Validation-errors"} {
	    set value [set ${block}(validate)]
	} else {
	    set mark [set ${block}($dataname)]
	    set value [StripQuotes [$CIF(txt) get $mark.l $mark.r]]	    
	    $CIF(txt) see $mark.l
	}
	set frame0 [$frame.0 getframe]
	grid columnconfig $frame0 2 -weight 1
	DisplayCIFvalue $frame0.l $dataname "" $value $block
	#grid $frame0.l -sticky w -column 1 -row 0
    }
}

# redisplay the last entry shown in showCIFbyTreeID
# this is used if the edit mode ($CIF(editmode)) changes or if edits are saved
proc RepeatLastshowCIFvalue {} {
    global CIF
    if {[CheckForCIFEdits]} return
    set lastLoopIndex $CIF(lastLoopIndex)

    catch {
	eval showCIFbyDataname $CIF(lastShownItem)
	# if we are in a loop, display the element
	if {[lindex [lindex $CIF(lastShownItem) 0] 1] == "loop"} {
	    $CIF(LoopSpinBox) setvalue @$lastLoopIndex
	    ShowLoopVar [lindex [lindex $CIF(lastShownItem) 0] 0] \
		    [lindex $CIF(lastShownItem) 1]
	}
	
    }
}

# used in BrowseCIF in response to the spinbox
# show entries in a specific row of a loop
proc ShowLoopVar {array loop} {
    global $array CIF
    # check for unsaved changes here
    if {$CIF(lastLoopIndex) != ""} {
	if {[CheckForCIFEdits]} return
    }

    set looplist [set ${array}($loop)]
    set index [$CIF(LoopSpinBox) getvalue]
    if {$index < 0} {
	$CIF(LoopSpinBox) setvalue first
	set index [$CIF(LoopSpinBox) getvalue]
    } elseif {$index > [llength [set ${array}([lindex $looplist 0])]]} {
	$CIF(LoopSpinBox) setvalue last
	set index [$CIF(LoopSpinBox) getvalue]
    }
    set CIF(lastLoopIndex) $index
    set frame [$CIF(displayFrame) getframe]
    set i 0
    foreach var $looplist {
	incr i
	set mark [lindex [set ${array}($var)] $index]
	# ignore invalid entries -- should not happen
	if {$mark == ""} {
	    $CIF(LoopSpinBox) setvalue first
	    return
	}
	set value [StripQuotes [$CIF(txt) get $mark.l $mark.r]]	    
	if {$i == 1} {$CIF(txt) see $mark.l}
	if {$CIF(editmode)} {
	    global CIFeditArr CIFinfoArr
	    set widget [$frame.$i getframe].l
	    set CIFeditArr($widget) $value
	    switch [winfo class $widget] {
		Text {
		    $widget delete 0.0 end
		    $widget insert end $value
		}
		Entry {
		    $widget config -fg black
		}
	    }
	    set CIFinfoArr($widget) [lreplace $CIFinfoArr($widget) 2 2 $index]
	} else {
	    [$frame.$i getframe].l config -text $value
	}
    }
}

# scan a number in crystallographic uncertainty representation
# i.e.: 1.234(12), 1234(23), 1.234e-2(14),  -1.234-08(14), etc.
proc ParseSU">ParseSU {num} {
    # is there an error on this value?
    if {![regexp {([-+eEdD.0-9]+)\(([0-9]+)\)} $num x a err]} {
	set a $num
	set err {}
    }
    # parse off an exponent, if present
    if {[regexp {([-+.0-9]+)[EeDd]([-+0-9]+)} $a x a1 exp]} {
	# [+-]###.###e+## or [+-]###.###D-## etc.
	set a $a1
	# remove leading zeros from exponent
	regsub {([+-]?)0*([0-9]+)} $exp {\1\2} exp
    } elseif {[regexp {([-+.0-9]+)([-+][0-9]+)} $a x a1 exp]} {
	# [+-]###.###+## or [+-]###.###-## etc. [no 
	set a $a1
	# remove leading zeros from exponent
	regsub {([+-]?)0*([0-9]+)} $exp {\1\2} exp
    } else {
	set exp 0
    }
    # now parse the main number and count the digits after the decimal
    set a2 {}
    set a3 {}
    regexp {^([-+0-9]*)\.?([0-9]*)$} $a x a2 a3
    set l [string length $a3]

    set val .
    set error {}
    if {[catch {
	set val [expr ${a2}.${a3} * pow(10,$exp)]
	if {$err != ""} {
	    set error [expr $err*pow(10,$exp-$l)]
	}
    }]} {
	# something above was invalid
	if {$err != ""} {
	    return "$val ."
	} else {
	    return $val
	}
    }
    if {$error == ""} {
	return $val
    } else {
	return [list $val $error]
    }
}

# a stand-alone routine for testing. Select, read and browse a CIF
proc Read_BrowseCIF {} {
    global tcl_platform
    if {$tcl_platform(platform) == "windows"} {
	set filetypelist {
	    {"CIF files" .CIF} {"All files" *}
	}
    } else {
	set filetypelist {
	    {"CIF files" .CIF} {"CIF files" .cif} {"All files" *}
	}
    }    
    set file [tk_getOpenFile -parent . -filetypes $filetypelist]
    if {$file == ""} return
    if {![file exists $file]} return
    pleasewait "Reading CIF from file"
    set blocks [ParseCIF $file]
    if {$blocks == ""} {
	donewait
	MessageBox -parent . -type ok -icon warning \
		-message "Note: no valid CIF blocks were read from file $filename"
	return
    }
    catch {donewait}
    set allblocks {}
    for {set i 1} {$i <= $blocks} {incr i} {
	lappend allblocks $i
    }
    if {$allblocks != ""} {
	BrowseCIF $allblocks "" .cif
	# wait for the window to close
	tkwait window .cif
    } else {
	puts "no blocks read"
    }
    # clean up -- get rid of the CIF arrays
    for {set i 1} {$i <= $blocks} {incr i} {
	global block$i
	catch {unset block$i}
    }
}

# this takes a block of text, strips off the quotes ("", '', [] or ;;)
proc StripQuotes {value} {
    set value [string trim $value]
    if {[string range $value end-1 end] == "\n;" && \
	    [string range $value 0 0] == ";"} {
	return [string range $value 1 end-2]
    } elseif {[string range $value end end] == "\"" && \
	    [string range $value 0 0] == "\""} {
	set value [string range $value 1 end-1]
    } elseif {[string range $value end end] == "'" && \
	    [string range $value 0 0] == "'"} {
	set value [string range $value 1 end-1]
    } elseif {[string range $value end end] == {]} && \
	    [string range $value 0 0] == {[}} {
	set value [string range $value 1 end-1]
    }
    return $value
}

# replace a CIF value in with a new value.
# add newlines as needed to make sure the new value does not 
# exceed CIF(maxlinelength) [defaults to 80] characters/line
proc ReplaceMarkedText {txt mark value} {
    $txt configure -state normal
    # is this a multi-line string?
    set num [string first \n $value]
    set l [string length $value]
    # are there spaces in the string?
    set spaces [string first " " $value]
    # if no, are there any square brackets? -- treat them as requiring quotes
    if {$spaces == -1} {set spaces [string first {[} $value]}
    # are there any reserved strings inside $value? If so, it must be quoted
    if {$spaces == -1} {
	set tmp [string toupper $value]
	foreach s {DATA_ LOOP_ SAVE_ STOP_ GLOBAL_} {
	    if {[set spaces [string first $s $tmp]] != -1} break
	}
    }
    # are there quotes inside the string?
    set doublequote [string first "\"" $value]
    set singlequote [string first {'} $value]
    # if we have either type of quotes, use semicolon quoting
    if {$singlequote != -1 && $doublequote != -1} {set num $l}

    # lines longer than 78 characters with spaces need to be treated 
    # as multiline
    if {$num == -1 && $l > 77 && $spaces != -1} {
	set num $l
    }
    if {$num != -1} {
	set tmp {}
	if {[lindex [split [$txt index $mark.l] .] 1] != 0} {
	    append tmp \n
	}
	append tmp ";"
	if {$num > 78} {
	    append tmp \n
	} else {
	    append tmp " "
	}
	append tmp $value "\n;"
	# is there something else on the line?
	set restofline [$txt get $mark.r [lindex [split [$txt index $mark.r] .] 0].end]
	if {[string trim $restofline] != ""} {
	    append tmp \n
	}
	$txt delete ${mark}.l ${mark}.r
	$txt insert ${mark}.l $tmp
	$txt configure -state disabled
	return
    } elseif {($spaces != -1 || [string trim $value] == "") \
	    && $doublequote == -1} {
	# use doublequotes, unless doublequotes are present inside the string
	set tmp "\""
	append tmp $value "\""
    } elseif {$spaces != -1 || [string trim $value] == ""} {
	# use single quotes, since doublequotes are present inside the string
	set tmp {'}
	append tmp $value {'}
    } else {
	# no quotes needed
	set tmp $value
    }
    # is there room on the beginning of the line to add the string?
    set l [string length $tmp]
    set pos [lindex [split [$txt index $mark.l] .] 0]
    if {$l + [string length [$txt get $pos.0 $mark.l]] <= 79} {
	# will fit
	$txt delete ${mark}.l ${mark}.r
	$txt insert ${mark}.l $tmp
    } else {
	# no, stick a CR in front of string
	$txt delete ${mark}.l ${mark}.r
	$txt insert ${mark}.l \n$tmp
    }
    # is rest of the line after the inserted string still too long?
    set pos [lindex [split [$txt index $mark.r] .] 0]
    if {[string length [$txt get $pos.0 $pos.end]] > 79} {
	$txt insert $mark.r \n
    }
    $txt configure -state disabled
}

# return the dictionary definition for a list of CIF data names
proc GetCIFDefinitions {datanamelist} {
    global CIF_dataname_index
    set l {}
    # compile a list of definition pointers
    foreach dataname $datanamelist {
	set pointer {}
	catch {
	    set pointer [lindex $CIF_dataname_index($dataname) 0]
	}
	lappend l [list $dataname $pointer]
    }
    set l [lsort -index 1 $l]
    set pp {}
    set dictdefs {}
    set def {}
    set nlist {}
    # merge items with duplicate definitions
    foreach item $l {
	# is this the first loop through?
	foreach {dataname pointer} $item {}
	if {$def == ""} {
	    foreach {nlist pp} $item {}
	    set def [ReadCIFDefinition $pp]
	} elseif {$pp == $pointer} {
	    # same as last
	    lappend nlist $dataname
	} else {
	    # add the last entry to the list
	    set pp $pointer
	    lappend dictdefs [list $nlist $def]
	    set nlist $dataname
	    if {$pointer == ""} {
		set def { Undefined dataname}
	    } else {
		# lookup name
		set def [ReadCIFDefinition $pointer]
	    }
	}
    }
    lappend dictdefs [list $nlist $def]
    return $dictdefs
}

# read the CIF definition for a dataname. The pointer contains 3 values
# a filename, the number of characters from the start of the file and
# the length of the definition.
proc ReadCIFDefinition {pointer} {
    global CIF
    set file {}
    set loc {}
    set line {}
    foreach {file loc len} $pointer {}
    if {$file != "" && $loc != "" && $loc != ""} {
	set fp {}
	foreach path $CIF(cif_path) {
	    catch {set fp [open [file join $path $file] r]}
	    if {$fp != ""} break
	}
	if {$fp == ""} return
	fconfigure $fp -translation binary
	catch {
	    seek $fp $loc
	    set line [read $fp $len]
	    close $fp
	    # remove line ends & superfluous spaces
	    regsub -all {\n} [StripQuotes $line] { } line
	    regsub -all {\r} $line { } line
	    regsub -all {  +} $line { } line
#	    regsub -all {  +} [StripQuotes $line] { } line
	}
    }
    return $line
}

proc ValidateCIFName {dataname} {
    global CIF_dataname_index
    if {[
	catch {
	    set CIF_dataname_index($dataname)
	}
    ]} {return "warning: dataname $dataname not defined"}
}

# validates that a CIF value is valid for a specific dataname
proc ValidateCIFItem {dataname item} {
    global CIF_dataname_index CIF
    # maximum line length
    set maxlinelength 80
    catch {set maxlinelength $CIF(maxlinelength)}
    if {[catch {
	foreach {type range elist esd units category} [lindex $CIF_dataname_index($dataname) 1] {}
    }]} {return}
    if {$type == "c"} {
	# string type constant
	set item [StripQuotes $item]
	# is it enumerated?
	if {$elist != ""} {
	    # check it against the list of values
	    foreach i [concat $elist . ?] {
		if {[string tolower $item] == [string tolower [lindex $i 0]]} {return}
	    }
	    return "error: value \"$item\" is not an allowed option for $dataname"
	} else {
	    # check it for line lengths
	    set l 0
	    set err {}
	    foreach line [split $item \n] {
		incr l
		if {[string length $line] > $maxlinelength} {lappend err $l}
	    }
	    if {$err != ""} {return "error: line(s) $err are too long"}
	}
	return
    } elseif {$type == ""} {
	return "error: dataname $dataname is not used for CIF data items"
    } elseif {$type == "n"} {
	# validate numbers
	set unquoted [StripQuotes $item]
	if {$unquoted == "?" || $unquoted == "."} return
	if {$unquoted != $item} {
	    set err "\nwarning: number $item is quoted for $dataname"
	    set item $unquoted
	} else {
	    set err {}
	}
	set v $item
	# remove s.u., if allowed & present
	set vals [ParseSU">ParseSU $item]
	if {[set v [lindex $vals 0]] == "."} {
	    return "error: value \"$item\" is not a valid number for $dataname$err"
	}
	if {$esd} {
	    if {[lindex $vals 1] == "."} {
		return "error: value \"$item\" for $dataname has an invalid uncertainty (esd)$err"
	    }
	} elseif {[llength $vals] == 2} {
	    return "error: \"$item\" is invalid for $dataname, an uncertainty (esd) is not allowed$err"
	}

	# now validate the range
	if {$range != ""} {
	    # is there a decimal point in the range?
	    set integer 0
	    if {[string first . $range] == -1} {set integer 1}
	    # pull out the range
	    foreach {min max} [split $range :] {}
	    if {$integer && int($v) != $v} {
		return "warning: value \"$item\" is expected to be an integer for $dataname$err"
	    }
	    if {$min != ""} {
		if {$v < $min} {
		    return "error: value \"$item\" is too small for $dataname (allowed range $range)$err"
		}
	    }
	    if {$max != ""} {
		if {$v > $max} {
		    return "error: value \"$item\" is too big for $dataname(allowed range $range)$err"
		}
	    }
	}
	return $err
    }
    return {}
}

# displays the dictionary definitions in variable defs into a text widget
proc ShowDictionaryDefinition {defs} {
    global CIF
    set deflist [GetCIFDefinitions $defs]
    catch {
	$CIF(defBox) delete 1.0 end
	foreach d $deflist {
	    foreach {namelist definition} $d {}
	    foreach n $namelist {
		$CIF(defBox) insert end $n dataname
		$CIF(defBox) insert end \n
	    }
	    $CIF(defBox) insert end \n
	    $CIF(defBox) insert end $definition
	    $CIF(defBox) insert end \n
	    $CIF(defBox) insert end \n
	}
	$CIF(defBox) tag config dataname -background yellow
    }
}

# create a widget to display a CIF value
proc DisplayCIFvalue {widget dataname loopval value block "row 0"} {
    global CIFeditArr CIFinfoArr
    global CIF CIF_dataname_index
    if {[
	catch {
	    foreach {type range elist esd units category} [lindex $CIF_dataname_index($dataname) 1] {}
	}
    ]} {
	set type c
	set elist {}
    }

    lappend CIF(widgetlist) $widget
    set CIFinfoArr($widget) {}

    if $CIF(editmode) {
	if {$loopval != ""} {
	    set widgetinfo [list $dataname $block [expr $loopval -1]]
	} else {
	    set widgetinfo [list $dataname $block 0]
	}
	set CIFeditArr($widget) $value
	set CIFinfoArr($widget) $widgetinfo

	if {$type == "n"} {
	    entry $widget -justify left -textvariable CIFeditArr($widget)
	    bind $widget  "CheckChanges $widget"
	    grid $widget -sticky nsw -column 1 -row $row
	    if {$units != ""} {
		set ws "${widget}u"
		label $ws -text "($units)" -bg yellow
		grid $ws -sticky nsw -column 2 -row $row
	    }
	} elseif {$elist != ""} {
	    set enum {}
	    foreach e $elist {
		lappend enum [lindex $e 0]
	    }
	    tk_optionMenu $widget CIFeditArr($widget) ""
	    FixBigOptionMenu $widget $enum "CheckChanges $widget"
	    AddSpecialEnumOpts $widget "CheckChanges $widget"
	    grid $widget -sticky nsw -column 1 -row $row
	} else {
	    # count the number of lines in the text
	    set nlines [llength [split $value \n]]
	    if {$nlines < 1} {
		set nlines 1
	    } elseif {$nlines > 10} {
		set nlines 10
	    }
	    set ws "${widget}s"
	    text $widget -height $nlines -width 80 -yscrollcommand "$ws set"
	    scrollbar $ws -command "$widget yview" -width 10 -bd 1
	    $widget insert end $value
	    bind $widget  "CheckChanges $widget"
	    if {$nlines > 1} {
		grid $ws -sticky nsew -column 1 -row $row
		grid $widget -sticky nsew -column 2 -row $row
	    } else {
		grid $widget -sticky nsew -column 1 -columnspan 2 -row $row
	    }
	}
    } else {
	label $widget -bd 2 -relief groove \
		-justify left -anchor w -text $value
	grid $widget -sticky nsw -column 1 -row $row
	if {$type == "n" && $units != ""} {
	    set ws "${widget}u"
	    label $ws -text "($units)" -bg yellow
	    grid $ws -sticky nsw -column 2 -row $row
	}
    }
}

# this is called to see if the user has changed the value for a CIF
# data item and to validate it.
#   save the change if $save is 1
#   return 1 if the widget contents has changed
proc CheckChanges {widget "save 0"} {
    global CIFeditArr CIFinfoArr CIF
    # maximum line length
    set maxlinelength 80
    catch {set maxlinelength $CIF(maxlinelength)}

    set CIF(errormsg) {}

    if {![winfo exists $widget]} return

    set dataname {}
    catch {
	foreach {dataname block index} $CIFinfoArr($widget) {}
    }
    # if this widget is a label, the info above will not be defined & checks are not needed
    if {$dataname == ""} {return 0}
    if {$dataname == "Parse-errors"} {return 0}
    if {$dataname == "Validation-errors"} {return 0}

    global ${block}
    set mark [lindex [set ${block}($dataname)] $index]
    if {$mark == ""} return
    set orig [StripQuotes [$CIF(txt) get $mark.l $mark.r]]

    # validate the entry
    set error {}
    set err {}
    switch [winfo class $widget] {
	Text {
	    set current [string trim [$widget get 1.0 end]]
	    set l 0
	    foreach line [set linelist [split $current \n]] {
		incr l
		if {[string length $line] > $maxlinelength} {
		    lappend err $l
		    lappend error "Error: line $l for $dataname is >$maxlinelength characters"
		}
	    }
	    if {$err != ""} {
		foreach l $err {
		    $widget tag add error $l.0 $l.end
		}
		$widget tag config error -foreground red
	    } else {
		$widget tag delete error
	    }
	    # see if box should expand
	    set clines [$widget cget -height]
	    if {$clines <= 2 && \
		    [string trim $orig] != [string trim $current]} {
		# count the number of lines in the text
		set nlines [llength $linelist]
		if {[lindex $linelist end] == ""} {incr nlines -1}
		if {$nlines == 2} {
		    $widget config -height 2
		} elseif {$nlines > 2} {
		    set i [lsearch [set s [grid info $widget]] -row]
		    set row [lindex $s [expr 1+$i]]
		    $widget config -height 3
		    set ws "${widget}s"
		    grid $ws -sticky nsew -column 1 -row $row
		    grid $widget -sticky nsew -column 2 -row $row
		}
	    }
	}
	Entry {
	    set current [string trim [$widget get]]
	    set err [ValidateCIFItem [lindex $CIFinfoArr($widget) 0] $current]
	    if {$err != "" && \
		    [string tolower [lindex $err 0]] != "warning:"} {
		lappend error $err
		$widget config -fg red
	    } else {
		$widget config -fg black
	    }
	}
	Menubutton {
	    set current $CIFeditArr($widget)
	}
	Label {
	    return 0
	}
    }
    if {[string trim $orig] != [string trim $current]} {
	if {$err != ""} {
	    set CIF(errormsg) $error
	} elseif {$save} {
	    SaveCIFedits $widget
	    return 0
	}
	return 1
    }
    return 0
}

# save the CIF edits into the CIF text widget
proc SaveCIFedits {widget} {
    global CIFeditArr CIFinfoArr CIF

    foreach {dataname block index} $CIFinfoArr($widget) {}
    global ${block}
    set mark [lindex [set ${block}($dataname)] $index]
    set orig [StripQuotes [$CIF(txt) get $mark.l $mark.r]]
    switch [winfo class $widget] {
	Text {
	    set current [string trim [$widget get 1.0 end]]
	}
	Entry {
	    set current [string trim [$widget get]]
	}
	Menubutton {
	    set current $CIFeditArr($widget)
	}
    }
    # save for undo & clear the redo list
    set CIF(redolist) {}
    if {[lindex [lindex $CIF(lastShownItem) 0] 1] == "loop"} {
	lappend CIF(undolist) [list $mark $orig \
		$CIF(lastShownItem) $CIF(lastShownTreeID) $CIF(lastLoopIndex)]
    } else {
	lappend CIF(undolist) [list $mark $orig \
		$CIF(lastShownItem) $CIF(lastShownTreeID)]
    }
    # count it
    incr CIF(changes)
    # make the change
    ReplaceMarkedText $CIF(txt) $mark $current
}

# add a new "row" to a CIF loop. At least for now, we only add at the end.
proc AddToCIFloop {block loop} {
    global $block CIF
    # check for unsaved changes here
    if {[CheckForCIFEdits]} return

    $CIF(txt) configure -state normal
    set looplist [set ${block}($loop)]
    set length [llength [set ${block}([lindex $looplist 0])]]
    # find the line following the last entry in the list
    set var [lindex $looplist end]
    set line [lindex [split [\
	    $CIF(txt) index [lindex [set ${block}($var)] end].r \
	    ] .] 0]
    incr line
    set epos $line.0
    $CIF(txt) insert $epos \n

    # insert a ? token for each entry & add to marker list for each variable
    set addlist {}
    foreach var $looplist {
	# go to next line?
	if {[string length \
		[$CIF(txt) get "$epos linestart" "$epos lineend"]\
		] > 78} {
	    $CIF(txt) insert $epos \n
	    set epos [$CIF(txt) index "$epos + 1c"]
	}
	$CIF(txt) insert $epos "? "
	incr CIF(markcount)
	$CIF(txt) mark set $CIF(markcount).l "$epos"
	$CIF(txt) mark set $CIF(markcount).r "$epos + 1c"
	$CIF(txt) mark gravity $CIF(markcount).l left
	$CIF(txt) mark gravity $CIF(markcount).r right
	set epos [$CIF(txt) index "$epos + 2c"]
	set index [llength [set ${block}($var)]]
	lappend ${block}($var) $CIF(markcount)
	lappend addlist [list $CIF(markcount) $var $index $block]
    }
    incr CIF(changes)
    lappend CIF(undolist) [list "loop add" $addlist \
	    $CIF(lastShownItem) $CIF(lastShownTreeID) $CIF(lastLoopIndex)]
    set CIF(redolist) {}

    # now show the value we have added
    set frame [$CIF(displayFrame) getframe]
    set max [lindex [$CIF(LoopSpinBox) cget -range] 1]
    incr max
    $CIF(LoopSpinBox) configure -range "1 $max 1"
    $CIF(LoopSpinBox) setvalue last
    ShowLoopVar $block $loop
    $CIF(txt) configure -state disabled
    $CIF(DeleteLoopEntry) configure -state normal
}

proc DeleteCIFRow {} {
    global CIF
    global CIFinfoArr CIFeditArr

    set delrow [$CIF(LoopSpinBox) getvalue]

    set msg {Are you sure you want to delete the following loop entries}
    append msg " (row number [expr 1+$delrow])?\n"
    set widget ""
    foreach widget $CIF(widgetlist) {
	set var [lindex $CIFinfoArr($widget) 0]
	append msg "\n$var\n\t"
	# get the value
	switch [winfo class $widget] {
	    Text {
		set value [string trim [$widget get 1.0 end]]
	    }
	    Entry {
		set value [string trim [$widget get]]
	    }
	    Menubutton {
		set value $CIFeditArr($widget)
	    }
	}
	append msg $value \n
    }
    if {$widget == ""} {
	error "this should not happen"
    }
    foreach {dataname block index} $CIFinfoArr($widget) {}
    global $block
    if {[llength [set ${block}($dataname)]] == 1} {
	MyMessageBox -parent . -title "Not only row" \
		-message {Sorry, this program is unable to delete all entries from a loop.} \
		-icon warning -type {Ignore} -default Ignore
	return
    }

    set ans [MyMessageBox -parent . -title "Delete Row?" \
		-message $msg \
		-icon question -type {Keep Delete} -default Keep]
    if {$ans == "keep"} {return}

    $CIF(txt) configure -state normal
    set deletelist {}
    foreach widget $CIF(widgetlist) {
	foreach {dataname block index} $CIFinfoArr($widget) {}
	global $block
	set mark [lindex [set ${block}($dataname)] $index]
	set orig [StripQuotes [$CIF(txt) get $mark.l $mark.r]]
	lappend deletelist [list $mark $dataname $index $block $orig]
	$CIF(txt) delete $mark.l $mark.r
	set ${block}($dataname) [lreplace [set ${block}($dataname)] $index $index]
    }
    set CIF(redolist) {}
    lappend CIF(undolist) [list "loop delete" $deletelist \
	    $CIF(lastShownItem) $CIF(lastShownTreeID) $CIF(lastLoopIndex)]
    # count it
    incr CIF(changes)

    $CIF(txt) configure -state disabled

    set max [lindex [$CIF(LoopSpinBox) cget -range] 1]
    incr max -1
    $CIF(LoopSpinBox) configure -range "1 $max 1"
    if {$index >= $max} {set index $max; incr index -1}
    $CIF(LoopSpinBox) setvalue @$index
    if {$max == 1} {$CIF(DeleteLoopEntry) configure -state disabled}
    # don't check for changes
    set CIF(lastLoopIndex) {}
    ShowLoopVar $block [lindex $CIF(lastShownItem) 1]
}

# display & highlight a line in the CIF text viewer
proc MarkGotoLine {line} {
    global CIF
    $CIF(txt) tag delete currentline
    $CIF(txt) tag add currentline $line.0 $line.end
    $CIF(txt) tag configure currentline -foreground blue
    $CIF(txt) see $line.0
}

# Extract a value from a CIF in the  CIF text viewer
proc ValueFromCIF {block item} {
    global $block CIF
    set val {}
    catch {
	set mark [set ${block}($item)]
	if {[llength $mark] == 1} {
	    set val [string trim [StripQuotes [$CIF(txt) get $mark.l $mark.r]]]
	} else {
	    foreach m $mark {
		lappend val [string trim [StripQuotes [$CIF(txt) get $m.l $m.r]]]
	    }
	}
    }
    return $val
}

proc UndoChanges {} {
    global CIF
    # save any current changes, if possible
    if {[CheckForCIFEdits]} return
    # are there edits to undo?
    if {[llength $CIF(undolist)] == 0} return

    foreach {mark orig lastShownItem lastShownTreeID lastLoopIndex} \
	    [lindex $CIF(undolist) end] {} 

    if {[llength $mark] == 1} {
	# get the edited value
	set edited [StripQuotes [$CIF(txt) get $mark.l $mark.r]]
	# make the change back
	ReplaceMarkedText $CIF(txt) $mark $orig
	# add this undo to the redo list
	lappend CIF(redolist) [list $mark $edited $lastShownItem \
		$lastShownTreeID $lastLoopIndex]
    } elseif {[lindex $mark 1] == "add"} {
	set deletelist {}
	$CIF(txt) configure -state normal
	foreach m $orig {
	    foreach {mark dataname index block} $m {}
	    # get the inserted value
	    set edited [StripQuotes [$CIF(txt) get $mark.l $mark.r]]	
	    $CIF(txt) delete $mark.l $mark.r
	    lappend deletelist [list $mark $dataname $index $block $edited]
	    global $block
	    set ${block}($dataname) [lreplace [set ${block}($dataname)] $index $index]
	}
	$CIF(txt) configure -state disabled
	# add this action to the redo list
	lappend CIF(redolist) [list "loop delete" $deletelist \
		$lastShownItem $lastShownTreeID $lastLoopIndex]
    } elseif {[lindex $mark 1] == "delete"} {
	set addlist {}
	foreach m $orig {
	    foreach {mark dataname index block orig} $m {}
	    # make the change back
	    ReplaceMarkedText $CIF(txt) $mark $orig
	    lappend addlist [list $mark $dataname $index $block]
	    global $block
	    set ${block}($dataname) [linsert [set ${block}($dataname)] $index $mark]
	}
	# show the entry that was added
	set lastLoopIndex $index
	# add this last entry to the redo list
	lappend CIF(redolist) [list "loop add" $addlist \
		$lastShownItem $lastShownTreeID $lastLoopIndex]
    }

    # drop the action from the undo list
    set CIF(undolist) [lreplace $CIF(undolist) end end]
    # count back
    incr CIF(changes) -1
    # scroll on the tree
    $CIF(tree) see $lastShownTreeID
    eval showCIFbyDataname $lastShownItem

    # if we are in a loop, display the element
    if {[lindex [lindex $lastShownItem 0] 1] == "loop"} {
	$CIF(LoopSpinBox) setvalue @$lastLoopIndex
	ShowLoopVar [lindex [lindex $lastShownItem 0] 0] \
		[lindex $lastShownItem 1]
    }
}


proc RedoChanges {} {
    global CIF
    # save any current changes, if possible
    if {[CheckForCIFEdits]} return
    # are there edits to redo?
    if {[llength $CIF(redolist)] == 0} return

    foreach {mark edited lastShownItem lastShownTreeID lastLoopIndex} \
	    [lindex $CIF(redolist) end] {} 

    if {[llength $mark] == 1} {
	# get the edited value
	set orig [StripQuotes [$CIF(txt) get $mark.l $mark.r]]
	# make the change back
	ReplaceMarkedText $CIF(txt) $mark $edited
	# add this action back to the undo list
	lappend CIF(undolist) [list $mark $orig $lastShownItem \
		$lastShownTreeID $lastLoopIndex]
	# count up
	incr CIF(changes)
    } elseif {[lindex $mark 1] == "add"} {
	set deletelist {}
	$CIF(txt) configure -state normal
	foreach m $edited {
	    foreach {mark dataname index block} $m {}
	    # get the inserted value
	    set edited [StripQuotes [$CIF(txt) get $mark.l $mark.r]]	
	    $CIF(txt) delete $mark.l $mark.r
	    lappend deletelist [list $mark $dataname $index $block $edited]
	    global $block
	    set ${block}($dataname) [lreplace [set ${block}($dataname)] $index $index]
	}
	$CIF(txt) configure -state disabled
	# add this action back to the undo list
	lappend CIF(undolist) [list "loop delete" $deletelist \
		$lastShownItem $lastShownTreeID $lastLoopIndex]
	# count up
	incr CIF(changes)
    } elseif {[lindex $mark 1] == "delete"} {
	set addlist {}
	foreach m $edited {
	    foreach {mark dataname index block orig} $m {}
	    # make the change back
	    ReplaceMarkedText $CIF(txt) $mark $orig
	    lappend addlist [list $mark $dataname $index $block]
	    global $block
	    set ${block}($dataname) [linsert [set ${block}($dataname)] $index $mark]
	}
	# show the entry that was added
	set lastLoopIndex $index
	# add this action back to the undo list
	lappend CIF(undolist) [list "loop add" $addlist \
		$lastShownItem $lastShownTreeID $lastLoopIndex]
	# count up
	incr CIF(changes)
    }
    
    # drop the action from the redo list
    set CIF(redolist) [lreplace $CIF(redolist) end end]
    # scroll on the tree
    $CIF(tree) see $lastShownTreeID
    eval showCIFbyDataname $lastShownItem
    
    # if we are in a loop, display the element
    if {[lindex [lindex $lastShownItem 0] 1] == "loop"} {
	$CIF(LoopSpinBox) setvalue @$lastLoopIndex
	ShowLoopVar [lindex [lindex $lastShownItem 0] 0] \
		[lindex $lastShownItem 1]
    }
}

# initialize misc variables
set CIF(changes) 0
set CIF(widgetlist) {}
set CIF(lastShownItem) {}
set CIF(lastLoopIndex) {}
set CIF(editmode) 0
set CIF(undolist) {}
set CIF(redolist) {}
set CIF(treeSelectedList) {}