#!/usr/bin/wish # $Id: browsecif.tcl 638 2009-12-04 23:09:31Z 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 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 {}}} { catch {destroy .msg} toplevel .msg wm transient .msg [winfo toplevel .] pack [frame .msg.f -bd 4 -relief groove] pack [message .msg.f.m -text "Please wait $message"] wm withdraw .msg update idletasks # place the message on top of the main window set x [expr [winfo x .] + [winfo width .]/2 - \ [winfo reqwidth .msg]/2 - [winfo vrootx .]] if {$x < 0} {set x 0} set y [expr [winfo y .] + [winfo height .]/2 - \ [winfo reqheight .msg]/2 - [winfo vrooty .]] if {$y < 0} {set y 0} wm geom .msg +$x+$y 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) # # 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 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 [catch { set fp [open $filename r] $txt insert end [read $fp] close $fp }] {return ""} 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} { # skip forward to the first non-blank character set pos [$txt search -regexp {[^[:space:]]} $pos end] # is this the end? if {$pos == ""} { set EOF 0 break } # 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 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 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 "" } } 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" CIFBrowserWindow $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 if {$selected == ""} {set selected $blocklist} # clear out old info, if any, from browser eval $CIF(tree) delete [$CIF(tree) nodes root] catch {unset CIFtreeindex} pack forget $CIF(EditSaveButton) $CIF(AddtoLoopButton) \ $CIF(LoopSpinBox) $CIF(DeleteLoopEntry) # delete old contents of frame set frame [$CIF(displayFrame) getframe] eval destroy [grid slaves $frame] # reset the scrollbars $CIF(tree) see 0 $CIF(displayFrame) xview moveto 0 $CIF(displayFrame) yview moveto 0 set num 0 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 copy] # show errors, if any foreach name [array names block$n errors] { $CIF(tree) insert end block$n [incr num] -text $name \ -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 folder] -data block$n set CIFtreeindex(block${n}$name) $num } } foreach loop [lsort [array names block$n loop_*]] { $CIF(tree) insert end block$n block${n}$loop -text $loop \ -image [Bitmap::get file] -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 folder] -data "block$n $loop" set CIFtreeindex(block${n}$name) $num } } } $CIF(tree) bindImage <1> showCIFbyTreeID $CIF(tree) bindText <1> showCIFbyTreeID } # 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 CIFBrowserWindow {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 width 900 if {$width > [winfo screenwidth .]} {set width [winfo screenwidth .]} grid columnconfigure $frame 0 -weight 1 -minsize $width grid rowconfigure $frame 0 -minsize 250 -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 pack [frame $pane.f] -fill x set CIF(EditSaveButton) [button $pane.f.b -text "Save Changes" -state disabled \ -command "SaveCIFedits"] set CIF(AddtoLoopButton) [button $pane.f.l -text "Add to loop"] set CIF(DeleteLoopEntry) [button $pane.f.d -text "Delete loop entry" \ -command DeleteCIFRow] set CIF(LoopSpinBox) [SpinBox $pane.f.sb -range "1 1 1" \ -label "Loop\nelement #" -labelwidth 10 -width 10] 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 {} { global CIF if {$CIF(entry_changed) != ""} { set ans [MyMessageBox -parent . -title "Discard Changes?" \ -message "You have changed this entry. Do you want to keep or discard this edit?" \ -icon question -type {Save Discard} -default Save] if {$ans == "save"} { SaveCIFedits # did this save anything? if {$CIF(entry_changed) != ""} { # if not, don't allow the mode/loop value to change set CIF(editmode) 1 catch { $CIF(LoopSpinBox) setvalue @$CIF(lastLoopIndex) } return 1 } } else { set CIF(entry_changed) {} $CIF(EditSaveButton) config -state disabled } } 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} { global CIF if {[CheckForCIFEdits]} return set pointer [$CIF(tree) itemcget $name -data] set dataname [$CIF(tree) itemcget $name -text] showCIFbyDataname $pointer $dataname } proc showCIFbyDataname {pointer dataname "loopindex {}"} { if {[CheckForCIFEdits]} return global CIF set CIF(lastShownItem) [list $pointer $dataname] # include a save button if {$CIF(editmode)} { pack $CIF(EditSaveButton) -side left } else { pack forget $CIF(EditSaveButton) } pack forget $CIF(AddtoLoopButton) $CIF(LoopSpinBox) $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 defined widgets 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])]] $CIF(LoopSpinBox) configure -range "1 $names 1" \ -command "ShowLoopVar ${block} $dataname" \ -modifycmd "ShowLoopVar ${block} $dataname" if {$loopindex == ""} { $CIF(LoopSpinBox) setvalue first } else { $CIF(LoopSpinBox) setvalue @$loopindex } pack $CIF(LoopSpinBox) -side right 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 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 == "errors"} { set value [set ${block}($dataname)] } 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 catch { eval showCIFbyDataname $CIF(lastShownItem) } } # 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 {[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] $CIF(EditSaveButton) config -state disabled } else { [$frame.$i getframe].l config -text $value } } } # Parse a number in CIF, that may include a SU (ESD) value # note that this routine will ignore spaces, quotes & semicolons proc ParseSU {value} { # if there is no SU just return the value if {[string first "(" $value] == -1} { return $value } # is there a decimal point? if [regexp {([-+]?[0-9]*\.)([0-9]*)\(([0-9]+)\)} $value junk a b err] { set ex [string length $b] return [list ${a}${b} [expr {pow(10.,-$ex)*$err}]] } if [regexp {([-+]?[0-9]*)\(([0-9]+)\)} $value junk a err] { return [list ${a} $err] } tk_dialog .err {ParseSU Error} \ "ParseSU: Error processing value $value" \ warning 0 Continue } # 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 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 80 characters/line proc ReplaceMarkedText {txt mark value} { # 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 quotes inside the string? set doublequote [string first "\"" $value] set singlequote [string first {'} $value] # if we have both types 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 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 } } # 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 } catch { seek $fp $loc set line [read $fp $len] close $fp # remove superfluous spaces regsub -all { +} [StripQuotes $line] { } line } } return $line } # validates that a CIF value is valid for a specific dataname proc ValidateCIFItem {dataname item} { global CIF_dataname_index if {[ catch { foreach {type range elist esd units} [lindex $CIF_dataname_index($dataname) 1] {} } ]} {return "warning: dataname $dataname not defined"} if {$type == "c"} { if {$elist != ""} { foreach i $elist { if {[string tolower $item] == [string tolower [lindex $i 0]]} {return} } return "error: value $item is not an allowed option for $dataname" } else { set l 0 set err {} foreach line [split $item \n] { incr l if {[string length $line] > 80} {lappend err $l} } if {$err != ""} {return "error: line(s) $err are too long"} return } } if {$type == ""} {return "error: dataname $dataname is not used for CIF data items"} # validate numbers if {$type == "n"} { if {$item == "?" || $item == "."} return set v $item # remove s.u., if allowed & present if {$esd} { regsub {\([0-9]+\)} $v {} v } if [catch {expr $v}] {return "error: value $item is not a valid number for $dataname"} 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 "error: value $item must be an integer for $dataname" } if {$min != ""} { if {$v < $min} { return "error: value $item is too small for $dataname" } } if {$max != ""} { if {$v > $max} { return "error: value $item is too big for $dataname" } } } } } # displays the dictionary definitions in variable defs into a text widget proc ShowDictionaryDefinition {defs} { global CIF set deflist [GetCIFDefinitions $defs] $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} [lindex $CIF_dataname_index($dataname) 1] {} } ]} { set type c set elist {} } lappend CIF(widgetlist) $widget if $CIF(editmode) { if {$loopval != ""} { set widgetinfo [list $dataname $block [expr $loopval -1]] } else { set widgetinfo [list $dataname $block 0] } if {$type == "n"} { set CIFeditArr($widget) $value set CIFinfoArr($widget) $widgetinfo 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 CIFeditArr($widget) $value set CIFinfoArr($widget) $widgetinfo 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" set CIFeditArr($widget) $value set CIFinfoArr($widget) $widgetinfo 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. If the value has changed, the "Save Changes" button is # made active. proc CheckChanges {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]] set err {} switch [winfo class $widget] { Text { set current [$widget get 1.0 end] set l 0 foreach line [set linelist [split $current \n]] { incr l if {[string length $line] > 80} {lappend err $l} } 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:"} { $widget config -fg red } else { $widget config -fg black } } Menubutton { set current $CIFeditArr($widget) } } if {[string trim $orig] != [string trim $current]} { if {$CIF(autosave_edits) && $err == ""} { lappend CIF(entry_changed) $widget SaveCIFedits return } if {[string first $widget $CIF(entry_changed)] == -1} { lappend CIF(entry_changed) $widget } $CIF(EditSaveButton) config -state normal } } # save the CIF edits into the CIF text widget proc SaveCIFedits {} { global CIFeditArr CIFinfoArr CIF # validate the entries set error {} foreach widget $CIF(entry_changed) { 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 [$widget get 1.0 end] set l 0 foreach line [split $current \n] { incr l if {[string length $line] > 80} { lappend error "Error: line $l for $dataname is >80 characters" } } } Entry { set current [string trim [$widget get]] set err [ValidateCIFItem [lindex $CIFinfoArr($widget) 0] $current] if {$err != "" && [lindex $err 0] != "warning:"} { lappend error $err } } } } if {$error != ""} { set msg "The attempted changes cannot be saved due to:\n" foreach err $error { append msg " " $err \n } append msg \n {Please correct and then press "Save Changes"} MyMessageBox -parent . -title "Invalid Changes?" \ -message $msg -icon error -type Continue -default continue return } foreach widget $CIF(entry_changed) { foreach {dataname block index} $CIFinfoArr($widget) {} global ${block} set mark [lindex [set ${block}($dataname)] $index] 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) } } ReplaceMarkedText $CIF(txt) $mark $value incr CIF(changes) } set CIF(entry_changed) {} $CIF(EditSaveButton) config -state disabled pack $CIF(EditSaveButton) -side left } # 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 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 foreach var $looplist { incr CIF(changes) # 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"] lappend ${block}($var) $CIF(markcount) } # 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 } 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} foreach widget $CIF(widgetlist) { foreach {dataname block index} $CIFinfoArr($widget) {} global $block set mark [lindex [set ${block}($dataname)] $index] $CIF(txt) delete $mark.l $mark.r set ${block}($dataname) [lreplace [set ${block}($dataname)] $index $index] } set max [lindex [$CIF(LoopSpinBox) cget -range] 1] incr max -1 $CIF(LoopSpinBox) configure -range "1 $max 1" $CIF(LoopSpinBox) setvalue last } # initialize misc variables set CIF(entry_changed) {} set CIF(changes) 0 set CIF(widgetlist) {} set CIF(lastShownItem) {} set CIF(lastLoopIndex) {} set CIF(autosave_edits) 0