wm withdraw .
set ::tool_name citool
# Copyright 1999-2004,2008-2010,2013 BitMover, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Platform specific setup for tcl scripts
# Copyright (c) 1999 Andrew Chang
# %W% %@%

proc bk_initPlatform {} \
{
	global	tcl_platform dev_null tmp_dir wish sdiffw file_rev
	global	file_start_stop file_stop line_rev keytmp file_old_new
	global 	bk_fs env 

	if [catch {wm withdraw .} err] {
		puts "DISPLAY variable not set correctly or not running X"
		exit 1
	}

	set sdiffw [list "bk" "ndiff" "--sdiff=1" "--ignore-trailing-cr"]
	set dev_null "/dev/null"
	set wish "wish"
	set tmp_dir  "/tmp"
	if {[info exists env(TMPDIR)] && [file writable $env(TMPDIR)]} {
		set tmp_dir $env(TMPDIR)
	}
	set keytmp "/var/bitkeeper"

	# Stuff related to the bk field seperator: ^A
	set bk_fs |
	set file_old_new {(.*)\|(.*)\|(.*)}
	set line_rev {([^\|]*)\|(.*)}

	set file_start_stop {(.*)@(.*)\.\.(.*)}
	set file_stop {(.*)@([0-9.]+$)}
	set file_rev {(.*)@([0-9].*)}
	set env(BK_GUI) "YES"
	catch { unset env(BK_NO_GUI_PROMPT) }

	# Determine the bk icon to associate with toplevel windows. If
	# we can't find the icon, don't set the global variable. This
	# way code that needs the icon can check for the existence of
	# the variable rather than checking the filesystem.
	set f [file join [exec bk bin] bk.xbm]
	if {[file exists $f]} {
		set ::wmicon $f
		# N.B. on windows, wm iconbitmap supports a -default option
		# that is not available on unix. Bummer. 
		catch {wm iconbitmap . @$::wmicon}
	}
}
# Copyright 2000-2013,2016 BitMover, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

proc gc {var {newValue ""}} \
{
	global	gc app

	## If we got a config var with no APP. prefix, check to see if
	## we have an app-specific config option first and force them
	## to use that if we do.
	if {[info exists app]
	    && ![string match "*.*" $var]
	    && [info exists gc($app.$var)]} {
		set var $app.$var
	}
	if {[llength [info level 0]] == 3} { set gc($var) $newValue }
	return $gc($var)
}

## An array of deprecated options and the warning to display to the
## user if they are found to be using said option.
array set deprecated {
"rev.showHistory"

"The rev.showHistory config option has been removed.
Please see 'bk help config-gui' for rev.showRevs and
rev.showCsetRevs for new options."
}

proc warn_deprecated_options {app} \
{
	global	gc deprecated

	foreach {opt desc} [array get deprecated $app.*] {
		if {![info exists gc($opt)]} { continue }
		puts ""
		foreach line [split $desc \n] {
			puts [string trim $line]
		}
	}
}

proc getConfig {prog} \
{
	global gc app env usergc

	# this causes variables like _RED_, _WHITE_, to exist in this proc
	defineSymbolicColors

	set app $prog

	option add *Label.borderWidth 1 100
	option add *Button.borderWidth 1 100
	option add *Menubutton.borderWidth 1 100
	option add *Menu.tearOff 0 widgetDefault
	option add *Menu.borderWidth 1 widgetDefault
	option add *Menu.highlightThickness 1 widgetDefault
	option add *Menu.activeBorderWidth 1 widgetDefault

	initFonts $app _d

	# colorscheme

	set _d(classicColors) 1		;# default to the old color scheme.

	set _d(tabwidth) 8		;# default width of a tab
	set _d(backup) ""		;# Make backups in ciedit: XXX NOTDOC 
	set _d(buttonColor) $SYSTEMBUTTONFACE	;# menu buttons
	set _d(diffOpts) ""		;# options to pass to diff
	set _d(diffHeight) 30		;# height of a diff window
	set _d(diffWidth) 65		;# width of side by side diffs
	set _d(geometry) ""		;# default size/location
	set _d(listBG) $GRAY91		;# topics / lists background
	set _d(mergeHeight) 24		;# height of a merge window
	set _d(mergeWidth) 80		;# width of a merge window
	set _d(newColor) #c4d7c5	;# color of new revision/diff
	set _d(noticeColor) #dbdfe6	;# messages, warnings
	set _d(oldColor) $GRAY88	;# color of old revision/diff
	set _d(searchColor) $ORANGE	;# highlight for search matches
	set _d(selectColor) $LIGHTBLUE	;# current file/item/topic
	set _d(statusColor) $LIGHTBLUE	;# various status windows
	set _d(minsize) 300		;# won't remember geometry if smaller
					 # than this width or height
	#XXX: Not documented yet
	set _d(logoBG) $WHITE		;# background for widget with logo
	set _d(selectBG) $NAVY		;# useful for highlighting text
	set _d(selectFG) $WHITE		;# useful for highlighting text
	set _d(altColumnBG) $BEIGE		;# alternate column background
	set _d(infoColor) $LIGHTYELLOW	;# color of info line in difflib
	set _d(textBG) $WHITE			;# text background
	set _d(textFG) $BLACK			;# text color
	set _d(scrollColor) $GRAY85		;# scrollbar bars
	set _d(troughColor) $LIGHTBLUE	;# scrollbar troughs
	set _d(warnColor) yellow		;# error messages
	set _d(emptyBG) black
	set _d(sameBG) white
	set _d(spaceBG) white
	set _d(changedBG) gray

	set _d(quit)	Control-q	;# binding to exit tool
	set _d(compat_4x) 0		;# maintain compatibility with 4x
					;# quirky bindings
	set _d(highlightOld) $YELLOW2	;# subline highlight color for old
	set _d(highlightNew) $YELLOW2	;# subline highlight color for new
	set _d(highlightsp) $ORANGE	;# subline highlight color in diffs
	set _d(topMargin) 2		;# top margin for diffs in a diff view
	set _d(diffColor) #ededed	;# color of diff lines
	set _d(activeDiffColor) $BKGREEN1 ;# active diff color
	set _d(activeOldColor) $_d(activeDiffColor)
	set _d(activeNewColor) $_d(activeDiffColor)
	set _d(newFont) bkFixedFont
	set _d(oldFont) bkFixedFont
	set _d(activeOldFont) bkFixedFont
	set _d(activeNewFont) bkFixedFont

	set _d(bug.popupBG) $BLUE
	set _d(support.popupBG) $BLUE
	set _d(ci.iconBG) $BKPALEOLIVE	;# background of some icons
	set _d(ci.csetIconBG) $BKBLUE1	;# background of some icons
	set _d(ci.quitSaveBG) $BKSLATEBLUE1	;# "quit but save" button
	set _d(ci.quitSaveActiveBG) $BKSLATEBLUE2	;# "quit but save" button
	set _d(ci.listBG) white	
	set _d(selectColor) #f0f0f0	;# current file/item/topic
	set _d(ci.saveBG) $GRAY94		;# background of save dialog
	set _d(ci.quitNosaveBG) $RED	;# "don't save" button
	set _d(ci.quitNosaveActiveBG) $WHITE ;# "don't save" button
	set _d(ci.dimFG) $GRAY50		;# dimmed text
	set _d(ci.progressBG) $WHITE		;# background of progress bar
	set _d(ci.progressColor) $BKSLATEBLUE1 ;# color of progress bar
	set _d(ci.editHeight) 30	;# editor height
	set _d(ci.editWidth) 80		;# editor width
	set _d(ci.excludeColor) $RED	;# color of the exclude X
	set _d(ci.editor) ciedit	;# editor: ciedit=builtin, else in xterm
	set _d(ci.display_bytes) 8192	;# number of bytes to show in new files
	set _d(ci.diffHeight) 30	;# number of lines in the diff window
	set _d(ci.rescan) 0		;# Do a second scan to see if anything
					;# changed. Values 0 - off 1 - on
	set _d(ci.csetBG) $GRAY94

	set _d(cset.listHeight) 12
	set _d(cset.annotation) ""   ;# annotation options (eg: "-aum")
	set _d(cset.doubleclick) 100 ;# XXX: NOTDOC

	set _d(diff.diffHeight) 50
	set _d(diff.searchColor) $LIGHTBLUE	;# highlight for search matches

	set _d(fm.redoBG) $PINK
	set _d(fm3.conflictBG) gray		;# Color for conflict message
	set _d(fm3.unmergeBG) gray	;# Color for unmerged message
	set _d(fm3.annotate) 1		;# show annotations
	set _d(fm3.comments) 1		;# show comments window
	set _d(fm3.escapeButtonFG) $YELLOW	;# foreground of escape button
	set _d(fm3.escapeButtonBG) $BLACK	;# background of escape button
	set _d(fm3.firstDiff) minus
	set _d(fm3.lastDiff) plus
	set _d(fm3.mergeColor) #b4b6cb	;# color of merge choices in merge win
	set _d(fm3.handColor) $_d(fm3.mergeColor) ;# color of hand merged choices
	set _d(fm3.nextConflict) braceright
	set _d(fm3.nextDiff) bracketright
	set _d(fm3.prevConflict) braceleft
	set _d(fm3.prevDiff) bracketleft
	set _d(fm3.sameColor) #efefef	;# color of unchanged line
	set _d(fm3.showEscapeButton) 1	;# show escape button?
	set _d(fm3.spaceColor) $BLACK	;# color of spacer lines
	set _d(fm3.toggleGCA) x		;# key to toggle GCA info
	set _d(fm3.toggleAnnotations) z	;# key to toggle annotations
	set _d(fm3.undo) u
	set _d(fm3.animateScrolling) 1	;# Use an animated scrolling effect
					;# when jumping conflict diff blocks.

	set _d(help.linkColor) $BLUE	;# hyperlinks
	set _d(help.topicsColor) $ORANGE	;# highlight for topic search matches
	set _d(help.height) 50		;# number of rows to display
	set _d(help.width) 79		;# number of columns to display
	set _d(help.helptext) ""	;# -f<helptextfile> - undocumented
	set _d(help.exact) 0		;# helpsearch, allows partial matches
	set _d(help.scrollbars) RR	;# sides for each scrollbar

	set _d(rename.listHeight) 8

	# N.B. 500ms is the hard-coded constant in tk used to detect
	# double clicks. We need a number slightly larger than that. The
	# book Practical Programming in Tcl/Tk, 4th ed. recommends 600. This
	# results in a noticable delay before a single-click is processed 
	# but there really is no other solution when a double-click must
	# override a single click, or a single-click action will take more
	# than 500ms and therefore preventing double-clicks from ever being
	# noticed.
	set _d(rev.doubleclick) 600  ;# XXX: NOTDOC
	set _d(rev.sashBG) $BLACK
	set _d(rev.canvasBG) #9fb6b8	  	;# graph background
	set _d(rev.commentBG) $LIGHTBLUE	;# background of comment text
	set _d(rev.arrowColor) $DARKBLUE	;# arrow color
	set _d(rev.mergeOutline) $DARKBLUE	;# merge rev outlines
	set _d(rev.revOutline) $DARKBLUE	;# regular rev outlines
	set _d(rev.revColor) $BKCADETBLUE	;# unselected box fills
	set _d(rev.localColor) $GREEN	;# local node (for resolve)
	set _d(rev.remoteColor) $RED	;# remote node (for resolve)
	set _d(rev.gcaColor) $WHITE		;# gca node (for resolve)
	set _d(rev.tagOutline) $YELLOW	;# outline of tagged nodes
	set _d(rev.badColor) $RED		;# color for "bad" revision numbers
	set _d(rev.selectColor) $BKSTEELBLUE ;# highlight color for selected tag
	set _d(rev.dateLineColor) $LIGHTBLUE ;# line that separates dates
	set _d(rev.dateColor) $BKBLACK1	;# dates at the bottom of graph
	set _d(rev.commentHeight) 5       ;# height of comment text widget
	set _d(rev.textWidth) 92	  ;# width of text windows
	set _d(rev.textHeight) 30	  ;# height of lower window
	set _d(rev.showRevs) 250	  ;# Num of revs to show in graph 
	set _d(rev.showCsetRevs) 50	  ;# Num of revs to show for a cset
	# XXX: not documented yet
	set _d(rev.savehistory) 5	  ;# Max # of files to save in file list
	set _d(rev.hlineColor) $WHITE	;# Color of highlight lines XXX:NOTDOC
	set _d(rev.annotate) "-Aur"	  ;# Options given to annotate

	set _d(setup.stripeColor) $BLUE ;# color of horizontal separator
	set _d(setup.mandatoryColor) $BKSLATEGRAY1 ;# mandatory fields
	set _d(bug.mandatoryColor) $BKSLATEGRAY1 ;# mandatory fields
	set _d(support.mandatoryColor) $BKSLATEGRAY1 ;# mandatory fields
	set _d(entryColor) $WHITE	   ;# Color of input fields

	set _d(search.width)		15
	set _d(search.buttonWidth)	15

	set _d(ignoreWhitespace)	0
	set _d(ignoreAllWhitespace)	0

	set _d(hlPercent)	0.5
	set _d(chopPercent)	0.5

	set _d(windows) 0
	set _d(aqua) 0
	set _d(x11) 0

	switch -exact -- [tk windowingsystem] {
	    win32 {
		set _d(windows) 1
		set _d(handCursor) "hand2"
		set _d(cset.leftWidth) 40
		set _d(cset.rightWidth) 80
		set _d(ci.filesHeight) 8
		set _d(ci.commentsHeight) 7	;# height of comment window
		set _d(buttonColor) $SYSTEMBUTTONFACE	;# menu buttons
		set _d(BG) $SYSTEMBUTTONFACE		;# default background
		# usable space
		set _d(padTop)		0
		set _d(padBottom)	28		; # someday, we'll 
							  # need to figure
							  # out the size
							  # of the taskbar
		set _d(padRight)	0
		set _d(padLeft)		0
		set _d(titlebarHeight)	20
		set rcfile [exec bk dotbk _bkgui config-gui]
	    } 
	    aqua {
		set _d(aqua) 1
		set _d(handCursor) "pointinghand"
		set _d(cset.leftWidth) 40
		set _d(cset.rightWidth) 80
		set _d(search.width) 4
		set _d(search.buttonWidth) 12
		set _d(ci.filesHeight) 8
		set _d(ci.commentsHeight) 7	;# height of comment window
		set _d(buttonColor) $SYSTEMBUTTONFACE	;# menu buttons
		set _d(BG) $SYSTEMBUTTONFACE		;# default background
		set _d(listBG) $WHITE
		#usable space
		set _d(padTop)		22              ; # someday we'll need
							  # to compute the
							  # size of the menubar
		set _d(padBottom)	0
		set _d(padRight)	0
		set _d(padLeft)		0
		set _d(titlebarHeight)	22
		set rcfile [exec bk dotbk .bkgui config-gui]
	    }
	    x11 {
		set _d(x11) 1

		option add *Scrollbar.borderWidth 1 100
		set _d(handCursor) "hand2"
		set _d(cset.leftWidth) 55
		set _d(cset.rightWidth) 80
		set _d(scrollWidth) 12		;# scrollbar width
		set _d(ci.filesHeight) 9	;# num files to show in top win
		set _d(ci.commentsHeight) 8	;# height of comment window
		set _d(fm.editor) "fm2tool"
		set _d(buttonColor) $SYSTEMBUTTONFACE	;# menu buttons
		set _d(BG) $GRAY85		;# default background
		# usable space (all of it in X11)
		set _d(padTop)		0
		set _d(padBottom)	0
		set _d(padRight)	0
		set _d(padLeft)		0
		set _d(titlebarHeight)	0
		if {$::tcl_platform(os) eq "Darwin"} {
			# We might be in X11 under Aqua, so leave room
			# for the menubar
			set _d(padTop)		22
			set _d(titlebarHeight)	22
		}
		set rcfile [exec bk dotbk .bkgui config-gui]
	    }
	    default {
		puts "Unknown windowing system"
		exit
	    }
	}

	set gc(activeNewOnly) 1

	set gc(bkdir) [file dirname $rcfile]
	if {[file readable $rcfile]} {
		source $rcfile
		warn_deprecated_options $app
	}

	## Save a copy of the gc array exactly as the user specified it.
	array set usergc [array get gc]

	## If the user specified some global option in their config-gui
	## file, write that same value into the app-specific value for
	## the current tool.  This ensures that all values from the user
	## overwrite any values we've set in here.
	foreach var [array names usergc] {
		if {[string match "*.*" $var]} { continue }
		set gc($app.$var) $usergc($var)
	}

	if {[info exists gc(classicColors)]} {
		set _d(classicColors) $gc(classicColors)
	}
	set _d($app.classicColors) $_d(classicColors)
	foreach p [list "" "$prog."] {
		if {[info exists gc(${p}classicColors)]} {
			set _d(${p}classicColors) $gc(${p}classicColors)
		}
	}

	if {[string is true -strict $_d($app.classicColors)]} {
		# set "classic" color scheme. Setting it this way
		# still lets users override individual colors by
		# setting both gc(classicColors) _and_ the color they
		# want to change
		set _d(oldColor) #C8A0FF
		set _d(newColor) #BDEDF5
		set _d(activeOldColor) $_d(oldColor)
		set _d(activeNewColor) $_d(newColor)
		set _d(activeOldFont) bkFixedBoldFont
		set _d(activeNewFont) bkFixedBoldFont
		set _d(noticeColor) $BKBLUE1
		set _d(searchColor) $YELLOW
		set _d(infoColor) $POWDERBLUE
		set _d(warnColor) $YELLOW
		set _d(highlight) $YELLOW2
		set _d(diffColor) $GRAY88
		set _d(activeDiffColor) $BKGREEN1
		set _d(fm3.conflictBG) $RED
		set _d(fm3.unmergeBG) $LIGHTYELLOW
		set _d(fm3.mergeColor) $LIGHTBLUE
		set _d(fm3.handColor) $LIGHTYELLOW
		set _d(fm3.sameColor) $BKTURQUOISE1
	}

	## Set these colors regardless of classic or not.
	set _d(diff.activeOldColor) $_d(activeDiffColor)
	set _d(diff.activeNewColor) $_d(activeDiffColor)
	set _d(diff.activeOldFont) bkFixedFont
	set _d(diff.activeNewFont) bkFixedFont

	set _d(cset.activeOldColor) $_d(activeDiffColor)
	set _d(cset.activeNewColor) $_d(activeDiffColor)
	set _d(cset.activeOldFont) bkFixedFont
	set _d(cset.activeNewFont) bkFixedFont

	set _d(fm.activeOldColor) $_d(activeDiffColor)
	set _d(fm.activeNewColor) $_d(activeDiffColor)
	set _d(fm.activeOldFont) bkFixedFont
	set _d(fm.activeNewFont) bkFixedFont

	## If the user specified showRevs but not showCsetRevs, use showRevs
	## for both values for backward compatibility.
	foreach p [list "" "$prog."] {
		if {[info exists gc(${p}showRevs)]
		    && ![info exists gc(${p}showCsetRevs)]} {
			set _d(${p}showCsetRevs) $gc(${p}showRevs)
		}
	}

	if {$prog eq "fm3"} {
		## For backward compatibility, if the user has specified
		## charColor in fm3tool, we'll use that as our subline
		## highlight color if they haven't also specified a
		## highlight color to use.
		foreach p [list "" "$prog."] {
			if {[info exists gc(${p}charColor)]
			    && ![info exists gc(${p}highlight)]} {
				set _d(${p}highlight) $gc(${p}charColor)
			}
		}
	}

	## If the user specified activeDiffColor but didn't give us anything
	## for activeNewColor or activeOldColor, fill in the activeDiffColor
	## for those values.
	foreach p [list "" "$prog."] {
		if {![info exists gc(${p}activeDiffColor)]} { continue }
		if {![info exists gc(${p}activeOldColor)]} {
			set _d(${p}activeOldColor) $gc(${p}activeDiffColor)
		}
		if {![info exists gc(${p}activeNewColor)]} {
			set _d(${p}activeNewColor) $gc(${p}activeDiffColor)
		}
	}

	# Pass one just copies all the defaults into gc unless they are set
	# already by the config file
	foreach index [array names _d] {
		if {! [info exists gc($index)]} {
			set gc($index) $_d($index)
			#puts "gc\($index) = $_d($index) (default)"
		}
	}

	# Pass to converts from global field to prog.field
	foreach index [array names gc] {
		if {[string first "." $index] == -1} {
			set i "$prog.$index"
			if {![info exists gc($i)]} {
				set gc($i) $gc($index)
				#puts "gc\($i) = $gc($i) from $index"
			}
		}
    	}

	if {![info exists gc(rev.graphFont)]} {
		if {$gc(fixedFont) eq "bkFixedFont"} {
			set gc(rev.graphFont) $gc(default.fixedFont)
		} else {
			set gc(rev.graphFont) $gc(fixedFont)
		}
	}
	if {![info exists gc(rev.graphBoldFont)]} {
		if {$gc(fixedBoldFont) eq "bkFixedBoldFont"} {
			set gc(rev.graphBoldFont) $gc(default.fixedBoldFont)
		} else {
			set gc(rev.graphBoldFont) $gc(fixedBoldFont)
		}
	}

	foreach var [array names gc *Font] {
		switch -- $gc($var) {
			"default" {
				set gc($var) bkButtonFont
			}
			"default bold" {
				set gc($var) bkBoldFont
			}
			"fixed" {
				set gc($var) bkFixedFont
			}
			"fixed bold" {
				set gc($var) bkFixedBoldFont
			}
		}
	}

	configureFonts $app

	option add *Text.tabStyle wordprocessor

	if {$gc($app.tabwidth) != 8} {
		option add *Text.tabs \
		    [expr {$gc($app.tabwidth) * [font measure bkFixedFont 0]}]
	}
}

proc initFonts {app var} \
{
	upvar 1 $var _d

	## Twiddle the default Tk fonts more to our liking.
	switch -- [tk windowingsystem] {
		"x11" {
			font configure TkTextFont -size -14
			font configure TkDefaultFont -size -14
		}
		"win32" {
			font configure TkFixedFont -size 8
		}
	}

	set font [font configure TkTextFont]
	set bold [dict replace $font -weight bold]

	set fixed     [font configure TkFixedFont]
	set fixedBold [dict replace $fixed -weight bold]

	set fonts [font names]
	if {"bkBoldFont" ni $fonts} {
		font create bkBoldFont {*}$bold
	}
	if {"bkButtonFont" ni $fonts} {
		font create bkButtonFont {*}$font
	}
	if {"bkNoticeFont" ni $fonts} {
		font create bkNoticeFont {*}$bold
	}
	if {"bkFixedFont" ni $fonts} {
		font create bkFixedFont {*}$fixed
	}
	if {"bkFixedBoldFont" ni $fonts} {
		font create bkFixedBoldFont {*}$fixedBold
	}

	set _d(default.boldFont) $bold
	set _d(default.buttonFont) $font
	set _d(default.noticeFont) $bold
	set _d(default.fixedFont) $fixed
	set _d(default.fixedBoldFont) $fixedBold

	set _d(boldFont)	bkBoldFont
	set _d(buttonFont)	bkButtonFont
	set _d(noticeFont)	bkNoticeFont
	set _d(fixedFont)	bkFixedFont
	set _d(fixedBoldFont)	bkFixedBoldFont

	if {[tk windowingsystem] eq "aqua"} {
		bind all <Command-0>     "adjustFontSizes 0"
		bind all <Command-plus>  "adjustFontSizes 1"
		bind all <Command-equal> "adjustFontSizes 1"
		bind all <Command-minus> "adjustFontSizes -1"
	} else {
		bind all <Control-0>     "adjustFontSizes 0"
		bind all <Control-plus>  "adjustFontSizes 1"
		bind all <Control-equal> "adjustFontSizes 1"
		bind all <Control-minus> "adjustFontSizes -1"
	}
}

proc configureFonts {app} \
{
	global	gc usergc

	set res [getScreenSize]
	foreach font {boldFont buttonFont noticeFont fixedFont fixedBoldFont} {
		set name bk[string toupper $font 0]

		## If they have a saved state for this font, use it.
		## Otherwise we use whatever is configured either from
		## initFonts or potentially from config-gui.
		if {[info exists usergc($font)]} {
			set f $usergc($font)
		} elseif {[info exists ::State($font@$res)]} {
			set f $::State($font@$res)
		} else {
			set f $gc($font)
		}
		if {[string index $f 0] ne "-"} { set f [font actual $f] }
		font configure $name {*}$f
		set gc($font) $name
		set gc($app.$font) $name
	}
}

proc adjustFontSizes {n} \
{
	global	gc

	set res [getScreenSize]
	foreach font {fixedFont fixedBoldFont} {
		set name bk[string toupper $font 0]
		if {$n == 0} {
			set opts $gc(default.$font)
		} else {
			set opts [font configure $name]
			set size [dict get $opts -size]
			if {$size >= 0} {
				if {$size <= 6 && $n < 0} { return }
				dict incr opts -size $n
			} else {
				if {$size >= -6 && $n < 0} { return }
				dict incr opts -size [expr {-$n}]
			}
		}
		font configure $name {*}$opts
		set ::State($font@$res) $opts
	}
}

# At one point, the bk guis used symbolic color names to define some
# widget colors. Experience has shown that some color names aren't
# portable across platforms. Bug <2002-12-09-004> shows that the color 
# "darkblue", for instance, doesn't exist on some platforms.
#
# Hard-coding hex values in getConfig() is more portable, but hard
# to read. Thus, this proc defines varibles which can be used in lieu
# of hex values. The names and their definitions are immutable; if you
# want to change a color in a GUI, don't change it here. Define a new
# symbolic name, or pick an existing symbolic name. Don't redefine an
# existing color name.
proc defineSymbolicColors {} \
{
	uplevel {
		# these are taken from X11's rgb.txt file
		set BEIGE		#f5f5dc
		set BLACK		#000000
		set BLUE		#0000ff
		set DARKBLUE		#00008b
		set GRAY50		#7f7f7f
		set GRAY85		#d9d9d9
		set GRAY88		#e0e0e0
		set GRAY91		#e8e8e8
		set GRAY94		#f0f0f0
		set GREEN		#00ff00
		set LIGHTBLUE		#add8e6
		set LIGHTYELLOW	#ffffe0
		set NAVY		#000080
		set ORANGE		#ffa500
		set PINK		#ffc0cb
		set POWDERBLUE	#b0e0e6
		set RED		#ff0000
		set WHITE		#ffffff
		set YELLOW		#ffff00
		set YELLOW2		#fffd56

		# This is used for menubuttons, and is based on the
		# "SystemButtonFace" on windows. 
		if {[tk windowingsystem] eq "win32"} {
#			set SYSTEMBUTTONFACE #d4d0c8
                        set SYSTEMBUTTONFACE systembuttonface
		} elseif {[tk windowingsystem] eq "aqua"} {
			set SYSTEMBUTTONFACE $WHITE
		} else {
			set SYSTEMBUTTONFACE [ttk::style lookup . -background]
		}

		# these are other colors for which no official name exists;
		# there were once hard-coded into getConfig(), but I've
		# given them symbolic names to be consistent with all of
		# the other colors. I tried to visually match them to a
		# similar color in rgb.txt and added a BK prefix
		set BKBLACK1		#181818
		set BKBLUE1		#b0b0e0
		set BKBLUE2		#a8d8e0
		set BKBLUE3		#b4e0ff
		set BKCADETBLUE		#9fb6b8
		set BKGREEN1		#2fedad
		set BKGREEN2		#a2dec3
		set BKPALEOLIVE		#e8f8a6
		set BKPLUM		#dfafdf
		set BKSLATEBLUE1	#a0a0ff
		set BKSLATEBLUE2	#c0c0ff
		set BKSLATEGRAY1	#deeaf4
		set BKSTEELBLUE		#adb8f6
		set BKTURQUOISE1	#1cc7d0
		set BKVIOLET1		#b48cff
	}
}
# Copyright 2011,2016 BitMover, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
namespace eval ttk::theme::bk {

    package provide ttk::theme::bk 0.1

    variable colors ; array set colors {
	-disabledfg	"#999999"

	-frame  	"#EEEEEE"
        -lighter        "#FBFBFB"
        -dark           "#DEDEDE"
        -darker         "#CDCDCD"
        -darkest        "#979797"
        -pressed        "#C5C5C5"
	-lightest 	"#ffffff"
	-selectbg	"#447BCD"
	-selectfg	"#ffffff"

        -activebutton   "#E5E8ED"
    }

    ttk::style theme create bk -parent clam -settings {
	ttk::style configure "." \
	    -background $colors(-frame) \
	    -foreground black \
	    -bordercolor $colors(-darkest) \
	    -darkcolor $colors(-dark) \
	    -lightcolor $colors(-lighter) \
	    -troughcolor $colors(-darker) \
	    -selectbackground $colors(-selectbg) \
	    -selectforeground $colors(-selectfg) \
	    -selectborderwidth 0 \
	    -font TkDefaultFont \
            -indicatorsize 12 \
	    ;

	ttk::style map "." \
	    -background [list disabled $colors(-frame) \
			     active $colors(-lighter)] \
	    -foreground [list disabled $colors(-disabledfg)] \
	    -selectbackground [list !focus $colors(-darkest)] \
	    -selectforeground [list !focus white] \
	    ;

	ttk::style configure TButton \
            -anchor center -width -8 -padding 1 -relief raised
	ttk::style map TButton \
	    -background [list \
			     disabled $colors(-frame) \
			     pressed $colors(-pressed) \
			     active $colors(-activebutton)] \
	    -lightcolor [list \
                            disabled $colors(-lighter) \
                            pressed "#C5C5C5" \
                            active  "#A9C1E7"] \
	    -darkcolor [list \
                            disabled $colors(-dark) \
                            pressed "#D6D6D6" \
                            active "#7BA1DA"] \
	    ;

	ttk::style configure Toolbutton -anchor center -padding 1 -relief flat
	ttk::style map Toolbutton \
	    -relief [list \
                        disabled flat \
                        selected sunken \
                        pressed  sunken \
                        active   raised] \
	    -background [list \
			     disabled $colors(-frame) \
			     pressed $colors(-pressed) \
			     active $colors(-activebutton)] \
	    -lightcolor [list \
                            pressed "#C5C5C5" \
                            active  "#A9C1E7"] \
	    -darkcolor [list \
                            pressed "#D6D6D6" \
                            active "#7BA1DA"] \
	    ;

	ttk::style configure TCheckbutton \
	    -indicatorbackground "#ffffff" -indicatormargin {1 1 4 1}
	ttk::style configure TRadiobutton \
	    -indicatorbackground "#ffffff" -indicatormargin {1 1 4 1}
	ttk::style map TCheckbutton -indicatorbackground \
	    [list  disabled $colors(-frame)  pressed $colors(-frame)]
	ttk::style map TRadiobutton -indicatorbackground \
	    [list  disabled $colors(-frame)  pressed $colors(-frame)]

	ttk::style configure TMenubutton \
            -width -8 -padding 1 -relief raised

	ttk::style configure TEntry -padding 1 -insertwidth 1
	ttk::style map TEntry \
	    -background [list  readonly $colors(-frame)] \
	    -bordercolor [list  focus $colors(-selectbg)] \
	    -lightcolor [list  focus #6490D2] \
	    -darkcolor [list  focus #60A0FF]

	ttk::style configure TCombobox -padding 1 -insertwidth 1
	ttk::style map TCombobox \
	    -background [list active $colors(-lighter) \
			     pressed $colors(-lighter)] \
	    -fieldbackground [list {readonly focus} $colors(-selectbg)] \
	    -foreground [list {readonly focus} $colors(-selectfg)] \
	    ;

	ttk::style configure TNotebook.Tab -padding {6 0 6 2}
	ttk::style map TNotebook.Tab \
	    -padding [list selected {6 2 6 2}] \
	    -background [list active $colors(-activebutton)] \
	    -lightcolor [list \
                active #6490D2 selected $colors(-lighter) {} $colors(-dark) \
            ] \
	    -darkcolor  [list active #60A0FF] \
	    ;

    	ttk::style configure TLabelframe \
	    -labeloutside true -labelinset 0 -labelspace 2 \
	    -borderwidth 2 -relief raised

	ttk::style configure TProgressbar -background #437BCC \
            -darkcolor #546F98 -lightcolor #467ED7

	ttk::style configure Sash -sashthickness 6 -gripcount 10
    }
}
# tooltip.tcl --
#
#       Balloon help
#
# Copyright (c) 1996-2007 Jeffrey Hobbs
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
# 
# RCS: @(#) $Id: tooltip.tcl,v 1.16 2008/12/01 23:37:16 hobbs Exp $
#
# Initiated: 28 October 1996


package require Tk 8.4
package require msgcat

#------------------------------------------------------------------------
# PROCEDURE
#	tooltip::tooltip
#
# DESCRIPTION
#	Implements a tooltip (balloon help) system
#
# ARGUMENTS
#	tooltip <option> ?arg?
#
# clear ?pattern?
#	Stops the specified widgets (defaults to all) from showing tooltips
#
# delay ?millisecs?
#	Query or set the delay.  The delay is in milliseconds and must
#	be at least 50.  Returns the delay.
#
# disable OR off
#	Disables all tooltips.
#
# enable OR on
#	Enables tooltips for defined widgets.
#
# <widget> ?-index index? ?-items id? ?-tag tag? ?message?
#	If -index is specified, then <widget> is assumed to be a menu
#	and the index represents what index into the menu (either the
#	numerical index or the label) to associate the tooltip message with.
#	Tooltips do not appear for disabled menu items.
#	If -item is specified, then <widget> is assumed to be a listbox
#	or canvas and the itemId specifies one or more items.
#	If -tag is specified, then <widget> is assumed to be a text
#	and the tagId specifies a tag.
#	If message is {}, then the tooltip for that widget is removed.
#	The widget must exist prior to calling tooltip.  The current
#	tooltip message for <widget> is returned, if any.
#
# RETURNS: varies (see methods above)
#
# NAMESPACE & STATE
#	The namespace tooltip is used.
#	Control toplevel name via ::tooltip::wname.
#
# EXAMPLE USAGE:
#	tooltip .button "A Button"
#	tooltip .menu -index "Load" "Loads a file"
#
#------------------------------------------------------------------------

namespace eval ::tooltip {
    namespace export -clear tooltip
    variable labelOpts
    variable tooltip
    variable G

    if {![info exists G]} {
        array set G {
            enabled     1
            fade        1
            FADESTEP    0.2
            FADEID      {}
            DELAY       500
            AFTERID     {}
            LAST        -1
            TOPLEVEL    .__tooltip__
        }
        if {[tk windowingsystem] eq "x11"} {
            set G(fade) 0 ; # don't fade by default on X11
        }
    }
    if {![info exists labelOpts]} {
	# Undocumented variable that allows users to extend / override
	# label creation options.  Must be set prior to first registry
	# of a tooltip, or destroy $::tooltip::G(TOPLEVEL) first.
	set labelOpts [list -highlightthickness 0 -relief solid -bd 1 \
			   -background lightyellow -fg black]
    }

    # The extra ::hide call in <Enter> is necessary to catch moving to
    # child widgets where the <Leave> event won't be generated
    bind Tooltip <Enter> [namespace code {
	#tooltip::hide
	variable tooltip
	variable G
	set G(LAST) -1
	if {$G(enabled) && [info exists tooltip(%W)]} {
	    set G(AFTERID) \
		[after $G(DELAY) [namespace code [list _show %W $tooltip(%W) cursor]]]
	}
    }]

    bind Menu <<MenuSelect>>	[namespace code { menuMotion %W }]
    bind Tooltip <Leave>	[namespace code [list hide 1]] ; # fade ok
    bind Tooltip <Any-KeyPress>	[namespace code hide]
    bind Tooltip <Any-Button>	[namespace code hide]
}

proc ::tooltip::tooltip {w args} {
    variable tooltip
    variable G
    switch -- $w {
	clear	{
	    if {[llength $args]==0} { set args .* }
	    clear $args
	}
	delay	{
	    if {[llength $args]} {
		if {![string is integer -strict $args] || $args<50} {
		    return -code error "tooltip delay must be an\
			    integer greater than 50 (delay is in millisecs)"
		}
		return [set G(DELAY) $args]
	    } else {
		return $G(DELAY)
	    }
	}
	fade	{
	    if {[llength $args]} {
		set G(fade) [string is true -strict [lindex $args 0]]
	    }
	    return $G(fade)
	}
	off - disable	{
	    set G(enabled) 0
	    hide
	}
	on - enable	{
	    set G(enabled) 1
	}
	default {
	    set i $w
	    if {[llength $args]} {
		set i [uplevel 1 [namespace code "register [list $w] $args"]]
	    }
	    set b $G(TOPLEVEL)
	    if {![winfo exists $b]} {
		variable labelOpts

		toplevel $b -class Tooltip
		if {[tk windowingsystem] eq "aqua"} {
		    ::tk::unsupported::MacWindowStyle style $b help none
		} else {
		    wm overrideredirect $b 1
		}
		catch {wm attributes $b -topmost 1}
		# avoid the blink issue with 1 to <1 alpha on Windows
		catch {wm attributes $b -alpha 0.99}
		wm positionfrom $b program
		wm withdraw $b
		eval [linsert $labelOpts 0 label $b.label]
		pack $b.label -ipadx 1
	    }
	    if {[info exists tooltip($i)]} { return $tooltip($i) }
	}
    }
}

proc ::tooltip::register {w args} {
    variable tooltip
    set key [lindex $args 0]
    while {[string match -* $key]} {
	switch -- $key {
	    -index	{
		if {[catch {$w entrycget 1 -label}]} {
		    return -code error "widget \"$w\" does not seem to be a\
			    menu, which is required for the -index switch"
		}
		set index [lindex $args 1]
		set args [lreplace $args 0 1]
	    }
	    -item - -items {
                if {[winfo class $w] eq "Listbox"} {
                    set items [lindex $args 1]
                } else {
                    set namedItem [lindex $args 1]
                    if {[catch {$w find withtag $namedItem} items]} {
                        return -code error "widget \"$w\" is not a canvas, or\
			    item \"$namedItem\" does not exist in the canvas"
                    }
                }
		set args [lreplace $args 0 1]
	    }
            -tag {
                set tag [lindex $args 1]
                set r [catch {lsearch -exact [$w tag names] $tag} ndx]
                if {$r || $ndx == -1} {
                    return -code error "widget \"$w\" is not a text widget or\
                        \"$tag\" is not a text tag"
                }
                set args [lreplace $args 0 1]
            }
	    -command {
		set command [lindex $args 1]
		set args [lreplace $args 0 1]
	    }
	    default	{
		return -code error "unknown option \"$key\":\
			should be -command, -index, -items or -tag"
	    }
	}
	set key [lindex $args 0]
    }
    if {[llength $args] != 1} {
	return -code error "wrong # args: should be \"tooltip widget\
		?-index index? ?-items item? ?-tag tag? message\""
    }
    if {$key eq ""} {
	clear $w
    } else {
	if {![winfo exists $w]} {
	    return -code error "bad window path name \"$w\""
	}
	set d [dict create message $key]
	if {[info exists command]} {
	    dict set d command $command
	}
	if {[info exists index]} {
	    set tooltip($w,$index) $d
	    return $w,$index
	} elseif {[info exists items]} {
	    foreach item $items {
		set tooltip($w,$item) $d
		if {[winfo class $w] eq "Listbox"} {
		    enableListbox $w $item
		} else {
		    enableCanvas $w $item
		}
	    }
	    # Only need to return the first item for the purposes of
	    # how this is called
	    return $w,[lindex $items 0]
        } elseif {[info exists tag]} {
            set tooltip($w,t_$tag) $d
            enableTag $w $tag
            return $w,$tag
	} else {
	    set tooltip($w) $d
	    bindtags $w [linsert [bindtags $w] end "Tooltip"]
	    return $w
	}
    }
}

proc ::tooltip::clear {{pattern .*}} {
    variable tooltip
    # cache the current widget at pointer
    set ptrw [winfo containing [winfo pointerx .] [winfo pointery .]]
    foreach w [array names tooltip $pattern] {
	unset tooltip($w)
	if {[winfo exists $w]} {
	    set tags [bindtags $w]
	    if {[set i [lsearch -exact $tags "Tooltip"]] != -1} {
		bindtags $w [lreplace $tags $i $i]
	    }
	    ## We don't remove TooltipMenu because there
	    ## might be other indices that use it

	    # Withdraw the tooltip if we clear the current contained item
	    if {$ptrw eq $w} { hide }
	}
    }
}

proc ::tooltip::show {w msg {i {}}} {
    if {![winfo exists $w]} { return }

    # Use string match to allow that the help will be shown when
    # the pointer is in any child of the desired widget
    if {([winfo class $w] ne "Menu")
	&& ![string match $w* [eval [list winfo containing] \
				   [winfo pointerxy $w]]]} {
	return
    }

    variable G

    after cancel $G(FADEID)
    set b $G(TOPLEVEL)
    # Use late-binding msgcat (lazy translation) to support programs
    # that allow on-the-fly l10n changes
    $b.label configure -text [::msgcat::mc $msg] -justify left
    update idletasks
    set screenw [winfo screenwidth $w]
    set screenh [winfo screenheight $w]
    set reqw [winfo reqwidth $b]
    set reqh [winfo reqheight $b]
    # When adjusting for being on the screen boundary, check that we are
    # near the "edge" already, as Tk handles multiple monitors oddly
    if {$i eq "cursor"} {
	set y [expr {[winfo pointery $w]+20}]
	if {($y < $screenh) && ($y+$reqh) > $screenh} {
	    set y [expr {[winfo pointery $w]-$reqh-5}]
	}
    } elseif {$i ne ""} {
	set y [expr {[winfo rooty $w]+[winfo vrooty $w]+[$w yposition $i]+25}]
	if {($y < $screenh) && ($y+$reqh) > $screenh} {
	    # show above if we would be offscreen
	    set y [expr {[winfo rooty $w]+[$w yposition $i]-$reqh-5}]
	}
    } else {
	set y [expr {[winfo rooty $w]+[winfo vrooty $w]+[winfo height $w]+5}]
	if {($y < $screenh) && ($y+$reqh) > $screenh} {
	    # show above if we would be offscreen
	    set y [expr {[winfo rooty $w]-$reqh-5}]
	}
    }
    if {$i eq "cursor"} {
	set x [winfo pointerx $w]
    } else {
	set x [expr {[winfo rootx $w]+[winfo vrootx $w]+
		     ([winfo width $w]-$reqw)/2}]
    }
    # only readjust when we would appear right on the screen edge
    if {$x<0 && ($x+$reqw)>0} {
	set x 0
    } elseif {($x < $screenw) && ($x+$reqw) > $screenw} {
	set x [expr {$screenw-$reqw}]
    }
    if {[tk windowingsystem] eq "aqua"} {
	set focus [focus]
    }
    # avoid the blink issue with 1 to <1 alpha on Windows, watch half-fading
    catch {wm attributes $b -alpha 0.99}
    wm geometry $b +$x+$y
    wm deiconify $b
    raise $b
    if {[tk windowingsystem] eq "aqua" && $focus ne ""} {
	# Aqua's help window steals focus on display
	after idle [list focus -force $focus]
    }
}

proc ::tooltip::_show {w d {i ""}} {
    set message [dict get $d message]
    if {[dict exists $d command]} {
	set message [uplevel #0 [dict get $d command]]
	if {$message eq ""} { return }
    }
    show $w $message $i
}

proc ::tooltip::menuMotion {w} {
    variable G

    if {$G(enabled)} {
	variable tooltip

        # Menu events come from a funny path, map to the real path.
        set m [string map {"#" "."} [winfo name $w]]
	set cur [$w index active]

	# The next two lines (all uses of LAST) are necessary until the
	# <<MenuSelect>> event is properly coded for Unix/(Windows)?
	if {$cur == $G(LAST)} return
	set G(LAST) $cur
	# a little inlining - this is :hide
	after cancel $G(AFTERID)
	catch {wm withdraw $G(TOPLEVEL)}
	if {[info exists tooltip($m,$cur)] || \
		(![catch {$w entrycget $cur -label} cur] && \
		[info exists tooltip($m,$cur)])} {
	    set G(AFTERID) [after $G(DELAY) \
		    [namespace code [list _show $w $tooltip($m,$cur) cursor]]]
	}
    }
}

proc ::tooltip::hide {{fadeOk 0}} {
    variable G

    after cancel $G(AFTERID)
    after cancel $G(FADEID)
    if {$fadeOk && $G(fade)} {
	fade $G(TOPLEVEL) $G(FADESTEP)
    } else {
	catch {wm withdraw $G(TOPLEVEL)}
    }
}

proc ::tooltip::fade {w step} {
    if {[catch {wm attributes $w -alpha} alpha] || $alpha <= 0.0} {
        catch { wm withdraw $w }
        catch { wm attributes $w -alpha 0.99 }
    } else {
	variable G
        wm attributes $w -alpha [expr {$alpha-$step}]
        set G(FADEID) [after 50 [namespace code [list fade $w $step]]]
    }
}

proc ::tooltip::wname {{w {}}} {
    variable G
    if {[llength [info level 0]] > 1} {
	# $w specified
	if {$w ne $G(TOPLEVEL)} {
	    hide
	    destroy $G(TOPLEVEL)
	    set G(TOPLEVEL) $w
	}
    }
    return $G(TOPLEVEL)
}

proc ::tooltip::listitemTip {w x y} {
    variable tooltip
    variable G

    set G(LAST) -1
    set item [$w index @$x,$y]
    if {$G(enabled) && [info exists tooltip($w,$item)]} {
	set G(AFTERID) [after $G(DELAY) \
		[namespace code [list _show $w $tooltip($w,$item) cursor]]]
    }
}

# Handle the lack of <Enter>/<Leave> between listbox items using <Motion>
proc ::tooltip::listitemMotion {w x y} {
    variable tooltip
    variable G
    if {$G(enabled)} {
        set item [$w index @$x,$y]
        if {$item ne $G(LAST)} {
            set G(LAST) $item
            after cancel $G(AFTERID)
            catch {wm withdraw $G(TOPLEVEL)}
            if {[info exists tooltip($w,$item)]} {
                set G(AFTERID) [after $G(DELAY) \
                   [namespace code [list _show $w $tooltip($w,$item) cursor]]]
            }
        }
    }
}

# Initialize tooltip events for Listbox widgets
proc ::tooltip::enableListbox {w args} {
    if {[string match *listitemTip* [bind $w <Enter>]]} { return }
    bind $w <Enter> +[namespace code [list listitemTip %W %x %y]]
    bind $w <Motion> +[namespace code [list listitemMotion %W %x %y]]
    bind $w <Leave> +[namespace code [list hide 1]] ; # fade ok
    bind $w <Any-KeyPress> +[namespace code hide]
    bind $w <Any-Button> +[namespace code hide]
}

proc ::tooltip::itemTip {w args} {
    variable tooltip
    variable G

    set G(LAST) -1
    set item [$w find withtag current]
    if {$G(enabled) && [info exists tooltip($w,$item)]} {
	set G(AFTERID) [after $G(DELAY) \
		[namespace code [list _show $w $tooltip($w,$item) cursor]]]
    }
}

proc ::tooltip::enableCanvas {w args} {
    if {[string match *itemTip* [$w bind all <Enter>]]} { return }
    $w bind all <Enter> +[namespace code [list itemTip $w]]
    $w bind all <Leave>	+[namespace code [list hide 1]] ; # fade ok
    $w bind all <Any-KeyPress> +[namespace code hide]
    $w bind all <Any-Button> +[namespace code hide]
}

proc ::tooltip::tagTip {w tag} {
    variable tooltip
    variable G
    set G(LAST) -1
    if {$G(enabled) && [info exists tooltip($w,t_$tag)]} {
        if {[info exists G(AFTERID)]} { after cancel $G(AFTERID) }
        set G(AFTERID) [after $G(DELAY) \
            [namespace code [list _show $w $tooltip($w,t_$tag) cursor]]]
    }
}

proc ::tooltip::enableTag {w tag} {
    if {[string match *tagTip* [$w tag bind $tag]]} { return }
    $w tag bind $tag <Enter> +[namespace code [list tagTip $w $tag]]
    $w tag bind $tag <Leave> +[namespace code [list hide 1]] ; # fade ok
    $w tag bind $tag <Any-KeyPress> +[namespace code hide]
    $w tag bind $tag <Any-Button> +[namespace code hide]
}

package provide tooltip 1.4.4
# Copyright 1999-2006,2008-2016 BitMover, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

if {[info exists ::env(BK_DEBUG_GUI)]} {
	proc InCommand {} {
		uplevel {puts "[string repeat { } [expr {[info level] - 1}]][info level 0]"}
	}

	proc newproc {name args body} {
		set body "InCommand\n$body"
		realproc $name $args $body
	}

	rename proc realproc
	rename newproc proc
}

if {![info exists ::env(BK_GUI_LEVEL)]
    || ![string is integer -strict $::env(BK_GUI_LEVEL)]} {
	set ::env(BK_GUI_LEVEL) 0
}
incr ::env(BK_GUI_LEVEL)

proc bk_toolname {} {
	if {[info exists ::tool_name]} {
		return $::tool_name
	}
	return [file tail [info script]]
}

proc bk_toplevel {} {
	if {[bk_toolname] eq "citool"} { return ".citool" }
	return "."
}

proc bk_initTheme {} \
{
	switch -- [tk windowingsystem] {
		"aqua" {
			set bg "systemSheetBackground"
		}

		"x11" {
			ttk::setTheme bk
		}
	}

	set bg [ttk::style lookup . -background]

	. configure -background $bg
	option add *background	$bg

	option add *Frame.background	$bg
	option add *Label.background	$bg
	option add *Toplevel.background	$bg
	option add *Listbox.background	#FFFFFF
	option add *Entry.background	#FFFFFF
	option add *Entry.borderWidth	1
	option add *Text.background	#FFFFFF
	## Work around a Tk bug in OS X.
	if {[tk windowingsystem] == "aqua"} {
		option add *Menu.background systemMenu
	}

	## Make the ReadOnly tag
	foreach event [bind Text] {
		set script [bind Text $event]
		if {[regexp -nocase {text(paste|insert|transpose)} $script]
		    || [regexp -nocase {%W (insert|delete|edit)} $script]} {
			continue
		}
		set script [string map {tk_textCut tk_textCopy} $script]
		bind ReadonlyText $event $script
	}
	bind ReadonlyText <Up>	    "%W yview scroll -1 unit; break"
	bind ReadonlyText <Down>    "%W yview scroll  1 unit; break"
	bind ReadonlyText <Left>    "%W xview scroll -1 unit; break"
	bind ReadonlyText <Right>   "%W xview scroll  1 unit; break"
	bind ReadonlyText <Prior>   "%W yview scroll -1 page; break"
	bind ReadonlyText <Next>    "%W yview scroll  1 page; break"
	bind ReadonlyText <Home>    "%W yview moveto 0; break"
	bind ReadonlyText <End>	    "%W yview moveto 1; break"
}

proc bk_init {} {
	set tool [bk_toolname]

	bk_initPlatform

	bk_initTheme

	## Include our tool name and Toplevel tags for .
	bindtags . [list $tool . Toplevel all]

	## Remove default Tk mouse wheel bindings.
	foreach event {MouseWheel 4 5} {
		foreach mod {"" Shift- Control- Command- Alt- Option-} {
			catch {bind Text <$mod$event> ""}
			catch {bind Listbox <$mod$event> ""}
		}
	}

	## Mouse wheel bindings
	if {[tk windowingsystem] eq "x11"} {
		bind all <4> {scrollMouseWheel %W y %X %Y -1}
		bind all <5> {scrollMouseWheel %W y %X %Y  1}
		bind all <Shift-4> {scrollMouseWheel %W x %X %Y -1}
		bind all <Shift-5> {scrollMouseWheel %W x %X %Y  1}

		bind wheel <4> {scrollMouseWheel %W y %X %Y -1}
		bind wheel <5> {scrollMouseWheel %W y %X %Y  1}
		bind wheel <Shift-4> {scrollMouseWheel %W x %X %Y -1}
		bind wheel <Shift-5> {scrollMouseWheel %W x %X %Y  1}
	} else {
		bind all <MouseWheel> {scrollMouseWheel %W y %X %Y %D}
		bind all <Shift-MouseWheel> {scrollMouseWheel %W x %X %Y %D}

		bind wheel <MouseWheel> {scrollMouseWheel %W y %X %Y %D}
		bind wheel <Shift-MouseWheel> {scrollMouseWheel %W x %X %Y %D}
	}

	if {[tk windowingsystem] eq "aqua"} {
		event add <<Redo>> <Command-Shift-z> <Command-Shift-Z>
	}

	bind Entry  <KP_Enter> {event generate %W <Return>}
	bind TEntry <KP_Enter> {event generate %W <Return>}
}

# Try to find the project root, limiting ourselves to 40 directories
proc cd2root { {startpath {}} } \
{
	set n 40
	if {$startpath != ""} {
		set dir $startpath
	} else {
		set dir "."
	}
	while {$n > 0} {
		set path [file join $dir BitKeeper etc]
		if {[file isdirectory $path]} {
			cd $dir
			return
		}
		set dir [file join $dir ..]
		incr n -1
	}
	return -1
}

proc cd2product {{path ""}} {
	set cmd [list exec bk root]
	if {$path ne ""} { lappend cmd $path }
	if {[catch { cd [{*}$cmd] } err]} {
		puts "Could not change directory to product root."
		exit 1
	}
}

proc resolveSymlink {filename} {
	catch {
		set original_path [file dirname $filename]
		set link_path [file readlink $filename]
		set filename [file join $original_path $link_path]
		# once we upgrade to tcl 8.4 we should also call 
		# [file normalize]...
	}
	return $filename
}

proc displayMessage {msg {exit {}}} \
{
	if {$exit != ""} {
		set title "Error"
		set icon "error"
	} else {
		set title "Info"
		set icon "info"
	}
	tk_messageBox -title $title -type ok -icon $icon -message $msg \
	    -parent [bk_toplevel]
	if {$exit == 1} {
		exit 1
	} else {
		return
	}
}

proc message {message args} \
{
	if {[dict exists $args -exit]} {
		set exit [dict get $args -exit]
		dict unset args -exit
	}

	if {![dict exists $args -parent]} {
		dict set args -parent [bk_toplevel]
	}

	set forceGui 0
	if {[dict exists $args -gui]} {
	    set forceGui 1
	    dict unset args -gui
	}

	if {!$forceGui && ([info exists ::env(BK_REGRESSION)]
	    || ($::env(BK_GUI_LEVEL) == 1
		&& $::tcl_platform(platform) ne "windows"))} {
		if {[info exists ::env(BK_REGRESSION)]
		    && $::env(BK_GUI_LEVEL) > 1} {
			append message " (LEVEL: $::env(BK_GUI_LEVEL))"
		}
		puts stderr $message
	} else {
		tk_messageBox {*}$args -message $message
	}

	if {[info exists exit]} {
		exit $exit
	}
}

# usage: centerWindow pathName ?width height?
#
# If width and height are supplied the window will be set to
# that size and that size will be used to compute the location
# of the window. Otherwise the requested width and height of the
# window will be used.
proc centerWindow {w args} \
{

	set w [winfo toplevel $w]

	if {[llength $args] > 0} {
		set width [lindex $args 0]
		set height [lindex $args 1]
	} else {
		set width [winfo reqwidth $w]
		set height [winfo reqheight $w]
	}
	set x [expr {round(([winfo vrootwidth $w] - $width) /2)}]
	set y [expr {round(([winfo vrootheight $w] - $height) /2)}]

	wm geometry $w +${x}+${y}
}

# this proc attempts to center a given line number in a text widget;
# similar to the widget's "see" option but with the requested line
# always centered, if possible. The text widget "see" command only
# attempts to center a line if it is "far out of view", so we first
# try to scroll the requested line as far away as possible, then
# scroll it back. Kludgy, but it seems to work.
proc centerTextLine {w line} \
{
	set midline "[expr {int([$w index end-1c] / 2)}].0"
	if {$line > $midline} {
		$w see 1.0
	} else {
		$w see end
	}
	update idletasks
	$w see $line
}

# From a Cameron Laird post on usenet
proc print_stacktrace {} \
{
	set depth [info level]
	puts "Current call stack shows"
	for {set i 1} {$i < $depth} {incr i} {
		puts "\t[info level $i]"
	}
}

proc tmpfile {name} \
{
	global	tmp_dir tmp_files tmp_filecount

	set prefix [file join $tmp_dir "bk_${name}_[pid]"]
	set filename "${prefix}_[incr tmp_filecount].tmp"
	while {[file exists $filename]} {
		set filename "${prefix}_[incr tmp_filecount].tmp"
	}
	lappend tmp_files $filename
	return $filename
}

## Setup a trace to cleanup any temporary files as we exit.
proc cleanupTmpfiles {args} \
{
	catch {
		global	tmp_files
		foreach file $tmp_files {
			file delete -force $file
		}
	}
}
trace add exec exit enter cleanupTmpfiles

proc loadState {appname} \
{
	catch {::appState load $appname ::State}
}

proc saveState {appname} \
{
	catch {::appState save $appname ::State}
}

proc getScreenSize {{w .}} \
{
	return [winfo vrootwidth $w]x[winfo vrootheight $w]
}

proc trackGeometry {w1 w2 width height} \
{	
	global	gc app

	# The event was caused by a different widget
	if {$w1 ne $w2} {return}

	# We don't want to save the geometry if the user maximized
	# the window, so only save if it's a 'normal' resize operation.
	# XXX: Only works on MS Windows
	if {[wm state $w1] eq "normal"} {
		set min $gc($app.minsize)
		set res [getScreenSize $w1]
		if {$width < $min || $height < $min} {
			debugGeom "Geometry ${width}x${height} too small"
			return
		}
		# We can't get width/height from wm geometry because if the 
		# app is gridded, we'll get grid units instead of pixels.
		# The parameters (%w %h) however, seem to 
		# be correct on all platforms.
		foreach {- - ox x oy y} [goodGeometry [wm geometry $w1]] {break}
		set ::State(geometry@$res) "${width}x${height}${ox}${x}${oy}${y}"
		debugGeom "Remembering $::State(geometry@$res)"
	}
}

# See if a geometry string is good. Returns a list with 
# [width, height, ox, x, oy , y] where ox and oy are the sign
# of the geometry string (+|-)
proc goodGeometry {geometry} \
{
	if {[regexp \
    	    {(([0-9]+)[xX]([0-9]+))?(([\+\-])([\+\-]?[0-9]+)([\+\-])([\+\-]?[0-9]+))?} \
	    $geometry - - width height - ox x oy y]} {
		return [list $width $height $ox $x $oy $y]
	}
	return ""
}

proc debugGeom {args} \
{
	global env

	if {[info exists env(BK_DEBUG_GEOMETRY)]} {
		puts stderr [join $args " "]
	}
}

proc restoreGeometry {app {w .}} \
{
	global State gc env

	debugGeom "start"
	# track geometry changes 
	bindtags $w [concat "geometry" [bindtags $w]]
	bind geometry <Configure> [list trackGeometry $w %W %w %h]

	set rwidth [winfo vrootwidth $w]
	set rheight [winfo vrootheight $w]
	set res ${rwidth}x${rheight}
	debugGeom "res $res"

	# get geometry from the following priority list (most to least)
	# 1. -geometry on the command line (which means ::geometry)
	# 2. _BK_GEOM environment variable
	# 3. State(geometry@res) (see loadState && saveState)
	# 4. gc(app.geometry) (see config.tcl)
	# 5. App request (whatever Tk wants)
	# We stop at the first usable geometry...

	if {[info exists ::geometry] && 
	    ([set g [goodGeometry $::geometry]] ne "")} {
		debugGeom "Took ::geometry"
	} elseif {[info exists env(_BK_GEOM)] &&
	    ([set g [goodGeometry $env(_BK_GEOM)]] ne "")} {
		debugGeom "Took _BK_GEOM"
	} elseif {[info exists State(geometry@$res)] &&
	    ([set g [goodGeometry $State(geometry@$res)]] ne "")} {
		debugGeom "Took State"
	} elseif {[info exists gc($app.geometry)] &&
	    ([set g [goodGeometry $gc($app.geometry)]] ne "")} {
		debugGeom "Took app.geometry"
	}
	
	# now get the variables
	if {[info exists g]} {
		foreach {width height ox x oy y} $g {break}
		debugGeom "config: $width $height $ox $x $oy $y"
	} else {
		set width ""
		set x ""
	}
	
	if {$width eq ""} {
		# We need to call update to force the recalculation of
		# geometry. We're assuming the state of the widget is
		# withdrawn so this won't cause a screen update.
		update
		set width [winfo reqwidth $w]
		set height [winfo reqheight $w]
	}
	
	if {$x eq ""} {
		foreach {- - ox x oy y} [goodGeometry [wm geometry $w]] {break}
	}
	debugGeom "using: $width $height $ox $x $oy $y"
	
	# The geometry rules are different for each platform.
	# E.g. in Mac OS X negative positions for the geometry DO NOT
	# correspond to the lower right corner of the app, it's ALWAYS
	# the top left corner. (This will change with Tk-8.4.12 XXX)
	# Thus, we ALWAYS specify the geometry as top left corner for
	# BOTH the app and the screen. The math may be harder, but it'll
	# be right.

	# Usable space
	set ux $gc(padLeft)
	set uy $gc(padTop)
	set uwidth [expr {$rwidth - $gc(padLeft) - $gc(padRight)}]
	set uheight [expr {$rheight - $gc(padTop) 
	    - $gc(padBottom) - $gc(titlebarHeight)}]
	debugGeom "ux: $ux uy: $uy uwidth: $uwidth uheight: $uheight"
	debugGeom "padLeft $gc(padLeft) padRight $gc(padRight)"
	debugGeom "padTop $gc(padTop) padBottom $gc(padBottom)"
	debugGeom "titlebarHeight $gc(titlebarHeight)"

	# Normalize the app's position. I.e. (x, y) is top left corner of app
	if {$ox eq "-"} {set x [expr {$rwidth - $x - $width}]}
	if {$oy eq "-"} {set y [expr {$rheight - $y - $height}]}

	if {![info exists env(BK_GUI_OFFSCREEN)]} {
		# make sure 100% of the GUI is visible
		debugGeom "Size start $width $height"
		set width [expr {($width > $uwidth)?$uwidth:$width}]
		set height [expr {($height > $uheight)?$uheight:$height}]
		debugGeom "Size end $width $height"

		debugGeom "Pos start $x $y"
		if {$x < $ux} {set x $ux}
		if {$y < $uy} {set y $uy}
		if {($x + $width) > ($ux + $uwidth)} {
			debugGeom "1a $ox $x $oy $y"
			set x [expr {$ux + $uwidth - $width}]
			debugGeom "1b $ox $x $oy $y"
		}
		if {($y + $height) > ($uy + $uheight)} {
			debugGeom "2a $ox $x $oy $y"
			set y [expr {$uy + $uheight - $height}]
			debugGeom "2b $ox $x $oy $y"
		}
		debugGeom "Pos end $x $y"
	} else {
		debugGeom "Pos start offscreen $x $y"
		# make sure at least some part of the window is visible
		# i.e. we don't care about size, only position
		# if the app is offscreen, we pull it so that 1/10th of it
		# is visible
		if {$x > ($ux + $uwidth)} {
			set x [expr {$ux + $uwidth - int($uwidth/10)}]
		} elseif {($x + $width) < $ux} {
			set x $ux
		}
		if {$y > ($uy + $uheight)} {
			set y [expr {$uy + $uheight - int($uheight/10)}]
		} elseif {($y + $height) < $uy} {
			set y $uy
		}
		debugGeom "Pos end offscreen $x $y"
	}


	# Since we are setting the size of the window we must turn
	# geometry propagation off
	catch {grid propagate $w 0}
	catch {pack propagate $w 0}

	debugGeom "${width}x${height}"
	# Don't use [wm geometry] for width and height because it 
	# treats the arguments as grid units if the widget is in grid mode.
	$w configure -width $width -height $height

	debugGeom "+$x +$y"
	wm geometry $w +${x}+${y}
}

# this removes hardcoded newlines from paragraphs so that the paragraphs
# will wrap when placed in a widget that wraps (such as the description
# of a step)
proc wrap {text} \
{
	if {$::tcl_version >= 8.2} {
		set text [string map [list \n\n \001 \n { }] $text]
		set text [string map [list \001 \n\n] $text]
	} else {
		regsub -all "\n\n" $text \001 text
		regsub -all "\n" $text { } text
		regsub -all "\001" $text \n\n text
	}
	return $text
}

# get a message from the bkmsg.doc message catalog
proc getmsg {key args} \
{
	# do we want to return something like "lookup failed for xxx"
	# if the lookup fails? What we really need is something more
	# like real message catalogs, where I can supply messages that
	# have defaults.
	set data ""
	set cmd [list bk getmsg $key]
	if {[llength $args] > 0} {
		lappend cmd [lindex $args 0]
	}
	set err [catch {set data [eval exec $cmd]}]
	return $data
}

# usage: bgExec ?options? command ?arg? ?arg ..?
#
# this command exec's a program, waits for it to finish, and returns
# the exit code of the exec'd program.  Unlike a normal "exec" call, 
# while the pipe is running the event loop is active, allowing the 
# calling GUI to be refreshed as necessary. However, this proc will 
# not allow the user to interact with the calling program until it 
# returns, by doing a grab on an invisible window.
#
# Upon completion, stdout from the command is available in the global
# variable bgExec(stdout), and stderr is in bgExec(stderr)
#
#
# Options:
# 
# -output proc
#
#    proc is the name of a proc to be called whenever output is
#    generated by the command. The proc will be called with two
#    arguments: the file descriptor (useful as a unique identifier) and
#    the data that was read in.
#
# example: 
#
#    text .t ; pack .t
#    proc showOutput {f string} {
#        .t insert end $string
#        return $string
#    }
#    set exitStatus [bgExec -output showOutput ls]
#
# Side effects:
#
# while running, this creates a temporary window named .__grab__<fd>,
# where <fd> is the file description of the open pipe

namespace eval ::bgExec {}
interp alias {} ::bgExec {} ::bgExec::bgExec

proc ::bgExec::bgExec {args} \
{
	global bgExec errorCode

	set outhandler ""
	while {[llength $args] > 1} {
		set arg [lindex $args 0]
		switch -exact -- $arg {
			-output {
				set outhandler [lindex $args 1]
				set args [lrange $args 2 end]
			}
			--	{
				set args [lrange $args 1 end]
				break
			}
			default	{break}
		}
	}

	set stderrFile [tmpfile "bgexec-stderr"]
	set run_fd [open |[list {*}$args 2> $stderrFile] "r"]
	fconfigure $run_fd -blocking false
	fileevent $run_fd readable [namespace code [list readFile $run_fd]]

	set bgExec(handler) $outhandler
	set bgExec(stdout) ""
	set bgExec(stderr) ""
	set bgExec(status) 0

	# Create a small, invisible window, and do a grab on it
	# so the user can't interact with the main program.
	set grabWin .__grab__$run_fd
	frame $grabWin -width 1 -height 1 -background {} -borderwidth 0
	place $grabWin -relx 1.0 -x -2 -rely 1.0 -y -2
	after idle "if {\[winfo exists $grabWin]} {grab $grabWin}"

	# This variable is set by the code that gets run via the 
	# fileevent handler when we get EOF on the pipe.
	vwait bgExec(status)

	catch {destroy $grabWin}

	# The pipe must be reconfigured to blocking mode before
	# closing, or close won't wait for the process to end. If
	# close doesn't wait, we can't get the exit status.
	fconfigure $run_fd -blocking true
	set ::errorCode [list NONE]
	catch {close $run_fd}
	if {[info exists ::errorCode] && 
	    [lindex $::errorCode 0] == "CHILDSTATUS"} {
		set exitCode [lindex $::errorCode 2]
	} else {
		set exitCode 0
	}

	if {[file exists $stderrFile]} {
		set f [open $stderrFile r]
		set bgExec(stderr) [read $f]
		close $f
		file delete $stderrFile
	}

	unset bgExec(handler)
	unset bgExec(status)

	return $exitCode
}

proc ::bgExec::handleOutput {f string} \
{
	global bgExec

	if {[info exists bgExec(handler)] && $bgExec(handler) != ""} {
		set tmp [$bgExec(handler) $f $string]
		append bgExec(stdout) $tmp
	} else {
		append bgExec(stdout) $string
	}
}

proc ::bgExec::readFile {f} \
{
	global bgExec

	# The channel is readable; try to read it.
	set status [catch { gets $f line } result]

	if { $status != 0 } {
		# Error on the channel
		set bgExec(status) $status

	} elseif { $result >= 0 } {
		# Successfully read the channel
		handleOutput $f "$line\n"

	} elseif { [eof $f] } {
		# End of file on the channel
		set bgExec(status) 1

	} elseif { [fblocked $f] } {
		# Read blocked.  Just return

	} else {
		# Something else; should never happen.
		set bgExec(status) 2
	}
}

proc popupMessage {args} \
{
	if {[llength $args] == 1} {
		set option ""
		set message [lindex $args 0]
	} else {
		set option [lindex $args 0]
		set message [lindex $args 1]
	}

	# export BK_MSG_GEOM so the popup will show in the right
	# place...
	if {[winfo viewable .] || [winfo viewable .citool]} {
		set x [expr {[winfo rootx .] + 40}]
		set y [expr {[winfo rooty .] + 40}]
		set ::env(BK_MSG_GEOM) "+${x}+${y}"
	}

	set tmp [tmpfile msg]
	set fp [open $tmp w]
	puts $fp $message
	close $fp

	# hopefully someday we'll turn the msgtool code into a library
	# so we don't have to exec. For now, though, exec works just fine.
	if {[info exists ::env(BK_REGRESSION)]} {
		# we are running in test mode; spew to stderr
		puts stderr $message
	} else {
		exec bk msgtool {*}$option -F $tmp
	}
}

# License Functions

proc checkLicense {license licsign1 licsign2 licsign3} \
{
	global dev_null

	# bk _eula -v has the side effect of popping up a messagebox
	# warning the user if the license is invalid. 
	set f [open "|bk _eula -v > $dev_null" w]
	puts $f "
	    license: $license
	    licsign1: $licsign1
	    licsign2: $licsign2
	    licsign3: $licsign3
	"

	set ::errorCode NONE
	catch {close $f}
		      
	if {($::errorCode == "NONE") || 
	    ([lindex $::errorCode 0] == "CHILDSTATUS" &&
	     [lindex $::errorCode 2] == 0)} {
		return 1
	}
	return 0
}

# Side Effect: the license data is put in the environment variable BK_CONFIG
proc getEulaText {license licsign1 licsign2 licsign3 text} \
{
	global env
	upvar $text txt

	# need to override any config currently in effect...
	set BK_CONFIG "logging:none!;"
	append BK_CONFIG "license:$license!;"
	append BK_CONFIG "licsign1:$licsign1!;"
	append BK_CONFIG "licsign2:$licsign2!;"
	append BK_CONFIG "licsign3:$licsign3!;"
	append BK_CONFIG "single_user:!;single_host:!;"
	set env(BK_CONFIG) $BK_CONFIG
	set r [catch {exec bk _eula -u} txt]
	if {$r} {set txt ""}
	return $r
}

proc normalizePath {path} \
{
	return [file join {*}[file split $path]]
}

# run 'script' for each line in the text widget
# binding 'var' to the contents of the line
# e.g.
#
# EACH_LINE .t myline {
#	puts $myline
# }
#
# would dump the contents of the text widget on stdout
# Note that 'myline' will still exist after the script
# is done. Also, if 'myline' existed before EACH_LINE
# is called, it will be stomped on.
proc EACH_LINE {widget var script} {
	upvar 1 $var line
	set lno 1.0
	while {[$widget compare $lno < [$widget index end]]} {
		set line [$widget get $lno "$lno lineend"]
		set lno [$widget index "$lno + 1 lines"]
		# careful, the script must be run at the end
		# because of 'continue' and 'break'
		uplevel 1 $script
	}
}

# Aqua stuff

proc AboutAqua {} \
{
	if {[winfo exists .aboutaqua]} {return}
	set version [exec bk version]
	toplevel .aboutaqua
	wm title .aboutaqua ""
	frame .aboutaqua.f
	::tk::unsupported::MacWindowStyle style .aboutaqua document {closeBox}
	label .aboutaqua.f.title \
	    -text "The BitKeeper Configuration Management System" \
	    -font {Helvetica 14 bold} \
	    -justify center
	label .aboutaqua.f.v \
	    -text $version \
	    -font {Helvetica 12 normal} \
	    -justify left
	label .aboutaqua.f.copyright \
	    -text "Copyright 2015 BitKeeper Inc." \
	    -font {Helvetica 11 normal} \
	    -justify center
	grid .aboutaqua.f.title -pady 2
	grid .aboutaqua.f.v -pady 2
	grid .aboutaqua.f.copyright -pady 2 -sticky we
	grid .aboutaqua.f  -padx 20 -pady 20 -sticky nswe
}

proc AquaMenus {} \
{
	menu .mb
	. configure -menu .mb
	menu .mb.apple -tearoff 0
	.mb.apple add command -label "About BitKeeper" -command AboutAqua
	.mb add cascade -menu .mb.apple
	menu .mb.help -tearoff 0
	.mb add cascade -menu .mb.help
	.mb.help add command \
	    -label "BitKeeper Help" -command {exec bk helptool &}
}

# Mac OS X needs a _real_ menubar 
if {[tk windowingsystem] eq "aqua"} {
	AquaMenus
}

proc GetTerminal {} {
	set term xterm
	if {[info exists ::env(TERMINAL)]} { set term $::env(TERMINAL) }
	if {[auto_execok $term] eq ""} { return }
	return $term
}

proc isComponent {path} {
	catch {exec bk repotype [file dirname $path]} res
	return [string equal $res "component"]
}

proc isChangeSetFile {path} {
	if {[file tail $path] eq "ChangeSet"
	    && [file isdir [file join [file dirname $path] BitKeeper etc]]} {
		return 1
	}
	return 0
}

proc sccsFile {type file} {
	return [file join [file dirname $file] SCCS $type.[file tail $file]]
}

proc sccsFileExists {type file} {
	set file [sccsFile $type $file]
	if {[catch {exec bk _test -f $file}]} { return 0 }
	return 1
}

proc inComponent {} {
    catch {exec bk repotype} res
    return [string equal $res "component"]
}

proc inRESYNC {} {
    set dir [file tail [exec bk root -S]]
    return [string equal $dir "RESYNC"]
}

## Attach any number of widgets (usually 2 diff widgets) to a single scrollbar.
## We remember the list of widgets for a given scrollbar and then make sure
## those widgets stay in sync when one of them is scrolled.
proc attachScrollbar {sb args} \
{
	global	gc

	set xy x
	if {[$sb cget -orient]  eq "vertical"} { set xy y }

	## Configure the scrollbar to call our custom scrolling function.
	$sb configure -command [list scrollWidgets $sb ${xy}view]

	## Keep track of which widgets are attached to this scrollbar
	## and then tell each widget what its new X/Y scrollcommand is.
	dict set gc(scrollbar.widgets) $sb $args
	foreach w $args {
		$w configure -${xy}scrollcommand [list setScrollbar $sb $w]
	}
}

## This gets called when you actually manipulate the scrollbar itself.  We
## just take the scrollbar, grab the list of widgets associated with it
## and scroll them all with the command given.
proc scrollWidgets {sb args} \
{
	global	gc

	## Just scroll everyone attached to the scrollbar with the same
	## command.
	foreach widg [dict get $gc(scrollbar.widgets) $sb] {
		$widg {*}$args
	}
}

## This gets called by an attached widget anytime something in the widget
## view has changed and it wants to update the scrollbar to tell it where
## it should be.  This happens on things like mousewheel movement and drag
## scrolling.
##
## Since the widget being controlled will already be moving by the proper
## amount, we just take any other widget in our list and make it match the
## exact coordinates that the primary widget is already at.
proc setScrollbar {sb w first last} \
{
	global	gc

	## Tell the scrollbar what to look like.
	$sb set $first $last
	if {![dict exists $gc(scrollbar.widgets) $sb]} { return }

	## Grab the current coordinates for the primary widget being scrolled.
	set x [lindex [$w xview] 0]
	set y [lindex [$w yview] 0]

	## Move all widgets that aren't the primary widget to the same point.
	foreach widg [dict get $gc(scrollbar.widgets) $sb] {
		if {$widg eq $w} { continue }
		$widg xview moveto $x
		$widg yview moveto $y
	}
}

proc scrollMouseWheel {w dir x y delta} \
{
	set widg [winfo containing $x $y]
	if {$widg eq ""} { set widg $w }

	switch -- [tk windowingsystem] {
	    "aqua"  { set delta [expr {-$delta}] }
	    "x11"   { set delta [expr {$delta * 3}] }
	    "win32" { set delta [expr {($delta / 120) * -3}] }
	}

	## If we fail to scroll the widget the mouse is
	## over for some reason, just scroll the widget
	## with focus.
	if {[catch {$widg ${dir}view scroll $delta units}]} {
		catch {$w ${dir}view scroll $delta units}
	}
}

proc isBinary { filename } \
{
    global	gc

    set fd [open $filename rb]
    set x  [read $fd $gc(ci.display_bytes)]
    catch {close $fd}
    return [regexp {[\x00-\x08\x0b\x0e-\x1f]} $x]
}

proc display_text_sizes {{onOrOff ""}} {
	if {$onOrOff ne ""} { set ::display_text_sizes $onOrOff }
	return $::display_text_sizes
}

set ::display_text_sizes on

proc displayTextSize {text w h} \
{
	if {!$::display_text_sizes} { return }
	if {![info exists ::textWidgets($text,w)]} { return }

	set oldW $::textWidgets($text,w)
	set oldH $::textWidgets($text,h)

	## Check to see if size has changed.
	if {abs($w - $oldW) <= 2 && abs($h - $oldH) <= 2} { return }

	set ::textWidgets($text,w) $w
	set ::textWidgets($text,h) $h

	## Don't do anything on the initial draw.
	if {$oldW == 0 && $oldH == 0} { return }

	if {[info exists ::textSize($text)]} {
		after cancel $::textSize($text)
	}

	if {$w <= 1 && $h <= 1} { return }

	set font  [$text cget -font]
	set fontW [font measure $font 0]
	set fontH [dict get [font metrics $font] -linespace]

	set cwidth  [expr {$w / $fontW}]
	set cheight [expr {$h / $fontH}]

	if {$cwidth <= 1 || $cheight <= 1} { return }

	set label $text.__size
	place $label -x 0 -y 0
	$label configure -text "${cwidth}x${cheight}"

	set ::textSize($text) [after 1000 [list hideTextDisplaySize $text]]
}

proc hideTextDisplaySize {w} \
{
	place forget $w.__size
}

## Trace the text command to grab new text widgets as they are
## created and add bind their <Configure> event to show text
## size when they are changed.
proc traceTextWidget {cmd code widget event} \
{
	set ::textWidgets($widget,w) 0
	set ::textWidgets($widget,h) 0

	## Bind the <Map> event so that the first time the text widget
	## is drawn, we configure our display text size popups.  Note
	## that the reason we do this is so that we don't display the
	## text size popups on the initial draw of the widget but only
	## after they've been resized at some point by the user.
	bind $widget <Map> {
	    bind %W <Map> ""
	    bind %W <Configure> "displayTextSize %%W %%w %%h"
	}
	label $widget.__size -relief solid -borderwidth 1 -background #FEFFE6
}
trace add exec text leave traceTextWidget


## This is actually overriding a core Tk proc that is called whenever
## the X11 paste selection code is called.  Where that code moves the
## insertion cursor before pasting, we just want to paste where the
## insert cursor already is.
proc ::tk::TextPasteSelection {w x y} \
{
    if {![catch {::tk::GetSelection $w PRIMARY} sel]} {
	    set oldSeparator [$w cget -autoseparators]
	    if {$oldSeparator} {
		    $w configure -autoseparators 0
		    $w edit separator
	    }
	    $w insert insert $sel
	    if {$oldSeparator} {
		    $w edit separator
		    $w configure -autoseparators 1
	    }
    }
    if {[$w cget -state] eq "normal"} {
	    focus $w
    }
}

## Override another Tk core proc.  Tk seems to think that on X11 we should
## not delete any current selection when pasting.  The more modern behavior
## is to always replace any current selection with the clipboard contents.
proc ::tk_textPaste {w} \
{
	global tcl_platform
	if {![catch {::tk::GetSelection $w CLIPBOARD} sel]} {
		set oldSeparator [$w cget -autoseparators]
		if {$oldSeparator} {
			$w configure -autoseparators 0
			$w edit separator
		}
		catch { $w delete sel.first sel.last }
		$w insert insert $sel
		if {$oldSeparator} {
			$w edit separator
			$w configure -autoseparators 1
		}
	}
}

#lang L
typedef struct hunk {
	int	li, ri;		/* left/right indices */
	int	ll, rl;		/* left/right lengths */
} hunk;


/*
 *    chg   index
 *    0
 *    0
 *    1 <--- a
 *    1
 *    1
 *    0 <--- b
 *    0
 *    0 <--- c
 *    1
 *    1
 *    1 <--- d
 *    0
 */
void
shrink_gaps(string S, int &chg[])
{
	int	i, n;
	int	a, b, c, d;
	int	a1, b1, c1, d1;

	n = length(chg);
	i = 0;
	/* Find find non-zero line for 'a' */
	while ((i < n) && (chg[i] == 0)) i++;
	if (i == n) return;
	a = i;
	/* Find next zero line for 'b' */
	while ((i < n) && (chg[i] == 1)) i++;
	if (i == n) return;
	b = i;

	while (1) {
		/* The line before the next 1 is 'c' */
		while ((i < n) && (chg[i] == 0)) i++;
		if (i == n) return;
		c = i - 1;

		/* The last '1' is 'd' */
		while ((i < n) && (chg[i] == 1)) i++;
		/* hitting the end here is OK */
		d = i - 1;

	again:
		/* try to close gap between b and c */
		a1 = a; b1 = b; c1 = c; d1 = d;
		while ((b1 <= c1) && (S[a1] == S[b1])) {
			a1++;
			b1++;
		}
		while ((b1 <= c1) && (S[c1] == S[d1])) {
			c1--;
			d1--;
		}

		if (b1 > c1) {
			/* Bingo! commit it */
			while (a < b) chg[a++] = 0;  /* erase old block */
			a = a1;
			while (a < b1) chg[a++] = 1;  /* write new block */
			a = a1;
			b = b1;
			while (d > c) chg[d--] = 0;
			d = d1;
			while (d > c1) chg[d--] = 1;
			c = c1;
			d = d1;

			/*
			 * now search back for previous block and start over.
			 * The last gap "might" be closable now.
			 */
			--a;
			c = a;
			while ((a > 0) && (chg[a] == 0)) --a;
			if (chg[a] == 1) {
				/* found a previous block */
				b = a+1;
				while ((a > 0) && (chg[a] == 1)) --a;
				if (chg[a] == 0) ++a;
				/*
				 * a,b nows points at the previous block
				 * and c,d points at the newly merged block
				 */
				goto again;
			} else {
				/*
				 * We were already in the first block so just 
				 * go on.
				 */
				a = a1;
				b = d+1;
			}
		} else {
			a = c+1;
			b = d+1;
		}
	}

}

/*
 * Move any remaining diff blocks align to whitespace boundaries if
 * possible. Adapted from code by wscott in another RTI.
 */
void
align_blocks(string S, int &chg[])
{
	int	a, b;
	int	n;

	n = length(chg);
	a = 0;
	while (1) {
		int	up, down;

		/*
		 * Find a sections of 1's bounded by 'a' and 'b'
		 */
		while ((a < n) && (chg[a] == 0)) a++;
		if (a >= n) return;
		b = a;
		while ((b < n) && (chg[b] == 1)) b++;
		/* b 'might' be at end of file */

		/* Find the maximum distance it can be shifted up */
		up = 0;
		while ((a-up > 0) && (S[a-1-up] == S[b-1-up]) &&
		    (chg[a-1-up] == 0)) {
			++up;
		}
		/* Find the maximum distance it can be shifted down */
		down = 0;
		while ((b+down < n) && (S[a+down] == S[b+down]) &&
		    (chg[b+down] == 0)) {
			++down;
		}
		if (up + down > 0) {
			int	best = 65535;
			int	bestalign = 0;
			int	i;

			/* for all possible alignments ... */
			for (i = -up; i <= down; i++) {
				int	a1 = a + i;
				int	b1 = b + i;
				int	cost = 0;

				/* whitespace at the beginning costs 2 */
				while (a1 < b1 && isspace(S[a1])) {
					cost += 2;
					++a1;
				}

				/* whitespace at the end costs only 1 */
				while (b1 > a1 && isspace(S[b1-1])) {
					cost += 1;
					--b1;
				}
				/* Any whitespace in the middle costs 3 */
				while (a1 < b1) {
					if (isspace(S[a1])) {
						cost += 3;
					}
					++a1;
				}
				/*
				 * Find the alignment with the lowest cost and
				 * if all things are equal shift down as far as
				 * possible.
				 */
				if (cost <= best) {
					best = cost;
					bestalign = i;
				}
			}
			if (bestalign != 0) {
				int	a1 = a + bestalign;
				int	b1 = b + bestalign;

				/* remove old marks */
				while (a < b) chg[a++] = 0;
				/* add new marks */
				while (a1 < b1) chg[a1++] = 1;
				b = b1;
			}
		}
		a = b;
	}
}

/*
 * Align the hunks such that if we find one char in common between
 * changed regions that are longer than one char, we mark the single
 * char as changed even though it didn't. This prevents the sl highlight
 * from matching stuff like foo|b|ar to a|b|alone #the b is common.
 */
hunk[]
align_hunks(string A, string B, hunk[] hunks)
{
	hunk	h, h1, nhunks[];
	int	x, y, lastrl, lastll;

	x = y = lastll = lastrl = 0;
	foreach (h in hunks) {
		if ((((h.li - x) <= h.ll) && ((h.li - x) <= lastll) &&
			isalpha(A[x..h.li - 1])) ||
		    (((h.ri - y) <= h.rl) && ((h.ri - y) <= lastrl) &&
			 isalpha(B[y..h.ri - 1]))) {
			h1.li = x;
			h1.ri = y;
			h1.ll = (h.li - x) + h.ll;
			h1.rl = (h.ri - y) + h.rl;
		} else {
			h1.li = h.li;
			h1.ri = h.ri;
			h1.ll = h.ll;
			h1.rl = h.rl;
		}
		lastll = h1.ll;
		lastrl = h1.rl;
		x = h.li + h.ll;
		y = h.ri + h.rl;
		push(&nhunks, h1);
	}
	return (nhunks);
}

/*
 * Compute the shortest edit distance using the algorithm from
 * "An O(NP) Sequence Comparison Algorithm" by Wu, Manber, and Myers.
 */
hunk[]
diff(string A, string B)
{
	int	M = length(A);
	int	N = length(B);
	int	D;
	int	reverse = (M > N) ? 1: 0;
	int	fp[], path[];
	struct {
		int	x;
		int	y;
		int	k;
	}	pc[];
	struct {
		int	x;
		int	y;
	}	e[];
	int	x, y, ya, yb;
	int	ix, iy;
	int	i, k, m, p, r;
	int	chgA[], chgB[], itmp[];
	string	tmp;
	hunk	hunks[], h;

	if (reverse) {
		tmp = A;
		A = B;
		B = tmp;
		M = length(A);
		N = length(B);
	}

	p = -1;
	fp = lrepeat(M+N+3, -1);
	path = lrepeat(M+N+3, -1);
	m = M + 1;
	D = N - M;

	do {
		p++;
		for (k = -p; k <= (D - 1); k++) {
			ya = fp[m+k-1] + 1;
			yb = fp[m+k+1];
			if (ya > yb) {
				fp[m+k] = y = snake(A, B, k, ya);
				r = path[m+k-1];
			} else {
				fp[m+k] = y = snake(A, B, k, yb);
				r = path[m+k+1];
			}
			path[m+k] = length(pc);
			push(&pc, {y - k, y, r});
		}
		for (k = D + p; k >= (D + 1); k--) {
			ya = fp[m+k-1] + 1;
			yb = fp[m+k+1];
			if (ya > yb) {
				fp[m+k] = y = snake(A, B, k, ya);
				r = path[m+k-1];
			} else {
				fp[m+k] = y = snake(A, B, k, yb);
				r = path[m+k+1];
			}
			path[m+k] = length(pc);
			push(&pc, {y - k, y, r});
		}
		ya = fp[m+D-1] + 1;
		yb = fp[m+D+1];
		if (ya > yb) {
			fp[m+D] = y = snake(A, B, D, ya);
			r = path[m+D-1];
		} else {
			fp[m+D] = y = snake(A, B, D, yb);
			r = path[m+D+1];
		}
		path[m+D] = length(pc);
		push(&pc, {y - D, y, r});
	} while (fp[m+D] < N);
	r = path[m+D];
	e = {};
	while (r != -1) {
		push(&e, {pc[r].x, pc[r].y});
		r = pc[r].k;
	}

	ix = iy = 0;
	x = y = 0;
	chgA = lrepeat(M, 0);
	chgB = lrepeat(N, 0);
	for (i = length(e)-1; i >= 0; i--) {
		while (ix < e[i].x || iy < e[i].y) {
			if (e[i].y - e[i].x > y - x) {
				chgB[iy] = 1;
				iy++; y++;
			} else if (e[i].y - e[i].x < y - x) {
				chgA[ix] = 1;
				ix++; x++;
			} else {
				ix++; x++; iy++; y++;
			}
		}
	}
	if (reverse) {
		tmp = A;
		A = B;
		B = tmp;
		itmp = chgA;
		chgA = chgB;
		chgB = itmp;
	}
	M = length(A);
	N = length(B);

	/* Now we need to minimize the changes by closing gaps */
	shrink_gaps(A, &chgA);
	shrink_gaps(B, &chgB);
	align_blocks(A, &chgA);
	align_blocks(B, &chgB);

	/* edit script length: D + 2 * p */
	for (x = 0, y = 0; (x < M) || (y < N); x++, y++) {
		if (((x < M) && chgA[x]) || ((y < N) && chgB[y])) {
			h.li = x;
			h.ri = y;
			for (; (x < M) && chgA[x]; x++);
			for (; (y < N) && chgB[y]; y++);
			h.ll = x - h.li;
			h.rl = y - h.ri;
			push(&hunks, h);
		}
	}
	hunks = align_hunks(A, B, hunks);
	return(hunks);
}

int
snake(string A, string B, int k, int y)
{
	int	x;
	int	M = length(A);
	int	N = length(B);

	x = y - k;
	while ((x < M) && (y < N) && (A[x] == B[y])) {
		x++;
		y++;
	}
	return (y);
}

int
slhSkip(hunk hunks[], int llen, string left, int rlen, string right)
{
	/* 
	 * If the subline highlight is more than this fraction
	 * of the line length, skip it. 
	 */
	float	hlfactor = gc("hlPercent");
	/*
	 * If the choppiness is more than this fraction of 
	 * line length, skip it.
	 */
	float	chopfactor = gc("chopPercent");

	/*
	 * Highlighting too much? Don't bother.
	 */
	if (llen > (hlfactor*length(left)) ||
	    rlen > (hlfactor*length(right))) {
		return (1);
	}
	/*
	 * Too choppy? Don't bother
	 */
	if ((length(hunks) > (chopfactor*length(left))) ||
	    (length(hunks) > (chopfactor*length(right)))) {
		return (1);
	}
	return (0);
}

// Do subline highlighting on two side-by-side diff widgets.
void
highlightSideBySide(widget left, widget right, string start, string stop, int prefix)
{
	int	i, line;
	string	llines[] = split(/\n/, (string)Text_get(left, start, stop));
	string	rlines[] = split(/\n/, (string)Text_get(right, start, stop));
	hunk	hunks[], h;
	int	llen, rlen;
	int	loff, roff;
	int	allspace;
	string	sl, sr;

	line = idx2line(start);
	for (i = 0; i < length(llines); ++i, ++line) {
		if ((llines[i][0..prefix] == " ") ||
		    (rlines[i][0..prefix] == " ")) continue;
		hunks = diff(llines[i][prefix..END], rlines[i][prefix..END]);
		unless (defined(hunks)) continue;
		llen = rlen = 0;
		allspace = 1;
		foreach (h in hunks) {
			llen += h.ll;
			rlen += h.rl;
			sl = llines[i][prefix+h.li..prefix+h.li+h.ll-1];
			sr = rlines[i][prefix+h.ri..prefix+h.ri+h.rl-1];
			if (sl != "") allspace = allspace && isspace(sl);
			if (sr != "") allspace = allspace && isspace(sr);
		}
		unless (allspace) {
			if (slhSkip(hunks,
			    llen, llines[i], rlen, rlines[i])) continue;
			foreach (h in hunks) {
				Text_tagAdd(left, "highlightold",
				    "${line}.${prefix + h.li}",
				    "${line}.${prefix + h.li + h.ll}");
				Text_tagAdd(right, "highlightnew",
				    "${line}.${prefix + h.ri}",
				    "${line}.${prefix + h.ri + h.rl}");
			}
		} else {
			loff = roff = 0;
			foreach (h in hunks) {
				h.li += loff;
				h.ri += roff;
				sl = Text_get(left,
				    "${line}.${prefix + h.li}",
				    "${line}.${prefix + h.li + h.ll}");
				sl = String_map({" ", "\u2423"}, sl);
				loff += length(sl);
				Text_tagAdd(left, "userData",
				    "${line}.${prefix + h.li}",
				    "${line}.${prefix + h.li + h.ll}");
				Text_insert(left,
				    "${line}.${prefix + h.li + h.ll}",
				    sl, "highlightsp bkMetaData");
				sr = Text_get(right,
				    "${line}.${prefix + h.ri}",
				    "${line}.${prefix + h.ri + h.rl}");
				sr = String_map({" ", "\u2423"}, sr);
				roff += length(sr);
				Text_tagAdd(right, "userData",
				    "${line}.${prefix + h.ri}",
				    "${line}.${prefix + h.ri + h.rl}");
				Text_insert(right,
				    "${line}.${prefix + h.ri + h.rl}",
				    sr, "highlightsp bkMetaData");
			}
		}
	}
}

// Do subline highlighting on stacked diff output in a single text widget.
void
highlightStacked(widget w, string start, string stop, int prefix)
{
	string	line;
	string	lines[];
	int	l = 0, hunkstart = 0;
	string	addlines[], sublines[];

	lines = split(/\n/, (string)Text_get(w, start, stop));
	/*
	 * Since the diffs are stacked, we don't want to highlight regions
	 * that are too big.
	 */
	//	if (length(lines) > 17) return;
	foreach (line in lines) {
		++l;
		if (line[0] == "+") {
			push(&addlines, line[prefix..END]);
			if (!hunkstart) hunkstart = l;
			if (l < length(lines)) continue;
		}
		if (line[0] == "-") {
			push(&sublines, line[prefix..END]);
			if (!hunkstart) hunkstart = l;
			if (l < length(lines)) continue;
		}
		if (defined(addlines) && defined(sublines)) {
			int	i;
			hunk	h, hunks[];
			int	lineA, lineB;
			int	llen, rlen;

			for (i = 0; hunkstart < l; ++hunkstart, ++i) {
				unless (defined(addlines[i])) break;
				unless (defined(sublines[i])) break;
				if (strlen(sublines[i]) > 1000 ||
				    strlen(addlines[i]) > 1000) break;
				hunks = diff(sublines[i], addlines[i]);
				lineA = hunkstart;
				lineB = hunkstart + length(sublines);
				unless (defined(hunks)) continue;
				llen = rlen = 0;
				foreach (h in hunks) {
					llen += h.ll;
					rlen += h.rl;
				}

				if (slhSkip(hunks,
				    llen, sublines[i], rlen, addlines[i])) {
					continue;
				}

				foreach (h in hunks) {
					Text_tagAdd(w, "highlightold",
					    "${lineA}.${h.li+prefix}",
					    "${lineA}.${h.li + h.ll +prefix}");
					Text_tagAdd(w, "highlightnew",
					    "${lineB}.${h.ri + prefix}",
					    "${lineB}.${h.ri + h.rl +prefix}");
				}
			}
		}
		hunkstart = 0;
		addlines = undef;
		sublines = undef;
	}
}

// getUserText
//
// Get data from a text widget as it actually is from the user. This means
// hiding any special characters or other bits we've inserted into the user's
// view and just returning them the real data.
//
// Currently this is only used for highlighted spaces as a result of the
// subline highlighting code, but this is where we want to add stuff in
// the future anytime we alter the user's view of the data.
string
getUserText(widget w, string idx1, string idx2)
{
	string	data;

	// Hide any BK characters we've inserted into the view and
	// show the actual user data as it was inserted.
	Text_tagConfigure(w, "userData", elide: 0);
	Text_tagConfigure(w, "bkMetaData", elide: 1);
	data = Text_get(w, displaychars: idx1, idx2);
	Text_tagConfigure(w, "userData", elide: 1);
	Text_tagConfigure(w, "bkMetaData", elide: 0);
	return (data);
}

/*
 * Windows gui doesn't have a stdout and stderr.
 * A straight system("foo") won't run foo because of this.
 * So put in a string and let the user know it happened.
 */
int
bk_system(string cmd)
{
	string	out, err;
	int	rc;

	if (!defined(rc = system(cmd, undef, &out, &err))) {
		tk_messageBox(title: "bk system",
			      message: "command: ${cmd}\n" . stdio_lasterr);
		return (undef);
	}
	if ((defined(out) && (out != "")) ||
	    (defined(err) && (err != ""))) {
		if (defined(out)) out = "stdout:\n" . out;
		if (defined(err)) err = "stderr:\n" . err;
		if (rc) {
			bk_error("bk system",
				 "command:\n${cmd}\n"
				 "${out}"
				 "${err}");
		} else {
			bk_message("bk system",
				 "command:\n${cmd}\n"
				 "${out}"
				 "${err}");
		}
	}
	return (rc);
}

/*
 * given "line.col" return just line
 */
int
idx2line(string idx)
{
	return ((int)split(/\./, idx)[0]);
}

void
configureDiffWidget(string app, widget w, ...args)
{
	string	which = args[0];

	// Old diff tag.
	Text_tagConfigure(w, "oldDiff",
	    font: gc("${app}.oldFont"),
	    background: gc("${app}.oldColor"));

	// New diff tag.
	Text_tagConfigure(w, "newDiff",
	    font: gc("${app}.newFont"),
	    background: gc("${app}.newColor"));

	if (defined(which)) {
		string	oldOrNew = which;

		// Standard diff tag.
		Text_tagConfigure(w, "diff",
		    font: gc("${app}.${oldOrNew}Font"),
		    background: gc("${app}.${oldOrNew}Color"));

		// Active diff tag.
		if (!gc("${app}.activeNewOnly") || oldOrNew == "new") {
			oldOrNew[0] = String_toupper(oldOrNew[0]);
			Text_tagConfigure(w, "d",
			    font: gc("${app}.active${oldOrNew}Font"),
			    background: gc("${app}.active${oldOrNew}Color"));
		}
	}

	// Highlighting tags.
	Text_tagConfigure(w, "select", background: gc("${app}.selectColor"));
	Text_tagConfigure(w, "highlightold",
			  background: gc("${app}.highlightOld"));
	Text_tagConfigure(w, "highlightnew",
			  background: gc("${app}.highlightNew"));
	Text_tagConfigure(w, "highlightsp",
	    background: gc("${app}.highlightsp"));
	Text_tagConfigure(w, "userData", elide: 1);
	Text_tagConfigure(w, "bkMetaData", elide: 0);

	// Message tags.
	Text_tagConfigure(w, "warning", background: gc("${app}.warnColor"));
	Text_tagConfigure(w, "notice", background: gc("${app}.noticeColor"));

	// Various other diff tags.
	Text_tagConfigure(w, "empty", background: gc("${app}.emptyBG"));
	Text_tagConfigure(w, "same", background: gc("${app}.sameBG"));
	Text_tagConfigure(w, "space", background: gc("${app}.spaceBG"));
	Text_tagConfigure(w, "changed", background: gc("${app}.changedBG"));

	if (defined(which)) {
		Text_tagConfigure(w, "minus",
		    background: gc("${app}.${which}Color"));
		Text_tagConfigure(w, "plus",
		    background: gc("${app}.${which}Color"));
		if (!gc("${app}.activeNewOnly") || which == "new") {
			Text_tagRaise(w, "d");
		}
	}

	Text_tagRaise(w, "highlightold");
	Text_tagRaise(w, "highlightnew");
	Text_tagRaise(w, "highlightsp");
	Text_tagRaise(w, "sel");
}

private int	debug{string};
private	FILE	debugf = undef;

void
debug_enter(string cmd, string op)
{
	unless (cmd) return;

	op = op;
	debug{cmd} = Clock_microseconds();
	fprintf(debugf, "ENTER ${cmd}\n");
	flush(debugf);
}

void
debug_leave(string cmd, int code, string result, string op)
{
	float	t;

	unless (cmd) return;

	op = op;
	code = code;
	result = result;
	if (defined(debug{cmd})) {
		t = Clock_microseconds() - debug{cmd};
		undef(debug{cmd});
	}

	if (defined(t)) {
		fprintf(debugf, "LEAVE ${cmd} (${t} usecs)\n");
	} else {
		fprintf(debugf, "LEAVE ${cmd}\n");
	}
	flush(debugf);
}

void
debug_init(string var)
{
	string	proc, procs[];

	unless (defined(var) && length(var)) return;
	if (var =~ m|^/|) debugf = fopen(var, "w");
	unless (defined(debugf)) debugf = stderr;

	procs = Info_procs("::*");
	foreach (proc in procs) {
		if (proc =~ /^::auto_/) continue;
		if (proc =~ /^::debug_/) continue;
		if (proc =~ /^::unknown/) continue;
		if (proc =~ /^::fprintf/) continue;
		Trace_addExec(proc, "enter", &debug_enter);
		Trace_addExec(proc, "leave", &debug_leave);
	}
}

string
bk_repogca(int standalone, string url, string &err)
{
	string	gca;
	string	opts = "--only-one";

	if (standalone) opts .= " -S";
	if (url && length(url)) opts .= " '${url}'";
	if (system("bk repogca ${opts}", undef, &gca, &err) != 0) {
		return (undef);
	}
	return (trim(gca));
}

void
bk_message(string title, string message)
{
	if (defined(getenv("BK_REGRESSION"))) {
		puts("stdout", message);
	} else {
		tk_messageBox(title: title, message: message);
	}
}

void
bk_error(string title, string message)
{
	if (defined(getenv("BK_REGRESSION"))) {
		puts("stderr", message);
	} else {
		tk_messageBox(title: title, message: message);
	}
}

void
bk_die(string message, int exitCode)
{
	bk_message("BitKeeper", message);
	exit(exitCode);
}

void
bk_dieError(string message, int exitCode)
{
	bk_error("BitKeeper Error", message);
	exit(exitCode);
}

void
bk_usage()
{
	string	usage;
	string	tool = basename(Info_script());

	system("bk help -s ${tool}", undef, &usage, undef);
	puts(usage);
	exit(1);
}
#lang tcl
# Copyright 2002-2003,2015-2016 BitMover, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file contains code to read and write application-specific state
# information
#
# usage: ::appState load "appname" arrayVariableName
#          fills array with app-specific keys and values
#
#        ::appStore save "appname" arrayVariableName ?version?
#           writes the array data to a persistent store for the
#           given application, in a format consistent with the version
#           number given
#
# A file read by these routines must have a line that looks like
# "data format version 1.0". At a later date we may support other versions
# as necessary.
#

# this is the primary application interface to these functions; all other
# functions are intended to be called only internally by this code
namespace eval ::appState {
	variable filename
	array set filename {}
}

proc ::appState {command app varname} \
{

	catch {uplevel ::appState::$command $app $varname} result
	return $result
}

proc ::appState::load {app varname {filename {}}} \
{

	upvar $varname state

	if {$filename == {}} {
		set filename [::appState::filename $app]
	}

	if {![file exists $filename]} {
		error "file doesn't exist: \"$filename\""

	} elseif {![file readable $filename]} {
		error "file not readable: \"$filename\""
	}

	if {[catch {
		set f [open $filename r]
		# reading the data in one block is much quicker than
		# reading one line at a time...
		set data [split [read $f [file size $filename]] \n]
		close $f
	} result]} {
		error "unexpected error: $result"
	}
	
	set version [::appState::getVersion data]

	set versionCommands [info commands ::appState::load-$version]

	if {[llength $versionCommands] == 1} {
		set state(Version) $version
		return [::appState::load-$version data state]
	} else {
		return 0
	}
}

proc ::appState::load-1.0 {datavar statevar} \
{
	upvar $datavar data
	upvar $statevar state

	set last [expr {[llength $data] - 1}]
	for {set i 0} {$i <= $last} {incr i} {
		set item [lindex $data $i]
		if {[regexp {^define ([^ ]+) (.*)} $item -> key value]} {
			set key [string trim $key]
			set value [string trim $value]
			set state($key) $value
		}
	}
}

proc ::appState::filename {app} \
{
	global tcl_platform
	variable filename

	# N.B. 'bk dotbk' has the side effect of moving the old config
	# files to the new location (old=prior to 3.0.2). 
	if {![info exists filename($app)]} {
		set new $app.rc
		if {[string equal $::tcl_platform(platform) "windows"]} {
			set old [file join _bkgui.d $app.rc]
		} else {
			set old [file join .bkgui.d $app.rc]
		}
		set filename($app) [exec bk dotbk $old $new]
	}

	return $filename($app)
}

proc ::appState::save {app statevar {version 1.0} {filename ""}} \
{
	upvar $statevar state

	if {$filename == ""} {
		set filename [::appState::filename $app]
	}

	# make sure the directory exists; if not, attempt to create it
	# Note that "file mkdir" is smart enough to do nothing if the
	# directory already exists, but it will throw an error if a file
	# exists with the same name as the directory.
	if {[catch {file mkdir [file dirname $filename]} result]} {
		error "unable to create directory:\
		       \"[file dirname $filename]\""
	}

	# If the directory still doesn't exist, punt
	if {![file isdirectory [file dirname $filename]]} {
		error "not a directory: \
		       \"[file dirname $filename]\""

	}

	# Danger Will Robinson! If you change the format of the line
	# that beings "data format version", be sure to change the
	# the getVersion proc appropriately...
	set f [open $filename w]
	puts $f "# This file was automatically generated by $app\
		 \n# [clock format [clock seconds]]\n\
		 \ndata format version $version\n"

	foreach key [lsort [array names state]] {
		if {[string match $key "Version"]} continue
		if {[string first \n $state($key)]>= 0} {
			puts $f "define $key <<END\n$state($key)\n<<END"
		} else {
			puts $f "define $key $state($key)"
		}
	}
	close $f

	return 1
}

# reads and discards all data up to and including a line that looks 
# like 'data format version <version number>'. The <version number> 
# is returned.
proc ::appState::getVersion {datavar} \
{
	upvar $datavar data

	set version 1.0
	set i 1
	set regex {data format version +(.*)$}
	foreach line $data {
		if {[regexp $regex $line -> version]} {
			# strip all data prior to and including the
			# version statement
			set data [lrange $data $i end]
			break
		}
	}

	return $version
}
	
# Copyright 2000-2005,2009,2011,2015-2016 BitMover, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -------------------------- Editor module -----------------------------------
# ciedit - a tool for editing files during checkin.

proc eat {fd} \
{
	global	edit_busy

	if {[gets $fd buf] >= 0} {
		puts $buf
	} elseif {[eof $fd]} {
		set edit_busy 0
		catch {close $fd}
	}
}

proc cmd_edit {which} \
{
	global	curLine edit_busy gc filename w env

	saveComments
	if {$edit_busy == 1} { return }
	set edit_busy 1
	if {[file writable $filename]} {
		if {$which == "gui"} {
			edit_widgets
			edit_file
			grab .edit
			tkwait variable ::edit_busy
			grab release .edit
		} elseif {$which == "fmtool"} {
			set old [tmpfile ciedit]
			catch [list exec bk get -qkp $filename > $old] err
			if {![file readable $old] || [file size $old] == 0} {
				# XXX - replace with popup when I merge 
				exec bk msgtool "Unable to bk get $filename"
				set edit_busy 0
				return
			}
			set merge [tmpfile ciedit]
			set cmd [list bk fmtool $old $filename $merge]
			set fd [open "| $cmd" r]
			fileevent $fd readable "eat $fd"
			vwait edit_busy
			if {[file readable $merge]} {
				if {!$gc(windows)} {
					set rwx \
					[file attributes $filename -permissions]
				}
				catch {file rename -force $merge $filename}
				if {!$gc(windows)} {
					file attributes \
					    $filename -permissions $rwx
				}
			} 
			catch {file delete $old $merge}
		} else {
			set term [GetTerminal]
			if {$term eq ""} {
				set edit_busy 0
				return
			}

			if {[info exists env(EDITOR)]} {
				set editor $env(EDITOR)
			} else {
				set editor vim
			}

			set geom "$gc(ci.editWidth)x$gc(ci.editHeight)"
			set cmd [list $term -g $geom -e $editor $filename]
			set fd [open "| $cmd" r]
			fileevent $fd readable "eat $fd"
			vwait edit_busy
		}
	}
	cmd_refresh 1
}

proc edit_widgets {} \
{
	global	adjust nextMark firstEditConfg edit_changed
	global	gc

	# Defaults
	if {$gc(windows) || $gc(aqua)} {
		set y 0
		set x 2
		set filesHt 9
	} else {
		set y 2
		set x 2
		set filesHt 7
	}
	set wmEdit "+1+47"
	set firstEditConfg 1
	set nextMark 0
	set adjust 0
	set edit_changed ""

	toplevel .edit
	wm geometry .edit $wmEdit
	wm protocol .edit WM_DELETE_WINDOW edit_exit
	set halfScreen [expr {$gc(ci.editHeight) / 2}]

	frame .edit.status -borderwid 2
		label .edit.status.l -font $gc(ci.fixedFont) \
		    -wid 84 -relief groove
		grid .edit.status.l -sticky ew
	frame .edit.menus -borderwid 2 -relief groove
		set bwid 5
		button .edit.menus.help -width $bwid \
		    -pady $y -font $gc(ci.buttonFont) -text "Help" \
		    -command { exec bk helptool edittool & }
		button .edit.menus.quit -width $bwid \
		    -pady $y -font $gc(ci.buttonFont) -text "Quit" \
		    -command edit_exit
		button .edit.menus.save -width $bwid \
		    -pady $y -font $gc(ci.buttonFont) -text "Save" \
		    -command edit_save
		button .edit.menus.next -width 12 \
		    -pady $y -font $gc(ci.buttonFont) -text "Next change" \
		    -command {edit_next 1}
		button .edit.menus.prev -width 15 \
		    -pady $y -font $gc(ci.buttonFont) -text "Previous change" \
		    -command {edit_next -1}
		pack .edit.menus.prev -side left
		pack .edit.menus.next -side left
		pack .edit.menus.save -side left
		pack .edit.menus.help -side left
		pack .edit.menus.quit -side left
	frame .edit.t -borderwidth 2 -relief raised
		text .edit.t.t -spacing1 1 -spacing3 1 -wrap none \
		    -bg $gc(ci.textBG) -fg $gc(ci.textFG) \
		    -font $gc(ci.fixedFont) \
		    -width $gc(ci.editWidth) \
		    -height $gc(ci.editHeight) \
		    -xscrollcommand { .edit.t.x1scroll set } \
		    -yscrollcommand { .edit.t.y1scroll set }
		    scrollbar .edit.t.x1scroll -orient horiz \
			-command ".edit.t.t xview" \
			-troughcolor $gc(ci.troughColor) \
			-background $gc(ci.scrollColor)
		    scrollbar .edit.t.y1scroll \
			-command ".edit.t.t yview" \
			-troughcolor $gc(ci.troughColor) \
			-background $gc(ci.scrollColor)
		grid .edit.t.t -row 0 -column 0 -sticky nsew
		grid .edit.t.y1scroll -row 0 -column 1 -rowspan 2 -sticky ns
		grid .edit.t.x1scroll -row 1 -column 0 -sticky ews
	grid .edit.status -row 0 -sticky nsew
	grid .edit.menus -row 1 -sticky nsew
	grid .edit.t -row 2 -sticky nsew
	grid rowconfigure .edit.t 0 -weight 1
	grid rowconfigure .edit 2 -weight 1

	grid columnconfigure .edit.status 0 -weight 1
	grid columnconfigure .edit.menus 0 -weight 1
	grid columnconfigure .edit.t 0 -weight 1
	grid columnconfigure .edit 0 -weight 1

	.edit.menus.prev configure -state disabled
	focus .edit.t.t

	bind .edit.t.t <Control-d> {
		.edit.t.t yview scroll $halfScreen units
		break
	}
	bind .edit.t.t <Control-u> {
		.edit.t.t yview scroll -$halfScreen units
		break
	}
	bind .edit.t.t <Control-n> { edit_next 1; break }
	bind .edit.t.t <Shift-Next> { edit_next 1; break }
	bind .edit.t.t <Control-p> { edit_next -1; break }
	bind .edit.t.t <Shift-Prior> { edit_next -1; break }

	bind .edit.t.t <Configure> {
		global gc firstEditConfg

		set x [winfo reqheight .edit.t.t]
		# This gets executed once, when we know how big the text is
		if {$firstEditConfg == 1} {
			set h [winfo reqheight .edit.t.t]
			set pixelsPerLine [expr {$h / $gc(ci.editHeight)}]
			set firstEditConfg 0
		}
		set x [expr {$x / $pixelsPerLine}]
		set gc(ci.editHeight) $x
		set halfScreen [expr {$gc(ci.editHeight) / 2}]
	}
	bind .edit.t.t <Next> " .edit.t.t yview scroll 1 pages; break"
	bind .edit.t.t <Prior> ".edit.t.t yview scroll -1 pages; break"
	bind .edit.t.t <Home> ".edit.t.t yview -pickplace 1.0; break"
	bind .edit.t.t <End> ".edit.t.t yview -pickplace end; break"
	bind .edit.t.t <Control-f> " .edit.t.t yview scroll 1 pages; break"
	bind .edit.t.t <Control-b> ".edit.t.t yview scroll -1 pages; break"
	bind .edit.t.t <Control-y> ".edit.t.t yview scroll -1 units; break"
	bind .edit.t.t <Control-e> ".edit.t.t yview scroll 1 units; break"
	bind .edit.t.t <Escape> "edit_exit"
	bind .edit.t.t <Alt-s> "edit_save"
	bind .edit.t.t <Delete> {
		set c [.edit.t.t get insert]
		if {$c == "\n"} { edit_adjust -1 }
	}
	bind .edit.t.t <BackSpace> {
		set c [.edit.t.t get "insert - 1 char"]
		if {$c == "\n"} { edit_adjust -1 }
		edit_changed
	}
	bind .edit.t.t <Return> { edit_adjust 1 }
	bind .edit.t.t <Key> {
		if {"%A" != "{}"} { edit_changed }
	}

	if {$gc(aqua)} {
		bind .edit.t.t <Command-w> edit_exit
		# XXX: Is it a good idea to allow people to quit citool
		# from ciedit? Most MacOS apps allow Command-q from anywhere...
		bind .edit.t.t <Command-q> cmd_done
	}

	.edit.t.t tag configure "new" -background $gc(ci.selectColor)
	.edit.t.t tag configure "current" -relief groove -borderwid 2
}

proc edit_changed {} \
{
	global	filename n nextMark edit_changed

	if {$edit_changed == ""} {
		set edit_changed "\[ modified ]"
	}
	.edit.status.l configure -text \
	    "\[ $filename ]$edit_changed on change $nextMark of $n"
}

proc edit_next {v} \
{
	global	filename n marks markEnds nextMark edit_changed

	if {($nextMark + $v <= 0) || ($nextMark + $v > $n)} { return }
	incr nextMark $v
	set m $marks($nextMark)
	set start $marks($nextMark)
	set stop $markEnds($nextMark)
	.edit.t.t yview $m.0
	.edit.t.t yview scroll -5 units
	.edit.t.t tag remove "current" 1.0 end
	.edit.t.t tag add "current" $start.0 "$stop.0 - 1 char"
	.edit.status.l configure -text \
	    "\[ $filename ]$edit_changed on change $nextMark of $n"
	.edit.t.t mark set insert "$start.0"
	if {$nextMark > 1} {
		.edit.menus.prev configure -state normal
	} else {
		.edit.menus.prev configure -state disabled
	}
	if {$nextMark < $n} {
		.edit.menus.next configure -state normal
	} else {
		.edit.menus.next configure -state disabled
	}
}

# This tries real hard to adjust the diff highlights after the user adds
# or deletes newlines.
proc edit_adjust {v} \
{
	global adjust

	incr adjust $v
	after idle {
		global	n marks markEnds

		set where [lindex [split [.edit.t.t index insert] .] 0]
		set i 1
		while {$i <= $n} {
			set new [expr {$marks($i) + $adjust}]
			if {$new > $where} {
			    #puts "where=$where incr start $marks($i) $adjust"
				incr marks($i) $adjust
			}
			set new [expr {$markEnds($i) + $adjust}]
			if {($new > $where) && ($markEnds($i) > $where)} {
			    #puts "where=$where incr stop $markEnds($i) $adjust"
				incr markEnds($i) $adjust
			}
			incr i 1
		}
		set adjust 0
		.edit.t.t tag remove "new" 1.0 end
		set i 1
		while {$i <= $n} {
			set start $marks($i).0
			set stop $markEnds($i).0
			.edit.t.t tag add "new"  $start $stop
			#puts "i=$i $marks($i) $markEnds($i)"
			incr i 1
		}
	}
}

proc edit_highlight {start stop} {
	global n markEnds marks
	.edit.t.t tag add "new" $start.0 "$stop.0 lineend"
	set marks($n) $start
	set markEnds($n) $stop
	#puts "n=$n $start $stop"
}

proc edit_file {} \
{
	global n filename sdiffw

	.edit.t.t configure -state normal
	.edit.t.t delete 1.0 end
	set old [tmpfile ciedit]
	catch [list exec bk get -qkp $filename > $old] err
	#displayMessage "$sdiffw ($old) ($filename) err=($err)"
	set d [open "| $sdiffw \"$old\" \"$filename\"" "r"]
	set f [open $filename "r"]
	set start 1
	set lineNo 1
	set n 0
	gets $d last
	gets $f data
	if {$last == "" || $last == " "} { set last "S" }
	while { [gets $d diff] >= 0 } {
		if {$diff == "" || $diff == " "} { set diff "S" }
		if {$diff != "<"} {
			.edit.t.t insert end "$data\n"
			set lastData $data
			gets $f data
			incr lineNo 1
		}
		if {$diff == "|"} { set diff ">" }
		if {$diff != $last} {
			switch $last {
			    # "S"	Don't care about sames.
			    # "<"	XXX - what do you do about deletes?
			    ">" { incr n 1; edit_highlight $start $lineNo }
			}
			set start $lineNo
			set last $diff
		}
	}
	catch {close $d} err
	catch {close $f} err
	.edit.t.t insert end "$data"
	switch $last {
	    # "S"	Don't care about sames.
	    # "<"	XXX - what do you do about deletes?
	    ">" { incr n 1; edit_highlight $start $lineNo }
	}
	edit_next 1
}

proc edit_save {} \
{
	global	edit_changed filename gc app

	if {[info exists gc($app.backup)] && ($gc($app.backup) != "")} {
		set backup [join [list $filename "bkp"] "~"]
		file copy -force $filename $backup
	}
	set d [open "$filename" "w"]
	puts -nonewline $d [.edit.t.t get 1.0 end]
	catch {close $d} err
	set edit_changed ""
	edit_exit
	cmd_refresh 1
}

proc confirm {what msg} \
{
	global w

	set ret [catch {toplevel .c}]
	if {$ret != 0} { return }

	frame .c.top
	    label .c.top.icon \
		-bitmap questhead
	    label .c.top.msg \
		-text $msg
	pack .c.top.icon -side left
	pack .c.top.msg -side right
	frame .c.sep \
		-height 2 \
		-borderwidth 1 \
		-relief sunken
	frame .c.controls
	    button .c.controls.ok \
		    -text "OK" \
		    -command $what
	    button .c.controls.cancel \
		    -text "cancel" \
		    -command "destroy .c"
	pack .c.controls.ok -side left -padx 4
	pack .c.controls.cancel -side right -padx 4
	pack .c.top -padx 8 -pady 8
	pack .c.sep -fill x -pady 4
	pack .c.controls -pady 4
	set x [expr {[winfo rootx .edit] + [winfo width $w(c_top)] - 250}]
	set y [expr {[winfo rooty .edit] + 120}]
	wm geometry .c "+$x+$y"
	wm title .c "Confirm Quit"
	wm transient .c $w(c_top)
}

# XXX - needs to figure out if we made any changes.
proc edit_exit {} \
{
	global	edit_busy edit_changed filename

	if {$edit_changed != ""} {
		confirm "edit_exit2" "Quit without saving $filename?"
	} else {
		edit_exit2
	}
}

proc edit_exit2 {} \
{
	global	edit_busy edit_changed

	set edit_busy 0
	destroy .edit
	if {$edit_changed != ""} {
		destroy .c
	}
}

# -------------------------- Editor done -----------------------------------

L {

#line 1 "common.l"
/*
 * Copyright 2008-2011,2013-2016 BitMover, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

struct	{
	string	wm;		    // Windowing system: aqua, win32 or x11
	string	tool;		    // Name of the tool for configuration
	string	cmd_quit;	    // Command to quit the tool
	string	cmd_next;	    // Command to move to the next thing
	string	cmd_prev;	    // Command to move to the previous thing
	widget	w_top;		    // Toplevel window of the tool
	widget	w_main;		    // Main widget for scrolling and focus
	widget	w_search;	    // Text box that is the main search widget
	widget	w_searchBar;	    // Standard search bar for all tools
	int	search_case;	    // Search case-sensitive
	float	search_idx[2];	    // Current search index
	int	search_highlight;   // Highlight all search matches?
	string	w_scrollbars{string}[]; // Hash of scrollbars and their widgets
} _bk;

_bk.cmd_quit = "exit";
_bk.wm = Tk_windowingsystem();

int	_bk_search_case = 0;
int	_bk_search_highlight = 0;

string
bgExecInfo(string opt)
{
	return(::set("::bgExec(${opt})"));
}

void
bk_init()
{
	if (_bk.tool == "") {
		bk_dieError("_bk.tool must be set before bk_init()", 1);
	}

	if ((string)_bk.w_top == "") {
		bk_dieError("_bk.w_top must be set before bk_init()", 1);
	}

	bk_initPlatform();
	bk_initTheme();
	loadState(_bk.tool);
	getConfig(_bk.tool);
}

void
bk_initGui()
{
	restoreGeometry(_bk.tool, _bk.w_top);
	wm("protocol", _bk.w_top, "WM_DELETE_WINDOW", _bk.cmd_quit);
	wm("deiconify", _bk.w_top);

	bk_initSearch();
	bk_initBindings();
}

void
bk_initBindings()
{
	string	w, widgets[];
	string	quit = gc("quit");

	// Add a special BK bindtag to every widget
	// in the application so that we can apply
	// bindings before everything else if we want.
	widgets = getAllWidgets(_bk.w_top);
	foreach (w in widgets) {
		string	tags[] = bindtags(w);
		bindtags(w, "BK ${tags}");
	}

	if (_bk.wm == "aqua") {
		bind("BK", "<Control-p>", "${_bk.cmd_prev}; break");
		bind("BK", "<Control-n>", "${_bk.cmd_next}; break");
		bind("BK", "<Command-q>", "${_bk.cmd_quit}; break");
		bind("BK", "<Command-w>", "${_bk.cmd_quit}; break");

		Event_add("<<Redo>>", "<Command-Shift-z>", "<Command-Shift-Z>");
	} else {
		bind("BK", "<Control-p>", "${_bk.cmd_prev}; break");
		bind("BK", "<Control-n>", "${_bk.cmd_next}; break");
		bind("BK", "<Control-q>", "${_bk.cmd_quit}; break");
	}

	w = _bk.w_main;
	bind("BK", "<Control-b>",  "${w} yview scroll -1 pages; break");
	bind("BK", "<Control-e>",  "${w} yview scroll  1 units; break");
	bind("BK", "<Control-f>",  "${w} yview scroll  1 pages; break");
	bind("BK", "<Control-y>",  "${w} yview scroll -1 units; break");
	bind("BK", "<${quit}>", _bk.cmd_quit);

	// Mouse wheel bindings
	if (_bk.wm == "x11") {
		bind("BK", "<4>", "scrollMouseWheel %W y %X %Y -1; break");
		bind("BK", "<5>", "scrollMouseWheel %W y %X %Y  1; break");
		bind("BK", "<Shift-4>","scrollMouseWheel %W x %X %Y -1; break");
		bind("BK", "<Shift-5>","scrollMouseWheel %W x %X %Y  1; break");
	} else {
		bind("BK", "<MouseWheel>",
		    "scrollMouseWheel %W y %X %Y %D; break");
		bind("BK", "<Shift-MouseWheel>",
		    "scrollMouseWheel %W x %X %Y %D; break");
	}

	if (_bk.wm == "aqua") {
		// On OS X, we want to create a special proc that
		// is called when the user selects Quit from the
		// application menu.
		eval("proc ::tk::mac::Quit {} {${_bk.cmd_quit}}");
	}
}

void
bk_exit(...args)
{
	int	exitCode = 0;

	saveState(_bk.tool);
	if (llength(args) == 1) {
		exitCode = (int)args[0];
	} else if (llength(args) == 2) {
		exitCode = (int)args[1];
		if (exitCode == 0) {
			bk_die((string)args[0], exitCode);
		} else {
			bk_dieError((string)args[0], exitCode);
		}
	}
	exit(exitCode);
}

private	string	_lockurl;
int
bk_lock()
{
	string	out, err;

	if (_lockurl) return (1); // Don't lock if we're already locked.
	system("bk lock -r -t --name=${_bk.tool}tool", undef, &out, &err);
	if (length(err)) return (0);
	_lockurl = trim(out);
	return (1);
}

void
bk_unlock()
{
	if (_lockurl) bk_system("bk _kill '${_lockurl}'");
	_lockurl = undef;
}

string
bk_locklist()
{
	string	out, err;

	system("bk lock -l", undef, &out, &err);
	return (err);
}

void
unlockOnExit(_argused ...args)
{
	bk_unlock();
}
Trace_addExec("exit", "enter", &unlockOnExit);

string[]
getAllWidgets(string top)
{
	string	w, widgets[];
	string	list[];

	widgets = winfo("children", top);
	foreach (w in widgets) {
		push(&list, w);
		widgets = getAllWidgets(w);
		foreach (w in widgets) {
			push(&list, w);
		}
	}
	return (list);
}

void
attachScrollbar(string sb, ...args)
{
	int	i;
	poly	w;
	string	widg, widgets[];
	string	orient = Scrollbar_cget(sb, orient:);

	foreach (w in args) {
		widgets[i++] = w;
	}
	if (orient == "horizontal") {
		Scrollbar_configure(sb, command: "${widgets[0]} xview");
		foreach (widg in widgets) {
			_bk.w_scrollbars{widg} = widgets;
			Widget_configure(widg,
			    xscrollcommand: "setScrollbar ${sb} ${widg}");
		}
	} else {
		Scrollbar_configure(sb, command: "${widgets[0]} yview");
		foreach (widg in widgets) {
			_bk.w_scrollbars{widg} = widgets;
			Widget_configure(widg,
			    yscrollcommand: "setScrollbar ${sb} ${widg}");
		}
	}
}

void
setScrollbar(string sb, string w, float first, float last)
{
	string	widg;
	float	x, y;
	float	xview[], yview[];

	Scrollbar_set(sb, first, last);
	unless (_bk.w_scrollbars{w}) return;
	xview = Widget_xview(w);
	yview = Widget_yview(w);
	x = xview[0];
	y = yview[0];
	foreach (widg in _bk.w_scrollbars{w}) {
		if (widg == w) continue;
		Widget_xview(widg, "moveto", x);
		Widget_yview(widg, "moveto", y);
	}
}

void
scrollMouseWheel(string w, string dir, int x, int y, int delta)
{
	int	d = delta;
	widget	widg = Winfo_containing(x, y);

	if (widg == "") widg = w;
	if (_bk.wm == "aqua") {
		d = -delta;
	} else if (_bk.wm == "x11") {
		d = delta * 3;
	} else if (_bk.wm == "win32") {
		d = (delta / 120) * -3;
	}
	// If we fail to scroll the widget the mouse is
	// over for some reason, just scroll the widget
	// with focus.
	try {
		if (dir == "x") {
			Widget_xviewScroll(widg, d, "units");
		} else {
			Widget_yviewScroll(widg, d, "units");
		}
	} catch {
		try {
			if (dir == "x") {
				Widget_xviewScroll(w, d, "units");
			} else {
				Widget_yviewScroll(w, d, "units");
			}
		} catch {}
	}
}

void
scrollTextY(widget w, float i, string what)
{
	if ((i != -1) && (i != 1) && ((what == "page") || (what == "pages"))) {
		// Scroll by fractions of a page.
		int	wh, lh;

		wh = Winfo_height((string)w)
		    - (Text_cget(w, pady:) * 2)
		    - (Text_cget(w, highlightthickness:) * 2);
		lh = Font_metrics(Text_cget(w, font:), linespace:);
		i = (wh / lh) * i;
		what = "units";
	}

	if (what == "top") {
		Text_yviewMoveto(w, 0.0);
	} else if (what == "bottom") {
		Text_yviewMoveto(w, 1.0);
	} else {
		Text_yviewScroll(w, (int)i, what);
	}
}
#line 1 "search.l"
/*
 * Copyright 2008-2009,2011,2015-2016 BitMover, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

void
bk_initSearch()
{
	widget	bar = "${_bk.w_top}.__searchBar";

	if (_bk.w_search == "") return;

	_bk.w_searchBar = bar;
	_bk.search_case = 0;
	_bk.search_idx[0] = 1.0;
	_bk.search_idx[1] = 1.0;
	_bk.search_highlight = 0;

	bind("BK", "<colon>", "toggleGoto %W 0");
	bind("BK", "<Control-g>", "toggleGoto %W 1");
	bind("BK", "<Command-g>", "toggleGoto %W 1");
	bind("BK", "<slash>", "toggleSearch %W 0");
	bind("BK", "<Control-s>", "toggleSearch %W 1");
	bind("BK", "<Command-s>", "toggleSearch %W 1");

	Text_tag(_bk.w_search, "configure", "searchHighlight",
	    background: "yellow");
	Text_tag(_bk.w_search, "configure", "searchMatch",
	    background: "green");

	ttk::frame(bar);
	ttk::label("${bar}.l");
	grid("${bar}.l", row: 0, column: 0);
	ttk::entry("${bar}.e");
	grid("${bar}.e", row: 0, column: 1);
	ttk::frame("${bar}.buttons");
	grid("${bar}.buttons", row: 0, column: 2);
	ttk::button("${bar}.buttons.next", text: "Next",
	    takefocus: 0, command: "searchSearch 1");
	pack("${bar}.buttons.next", side: "left");
	ttk::button("${bar}.buttons.prev", text: "Previous",
	    takefocus: 0, command: "searchSearch -1");
	pack("${bar}.buttons.prev", side: "left");
	ttk::checkbutton("${bar}.buttons.highlight",
	    text: "Highlight all", takefocus: 0,
	    variable: "::_bk_search_highlight",
	    command: "searchToggleHighlight");
	pack("${bar}.buttons.highlight", side: "left");
	ttk::checkbutton("${bar}.buttons.matchCase",
	    text: "Match case", takefocus: 0,
	    variable: "::_bk_search_case",
	    command: "searchToggleMatchCase");
	pack("${bar}.buttons.matchCase", side: "left");
	ttk::label("${bar}.msg");
	grid("${bar}.msg", row: 0, column: 3, padx: "10 0");
}

void
popupSearchBar(string text, int search)
{
	widget	bar = _bk.w_searchBar;

	bind("${bar}.e", "<Escape>", "closeSearchBar");
	if (search) {
		bind("${bar}.e", "<Key>", "after idle searchTypeAhead");
		bind("${bar}.e", "<Return>", "searchSearch 1");
		bind("${bar}.e", "<Shift-Return>", "searchSearch -1");
		bind("${bar}.e", "<Control-n>", "searchSearch 1");
		bind("${bar}.e", "<Control-p>", "searchSearch -1");
		bind("${bar}.e", "<Command-n>", "searchSearch 1");
		bind("${bar}.e", "<Command-p>", "searchSearch -1");
	} else {
		bind("${bar}.e", "<Return>", "searchGoto");
	}

	grid(bar, column: 0, columnspan: 100, sticky: "ew");
	Label_configure((widget)"${bar}.l", text: text);
	if (search) {
		grid("${bar}.buttons");
	} else {
		grid("remove", "${bar}.buttons");
	}
	Entry_selection((widget)"${bar}.e", "clear");
	Entry_selection((widget)"${bar}.e", "range", 0, "end");
	update("idletasks");
	focus(force:, "${bar}.e");
}

void
toggleGoto(widget w, int force)
{
	string	wclass = winfo("class", w);

	if (!force
	    && (wclass == "Entry" || wclass == "TEntry" || wclass == "Text")
	    && Widget_cget(w, state:) == "normal") return;

	popupSearchBar("Go To Line:", 0);
}

void
toggleSearch(widget w, int force)
{
	string	wclass = winfo("class", w);

	if (!force
	    && (wclass == "Entry" || wclass == "TEntry" || wclass == "Text")
	    && Widget_cget(w, state:) == "normal") return;

	popupSearchBar("Find:", 1);
}

void
closeSearchBar()
{
	grid("remove", _bk.w_searchBar);
	focus(force: _bk.w_search);
}

void
searchGoto()
{
	string	idx = Entry_get((widget)"${_bk.w_searchBar}.e");

	Label_configure((widget)"${_bk.w_searchBar}.msg", text: "");
	if (String_is("integer", strict:, idx)) {
		if ((int)idx < 0) {
			idx = "end ${idx} lines";
		} else {
			idx = "1.0 + ${idx} lines";
		}
	}

	if (::catch("${_bk.w_search} see \"${idx}\"")) {
		Label_configure((widget)"${_bk.w_searchBar}.msg",
		    text: "Bad line number");
		return;
	}
	Entry_selection("${_bk.w_searchBar}.e", "clear");
	Entry_selection("${_bk.w_searchBar}.e", "range", 0, "end");
}

void
searchTypeAhead()
{
	float	idx, start;
	int	len, wrapped;
	string	opts[] = {"-regexp", "-count", "len"};
	string	msg, stop;
	string	search = Entry_get((widget)"${_bk.w_searchBar}.e");

	Text_tag(_bk.w_search, "remove", "searchMatch", 1.0, "end");
	Label_configure((widget)"${_bk.w_searchBar}.msg", text: "");

	if (search == "") return;

	if (_bk_search_case) {
		opts[3] = "--";
	} else {
		opts[3] = "-nocase";
		opts[4] = "--";
	}
	msg = "Reached end of page, continued from top";
	start = _bk.search_idx[1];
	stop = "end";

	::catch("${_bk.w_search} search ${opts} \"${search}\" ${start} ${stop}",
	    &idx);

	unless (String_is("double", strict:, idx)) {
		start = 1.0;
		wrapped = 1;
		::catch("${_bk.w_search} search ${opts} \"${search}\" "
		    "${start} ${stop}", &idx);
	}

	if (String_is("double", strict:, idx)) {
		float	idx2 = Text_index(_bk.w_search, "${idx} + ${len} c");

		if (wrapped) {
			Label_configure((widget)"${_bk.w_searchBar}.msg",
			    text: msg);
		}

		Text_tag(_bk.w_search, "add", "searchMatch", idx, idx2);
		Text_see(_bk.w_search, idx);
	} else {
		Label_configure((widget)"${_bk.w_searchBar}.msg",
		    text: "Phrase not found");
	}
}

void
searchSearch(int dir)
{
	float	idx;
	int	i, len, wrapped;
	string	opts[] = {"-regexp", "-count", "len"};
	string	msg, stop, start;
	string	search = Entry_get((widget)"${_bk.w_searchBar}.e");

	Text_tag(_bk.w_search, "remove", "searchMatch", 1.0, "end");
	Label_configure((widget)"${_bk.w_searchBar}.msg", text: "");

	if (search == "") return;

	i = llength(opts);
	if (dir < 0) {
		opts[i++] = "-backwards";
		stop  = "1.0";
		start = (string)_bk.search_idx[0];
		msg = "Reached top of page, continued from bottom";
	} else {
		opts[i++] = "-forwards";
		stop  = "end";
		start = (string)_bk.search_idx[1];
		msg = "Reached end of page, continued from top";
	}

	unless (_bk_search_case) {
		opts[i++] = "-nocase";
	}
	opts[i++] = "--";

	::catch("${_bk.w_search} search ${opts} \"${search}\" ${start} ${stop}",
	    &idx);

	unless (String_is("double", strict:, idx)) {
		wrapped = 1;
		if (dir < 0) {
			start = "end";
		} else {
			start = "1.0";
		}
		::catch("${_bk.w_search} search ${opts} \"${search}\" "
		    "${start} ${stop}", &idx);
	}

	if (String_is("double", strict:, idx)) {
		float	idx2 = Text_index(_bk.w_search, "${idx} + ${len} c");

		if (wrapped) {
			Label_configure((widget)"${_bk.w_searchBar}.msg",
			    text: msg);
		}

		_bk.search_idx[0] = idx;
		_bk.search_idx[1] = Text_index(_bk.w_search,
		    "${idx} + ${len} chars");
		Text_tag(_bk.w_search, "add", "searchMatch", idx, idx2);
		Text_see(_bk.w_search, idx);
	} else {
		Label_configure((widget)"${_bk.w_searchBar}.msg",
		    text: "Phrase not found");
	}
}

void
searchToggleHighlight()
{
	int	i, ilen;
	int	idxs[], lengths[];
	string	search;
	string	opts[] = {"-all", "-count", "lengths"};

	Text_tag(_bk.w_search, "remove", "searchHighlight", 1.0, "end");
	unless (_bk_search_highlight) return;

	search = Entry_get((widget)"${_bk.w_searchBar}.e");
	i = llength(opts);
	unless (_bk_search_case) {
		opts[i++] = "-nocase";
	}
	opts[i++] = "--";

	::catch("${_bk.w_search} search ${opts} \"${search}\" 1.0 end", &idxs);	

	ilen = llength(idxs);
	for (i = 0; i < ilen; ++i) {
		int	idx = idxs[i];
		int	len = lengths[i];
		int	idx2 = Text_index(_bk.w_search, "${idx} + ${len} c");
		Text_tag(_bk.w_search, "add", "searchHighlight", idx, idx2);
	}

}

void
searchToggleMatchCase()
{

}
#line 1 "listbox.l"
/*
 * Copyright 2011,2014-2016 BitMover, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

class ListBox {

typedef struct {
	string	text;
	string	image;
	string	bg;
	string	fg;
	string	font;
	poly	data;
	int	redraw;
} item;

instance {
	private	int	rowCount;
	private	string	itemList[];
	private	item	items{string};
	private	string	varname;
	private	int	redraw;
	private	string	state;
	public	string	selected;

	public	widget	w_path;
	public	widget	w_table;
	public	widget	w_vscroll;
	public	widget	w_hscroll;
}

constructor
ListBox_init(widget pathName, ...args)
{
	string	top;
	string	opts{string} = (string{string})args;

	Package_require("Tktable");

	self->redraw    = 1;
	self->rowCount  = 0;
	self->w_path    = pathName;
	self->w_table   = "${w_path}.table";
	self->w_vscroll = "${w_path}.vscroll";
	self->w_hscroll = "${w_path}.hscroll";

	ttk::frame(w_path);

	table(w_table, cols: 2, rows: 0, relief: "flat",
	    colstretchmode: "last", cursor: "", anchor: "w",
	    multiline: 0, selecttype: "row", selectmode: "single",
	    resizeborders: "none", titlerows: 0, state: "disabled",
	    highlightthickness: 0, command: "ListBox_GetText ${self} %r %c",
	    exportselection: 0, 
	    xscrollcommand: {w_hscroll, "set"},
	    yscrollcommand: {w_vscroll, "set"});
	if (length(args)) ListBox_configure(self, (expand)opts);
	Table_tagConfigure(w_table, "sel", relief: "flat");
	top = Winfo_toplevel((string)w_path);
	bindtags(w_table, {w_table, "ListBox", top, "all"});
	Table_width(w_table, 0, -20);

	ttk::scrollbar(w_vscroll, orient: "vertical",
	    command: {w_table, "yview"});
	ttk::scrollbar(w_hscroll, orient: "horizontal",
	    command: {w_table, "xview"});

	grid(w_table,   row: 0, column: 0, sticky: "nesw");
	grid(w_vscroll, row: 0, column: 1, sticky: "ns");
	grid(w_hscroll, row: 1, column: 0, sticky: "ew");
	Grid_rowconfigure((string)w_path, w_table, weight: 1);
	Grid_columnconfigure((string)w_path, w_table, weight: 1);

	bind("ListBox", "<1>", "ListBox_Click ${self} %x %y");
	bind(w_table, "<<SelectItem>>", "ListBox_select ${self} %d");
	return (self);
}

public poly
ListBox_bind(ListBox self, ...args)
{
	return (bind(self->w_table, (expand)args));
}

public string
ListBox_cget(ListBox self, string option)
{
	if (option == "-redraw") {
		return ((string)self->redraw);
	} else if (option == "-state") {
		return (self->state);
	} else {
		return (Table_cget(self->w_table, option));
	}
}

public void
ListBox_configure(ListBox self, ...args)
{
	string	option, value;

	foreach (option, value in args) {
		if (option == "-redraw") {
			self->redraw = String_isTrue(value);
		} else if (option == "-state") {
			self->state = value;
			if (value == "disabled") ListBox_selectionClear(self);
		} else {
			Table_configure(self->w_table, option, value);
		}
	}
}

public void
ListBox_grid(ListBox self, ...args)
{
	if (String_index(args[0], 0) == "-") {
		grid(w_path, (expand)args);
	} else {
		grid(args[0], w_path, (expand)args[1..END]);
	}
}

public void
ListBox_itemDelete(ListBox self, ...args)
{
	int	idx;
	int	low = length(self->itemList);
	string	itemName;

	foreach (itemName in args) {
		if ((idx = ListBox_index(self, itemName)) < 0) continue;
		if (idx < low) low = idx;
		undef(self->itemList[idx]);
		undef(self->items{itemName});
	}
	ListBox_RedrawRows(low);
	ListBox_Redraw();
}

public int
ListBox_exists(ListBox self, string itemName)
{
	return (defined(self->items{itemName}));
}

public int
ListBox_index(ListBox self, string itemName)
{
	return (lsearch(exact: self->itemList, itemName));
}

public string
ListBox_itemInsert(ListBox self, string idx, ...args)
{
	item	i;
	string	id;
	string	opts{string} = (string{string})args;

	id = "item" . (string)++self->rowCount;
	if (defined(opts{"-id"})) id = opts{"-id"};
	i.bg     = opts{"-background"};
	i.fg     = opts{"-foreground"};
	i.text   = opts{"-text"};
	i.data   = opts{"-data"};
	i.image  = opts{"-image"};
	i.redraw = 1;

	if (idx == "end") {
		push(&self->itemList, id);
	} else {
		self->itemList = linsert(self->itemList, idx, id);
		ListBox_RedrawRows((int)idx);
	}
	self->items{id} = i;
	ListBox_Redraw();
	return (id);
}

public string
ListBox_item(ListBox self, poly index)
{
	int	idx;

	if (index == "end") {
		idx = self->rowCount - 1;
	} else {
		idx = (int)index;
	}
	return (self->itemList[idx]);
}

public string
ListBox_itemcget(ListBox self, string itemName, string option)
{
	unless (defined(self->items{itemName})) return (undef);

	if (option == "-data") {
		return (self->items{itemName}.data);
	} else if (option == "-text") {
		return (self->items{itemName}.text);
	} else if (option == "-image") {
		return (self->items{itemName}.image);
	} else {
		return (undef);
	}
}

public string
ListBox_itemconfigure(ListBox self, string itemName, ...args)
{
	int	redrawRow = 0;
	string	option, value;

	unless (defined(self->items{itemName})) return (undef);

	foreach (option, value in args) {
		if (option == "-data") {
			self->items{itemName}.data = value;
		} else if (option == "-text") {
			self->items{itemName}.text = value;
		} else if (option == "-image") {
			redrawRow = 1;
			self->items{itemName}.image  = value;
		} else if (option == "-font") {
			redrawRow = 1;
			self->items{itemName}.font  = value;
		} else if (option == "-background") {
			redrawRow = 1;
			self->items{itemName}.bg = value;
		} else if (option == "-foreground") {
			redrawRow = 1;
			self->items{itemName}.fg = value;
		} else {
			return (undef);
		}
	}

	if (redrawRow) {
		self->items{itemName}.redraw = 1;
		ListBox_Redraw();
	}
	return (args[END]);
}

public string[]
ListBox_items(ListBox self)
{
	return (self->itemList);
}

public void
ListBox_pack(ListBox self, ...args)
{
	if (String_index(args[0], 0) == "-") {
		pack(w_path, (expand)args);
	} else {
		pack(args[0], w_path, (expand)args[1..END]);
	}
}

public void
ListBox_redraw(ListBox self)
{
	Table_configure(self->w_table, rows: length(self->itemList));
}

public string
ListBox_see(ListBox self, string itemName)
{
	int	row;
	string	cell;

	unless (defined(self->items{itemName})) return (undef);
	row  = ListBox_index(self, itemName);
	cell = ListBox_GetCell(row, "-image");
	Table_see(self->w_table, cell);
	return (itemName);
}

public void
ListBox_select(ListBox self, string itemName)
{
	int	idx = ListBox_index(self, itemName);

	self->selected = itemName;
	ListBox_selectionClear(self);
	ListBox_selectionSet(self, idx, idx);
}

public void
ListBox_selectionClear(ListBox self)
{
	Table_selectClearAll(self->w_table);
}

public string
ListBox_selectionGet(ListBox self)
{
	return (self->selected);
}

public void
ListBox_selectionSet(ListBox self, int first, int last)
{
	Table_selectionSet(self->w_table, "${first},1", "${last},1");
}

// PRIVATE FUNCTIONS

public void
ListBox_Click(ListBox self, int x, int y)
{
	string	idx = "@${x},${y}";
	int	row = Table_index(self->w_table, idx, "row");
	int	col = Table_index(self->w_table, idx, "col");
	string	itemName = ListBox_item(self, row);

	if (self->state == "disabled") return;

	if (col == 0) {
		Event_generate((string)self->w_table, "<<ClickIcon>>",
		    data: itemName);
	} else if (col == 1) {
		Event_generate((string)self->w_table, "<<SelectItem>>",
		    data: itemName);
	}
}

// ListBox_GetText
//
//	Called when Tktable wants to get the text value of a cell.  Since
//	our text only appears in column 1, we don't care about anything else.
//
public string
ListBox_GetText(ListBox self, int row, int col)
{
	string	itemName;

	unless (col == 1) return("");

	itemName = ListBox_item(self, row);
	unless (itemName && self->items{itemName}) return("");

	if (self->items{itemName}.redraw) {
		string	tag;

		self->items{itemName}.redraw = 0;
		tag = ListBox_ImageTag(self->items{itemName});
		if (tag) Table_tagCell(self->w_table, tag, "${row},0");

		tag = ListBox_GetRowTag(self->items{itemName});
		if (tag) Table_tagRow(self->w_table, tag, row);
	}
	return (self->items{itemName}.text);
}

//
// ListBox_RedrawRows
//
//	Mark rows after a certain index as needing to be redrawn.  When
//	Tktable calls DrawRow, the image tag will be re-applied so that
//	it gets redrawn.  This is done to all nodes below where something
//	new was added or deleted.
private void
ListBox_RedrawRows(int first)
{
	int	i, len;
	string	itemName;

	len = length(self->itemList);
	for (i = first; i < len; ++i) {
		itemName = self->itemList[i];
		self->items{itemName}.redraw = 1;
	}
}

private string
ListBox_GetCell(int row, string option)
{
	if (option == "-image") {
		return ("${row},0");
	} else if (option == "-text") {
		return ("${row},1");
	}
}

private string
ListBox_ImageTag(item i)
{
	string	tag;
	
	unless (i.image) return (undef);

	tag = "image-${i.image}";
	unless (Table_tagExists(self->w_table, tag)) {
		Table_tagConfigure(self->w_table, tag, image: i.image);
	}
	return (tag);
}

private string
ListBox_GetRowTag(item i)
{
	string	tag = "row";

	unless (i.bg || i.fg || i.font) return (undef);

	if (i.bg) tag .= "-${i.bg}";
	if (i.fg) tag .= "-${i.fg}";
	if (i.font) tag .= "-${i.font}";
	unless (Table_tagExists(self->w_table, tag)) {
		Table_tagConfigure(self->w_table, tag,
		    font: i.font, background: i.bg, foreground: i.fg);
	}
	return (tag);
}

private void
ListBox_Redraw()
{
	if (self->redraw) ListBox_redraw(self);
}

} /* class ListBox */
#line 1 "citool.l"
/*
 * Copyright 2008-2016 BitMover, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

_bk.tool = "ci";
_bk.cmd_prev = "movePrevious";
_bk.cmd_next = "moveNext";
_bk.cmd_quit = "quit";
_bk.w_top = ".citool";
_bk.w_main = ".citool.lower.diffs";
_bk.w_search = ".citool.lower.diffs";

extern string filename;
extern int edit_busy;
edit_busy = 0;

private int	_done = 0;
private string	_quit = "";

private	string	_ignore_dir;
private	string	_ignore_type;
private	string	_ignore_action;
private	string	_ignore_pattern;
private	string	_ignore_dirpattern;
private	string	_ignore_types[] = {"file", "dirpattern", "pattern", "dirprune"};

typedef	struct	sfile {
	string	node;		// node ID within the listbox
	string	file;		// full path to the file
	string	name;		// display name for this file
	string	type;		// new, modified, pending
	string	icon;		// new, modified, excluded, done
	string	rev;		// rev of a pending file
	string	component;	// component this file belongs to
	int	excluded;	// file is hard excluded by the user or not?
	int	inProduct;	// Is this file part of the product?
	int	commented;	// Whether this file has comments
} sfile;

string  msgs{string};	    // standard messages
string  img_new;	    // Tk image for new/extra files
string  img_cset;	    // Tk image for the changeset file
string  img_done;	    // Tk image for files that are ready to go
string  img_exclude;	    // Tk image for files to exclude from cset
string  img_modified;	    // Tk image for modified but not commented
string	img_checkon;	    // Tk image for included (checkmark)
string	img_checkoff;	    // Tk image for not included (no checkmark)
string	img_notincluded;    // Tk image for files soft-excluded

struct {
	string	cwd;		    // current directory
	string	root;		    // root directory of the repo
	string	dotbk;		    // dotbk directory
	string	compsOpts;	    // options for bk comps
	string	nfilesOpts;	    // options for bk nfiles
	int	includeProduct;     // include the PRODUCT
	int	no_extras;	    // don't scan whole tree
	int	resolve;	    // is this commit from the resolver
	int	partial;	    // is this a partial commit from resolve
	int	nfiles;		    // number of files in repo
	int	nested;		    // are we dealing with a nested repo?
	int	dashs;		    // did we get a -sALIAS on the command line?
	string	dir;		    // dir passed on the command line
	int	changed;	    // has anything changed where we need to
				    // prompt the user on quit?
	string	comments;	    // file comments before changes
	string[] oldcomments{string}; // backup comment hash
	int	commented;	    // do we have comments in the text widget
	int	last_update;	    // time of last GUI update
	int	sfiles_last;	    // last count of sfiles read
	int	sfiles_done;	    // total count of sfiles read
	int	sfiles_found;	    // track how many files we found while
				    // reading each component
	int	sfiles_pending;	    // track how many pending files we found
				    // while reading each component
	int	sfiles_reading;	    // lock variable for reading sfiles output
	int	sfiles_scanning;    // are we currently scanning for files?
	string	sfiles_component;   // component currently being scanned
	int	sfiles_insertIdx;   // listbox index for inserting new files
	int	sfiles_ndone;	    // how many components have we scanned
	int	sfiles_total;	    // how many components we have total
	string  templates{string};  // template comments
	sfile	files{string};	    // a hash of files by name
	string[] filelist;	    // list of files from the command line
	string	clipboard;	    // contents of the cut-and-paste clipboard
	string	trigger_sock;	    // open socket to accept trigger output
	string	trigger_output;	    // Output from triggers during a commit
	string[] allComponents;	    // a list of all components in the repo
	string[] components{string}; // a hash indexed by component path of
				     // components in a product
	string[] cset_commit;	    // A list of extra files to commit in the
				    // product.
	string	 pendingNodes{string}; // A hash of components pointing to the
				       // pending node in the list.
	int	 showPending{string}; // A hash of components and the current
				      // state of their pending check box.
	int	cnt_new;	    // Count of new files in tool
	int	cnt_newC;	    // Count of commented new files
	int	cnt_total;	    // Count of total files in tool
	int	cnt_excluded;	    // Count of files excluded in tool
	int	cnt_modified;	    // Count of modified files in tool
	int	cnt_modifiedC;	    // Count of commented modified files
	int	cnt_commented;	    // Count of total commented files
	int	commitSwitch;	    // 0 when the user first presses commit
				    // becomes 1 after the first button press
				    // signaling that we're ready to commit
	int	doDiscard;	    // switch to require clicking discard twice
	int	committing;	    // true if we're in the middle of committing
	int	progressbar;	    // progress bar is running
	string	selecting;	    // node we are in the process of selecting
	string	selected;	    // the selected file node
	widget	w_top;		    // toplevel window widget
	widget	w_upperF;	    // upper frame widget
	widget	w_lowerF;	    // lower frame widget
	ListBox	o_files;	    // file listbox object
	widget	w_files;	    // file list box widget
	widget	w_commentF;	    // scrolled window that holds the comments
	widget	w_comments;	    // comments text box
	widget	w_diffs;	    // diffs (lower) text box
	widget	w_buttons;	    // frame that holds the buttons
	widget	w_statusF;	    // status bar frame
	widget	w_status;	    // status label on the bottom of the window
	widget	w_progress;	    // progress bar on the status bar
	widget	w_ignoreDlg;	    // ignore dialog
	string	ignore_dir;	    // subdirectory for the current ignore file
	string	font_normal;	    // Normal font for the file list
	string{string} font_underline;  // An underlined font for the file list
	string{string} font_overstrike; // A striked-out  font for the file list
	string	cfiles[];	    // a list of cfiles to dump
	int	cfiles_idx;	    // current cfile
	FILE	cfiles_pipe;	    // open pipe for the current cfile operation
	int	cfiles_prefix;	    // number of spaces to prefix
	string	cfile_comments;	    // comments on the current cfile

	// Keep a hash of components and info about them.  We use this
	// as a quick lookup to know if a component has some files with
	// comments or whether it has been commented itself.  We also
	// store the comments of each components (once we fetch them)
	// for fast lookups from other components.
	struct	component {
		int	product;	// Is this the product component?
		string	comments;	// The comments for this component
		int	commentedFiles;	// Number of files with comments
		string	csetfile;	// Path to the cset file for this comp
	} compinfo{string};
} self;

typedef	void &bk_callback(FILE pipe);

private string	_bk_output{FILE};

// Call out to BK in a way that doesn't block the GUI and wait for
// a result.
string
bk(string cmdline, _optional string mode)
{
	FILE	pipe;
	int	progress;

	progress = startProgressBar(1000);// Start a progress bar after a second
	pipe = openBK(cmdline, &readBK, mode);
	_bk_output{pipe} = "";
	waitForBK(pipe);
	if (progress) stopProgressBar();
	return (trim(_bk_output{pipe}));
}

FILE
openBK(string cmdline, bk_callback callback, _optional string mode)
{
	FILE	pipe;

	unless (mode) mode = "r";
	pipe = popen("bk --sigpipe ${cmdline}", mode);
	unless (pipe) return (undef);

	fconfigure(pipe, blocking: 0, buffering: "line");
	fileevent(pipe, "readable", {callback, pipe});
	return (pipe);
}

void
waitForBK(FILE pipe)
{
	vwait("::_bk_done(${pipe})"); // Have to use a Tcl array here.
}

void
closeBK(FILE pipe)
{
	if (pipe) {
		pclose(pipe);
		set("::_bk_done(${pipe})", 1);
	}
}

void
readBK(FILE pipe)
{
	string	data = "";

	if (eof(pipe)) {
		closeBK(pipe);
		return;
	}

	read(pipe, &data);
	_bk_output{pipe} .= data;
}

string
joinpath(...args)
{
	return(file("join", (expand)args));
}

string
trimright(string s, _optional string p)
{
	if (p) {
		return (String_trimright(s, p));
	} else {
		return (String_trimright(s));
	}
}

int
nfiles(string opts)
{
	string	res;

	res = bk("nfiles ${opts}");
	unless (String_isInteger(strict: res)) return (0);
	return ((int)res);
}

int
nested()
{
	string	res = bk("repotype");

	return (res != "traditional");
}

string[]
getAllNodes()
{
	return (ListBox_items(self.o_files));
}

string
getSelectedNode()
{
	return (self.selected);
}

int
haveSelection()
{
	return (self.selected || self.selecting);
}

sfile
getSelectedFile()
{
	string	node;
	string	file;

	unless (node = getSelectedNode()) {
		return (undef);
	}
	unless (file = ListBox_itemcget(self.o_files, node, data:)) {
		return (undef);
	}
	return (self.files{file});
}

sfile
getCsetFile(string comp)
{
	string	file;

	unless (defined(self.components{comp}[END])) return (undef);
	file = self.components{comp}[END];
	return (self.files{file});
}

sfile
getIgnoreFile(string comp)
{
	string	file = "BitKeeper/etc/ignore";

	if (comp != self.root) file = getRelativePath(comp, "") . "/${file}";
	unless (defined(self.files{file})) return (undef);
	return (self.files{file});
}

string
getRelativePath(string path, string root)
{
	int	len;

	if (root == "") root = self.root;
	if (root[END] != "/") root .= "/";

	len = length(root);
	if (strneq(root, path, len)) {
		path = path[len..END];
	}
	return (path);
}

int
componentHasComments(string component)
{
	return (self.compinfo{component}.commentedFiles > 0);
}

void
insertFile(sfile sf)
{
	unless (defined(sf.component)) {
		sf.component = self.sfiles_component;
	}
	sf.inProduct = (sf.component == self.root);

	// Increment the total counts for the various file
	// types to initialize the values.  The count of
	// commented files is not modified here because it
	// changes throughout the use of the tool.
	if (sf.type == "new") {
		++self.cnt_new;
		++self.cnt_total;
	} else if (sf.type == "pending") {
		++self.cnt_total;
		++self.cnt_commented;
		++self.sfiles_pending;
	} else if (sf.type == "modified") {
		++self.cnt_modified;
		++self.cnt_total;
	}

	unless (sf.type == "pending") {
		poly	idx = "end";
		hash	opts;

		if (defined(self.sfiles_insertIdx)) {
			idx = self.sfiles_insertIdx++;
		}
		opts{"-text"} = sf.name;
		opts{"-data"} = sf.name;
		if (sf.type == "cset") opts{"-background"} = gc("ci.csetBG");
		sf.node = ListBox_itemInsert(self.o_files, idx, (expand)opts);
		configureFile(sf);
	}

	++self.sfiles_found;
	self.files{sf.name} = sf;
	push(&self.components{sf.component}, sf.name);
	if (sf.commented) ++self.compinfo{sf.component}.commentedFiles;

	if (!haveSelection() && (sf.type == "modified") && !isCommented(sf)) {
		selectFile(sf.node);
	}
	updateGUI();
}

void
removeFile(sfile sf, int removeCset)
{
	int	i = 0;
	string	file, component;
	sfile	sel = getSelectedFile();

	component = sf.component;
	if (sf.type == "new") {
		unlink(sf.file);
	} else {
		exec("bk", "unedit", sf.file);
	}

	--self.cnt_total;
	if (sf.type == "new") {
		--self.cnt_new;
	} else if (sf.type == "modified") {
		--self.cnt_modified;
	}
	if (sf.file == sel.file) {
		moveNext();
		updateStatus();
	}
	deleteCommentFile(sf);
	if (sf.commented) --self.compinfo{sf.component}.commentedFiles;
	foreach (file in self.components{component}) {
		if (file == sf.name) {
			undef(self.components{component}[i]);
			break;
		}
		++i;
	}

	ListBox_itemDelete(self.o_files, sf.node);
	ListBox_select(self.o_files, self.selected);

	undef(self.files{sf.name});
	if (removeCset
	    && !sf.inProduct && length(self.components{component}) <= 1) {
		sfile	cset = getCsetFile(component);

		undef(self.files{cset.name});
		undef(self.components{component});
		ListBox_itemDelete(self.o_files, cset.node);
		moveNext();
	}
}

void
deleteFileFromList(sfile sf)
{
	int	idx;
	sfile	sel = getSelectedFile();

	--self.cnt_total;
	if (sf.type == "new") {
		--self.cnt_new;
	} else if (sf.type == "modified") {
		--self.cnt_modified;
	}
	updateStatus();

	idx = ListBox_index(self.o_files, sf.node);
	ListBox_itemDelete(self.o_files, sf.node);
	if (sf.file == sel.file) {
		string	node = ListBox_item(self.o_files, idx);

		if (node == "") {
			moveNext();
		} else {
			selectFile(node);
		}
	}
}

void
configureFile(sfile sf)
{
	sfile	cset;
	string	img, old, node, comp, file;

	unless (defined(sf)) return;

	node = sf.node;
	if (!defined(node) || node == "") return;

	old = ListBox_itemcget(self.o_files, node, image:);
	if (old == img_done) {
		if (sf.type == "new") {
			--self.cnt_newC;
			--self.cnt_commented;
		} else if (sf.type == "modified") {
			--self.cnt_modifiedC;
			--self.cnt_commented;
		}
	} else if (old == img_exclude) {
		--self.cnt_excluded;
	}

	// Clear the item back to a normal state.
	ListBox_itemconfigure(self.o_files, node, foreground: "black",
	    font: self.font_normal);

	if (sf.excluded) {
		// Excluded.  Mark it with a red X.
		++self.cnt_excluded;
		ListBox_itemconfigure(self.o_files, node,
		    image: img_exclude, font: self.font_overstrike);
		return;
	}

	unless (isCommented(sf)) {
		// No comment, so just draw its original icon.
		ListBox_itemconfigure(self.o_files, node, image: sf.icon);
		return;
	}

	unless (sf.type == "cset") {
		// Commented but not a cset file.  Mark it ready.
		// Increment the proper counts for the file type.
		if (sf.type == "new") {
			++self.cnt_newC;
			++self.cnt_commented;
		} else if (sf.type == "modified") {
			++self.cnt_modifiedC;
			++self.cnt_commented;
		}
		ListBox_itemconfigure(self.o_files, node, image: img_done);
		return;
	}

	// Now we're talking about a ChangeSet with comments.
	// We need to figure out how best to draw this.
	img = img_notincluded;
	if (sf.inProduct) {
		// Product ChangeSet
		// Check all components to see if any are ready to
		// commit.  If so, we can mark the product cset as ready.
		foreach (comp in self.components) {
			cset = getCsetFile(comp);
			if (comp == sf.component) continue;
			if (isExcluded(cset)) continue;
			unless (componentHasComments(comp)) continue;
			img = img_done;
			break;
		}
	}

	// Check all files in the component (or product) and see
	// if there are any ready to commit.  If so, mark the cset ready.
	if (img == img_notincluded) {
		foreach (file in self.components{sf.component}) {
			sf = self.files{file};
			if (sf.excluded) continue;
			if (sf.type == "cset") continue;
			unless (isCommented(sf)) continue;
			img = img_done;
			break;
		}
	}

	ListBox_itemconfigure(self.o_files, node, image: img);

	if (img == img_notincluded) {
		ListBox_itemconfigure(self.o_files, node, foreground: "gray");
	}
}

void
readSfiles(FILE pipe)
{
	string	line;

	unless (line = <pipe>) {
		closeBK(pipe);
		return;
	}

	if (line[0] == "F") {
		addSfilesLines({line[2..END]});
	} else if (line[0] == "P") {
		int	nums[] = (int[])split(line[2..END]);

		if (nums[0] != self.sfiles_last) {
			self.sfiles_done += nums[0] - self.sfiles_last;
			self.sfiles_last = nums[0];
			if (self.nfiles > 0) {
				Progressbar_configure(self.w_progress, value:
				    (100 * self.sfiles_done) / self.nfiles);
			}
		}
	}
}

void
updateGUI()
{
	if ((Clock_milliseconds() - self.last_update) >= 300) {
		ListBox_redraw(self.o_files);
		Update_idletasks();
		self.last_update = Clock_milliseconds();
	}
}

int
startProgressBar(_optional int afterWhen)
{
	if (self.progressbar) return (0);

	if (afterWhen) {
		after(afterWhen, "startProgressBar");
	} else {
		self.progressbar = 1;
		Progressbar_configure(self.w_progress,
		    mode: "indeterminate", value: 0);
		Progressbar_start(self.w_progress, 10);
	}
	return (1);
}

void
stopProgressBar()
{
	self.progressbar = 0;
	After_cancel("startProgressBar");
	Progressbar_stop(self.w_progress);
	Progressbar_configure(self.w_progress, mode: "determinate", value: 0);
}

void
addSfilesLines(string lines[])
{
	sfile	sf;
	string	line, file;
	int	extra, modified, pending, hasComments;

	foreach (line in lines) {
		file = join(" ", split(/ /, line)[1..END]);
		file = getRelativePath(file, self.root);
		extra = line[0] == "x";
		modified = line[2] == "c";
		pending = line[3] == "p";
		hasComments = line[6] == "y";
		sf.file = self.root . "/" . file;
		sf.name = file;
		sf.excluded = 0;
		sf.inProduct = 0;
		sf.commented = hasComments;

		if (pending) {
			sf.type = "pending";
			sf.icon = img_done;
			sf.name =~ /(.*)\|(.*)/;
			sf.file = self.root . "/" . $1;
			sf.rev  = $2;
			sf.name = $1 . "@" . $2;
			sf.commented = 1; // pending files are always commented

			if (sf.rev == "1.0") continue;
			insertFile(sf);

			if (!modified || defined(self.files{$1})) continue;

			// We have a file that is both pending and modified.
			// Reset a few of the sfile values and fall through
			// to the rest of the code to add another entry for
			// the modified file.
			sf.name = $1;
			sf.rev  = undef;
			sf.commented = hasComments;
		}
		if (modified) {
			sf.type = "modified";
			sf.icon = img_modified;
		}
		if (hasComments && !modified && !extra) continue;
		if (extra) {
			sf.type = "new";
			sf.icon = img_new;
		}

		insertFile(sf);
	}
}

void
addFiles(string files[])
{
	FILE	fd;
	string	tmp, file, lines[];
	string	opts = "";

	tmp = tmpfile("citool");
	unless (self.no_extras) opts = "-x";
	fd = popen("bk sfiles -cgvypA ${opts} -o'${tmp}' -", "r+");
	puts(fd, join("\n", files));
	pclose(fd);

	fd = fopen(tmp, "r");
	while(defined(file = fgetline(fd))) {
		push(&lines, file);
	}
	fclose(fd);
	unlink(tmp);
	addSfilesLines(lines);
	if (self.sfiles_pending > 0) insertPendingNode(self.root);
	unless (self.partial) insertCsetFile(self.root);
}

void
insertCsetFile(string comp)
{
	sfile	sf;
	string	comments;

	// Don't insert a second cset file.
	if (self.compinfo{comp}.csetfile) return;

	if (comp == self.root) {
		sf.inProduct = 1;
		sf.name = "ChangeSet";
	} else {
		sf.inProduct = 0;
		sf.name = getRelativePath(comp, "") . " ChangeSet";
	}

	sf.type = "cset";
	sf.file = joinpath(comp, "ChangeSet");
	sf.icon = img_cset;
	sf.component = comp;
	sf.excluded = 0;

	comments = getCommentsFromDisk(sf);
	sf.commented = isRealComment(sf, comments);
	if (sf.commented) self.compinfo{comp}.comments = comments;

	insertFile(sf);
	self.compinfo{comp}.csetfile = sf.file;
}

void
insertIgnoreFile(string comp)
{
	sfile	sf;

	sf.name = "BitKeeper/etc/ignore";
	if (comp == self.root) {
		sf.inProduct = 1;
	} else {
		sf.inProduct = 0;
		sf.name = joinpath(getRelativePath(comp, ""), sf.name);
	}

	sf.type = "modified";
	sf.file = joinpath(comp, "BitKeeper/etc/ignore");
	sf.icon = img_modified;
	sf.component = comp;
	sf.excluded = 0;
	sf.commented = 0;

	// Insert the ignore file just above the cset file.
	self.sfiles_insertIdx = ListBox_index(self.o_files,
	    getCsetFile(comp).node);
	insertFile(sf);
}

void
insertPendingNode(string comp)
{
	string	txt;
	poly	idx = "end";
	string	img = img_done;

	// Don't insert a second pending node.
	if (defined(self.pendingNodes{comp})) return;

	txt = "Show ${self.sfiles_pending} pending delta";
	if (self.sfiles_pending != 1) txt .= "s";
	if (self.nested) {
		txt .= " in ";
		if (comp == self.root) {
			txt .= "the Product";
		} else {
			txt .= getRelativePath(comp,"");
		}
	}

	unless (self.showPending{comp}) self.showPending{comp} = 0;

	if (defined(self.sfiles_insertIdx)) idx = self.sfiles_insertIdx++;
	self.pendingNodes{comp} =
	    ListBox_itemInsert(self.o_files, idx,
		id: "pending-${comp}",
		image: img, data: comp,
		font: self.font_underline,
		fill: "blue", text: txt);
}

void
getComponentList()
{
	if (self.nested) {
		string	comp, compdata;

		setStatus("Getting component list...");
		compdata = bk("comps ${self.compsOpts}");
		foreach (comp in split(/\n/, compdata)) {
			if (comp == ".") continue;
			comp = joinpath(self.root, comp);
			self.allComponents[END+1] = comp;
			self.compinfo{comp}.product = 0;
			self.compinfo{comp}.commentedFiles = 0;
		}
	}

	if (self.includeProduct) {
		self.allComponents[END+1] = self.root;
		self.compinfo{self.root}.product = 1;
		self.compinfo{self.root}.commentedFiles = 0;
	}

}

void
getNumFiles()
{
	setStatus("Getting file counts...");
	self.nfiles = nfiles(self.nfilesOpts);
}

void
processFiles(string files[])
{
	self.sfiles_scanning = 1;
	self.sfiles_component = self.root;
	setStatus("Scanning for files...");
	updateButtons();

	// If nfiles gave us nothing, start the indeterminate progress
	// bar so we can show the user we're still alive.
	if (self.nfiles <= 0) {
		startProgressBar();
	} else {
		self.progressbar = 1; // We'll do progress ourselves
	}

	if (length(files) > 0) {
		addFiles(files);
	} else {
		findFiles();
	}

	self.sfiles_component = undef;
	self.sfiles_scanning = 0;

	if (self.cnt_total == 0) {
		bk_dieError("No files found to checkin", 0);
	}

	// If nothing was selected by the scan or by the user, go ahead
	// and select the ChangeSet now.
	if (!defined(getSelectedFile()) && defined(self.files{"ChangeSet"})) {
		selectFile(self.files{"ChangeSet"}.node);
	}

	if (!defined(getSelectedFile())) {
		// We don't have a ChangeSet file or any uncommented files,
		// so we'll just select the first file in the root component.
		string	file = self.components{self.root}[0];

		if (file && self.files{file}) {
			selectFile(self.files{file}.node);
		}
	}

	if (self.nfiles <= 0) {
		stopProgressBar();
	} else {
		self.progressbar = 0;
		Progressbar_configure(self.w_progress, value: 100);
		update();
	}
	configureCsetNodes();
	updateStatus();
	updateButtons();
	Progressbar_configure(self.w_progress, value: 0);
}

void
findFiles()
{
	string	comp;

	self.sfiles_ndone = 0;
	self.sfiles_total = length(self.allComponents);
	foreach (comp in self.allComponents) {
		++self.sfiles_ndone;
		self.sfiles_last = 0;
		scanComponent(comp);
	}
	chdir(self.root);
	self.sfiles_ndone = undef;
}

void
scanComponent(string comp)
{
	FILE	pipe;
	string	cmd;
	int	progress;
	string	opts = "--gui -vgcpA";

	// Don't recurse if a directory is named on the command line
	if (self.dir) {
		opts .= " -1";
		chdir(self.dir);
	} else {
		chdir(comp);
	}

	self.sfiles_found = 0;
	self.sfiles_pending = 0;
	self.sfiles_component = comp;
	updateStatus();

	unless (self.resolve || self.no_extras) {
		// Look for extra files when we're not in the resolver.
		opts .= " -x";
	}
	cmd = "sfiles ${opts} --relpath='${self.root}' 2>@1";
	progress = startProgressBar();
	pipe = openBK(cmd, &readSfiles);
	waitForBK(pipe);
	if (progress) stopProgressBar();
	self.sfiles_component = undef;
	updateStatus();

	if (self.sfiles_pending) {
		insertPendingNode(comp);
	}

	if (self.sfiles_found || (comp == self.root)) {
		insertCsetFile(comp);
	}

	if (!self.sfiles_found && (comp != self.root)) {
		undef(self.components{comp});
	}
}

void
rescanComponent(string comp)
{
	int	idx;
	sfile	sf, cset;
	string	file, node, nodes[];

	idx = ListBox_index(self.o_files, getSelectedFile().node);

	// Delete all files in this component from our hash and
	// build a list of nodes to pass the listbox for deletion.
	foreach (file in self.components{comp}) {
		sf = self.files{file};

		// Don't erase the cset file.
		if (sf.type == "cset") {
			cset = sf;
			continue;
		}

		push(&nodes, sf.node);
		undef(self.files{file});
	}

	// If this component has a pending node in the list, delete
	// that too.  It will be re-added on rescan if necessary.
	if (self.pendingNodes{comp}) {
		push(&nodes, self.pendingNodes{comp});
		undef(self.pendingNodes{comp});
	}

	undef(self.components{comp});
	ListBox_itemDelete(self.o_files, (expand)nodes);

	self.compinfo{comp}.comments = undef;
	self.compinfo{comp}.commentedFiles = 0;
	self.sfiles_insertIdx = ListBox_index(self.o_files, cset.node);
	scanComponent(comp);

	if (self.sfiles_found || (comp == self.root)) {
		// Found something on the scan.  Push our old
		// cset file back onto the end of the list.
		// We never removed it from the listbox.
		push(&self.components{comp}, cset.name);
	} else {
		// We came up empty on rescan, so we want to remove
		// the old component cset file.  This shouldn't really
		// ever happen since, in theory, the ignore file now
		// has changes, so let's be good little programmers and
		// check for it anyway.
		undef(self.compinfo{comp});
		undef(self.components{comp});
		undef(self.files{cset.name});
		ListBox_itemDelete(self.o_files, cset.node);
	}

	node = ListBox_item(self.o_files, idx);
	unless (node) node = cset.node;
	unless (node) node = ListBox_item(self.o_files, 0);
	selectFile(node);
}

void
addButton(string buttonName, string text, string command)
{
	string	path = "${self.w_buttons}.${buttonName}";

	ttk::button(path,
	    text: text,
	    command: command);
	pack(path, side: "top", fill: "x", pady: 2);
}

void
configureButton(string buttonName, ...args)
{
	string	path = "${self.w_buttons}.${buttonName}";

	eval("${path}", "configure", args);
}

void
insertTopText(string text, int clearTextBox)
{
	widget	textbox = self.w_comments;
	string	state = Text_cget(textbox, state:);

	Text_configure(textbox, state: "normal");
	if (clearTextBox) {
		Text_delete(textbox, 1.0, "end");
	}
	Text_insert(textbox, "end", text);
	Text_configure(textbox, state: state);
}

void
insertBottomText(string text, int clearTextBox)
{
	widget	textbox = self.w_diffs;

	if (clearTextBox) {
		Text_delete(textbox, 1.0, "end");
	}
	Text_insert(textbox, "end", text);
}

void
scrollToBottom()
{
	Text_see(self.w_diffs, "end");
}

void
topMessage(string message, string tag)
{
	Text_configure(self.w_comments, state: "normal");
	Text_delete(self.w_comments, 1.0, "end");
	Text_insert(self.w_comments, "end", message, tag);
	Text_insert(self.w_comments, "end", "\n", tag);
	Text_configure(self.w_comments, state: "disabled");
}

void
bottomMessage(string message, string tag)
{
	Text_delete(self.w_diffs, 1.0, "end");
	Text_insert(self.w_diffs, "end", message, tag);
	Text_insert(self.w_diffs, "end", "\n", tag);
}

void
gui()
{
	string	f;
	widget	w;
	widget	top = ".citool";
	string	tags[];

	self.w_top      = top;
	self.w_upperF   = "${top}.upper";
	self.w_files    = "${top}.upper.files";
	self.w_commentF = "${top}.upper.comments";
	self.w_comments = "${top}.upper.comments.t";
	self.w_buttons  = "${top}.upper.buttons";
	self.w_lowerF   = "${top}.lower";
	self.w_diffs    = "${top}.lower.diffs";
	self.w_statusF  = "${top}.status";
	self.w_status   = "${top}.status.text";
	self.w_progress = "${top}.status.progress";

	toplevel(top);
	wm("withdraw", top);
	wm("title", top, "Check In Tool");
	wm("minsize", top, 500, 480);

	entry("${top}.x11buffer");

	grid("rowconfigure", top, 1, weight: 1);
	grid("columnconfigure", top, 0, weight: 1);

	ttk::frame(self.w_upperF);
	grid(self.w_upperF, row: 0, column: 0, sticky: "nesw");

	grid("rowconfigure", self.w_upperF, 0, weight: 1);
	grid("columnconfigure", self.w_upperF, 0, weight: 1);

	self.o_files = ListBox_init(self.w_files,
	    background: gc("ci.listBG"), height: gc("ci.filesHeight"),
	    font: gc("ci.fixedFont"));
	ListBox_grid(self.o_files, row: 0, column: 0, sticky: "nesw");
	ListBox_bind(self.o_files, "<<ClickIcon>>",  "toggleFile %d");
	ListBox_bind(self.o_files, "<<SelectItem>>", "selectFile %d");

	bind("all", "<Return>", "togglePending {}");
	bind("all", "<KP_Enter>", "event generate %W <Return>");

	f = ListBox_cget(self.o_files, font:);
	self.font_normal = f;
	self.font_underline = Font_configure(f);
	self.font_overstrike = Font_configure(f);
	self.font_underline{"-underline"} = "1";
	self.font_overstrike{"-overstrike"} = "1";

	ScrolledWindow(self.w_commentF, auto: "none");
	grid(self.w_commentF, row: 1, column: 0, sticky: "nesw");

	text(self.w_comments, relief: "sunken", borderwidth: 1,
	    font: gc("ci.fixedFont"), wrap: "none", highlightthickness: 0,
	    background: gc("ci.textBG"), foreground: gc("ci.textFG"), undo: 1,
	    width: 70, height: gc("ci.commentsHeight"), state: "disabled");
	ScrolledWindow_setwidget(self.w_commentF, self.w_comments);
	tags = bindtags(self.w_comments);
	tags[END+1] = "Comments";
	bindtags(self.w_comments, tags);
	bind("Comments", "<KeyRelease>", "commentChanged");
	bind("Comments", "<<PasteSelection>>", "commentChanged");
	bind(self.w_comments, "<<Selection>>", "copyX11Buffer %W");
	Text_tag(self.w_comments, "configure", "message",
	    background: gc("ci.noticeColor"));
	Text_tag(self.w_comments, "configure", "warning",
	    background: gc("ci.warnColor"));

	ttk::frame(self.w_buttons);
	grid(self.w_buttons, row: 0, column: 1,
	    rowspan: 2, sticky: "ne", padx: 2);
	addButton("cut", "Cut", "cutComments");
	addButton("paste", "Paste", "pasteComments");
	configureButton("paste", state: "disabled");
	addButton("rescan", "Rescan", "rescan");
	addButton("checkin", "Checkin", "doCommit");
	ttk::menubutton("${self.w_buttons}.edit",
	    text: "Edit", menu: "${self.w_buttons}.edit.menu");
	pack("${self.w_buttons}.edit", side: "top", fill: "x");
	menu("${self.w_buttons}.edit.menu", tearoff: 0);
	Menu_add((widget)"${self.w_buttons}.edit.menu", "command",
	    label: "Fmtool", command: "launchEditor fmtool");
	Menu_add((widget)"${self.w_buttons}.edit.menu", "command",
	    label: "TK editor", command: "launchEditor gui");
	if (gc("x11")) {
		Menu_add((widget)"${self.w_buttons}.edit.menu",
		    "command", label: "Xterm editor",
		    command: "launchEditor xterm");
	}
	addButton("history", "History", "launchRevtool");
	addButton("difftool", "Diff tool", "launchDifftool");
	addButton("discard", "Discard", "discardChanges");
	addButton("help", "Help", "launchHelptool");
	addButton("quit", "Quit", "quit");

	ScrolledWindow(self.w_lowerF, auto: "none");
	grid(self.w_lowerF, row: 1, column: 0, sticky: "nesw");

	text(self.w_diffs, relief: "sunken", borderwidth: 1,
	    font: gc("ci.fixedFont"), wrap: "none", highlightthickness: 0,
	    background: gc("ci.textBG"), foreground: gc("ci.textFG"),
	    width: 81, height: gc("ci.diffHeight"), insertwidth: 0);
	bindtags(self.w_diffs, {self.w_diffs, "ReadonlyText", top, "all"});
	bind(self.w_diffs, "<<Selection>>", "copyX11Buffer %W");
	ScrolledWindow_setwidget(self.w_lowerF, self.w_diffs);
	configureDiffWidget("ci", self.w_diffs);

	if (gc("aqua")) {
		StatusBar(self.w_statusF, showresize: 0);
		grid(self.w_statusF, row: 2, column: 0, sticky: "ew",
		    padx: "0 15");
	} else {
		StatusBar(self.w_statusF);
		grid(self.w_statusF, row: 2, column: 0, sticky: "ew");
	}

	ttk::label(self.w_status);
	StatusBar_add(self.w_statusF, self.w_status, sticky: "ew", weight: 1);
	ttk::progressbar(self.w_progress);
	StatusBar_add(self.w_statusF, self.w_progress,
	    separator: 0, sticky: "e");
	setStatus("Initializing...");

	bind(top, "<FocusIn>", "handle_focus %W");
	bind(self.w_diffs, "<ButtonRelease-1>", "handle_release %W");
	bind("BK", "<Control-l>", "refreshSelectedFile; break");
	bind("BK", "<Control-t>", "toggleSelectedFile; break");
	bind("BK", "<Control-Shift-x>", "cutComments; break");
	bind("BK", "<Control-Shift-X>", "cutComments; break");
	bind("BK", "<Control-Shift-v>", "pasteComments; break");
	bind("BK", "<Control-Shift-V>", "pasteComments; break");
	bind("BK", "<Control-Shift-t>", "toggleAllNewFiles; break");
	bind("BK", "<Control-Shift-T>", "toggleAllNewFiles; break");
	bind("BK", "<Control-Return>", "doCommit; break");
	bind("BK", "<Control-i>", "ignoreSelectedFile; break");

	// Navigation bindings.
	w = self.w_diffs;
	bind("BK", "<Home>", "scrollTextY ${w} 0 top; break");
	bind("BK", "<End>", "scrollTextY ${w} 0 bottom; break");
	bind("BK", "<Prior>", "scrollTextY ${w} -1 page; break");
	bind("BK", "<Next>", "scrollTextY ${w} 1 page; break");
	bind("BK", "<Control-u>", "scrollTextY ${w} -0.5 pages; break");
	bind("BK", "<Control-d>", "scrollTextY ${w}  0.5 pages; break");

	w = self.w_comments;
	if (gc("aqua")) {
		string	anchor = tk::TextAnchor(w);

		bind("BK", "<Command-Shift-x>", "cutComments; break");
		bind("BK", "<Command-Shift-X>", "cutComments; break");
		bind("BK", "<Command-Shift-v>", "pasteComments; break");
		bind("BK", "<Command-Shift-V>", "pasteComments; break");

		bind(w, "<Command-a>", "%W tag add sel 1.0 end; break");
		bind(w, "<Command-Up>", "%W mark set insert 1.0; break");
		bind(w, "<Command-Down>", "%W mark set insert end; break");
		bind(w, "<Command-Left>",
		    "%W mark set insert {insert display linestart}; break");
		bind(w, "<Command-Right>",
		    "%W mark set insert {insert display lineend}; break");
		bind(w, "<Command-Down>", "%W mark set insert end; break");
		bind(w, "<Command-Shift-Up>", "%W mark set ${anchor} insert;"
		    . bind("Text", "<Control-Shift-Key-Home>") . "; break");
		bind(w, "<Command-Shift-Down>", "%W mark set ${anchor} insert;"
		    . bind("Text", "<Control-Shift-Key-End>") . "; break");
		bind(w, "<Command-Shift-Left>",
		    "%W tag add sel {insert display linestart} insert;"
		    "%W mark set insert {insert display linestart}; break");
		bind(w, "<Command-Shift-Right>",
		    "%W tag add sel insert {insert display lineend};"
		    "%W mark set insert {insert display lineend}; break");
		bind(w, "<Command-BackSpace>",
		    "%W delete {insert display linestart} insert;"
		    "break");
	} else {
		bind(w, "<Control-a>", "%W tag add sel 1.0 end; break");
	}

	if (gc("ci.compat_4x")) {
		w = self.w_diffs;
		bind("BK", "<Shift-Up>", "scrollTextY ${w} -1 unit; break");
		bind("BK", "<Shift-Down>", "scrollTextY ${w} 1 unit; break");
	}

	bk_initGui();
	updateButtons();
	update();
}

int
selectFile(string node)
{
	sfile	sf;
	sfile	sel = getSelectedFile();
	string	file;

	unless (ListBox_exists(self.o_files, node)) return (0);
	file = ListBox_itemcget(self.o_files, node, data:);

	self.selecting = node;
	saveComments();
	saveX11Buffer();
	stopCfileComments();
	self.doDiscard = 0;
	self.commitSwitch = 0;
	self.selected = undef;
	ListBox_selectionClear(self.o_files);
	if (node =~ /pending/) {
		togglePending(node);
		goto done;
	}

	ListBox_select(self.o_files, node);
	ListBox_see(self.o_files, node);
	Update_idletasks();

	sf = self.files{file};
	self.comments = getCommentsFromDisk(sf);

	// Update commented state of this file based on what we just got.
	self.commented = setComments(sf, self.comments);

	self.selected = node;
	configureFile(sel); // redraw the previous file's icon
	showFile(sf);
	updateButtons();
done:
	self.selecting = undef;
	return (1);
}

void
refreshSelectedFile()
{
	string	node = getSelectedNode();
	int	yview[] = Text_yview(self.w_diffs);
	int	y = yview[0];

	selectFile(node);
	Text_yview(self.w_diffs, "moveto", y);
}

/* commentChanged()
 *
 * Called when the actual contents of the comment text widget changes.
 * This means every time there's a keypress or a paste.
 */
void
commentChanged()
{
	sfile	sf = getSelectedFile();
	string	comments;

	unless (defined(sf)) return;
	if (self.commitSwitch) return;
	if (sf.type == "pending") return;

	comments = getCurrentComments();
	if (((comments == "") && sf.commented)
	    || ((comments != "") && !sf.commented)) {
		setComments(sf, comments);
		redrawFile(sf.node);
		configureCsetNodes();
	}
}

void
copyX11Buffer(widget t)
{
	string	sel = "";
	widget	e = "${self.w_top}.x11buffer";

	// Changing files clears the text widget which triggers the
	// <<Selection>> event, so we want to ignore any calls while
	// we're switching files.
	if (self.selecting) return;

	// Get the current selection from the calling text widget and
	// store the selected text in a hidden entry we can use later
	// to recall the X11 buffer.
	if (length(Text_tagRanges(t, "sel"))) {
		sel = Text_get(t, "sel.first", "sel.last");
	}
	Entry_delete(e, 0, "end");
	Entry_insert(e, 0, sel);
}

void
saveX11Buffer()
{
	widget	e = "${self.w_top}.x11buffer";

	// Select the contents of our X11 entry to put them back into
	// the X11 copy-paste buffer.  If we did this before switching
	// files it would delete the actual user selection.
	Entry_selectionRange(e, 0, "end");
}

void
configureCsetNodes()
{
	sfile	cset;
	string	comp;

	foreach (comp in self.components) {
		cset = getCsetFile(comp);
		configureFile(cset);
	}
	updateButtons();
}

void
redrawFile(string node)
{
	string	file;

	if (node == "") return;
	file = ListBox_itemcget(self.o_files, node, data:);
	configureFile(self.files{file});
	updateStatus();
	updateButtons();
}

void
toggleFile(string node)
{
	sfile	sf;
	string	file = ListBox_itemcget(self.o_files, node, data:);

	saveComments();
	if (node =~ /pending/) {
		// We are toggling one of the pending nodes.  We want
		// to walk all of the pending deltas in this component
		// and see if they are all in the same state.  If they
		// are all in the same state, we will toggle all of them
		// with this pending node.  If any delta is not in the
		// same state as the rest, the toggle is ignored.
		int	toggle = 1;
		string	comp = file;
		string	img = ListBox_itemcget(self.o_files, node, image:);
		string	pending[];

		foreach (file in self.components{comp}) {
			sf = self.files{file};
			unless (sf.type == "pending") continue;
			pending[END+1] = file;
			if ((sf.excluded && (img != img_exclude))
			    || (!sf.excluded && (img == img_exclude))) {
				toggle = 0;
				break;
			}
		}

		if (toggle) {
			int	exclude;
			if (img == img_exclude) {
				exclude = 0;
				img = img_done;
			} else {
				exclude = 1;
				img = img_exclude;
			}
			foreach (file in pending) {
				self.files{file}.excluded = exclude;
				configureFile(self.files{file});
			}
			ListBox_itemconfigure(self.o_files, node, image: img);
		}
		return;
	}
	
	if (self.resolve) return;

	sf = self.files{file};
	if ((sf.type == "new") && (getComments(sf) == "")) {
		string	comment = "New BitKeeper file ``${sf.name}''";

		if (node == getSelectedNode()) {
			self.comments  = comment;
			insertTopText(comment, 1);
		}
		setComments(sf, comment);
		writeComments(sf, comment);
	} else {
		self.files{file}.excluded = !sf.excluded;
	}

	if (node == getSelectedNode()) {
		ListBox_selectionClear(self.o_files);
		selectFile(node);
	} else {
		sf = getSelectedFile();
		if (sf.type == "cset") showCsetContents(sf);
	}

	redrawFile(node); // this will call configureFile to update the counts.
	configureCsetNodes();
}

void
toggleSelectedFile()
{
	string	node = getSelectedNode();

	toggleFile(node);
}

void
toggleAllNewFiles()
{
	sfile	sf;
	string	file;

	saveComments();
	foreach (file => sf in self.files) {
		if (sf.type == "new") {
			toggleFile(sf.node);
		}
	}
}

void
togglePending(string node)
{
	int	idx;
	sfile	sf;
	string	text, comp, file, sel;

	if (node == "") node = getSelectedNode();
	unless (node =~ /pending/) return;

	idx = ListBox_index(self.o_files, node);
	if (idx == -1) return;

	comp = ListBox_itemcget(self.o_files, node, data:);
	unless (defined(self.showPending{comp})) return;

	text = ListBox_itemcget(self.o_files, node, text:);
	if (self.showPending{comp}) {
		self.showPending{comp} = 0;
		text =~ s/^Hide/Show/;
		foreach (file in self.components{comp}) {
			sf = self.files{file};
			unless (sf.type == "pending") continue;
			ListBox_itemDelete(self.o_files, sf.node);
			self.files{file}.node = "";
		}
	} else {
		self.showPending{comp} = 1;
		text =~ s/^Show/Hide/;
		foreach (file in self.components{comp}) {
			sf = self.files{file};
			unless (sf.type == "pending") continue;
			sf.node = ListBox_itemInsert(self.o_files,
			    (string)++idx, text: sf.name, data: sf.name);
			self.files{sf.name}.node = sf.node;
			configureFile(sf);
			unless (defined(sel)) sel = sf.node;
		}
	}
	ListBox_itemconfigure(self.o_files, node, text: text);
	updateGUI();
	selectPendingNode(node);
}

void
updateButtons()
{
	sfile	cset;
	sfile	sf = getSelectedFile();
	int	commit = 0;
	string	state = "normal";
	widget	b, buttons[] = Winfo_children((string)self.w_buttons);

	if (self.committing) state = "disabled";

	foreach (b in buttons) {
		Button_configure(b, state: state);
	}
	if (self.committing) return;

	unless (length(self.clipboard)) {
		configureButton("paste", state: "disabled");
	}

	unless (length(getCurrentComments())) {
		configureButton("cut", state: "disabled");
	}

	if (self.resolve) {
		configureButton("discard", state: "disabled");
	}

	configureButton("rescan", text: "Rescan");
	configureButton("checkin", text: "Checkin");
	configureButton("history", text: "History", command: "launchRevtool");

	cset = self.files{"ChangeSet"};
	if (self.cnt_commented && defined(cset)
	    && isCommented(cset) && !isExcluded(cset)) {
		commit = 1;
		configureButton("checkin", text: "Commit");
	}

	if (defined(sf)) {
		if (sf.type == "pending") {
			configureButton("edit", state: "disabled");
			configureButton("difftool", state: "disabled");
			configureButton("discard", state: "disabled");
		} else if (sf.type == "new") {
			configureButton("difftool", state: "disabled");
			configureButton("history", text: "Ignore",
			    command: "ignoreDialog");
		}
		if (sf.type == "pending" && !commit) {
			configureButton("checkin", state: "disabled");
		}
	} else {
		configureButton("cut", state: "disabled");
		configureButton("edit", state: "disabled");
		configureButton("paste", state: "disabled");
		configureButton("rescan", state: "disabled");
		configureButton("checkin", state: "disabled");
		configureButton("history", state: "disabled");
		configureButton("difftool", state: "disabled");
		configureButton("discard", state: "disabled");
	}

	if (self.sfiles_scanning) {
		configureButton("checkin", state: "disabled");
		configureButton("rescan", text: "Scanning", state: "disabled");
	}
}

void
setStatus(string status)
{
	Label_configure(self.w_status, text: status);
	Update_idletasks();
}

void
updateStatus()
{
	string	status;

	if (self.sfiles_component) {
		string	comp;

		if (self.sfiles_component == self.root) {
			comp = "product";
		} else {
			comp = getRelativePath(self.sfiles_component,
			    self.root);
		}

		if (self.sfiles_ndone) {
			comp .= " (${self.sfiles_ndone}/${self.sfiles_total})";
		}

		setStatus("Scanning ${comp}...");
		return;
	}

	append(&status, "${self.cnt_newC}/${self.cnt_new} "
	    "new files selected, ");
	append(&status, "${self.cnt_modifiedC}/${self.cnt_modified} "
	    "modified files selected");
	if (self.cnt_excluded) {
		append(&status, ", ${self.cnt_excluded} excluded");
	}
	setStatus(status);
}

int
isProductCsetComment(sfile sf, string comment)
{
	if (comment == "") return (0);
	if (!sf.inProduct && (sf.type == "cset")) {
		sfile   cset = self.files{"ChangeSet"};
		return (comment == getComments(cset));
	}
	return (0);
}

int
isTemplateComment(sfile sf, string comment)
{
	if (comment == "") return (0);
	unless (defined(self.templates{sf.name})) return (0);
	return (comment == self.templates{sf.name});
}

int
isRealComment(sfile sf, string comment)
{
	return ((comment != "")
	    && !isTemplateComment(sf, comment)
	    && !isProductCsetComment(sf, comment));
}

void
deleteCommentFile(sfile sf)
{
	string	file = sf.file;

	if (sf.type == "pending") append(&file, "@" . sf.rev);
	bk("cfile rm '${file}'");
}

void
enableComments()
{
	Text_configure(self.w_comments, state: "normal");
}

void
disableComments()
{
	Text_configure(self.w_comments, state: "disabled");
}

void
writeComments(sfile sf, string comments)
{
	FILE	fd;

	self.changed = 1;
	fd = popen("bk cfile save '${sf.file}'", "w");
	puts(fd, comments);
	pclose(fd);
}

void
saveComments()
{
	string	key, msg, comments;
	sfile	sf = getSelectedFile();

	unless (defined(sf)) return;
	if (Text_cget(self.w_comments, state:) != "normal") return;

	comments = getCurrentComments();

	unless (setComments(sf, comments)) {
		if (self.commented) deleteCommentFile(sf);
	} else {
		foreach (key => msg in msgs) {
			if (comments == msg) return;
		}
		if (comments != self.comments) {
			self.oldcomments{sf.name}[END+1] = comments;
			writeComments(sf, comments);
			saveBackupComments();
		}
	}
	redrawFile(sf.node);
}

void
readBackupComments()
{
	FILE	fp;
	string	data;
	string	backup = joinpath(self.dotbk, "citool.comments");

	unless (exists(backup)) return;

	unless (fp = fopen(backup, "r")) return;
	read(fp, &data);
	self.oldcomments = (string{string}[])data;
	fclose(fp);
}

void
saveBackupComments()
{
	FILE	fp;
	string	file, comments[];
	string	backup = joinpath(self.dotbk, "citool.comments");

	fp = fopen(backup, "w");
	foreach (file => comments in self.oldcomments) {
		comments = comments[END-19..END];
		self.oldcomments{file} = comments;
		puts(fp, {file, comments});
	}
	fclose(fp);
}

void
selectPendingNode(string node)
{
	saveComments();
	disableComments();
	insertTopText("", 1);
	insertBottomText("", 1);
	self.selected = node;
	ListBox_see(self.o_files, node);
	ListBox_select(self.o_files, node);
	updateButtons();
}

void
moveNext()
{
	string	node;
	string	sel = getSelectedNode();
	int	idx = ListBox_index(self.o_files, sel);

	unless (node = ListBox_item(self.o_files, ++idx)) return;
	if (node =~ /pending/) {
		selectPendingNode(node);
	} else {
		selectFile(node);
	}
}

void
movePrevious()
{
	string	node;
	string	sel = getSelectedNode();
	int	idx = ListBox_index(self.o_files, sel);

	unless (node = ListBox_item(self.o_files, --idx)) return;
	if (node =~ /pending/) {
		selectPendingNode(node);
	} else {
		selectFile(node);
	}
}

void
quit()
{
	int	x, y;
	widget	top = ".c";
	string	bg = gc("ci.saveBG");
	string	image;

	saveComments();
	if (self.changed) {
		if (Winfo_exists((string)top)) return;

		toplevel(top);
		Wm_title((string)top, "Quit Citool?");
		Wm_resizable((string)top, 0, 0);
		Toplevel_configure(top, borderwidth: 0, background: bg);
		image = joinpath(getenv("BK_BIN"), "gui", "images",
		    "bklogo.gif");
		if (exists(image)) {
			string	logo = Image_createPhoto(file: image);

			label(".c.logo", image: logo,
			    background: gc("ci.logoBG"),
			    borderwidth: 3);
			pack(".c.logo", fill: "x");
		}
		button(".c.save", font: gc("ci.noticeFont"),
		    background: gc("ci.quitSaveBG"),
		    activebackground: gc("ci.quitSaveActiveBG"),
		    text: "Quit but save comments",
		    command: "setQuit pending");
		pack(".c.save", padx: 2, pady: 3, fill: "x");
		button(".c.cancel", font: gc("ci.noticeFont"),
		    text: "Do not exit citool",
		    command: "setQuit cancel");
		pack(".c.cancel", padx: 2, pady: 3, fill: "x");
		button(".c.quit", font: gc("ci.noticeFont"),
		    background: gc("ci.quitNosaveBG"),
		    activebackground: gc("ci.quitNosaveBG"),
		    activeforeground: gc("ci.quitNosaveActiveBG"),
		    text: "Quit without saving comments",
		    command: "setQuit all");
		pack(".c.quit", padx: 2, pady: 3, fill: "x");

		x = winfo("rootx", self.w_top)
		    + winfo("width", self.w_top) - 220;
		y = winfo("rooty", self.w_top) + 203;
		wm("geometry", ".c", "+${x}+${y}");
		wm("transient", ".c", self.w_top);
		grab(".c");
		vwait(&_quit);
		destroy(".c");

		if (_quit == "cancel") return;
		if (_quit == "all") {
			deleteAllComments();
		} else if (_quit == "pending") {
			deletePendingComments();
		}
	}

	bk_exit();
}

void
createImages()
{
	string	path = joinpath(getenv("BK_BIN"), "gui", "images");;

	img_new = Image_createPhoto(file: joinpath(path, "ci-new.gif"));
	img_cset = Image_createPhoto(file: joinpath(path, "ci-cset.gif"));
	img_done = Image_createPhoto(file: joinpath(path, "ci-done.gif"));
	img_exclude = Image_createPhoto(file: joinpath(path, "ci-exclude.gif"));
	img_modified = 
	    Image_createPhoto(file: joinpath(path, "ci-modified.gif"));
	img_notincluded
	    = Image_createPhoto(file: joinpath(path, "ci-notincluded.gif"));
	img_checkon = Image_createPhoto(file: joinpath(path, "check_on.gif"));
	img_checkoff = Image_createPhoto(file: joinpath(path, "check_off.gif"));
}

string
getComments(sfile sf)
{
	if (defined(sf.node) && (sf.node == getSelectedNode())) {
		return (getCurrentComments());
	} else {
		return (getCommentsFromDisk(sf));
	}
}

string
getCommentsFromDisk(sfile sf, _optional int prefix)
{
	string	comments = "";

	/*
	 * We sometimes get called with self.files{"ChangeSet"} when
	 * in fact, there is no sf struct for "ChangeSet".
	 */
	unless (sf) return (comments);
	if ((sf.type == "cset") && self.compinfo{sf.component}.comments) {
		// We have cached comments for this cset file.
		comments = self.compinfo{sf.component}.comments;
	} else if (sf.type == "pending") {
		comments = bk("prs -hd'$each(:C:){(:C:)\n}' "
		    "-r${sf.rev} '${sf.file}'");
	} else {
		comments = bk("cfile print '${sf.file}'");
		if (comments == "") {
			// If we have a template for this file name, plug in
			// the template comments.  This is currently only
			// supported for ChangeSet files.
			if (defined(self.templates{sf.name})) {
				comments = self.templates{sf.name};
			}

			// If this is a component cset file, and the component
			// has commented files but no comment itself, plug in
			// the comments from the product cset.
			if (self.nested
			    && (sf.type == "cset") && !sf.inProduct
			    && componentHasComments(sf.component)) {
				comments = getComments(self.files{"ChangeSet"});
			}
		}

		if ((sf.type == "cset") && isRealComment(sf, comments)) {
			// If we have a real comment and this is a cset file,
			// cache those comments for fast lookup later.
			self.compinfo{sf.component}.comments = comments;
		}
	}

	if (prefix) {
		string	p = sprintf("%${prefix}s", "");

		comments = p . String_map({"\n", "\n" . p}, comments);
	}
	return (comments);
}

int
isCommented(sfile sf)
{
	if (sf.commented) return (1);

	// A cset file can be commented without actually being marked so
	// because it can inherit the comments from the product ChangeSet
	// if they exist.  If this is a cset, return the commented status
	// of the product.
	if (sf.type == "cset" && self.files{"ChangeSet"}.commented) return (1);
	return (0);
}

/* setComments()
 *
 * Update the commented state for a file.  We don't actually store comments
 * in memory for files, but we do save them for csets.  If the file is not
 * a cset file, update the count of commented files for the file's component
 * as well.
 */
int
setComments(sfile sf, string comments)
{
	string	comp = sf.component;
	int	commented = isRealComment(sf, comments);

	self.files{sf.name}.commented = commented;
	if ((sf.type == "cset")) {
		self.compinfo{comp}.comments = commented ? comments : undef;
	} else {
		// Only modify the count if the previous state is changed.
		if (commented && !sf.commented) {
			++self.compinfo{comp}.commentedFiles;
		} else if (!commented && sf.commented) {
			--self.compinfo{comp}.commentedFiles;
		}
	}

	return (commented);
}

int
isExcluded(sfile sf)
{
	if (sf.excluded) return (1);
	if (ListBox_itemcget(self.o_files, sf.node, image:)
	    == img_notincluded) return (1);
	return (0);
}

void
clearComments()
{
	string	state = Text_cget(self.w_comments, state:);

	enableComments();
	self.comments = undef;
	self.commented = 0;
	Text_delete(self.w_comments, 1.0, "end");
	Text_configure(self.w_comments, state: state);
}

string
getCurrentComments()
{
	return(Text_get(self.w_comments, 1.0, "end - 1 char"));
}

void
showFileContents(sfile sf)
{
	string	type = ftype(sf.file);

	unless (exists(sf.file)) {
		puts("Removing non-existent file \"${sf.name}\" from list box");
		removeFile(sf, 1);
		return;
	}

	if (type == "link") {
		Text_insert(self.w_diffs, "end",
		    "${sf.name}:\t(new file) type: ${type}");
	} else if (type == "file") {
		int	fsize = size(sf.file);
		int	bytes = (int)gc("ci.display_bytes");
		string	msg;
		string	contents;

		if (fsize > bytes) {
			msg = sprintf("showing %d of %d bytes", bytes, fsize);
		} else {
			msg = "${fsize} bytes";
		}
		if (isBinary(sf.file)) {
			contents = "<<binary file, ${fsize} bytes not shown.>>";
			msg = "";
		} else {
			FILE	fd;

			fd = fopen(sf.file, "r");
			read(fd, &contents, bytes);
			fclose(fd);
		}

		Text_insert(self.w_diffs, "end",
		    "${sf.name}:\t(new file) ${msg}\n\n");
		Text_insert(self.w_diffs, "end", "${contents}\n");
	} else {
		Text_insert(self.w_diffs, "end",
		    "${sf.name}:\tUNSUPPORTED FILE TYPE (${type})");
	}
}

void
showCsetContents(sfile sf)
{
	string	comp, file, comments, files[], comps[];

	if (sf.inProduct && self.cnt_commented == 0) {
		// Product ChangeSet file and no comments at all.
		bottomMessage(msgs{"noFileComments"}, "warning");
		disableComments();
		return;
	}

	if (!sf.inProduct && !componentHasComments(sf.component)) {
		// Component ChangeSet and no comments on any files
		// in this component.
		bottomMessage(msgs{"noFileComments"}, "warning");
		disableComments();
		return;
	}

	comments = getComments(sf);
	if (isProductCsetComment(sf, comments)) {
		// If this is a cset comment inherited from the
		// product, select the whole thing within the
		// comment window.
		Text_tagRemove(self.w_comments, "sel", 1.0, "end");
		Text_tagAdd(self.w_comments, "sel", 1.0, "end");
	}

	if (sf.inProduct) {
		comps = keys(self.components);
	} else {
		comps = {sf.component};
	}

	bottomMessage(msgs{"changeset"}, "notice");

	self.cfiles = undef;
	foreach (comp in comps) {
		sfile	s = getCsetFile(comp);

		if (isExcluded(s)) continue;
		unless (sf.inProduct) {
			unless (componentHasComments(comp)) continue;
			unless ((s.file == sf.file) || isCommented(s)) continue;
		}
		push(&files, s.name);

		foreach (file in self.components{comp}) {
			s = self.files{file};
			if (s.excluded) continue;
			if (s.type == "cset") continue;
			unless (isCommented(s)) continue;
			push(&files, s.name);
		}
	}

	getCfileComments(files, 2);
}

/* getCfileComments
 *
 * Take a list of files and open up `bk cfiles dump` to dump all of the
 * comments for those files.  We open the pipe and then setup a file event
 * to read the output lines.  This function DOES wait for all of the files
 * to complete, but it DOES NOT block the GUI while running.
 */
void
getCfileComments(string files[], int prefix)
{
	FILE	pipe;
	int	progress;

	unless (length(files)) return;

	self.cfiles = files;
	self.cfiles_idx = 0;
	self.cfiles_prefix = prefix;

	chdir(self.root);
	pipe = openBK("cfile dump --prefix=${prefix}", &fillComments, "r+");
	unless (pipe) return;

	self.cfiles_pipe = pipe;

	progress = startProgressBar();
	fillComments(pipe); // call the first time to get the ball rolling
	if (self.cfiles_pipe) waitForBK(pipe);
	if (progress) stopProgressBar();
}

/* fillComments
 *
 * Read a single line from the pipe and process it.  A blank line indicates
 * one file is done, and `bk cfiles dump` is ready for the next file.  Note
 * that we handle ChangeSet files internally because they are a special case.
 */
void
fillComments(FILE pipe)
{
	sfile	sf;
	string	file, data, comments;

	read(pipe, &data);
	if (length(data)) {
		// Got some data.  Append it to our current string.
		self.cfile_comments .= data;

		// Not to the end of the data yet, so bail.
		unless ((data =~ /\n\n$/)) return;

		comments = trimright(self.cfile_comments);
		if (length(comments)) {
			Text_insert(self.w_diffs, "end", comments);
			Text_insert(self.w_diffs, "end", "\n\n");
		}

		self.cfile_comments = "";
	}

	// We finished the previous file.  Grab the next file
	// off the stack and see what we need to do.  We handle
	// pending and ChangeSet files ourselves and pass everything
	// else off to cfiles on the pipe.

	while (1) {
		file = self.cfiles[self.cfiles_idx++];

		unless (file && self.cfiles_pipe) {
			// No files left, close the pipe.
			stopCfileComments();
			return;
		}
		sf = self.files{file};

		// We handle ChangeSet and pending files ourselves
		// since they can have comments that don't live on disk
		// and are therefore unknown to bk cfile.
		if ((sf.type == "cset") || (sf.type == "pending")) {
			int	p1, p2;

			p1  = (sf.type == "cset") ? 0 : self.cfiles_prefix;
			p2 = p1 + 2;

			comments = trimright(getCommentsFromDisk(sf, p2));
			Text_insert(self.w_diffs, "end", sprintf("%${p1}s",""));
			Text_insert(self.w_diffs, "end", file . "\n");
			if ((sf.type == "cset") && length(comments) == 0) {
				Text_insert(self.w_diffs, "end", "\n");
			} else {
				Text_insert(self.w_diffs, "end", comments);
				Text_insert(self.w_diffs, "end", "\n\n");
			}
			continue;
		}

		// Tell bk cfiles to do the next file.
		puts(pipe, file);
		flush(pipe);
		break;
	}
}

/* stopCfileComments
 *
 * Stop any running `bk cfile dump` operation and close the pipe.
 */
void
stopCfileComments()
{
	unless (self.cfiles_pipe) return;

	// Close the pipe and signal that we're done so that the vwait
	// in showCsetContents() will release and finish.
	closeBK(self.cfiles_pipe);
	self.cfiles_pipe = undef;
}

void
showCheckinContents()
{
	sfile	sf;
	string	file, files[];

	Text_delete(self.w_diffs, 1.0, "end");
	foreach (file => sf in self.files) {
		if (sf.excluded) continue;
		if (sf.type == "cset") continue;
		unless (isCommented(sf)) continue;
		push(&files, sf.name);
	}

	getCfileComments(files, 0);
}

void
showFile(sfile sf)
{
	FILE	fd;
	string	c, tag, line;
	string	comments;

	// Insert comments into the comment box.
	enableComments();
	comments = self.comments;
	insertTopText(comments, 1);
	Text_delete(self.w_diffs, 1.0, "end");
	after("idle", "focus ${self.w_comments}");

	if (sf.type == "cset") {
		showCsetContents(sf);
		return;
	} else if (sf.type == "new") {
		showFileContents(sf);
		return;
	}

	if (sf.rev) {
		// Pending file.
		string	file = getRelativePath(sf.file, sf.component);

		disableComments();
		bottomMessage(msgs{"pendingFile"}, "notice");
		if (basename(file) == "ChangeSet") {
			string	dir = dirname(file);
			Text_insert(self.w_diffs, "end", "\n");
			Text_insert(self.w_diffs, "end",
			    bk("changes -S -v -r${sf.rev} '${dir}'"), "");
			return;
		}
		Text_insert(self.w_diffs, "end",
		    "\n bk diffs -up -R${sf.rev} ${file}\n", "notice");
		Text_insert(self.w_diffs, "end", "\n");
		fd = popen("bk diffs -up -R${sf.rev} '${sf.file}'", "r");
	} else {
		string	sinfo = bk("sinfo '${sf.file}'");

		sinfo = getRelativePath(sinfo, self.root);
		Text_insert(self.w_diffs, "end", sinfo);
		Text_insert(self.w_diffs, "end", "\n\n");

		fd = popen("bk diffs -up '${sf.file}'", "r");
	}

	gets(fd); gets(fd); gets(fd);
	while (defined(line = fgetline(fd))) {
		c = line[0];
		tag = "";
		if (c == "+") {
			tag = "newDiff";
		} else if (c == "-") {
			tag = "oldDiff";
		}
		Text_insert(self.w_diffs, "end", "${line}\n", tag);
	}
	pclose(fd);
	highlightStacked(self.w_diffs, "1.0", "end", 1);
}

void
cutComments()
{
	string	comments = getCurrentComments();

	if (comments == "") return;
	self.clipboard = comments;
	if (Text_cget(self.w_comments, state:) == "normal") {
		clearComments();
		saveComments();
	}
	updateButtons();
}

void
pasteComments()
{
	sfile	sf;
	string	node, file;
	string	sel = getSelectedNode();
	int	idx = ListBox_index(self.o_files, sel);

	unless (length(self.clipboard)) return;

	if (Text_cget(self.w_comments, state:) == "normal") {
		insertTopText(self.clipboard, 1);
		commentChanged();
		saveComments();
	}

	// Find the next uncommented file and skip down.
	while (node = ListBox_item(self.o_files, ++idx)) {
		if (node =~ /pending/) continue;
		file = ListBox_itemcget(self.o_files, node, data:);
		sf = self.files{file};
		if (isCommented(sf)) continue;
		break;
	}
	if (node) selectFile(node);
}

void
launchDifftool()
{
	sfile	sf = getSelectedFile();

	exec("bk", "difftool", sf.file, "&");
}

void
launchHelptool()
{
	exec("bk", "helptool", "citool", "&");
}

void
launchRevtool()
{
	sfile	sf = getSelectedFile();

	exec("bk", "revtool", sf.file, "&");
}

void
launchEditor(string which)
{
	sfile	sf = getSelectedFile();

	// Set the external filename variable for the editor.
	filename = sf.file;

	cmd_edit(which);

	if (sf.file =~ /BitKeeper\/etc\/ignore$/) {
		rescanComponent(sf.component);
	}
}

void
doCommit()
{
	int	res;
	string	comp;
	sfile	cset = self.files{"ChangeSet"};

	stopCfileComments();

	saveComments();

	unless (self.cnt_commented) {
		bottomMessage(msgs{"noFileComments"}, "warning");
		return;
	}

	if (self.resolve && !self.partial) {
		// We're running from the resolver.  We want to make sure
		// all non-extra files have been commented and that the
		// changeset has a comment.
		if (self.cnt_commented < self.cnt_total) {
			bottomMessage(msgs{"resolveAllComments"}, "warning");
			return;
		} else if (!isCommented(cset)) {
			topMessage(msgs{"resolveCset"}, "message");
			return;
		}
	}

	unless (self.commitSwitch) {
		self.commitSwitch = 1;
		if (self.partial) {
			topMessage(msgs{"noCsetOK"}, "message");
		} else if (isCommented(cset) && !isExcluded(cset)) {
			showCsetContents(self.files{"ChangeSet"});
			topMessage(msgs{"gotCset"}, "message");
		} else {
			topMessage(msgs{"noCset"}, "message");
			showCheckinContents();
		}
		return;
	}

	// If we return an error, make them hit Commit twice again.
	self.commitSwitch = 0;

	/* release our lock */
	bk_unlock();

	// See if someone else has a lock.
	if (isRepoLocked()) {
		popupMessage(E: bk_locklist() . "\n" . msgs{"repoLocked"});
		bk_lock();
		return;
	}

	self.committing = 1;
	self.selected = undef;
	ListBox_selectionClear(self.o_files);
	ListBox_configure(self.o_files, state: "disabled");
	disableComments();
	updateButtons();
	update();

	insertBottomText("Committing changes...\n", 1);
	if (self.nested) {
		foreach (comp in self.components) {
			if (comp == self.root) continue;
			res = commitComponent(comp);
			if (res == -1) break;
		}
	}

	self.committing = 0;
	self.commitSwitch = 0;
	if (res == -1) return;

	res = commitComponent(self.root);
	if (res == -1) return;

	if (res == 0) {
		ListBox_configure(self.o_files, state: "normal");
		updateButtons();
		/*
		 * rescan needed because files and components
		 * may have been delta'ed commited before
		 * the pre-commit trigger failure/rejection
		 */
		After_idle("rescan");
		return;
	}

	deletePendingComments();

	if (String_isTrue(strict:, gc("ci.rescan")) && !self.resolve) {
		After_idle("rescan");
	} else {
		bk_exit();
	}
}

int
commitComponent(string comp)
{
	int	res = 1;
	sfile	sf, cset;
	int	csetExcluded, csetCommented;
	string	file, comments;
	string	checkin[], commit[];

	if (comp == self.root) commit = self.cset_commit;

	chdir(comp);

	// Check the cset settings now before we do the checkin
	// because the checkin will delete the c.files and make
	// it appear as though files aren't commented.
	cset = getCsetFile(comp);
	comments = getComments(cset);
	csetExcluded = isExcluded(cset);
	csetCommented = isCommented(cset);

	foreach (file in self.components{comp}) {
		sf = self.files{file};
		if (sf.excluded) continue;
		if (sf.type == "cset") continue;
		if (isCommented(sf)) {
			unless (csetExcluded) commit[END+1] = sf.file;
			if (sf.type != "pending") checkin[END+1] = sf.file;
		}
	}

	if (length(checkin)) {
		FILE	fd;
		STATUS	st;

		if (comp == self.root) {
			insertBottomText("Checking in files...", 0);
		} else {
			insertBottomText("Checking in files for "
			    "${getRelativePath(comp, self.root)}...\n", 0);
		}
		scrollToBottom();
		fd = popen("bk ci -a -c -q -", "w+", &read_popen_error);
		foreach (file in checkin) {
			file = getRelativePath(file, comp);
			puts(fd, file);
		}
		pclose(fd, &st);
		unless (st.exit == 0) {
			string	msg = "The checkin failed."
			    " See above for the reason.\n";
			fail_commit(msg);
			return (-1);
		}
	}

	if (!self.partial && csetCommented && !csetExcluded && length(commit)) {
		FILE	fd;
		int	err;
		string	line;
		string	list[];
		string	msg;
		string	tmp1 = tmpfile("bk_cfiles");
		string	tmp2 = tmpfile("bk_cicomment");

		unless (comp == self.root) {
			// If we're committing in a component, add our
			// component ChangeSet file to the list of files
			// to commit in the product.
			self.cset_commit[END+1] = joinpath(comp, "ChangeSet");
		}

		fd = fopen(tmp2, "w");
		puts(nonewline:, fd, comments);
		fclose(fd);

		fd = fopen(tmp1, "w");
		commit = lsort(unique:, commit);
		foreach (file in commit) {
			line = bk("sfiles -pC '${file}'");
			if (line == "") continue;
			puts(fd, getRelativePath(line, comp));
		}
		fclose(fd);

		self.trigger_output = "";
		self.trigger_sock = socket(myaddr: "localhost",
		    server: "triggerAccept", 0);
		list = fconfigure(self.trigger_sock, sockname:);
		putenv("_BK_TRIGGER_SOCK=localhost:${list[2]}");

		msg = "Committing";
		if (comp == self.root) {
			append(&msg," in product");
		} else if (self.nested) {
			append(&msg," in ${getRelativePath(comp, self.root)}");
		}
		insertBottomText("${msg}...\n", 0);
		scrollToBottom();
		update();
		unless (self.resolve) {
			err = bgExec("bk", "commit", "-S", "-dq",
			    "-l${tmp1}", "-Y${tmp2}");
		} else {
			err = bgExec("bk", "commit", "-S", "-dq", "-R",
			    "-l${tmp1}", "-Y${tmp2}");
		}

		if ((bgExecInfo("stderr") != "")
		    || (bgExecInfo("stdout") != "")) {
			string	type;
			string	message = "bk commit";

			if (err != 0 && err != 100) {
				type = "-E";
				message .= " failed with error ${err}:";
			} else {
				type = "-I";
				message .= " output:";
			}
			message .= "\n";


			if (bgExecInfo("stderr") != "") {
				message .= trim(bgExecInfo("stderr"));
				if (bgExecInfo("stdout") != "") {
					append(&message, "\n--\n");
					append(&message,
					    trim(bgExecInfo("stdout")));
				}
			} else {
				message .= trim(bgExecInfo("stdout"));
			}

			if ((self.trigger_output =~ /pre-commit failed/)
			    && (err == 2)) {
				res = 0;
			} else {
				fail_commit(message);
				res = -1;
			}
		}

		if (res == 1) deleteCommentFile(cset);
	}
	chdir(self.root);
	return (res);
}

void
fail_commit(string msg)
{
	msg .= "\n\nCorrect the problem and then"
	" rescan for changes,\nor you can Quit"
	" citool and try again later.\n";

	Text_insert(self.w_diffs, "end", "\n\n", "", msg, "warning");
	configureButton("rescan", state: "normal");
	configureButton("quit", state: "normal");

	disable_file_list();
	insertTopText("", 1);
}

void
read_popen_error(string cmd, FILE fp)
{
	string	err;
	
	cmd = cmd;
	if (read(fp, &err) == -1 || err == "") return;
	insertBottomText("\nError during checkin\n\n", 0);
	insertBottomText(err, 0);
	insertBottomText("\n", 0);
	Update_idletasks();
}

void
triggerAccept(string sock, string addr, int port)
{
	if (0) {
		port = 0;
		addr = "";
	}
	fconfigure(sock, blocking: 0, buffering: "line");
	fileevent(sock, "readable", "triggerRead ${sock}");
}

void
triggerRead(string sock)
{
	string	line;

	if (eof(sock)) {
		close(sock);
		return;
	}

	unless(defined(line = fgetline(sock))) return;
	insertBottomText("${line}\n", 0);
	self.trigger_output .= line . "\n";
}

void
deleteAllComments()
{
	sfile	sf;
	string	file;

	foreach (file => sf in self.files) {
		if (isCommented(sf)) deleteCommentFile(sf);
	}
}

void
deletePendingComments()
{
	sfile	sf;
	string	file;

	foreach (file => sf in self.files) {
		unless (sf.type == "pending") continue;
		append(&file, "@", sf.rev);
		deleteCommentFile(sf);
	}
}

void
ignoreDialog()
{
	string	which, subdir, opts[];
	widget	dialog;
	int	w, h, x, y;
	int	row = 0;
	sfile	sf = getSelectedFile();
	string	padx = "10 0", pady = "1";

	dialog = toplevel("${self.w_top}.__dialog", borderwidth: 5);
	self.w_ignoreDlg = dialog;
	Wm_withdraw((string)dialog);
	Wm_protocol((string)dialog, "WM_DELETE_WINDOW", "ignoreDone cancel");
	Wm_title((string)dialog, "Ignore What?");
	Wm_resizable((string)dialog, 0, 0);
	Wm_transient((string)dialog, self.w_top);
	bind(dialog, "<Control-i>", "ignoreSelectedFile");
	bind(dialog, "<Return>", "ignoreDone apply");
	bind(dialog, "<Escape>", "ignoreDone cancel");
	Grid_columnconfigure((string)dialog, 0, weight: 1);

	_ignore_type = "file";
	_ignore_pattern  = "*" . File_extension(sf.name);
	_ignore_dirpattern  = "*" . File_extension(sf.name);
	self.ignore_dir = dirname("${self.root}/${sf.name}");
	self.ignore_dir =~ s/^${sf.component}//g;
	self.ignore_dir =~ s/^\///g;
	_ignore_dir = self.ignore_dir . "/";

	if (sf.component == self.root) {
		subdir = "";
	} else {
		subdir = getRelativePath(sf.component, self.root) . "/";
	}

	ttk::label("${dialog}.l", text: "Ignore");
	grid("${dialog}.l", row: row, column: 0, sticky: "ew");

	++row;
	which = "file";
	ttk::radiobutton("${dialog}.r_${which}",
	    variable: &_ignore_type, value: which,
	    text: "this file only (Shortcut: Ctrl-i)",
	    command: &ignoreSelect);
	grid("${dialog}.r_${which}", row: row, column: 0, sticky: "w",
	    padx: padx, pady: pady);

	if (self.ignore_dir != "") {
		++row;
		which = "dirpattern";
		ttk::frame("${dialog}.f_${which}");
		grid("${dialog}.f_${which}", row: row, column: 0, sticky: "ew",
		    padx: padx, pady: pady);
		ttk::radiobutton("${dialog}.r_${which}",
		    variable: &_ignore_type, value: which,
		    text: "glob pattern like", command: &ignoreSelect);
		pack("${dialog}.r_${which}", in: "${dialog}.f_${which}",
		    side: "left");
		ttk::entry("${dialog}.e_${which}", width: 10, state: "disabled",
		    textvariable: &_ignore_dirpattern);
		bind("${dialog}.e_${which}", "<1>", "ignoreClickLine ${which}");
		pack("${dialog}.e_${which}", in: "${dialog}.f_${which}",
		    side: "left", expand: 1, fill: "x");
		ttk::label("${dialog}.l_${which}", text: "in this directory");
		bind("${dialog}.l_${which}", "<1>", "ignoreClickLine ${which}");
		pack("${dialog}.l_${which}", in: "${dialog}.f_${which}",
		    side: "left");
	}

	++row;
	which = "pattern";
	ttk::frame("${dialog}.f_${which}");
	grid("${dialog}.f_${which}", row: row, column: 0, sticky: "ew",
	    padx: padx, pady: pady);
	ttk::radiobutton("${dialog}.r_${which}",
	    variable: &_ignore_type, value: which,
	    text: "glob pattern like", command: &ignoreSelect);
	pack("${dialog}.r_${which}", in: "${dialog}.f_${which}", side: "left");
	ttk::entry("${dialog}.e_${which}", width: 10, state: "disabled",
	    textvariable: &_ignore_pattern);
	bind("${dialog}.e_${which}", "<1>", "ignoreClickLine ${which}");
	pack("${dialog}.e_${which}", in: "${dialog}.f_${which}",
	    side: "left", expand: 1, fill: "x");
	ttk::label("${dialog}.l_${which}",
	    text: "in *all* directories in this component");
	bind("${dialog}.l_${which}", "<1>", "ignoreClickLine ${which}");
	pack("${dialog}.l_${which}", in: "${dialog}.f_${which}", side: "left");

	if (self.ignore_dir != "") {
		++row;
		which = "dirprune";
		ttk::frame("${dialog}.f_${which}");
		grid("${dialog}.f_${which}", row: row, column: 0, sticky: "ew",
		    padx: padx, pady: pady);
		ttk::radiobutton("${dialog}.r_${which}",
		    variable: &_ignore_type, value: which,
		    text: subdir, command: &ignoreSelect);
		pack("${dialog}.r_${which}", in: "${dialog}.f_${which}",
		    side: "left");
		ttk::entry("${dialog}.e_${which}", state: "disabled",
		    width: 10, textvariable: &_ignore_dir);
		bind("${dialog}.e_${which}", "<1>", "ignoreClickLine ${which}");
		pack("${dialog}.e_${which}", in: "${dialog}.f_${which}",
		    side: "left", expand: 1, fill: "x");
		ttk::label("${dialog}.l_${which}",
		    text: "and all directories below it");
		bind("${dialog}.l_${which}", "<1>", "ignoreClickLine ${which}");
		pack("${dialog}.l_${which}", in: "${dialog}.f_${which}",
		    side: "left");
	}

	++row;
	ttk::frame("${dialog}.buttons");
	grid("${dialog}.buttons", row: row, column: 0,
	    sticky: "e", pady: "10 5");
	ttk::button("${dialog}.buttons.apply", text: "Apply",
	    command: "ignoreDone apply");
	pack("${dialog}.buttons.apply", side: "left", padx: 5);
	ttk::button("${dialog}.buttons.cancel", text: "Cancel",
	    command: "ignoreDone cancel");
	pack("${dialog}.buttons.cancel", side: "left", padx: 5);

	update();
	w = Winfo_width((string)self.w_top);
	h = Winfo_reqheight((string)dialog);
	x = Winfo_rootx((string)self.w_files);
	y = Winfo_rooty((string)self.w_files)
	    + Winfo_height((string)self.w_files);
	Wm_geometry((string)dialog, "${w}x${h}+${x}+${y}");
	Wm_deiconify((string)dialog);
	while (1) {
		Grab_set((string)dialog);
		Tkwait_variable(&_ignore_action);
		Grab_release((string)dialog);

		if (_ignore_action == "cancel") break;
		if (_ignore_type != "dirprune") break;

		// Check for sfiles beneath the given directory.  If we find
		// any, tell them they can't do that and drop back to
		// the dialog.

		chdir(sf.component);
		which = trim(`bk --sigpipe sfiles '${_ignore_dir}' | head -1`);

		unless (which == "") {
			tk_messageBox(parent: dialog, title: "Not allowed",
			    message: "You cannot prune a directory that"
				" contains version-controlled files");
			continue;
		}

		break;
	}

	destroy(dialog);

	if (_ignore_action == "cancel") return;

	if (_ignore_type == "pattern") {
		push(&opts, _ignore_pattern);
	} else if (_ignore_type == "dirpattern") {
		push(&opts, self.ignore_dir, _ignore_dirpattern);
	} else if (_ignore_type == "dirprune") {
		push(&opts, _ignore_dir);
	}
	appendIgnoreLine(_ignore_type, sf, (expand)opts);
}

void
appendIgnoreLine(string type, sfile sf, ...args)
{
	FILE	fp = openIgnoreFile(sf.component);

	if (type == "pattern") {
		puts(fp, args[0]);
	} else if (type == "dirpattern") {
		puts(fp, "${args[0]}/${args[1]}");
	} else if (type == "file") {
		string	file = "${self.root}/${sf.name}";

		file =~ s/^${sf.component}\///g;
		puts(fp, file);
	} else if (type == "dirprune") {
		string	dir = trim(String_trim(args[0], "/"));
		unless (dir == "") {
			puts(fp, "${dir} -prune");
		}
	}
	fclose(fp);
	if (type == "file") {
		string	comp;

		sf = getSelectedFile();
		comp = sf.component;
		unless (getIgnoreFile(comp)) insertIgnoreFile(comp);
		deleteFileFromList(sf);
	} else {
		rescanComponent(sf.component);
	}
}

void
ignoreClickLine(string which)
{
	widget	radio = "${self.w_ignoreDlg}.r_${which}";

	unless (Radiobutton_instate(radio, "selected")) {
		Radiobutton_invoke(radio);
	}
}

void
ignoreDone(string which)
{
	_ignore_action = which;
}

void
ignoreSelect()
{
	widget	w, e;

	// Disable the entry boxes of other radiobuttons.
	foreach (w in getAllWidgets(self.w_ignoreDlg)) {
		unless (Winfo_class((string)w) == "TEntry") continue;
		w =~ /e_(.*)$/;
		if ($1 == _ignore_type) {
			e = w;
			Entry_configure(w, state: "normal");
		} else {
			Entry_configure(w, state: "disabled");
		}
	}

	if (e) {
		focus(e);
		Entry_icursor(e, "end");
		Entry_selectionRange(e, 0, "end");
	}
}

void
ignoreSelectedFile()
{
	sfile	sf = getSelectedFile();

	unless (sf.type == "new") return;

	if (Winfo_exists((string)self.w_ignoreDlg)) {
		destroy(self.w_ignoreDlg);
	}

	appendIgnoreLine("file", sf);
}

FILE
openIgnoreFile(string comp)
{
	string	ignoreFile = joinpath(comp, "BitKeeper/etc/ignore");

	unless (defined(bk_system("bk edit -q '${ignoreFile}'"))) {
		tk_messageBox(parent: self.w_top, title: "Error",
		    message: "Could not edit ignore file");
		return undef;
	}
	return (fopen(ignoreFile, "a+"));
}

void
discardChanges()
{
	sfile	sf = getSelectedFile();

	if ((sf.type == "cset") || (sf.type == "pending")) return;
	saveComments();
	unless (self.doDiscard) {
		self.doDiscard = 1;
		if (sf.type == "new") {
			topMessage(msgs{"deleteNew"}, "message");
		} else {
			topMessage(msgs{"unedit"}, "message");
		}
		return;
	}

	self.doDiscard = 0;

	// If it's an ignore file, remove the file but don't delete
	// the cset file.  The component rescan will remove the cset
	// file if it's necessary.
	if (sf.name =~ /BitKeeper\/etc\/ignore$/) {
		removeFile(sf, 0);
		rescanComponent(sf.component);
	} else {
		removeFile(sf, 1);
	}
}

int
isRepoLocked()
{
	return (system("bk lock -q") == 1);
}

void
cmd_refresh(int restore)
{
	// This function is a remnant from the old citool used by ciedit.tcl.

	restore = 0;
	refreshSelectedFile();
}

void
setQuit(string value)
{
	_quit = value;
}

void
rescan()
{
	saveComments();
	init();

	ListBox_configure(self.o_files, state: "normal");
	ListBox_itemDelete(self.o_files, (expand)getAllNodes());
	Progressbar_configure(self.w_progress, value: 0);
	StatusBar_add(self.w_statusF, self.w_progress,
	    separator: 0, sticky: "e");
	update();

	getComponentList();
	getNumFiles();
	processFiles(self.filelist);
}

void
handle_release(widget w)
{
	if (length(Text_tagRanges(w, "sel"))) return;
	focus(self.w_comments);
}

void
handle_focus(widget w)
{
	if (w == self.w_diffs || w == self.w_lowerF) return;
	focus(self.w_comments);
}

void
disable_file_list()
{
        string  node, nodes[];

        ListBox_configure(self.o_files, state: "disabled");
        nodes = getAllNodes();
        foreach (node in nodes) {
                ListBox_itemconfigure(self.o_files, node,
                    foreground: "gray");
        }
}

void
initMsgs()
{
// Don't make comments wider than 65 chars
//--------|---------|---------|---------|---------|---------|----
	msgs{"nonrc"} = "\n"
"  Not currently under revision control. \n"
"  Click on the file-type icon or start typing comments \n"
"  if you want to include this file in the current ChangeSet\n";
	msgs{"gotCset"} = "\n"
"  Click \[Commit] again to check in and create this ChangeSet,\n"
"  or type Control-l to go back to back and work on the comments.\n";
	msgs{"onlyPending"} = "\n"
"  Since there are only pending files selected, you must\n"
"  create a ChangeSet comment in order to commit.\n\n"
"  Type Control-l to go back and provide ChangeSet comments.\n";
	msgs{"noCset"} = "\n"
"  Notice: this will not group and commit the deltas listed below\n"
"  into a ChangeSet, because there are no ChangeSet comments or\n"
"  because the ChangeSet has been excluded.\n"
"  Click \[Checkin] again to check in only the commented deltas,\n"
"  or type Control-l to go back and provide ChangeSet comments.\n";
	msgs{"resolveCset"} = "\n"
"  You must provide comments for the ChangeSet file when resolving.\n"
"  Type Control-l to go back and do so.\n";
	msgs{"noCsetOK"} = "\n"
"  Click \[Checkin] again to check in and create these deltas,\n"
"  or type Control-l to go back to back and work on the comments.\n";
	msgs{"unedit"} = "\n"
"  Click \[Discard] again if you really want to unedit this file,\n"
"  or type Control-l to go back and work on the comments.\n\n"
"  Warning!  The changes to this file shown below will be lost.\n";
	msgs{"deleteNew"} = "\n"
"  Click \[Discard] again if you really want to delete this file,\n"
"  or type Control-l to leave this file in place.\n\n"
"  Warning!  The file below will be deleted if you click \[Discard]\n";
	msgs{"noFileComments"} = "\n"
"No files have comments yet, so no ChangeSet can be created.\n"
"Type Control-l to go back and provide some comments.\n";
	msgs{"changeset"} = "\n"
"Please describe the change which is implemented in the deltas listed below.\n"
"Describe the change as an idea or concept; your description will be used by\n"
"other people to decide to use or not to use this changeset.\n\n"
"If you provide a description, the deltas will be grouped into a ChangeSet,\n"
"making them available to others.  If you do not want to do that yet, just\n"
"click Checkin without typing in comments here, and no ChangeSet will be "
"made.\n\n"
"NOTE: Any component ChangeSet that is not commented will receive the same\n"
"comment as the product ChangeSet.\n";
	msgs{"pendingFile"} =
" This delta has been previously checked in and is in pending state.\n"
" That means that you can not modify these comments, and that this delta\n"
" will be included in the ChangeSet when you next create a ChangeSet.";
	msgs{"repoLocked"} =
"A checkin cannot be made at this time.\n"
"Try again later.";
	msgs{"resolveAllComments"} =
"All files must have comments when merging.\n"
"Type Control-l to go back and provide comments for all files.\n";
}

// Test functions.
// 
// These are very simple functions called by the testing harness to query
// internal information from tool.

string
test_getFiles()
{
	string	data, text, node, nodes[];

	nodes = getAllNodes();
	foreach (node in nodes) {
		text = ListBox_itemcget(self.o_files, node, text:);
		data .= text . "\n";
	}
	return (data);

}

string
test_getComments()
{
	return (getCurrentComments());
}

string
test_getDiffs()
{
	return(Text_get(self.w_diffs, 1.0, "end - 1 char"));
}

string
test_getPasteBuffer()
{
	return (self.clipboard);
}
                                                              
void
test_selectFile(string file)
{
	unless (selectFile(self.files{file}.node)) {
		puts("${file} is not in the file list, but it should be");
		exit(1);
	}
}

void
test_selectNext()
{
	moveNext();
}

string
test_findFileInList(string file, sfile &sf)
{
	string	node, nodes[];

	nodes = getAllNodes();
	foreach (node in nodes) {
		if (ListBox_itemcget(self.o_files, node, text:) == file) {
			sf = self.files{file};
			return (node);
		}
	}
	return (undef);
}

void
test_fileIsSelected(string file)
{
	string	sel = getSelectedNode();

	if (ListBox_itemcget(self.o_files, sel, text:) == file) return;
	puts("${file} is not the selected file, but it should be");
	exit(1);
}

void
test_fileIsInList(string file)
{
	sfile	sf;

	unless (defined(test_findFileInList(file, &sf))) {
		puts("${file} is not in the file list, but it should be");
		exit(1);
	}
}

void
test_fileIsNotInList(string file)
{
	sfile	sf;

	if (defined(test_findFileInList(file, &sf))) {
		puts("${file} is in the file list, but it should not be");
		exit(1);
	}
}

void
test_fileHasIcon(string file, string want)
{
	sfile	sf;
	string	icon = "unknown";
	string	node = test_findFileInList(file, &sf);

	unless (defined(node)) {
		puts("${file} is not in the file list, but it should be");
		exit(1);
	}

	if (defined(node)) {
		string	img = ListBox_itemcget(self.o_files, node, image:);

		if (img == img_new) {
			icon = "extra";
		} else if (img == img_cset) {
			icon = "cset";
		} else if (img == img_done) {
			icon = "done";
		} else if (img == img_exclude) {
			icon = "excluded";
		} else if (img == img_modified) {
			icon = "modified";
		} else if (img == img_notincluded) {
			icon = "notincluded";
		}
	}

	if (want != icon) {
		puts("${file} has the ${icon} icon but it should be ${want}");
		exit(1);
	}
}

void
test_fileHasComments(string file, string comment)
{
	sfile	sf;
	string	node = test_findFileInList(file, &sf);
	string	comments;

	unless (defined(node)) test_fileIsInList(file);
	comments = getComments(sf);
	if (comment != comments) {
		puts("${file} does not have the right comments");
		puts("should be:");
		puts(comment);
		puts("but got:");
		puts(comments);
		exit(1);
	}
}

void
test_inputComment(string comment)
{
	test_inputString(comment,  self.w_comments);
}

void
test_togglePending()
{
	string	comp;

	foreach (comp in self.components) {
		togglePending(self.pendingNodes{comp});
	}
}

void
test_toggleFile(string file)
{
	sfile	sf;
	string	node = test_findFileInList(file, &sf);

	unless (defined(node)) test_fileIsInList(file);
	toggleFile(node);
}

void
test_selectComment(string idx1, string idx2)
{
	widget	textbox = self.w_comments;

	Text_tagRemove(textbox, "sel", "1.0", "end");
	Text_tagAdd(textbox, "sel", idx1, idx2);
}

void
test_discardFile(string file)
{
	sfile	sf;
	string	node = test_findFileInList(file, &sf);

	unless (defined(node)) test_fileIsInList(file);
	selectFile(node);
	discardChanges();
	discardChanges();
}

void
test_ignoreFile(string file)
{
	sfile	sf;
	string	node = test_findFileInList(file, &sf);

	unless (defined(node)) test_fileIsInList(file);
	selectFile(node);
	appendIgnoreLine("file", sf);
}

void
test_ignoreDir(string dir, _optional string pattern)
{
	sfile	sf = getSelectedFile();

	if (pattern) {
		appendIgnoreLine("dirpattern", sf, dir, pattern);
	} else {
		appendIgnoreLine("dirprune", sf, dir);
	}
}

void
test_ignorePattern(string pattern)
{
	sfile	sf = getSelectedFile();

	appendIgnoreLine("pattern", sf, pattern);
}

void
init()
{
	self.dashs = 0;
	self.nfiles = 0;
	self.changed = 0;
	self.last_update = 0;
	self.sfiles_last = 0;
	self.sfiles_done = 0;
	self.sfiles_found = 0;
	self.sfiles_pending = 0;
	self.sfiles_reading = 0;
	self.sfiles_scanning = 0;
	self.commitSwitch = 0;
	self.doDiscard = 0;
	self.committing = 0;
	self.cnt_new = 0;
	self.cnt_newC = 0;
	self.cnt_total = 0;
	self.cnt_excluded = 0;
	self.cnt_modified = 0;
	self.cnt_modifiedC = 0;
	self.cnt_commented = 0;
	self.compsOpts = "-ch";
	self.nfilesOpts = "--cache-only";
	self.includeProduct = 1;
	self.selected = undef;
	self.files = undef;
	self.compinfo = undef;
	self.components = undef;
	self.pendingNodes = undef;
	self.allComponents = undef;
}

void
main(_argused int argc, string argv[])
{
	string	arg, file, files[];
	int	quit = 0;
	string	lopts[] = {"no-extras", "quit"};

	require("BWidget");
	Widget::theme(1);

	debug_init(getenv("BK_DEBUG_CITOOL"));

	display_text_sizes(0);
	bk_init();
	gui();
	update();

	arg = bk("-P sane 2>@1");
	if (arg != "") bk_dieError(arg, 1);

	init();
	self.cwd = pwd();
	self.dotbk = bk("dotbk");

	self.partial = 0;
	self.resolve = 0;
	self.no_extras = 0;
	while (arg = getopt(argv, "PRs;", lopts)) {
		switch (arg) {
		    case "P":
			self.partial = 1;
			break;
		    case "R":
			self.resolve = 1;
			break;
		    case "s":
			self.dashs = 1;
			if (optarg == "^PRODUCT") self.includeProduct = 0;
			self.compsOpts .= " -s${optarg}";
			self.nfilesOpts .= " -s${optarg}";
			break;
		    case "no-extras":
			self.no_extras++;
			self.compsOpts .= " --no-extras";
			self.nfilesOpts .= " --use-scancomp";
			break;
		    case "quit":
			quit = 1;
			break;
		    case "":
			bk_usage();
			break;
		}
	}
	files = argv[optind..END];

	self.nested = 0;
	if (llength(files) == 0) {
		// If no files or directory were passed on the command-line, we
		// check to see if we're in a nested component or product and
		// make the root product our directory.
		self.root = bk("root");
		self.nested = nested();
	} else if ((length(files) == 1) && isdir(files[0])) {
		self.dir = File_normalize(files[0]);
		if (self.dashs) {
			bk_dieError("Cannot specify -s together with a"
			    " directory", 1);
		}
		self.root = bk("root -R '${files[0]}'");
		files = undef;
	} else if ((length(files) == 1) && files[0] == "-") {
		if (nested()) {
		    bk_dieError("Reading files from stdin not supported"
			" in a nested repository.", 1);
		}
		if (self.dashs) {
			bk_dieError("Cannot specify -s together with -", 1);
		}
		files = undef;
		while (defined(file = fgetline(stdin))) {
			if (isdir(file)) bk_usage();
			push(&files, file);
		}
		self.root = bk("root");
	} else {
		string	filelist[] = files;

		if (self.dashs) {
			bk_dieError("Cannot specify -s together with files", 1);
		}
		files = undef;
		foreach (file in filelist) {
			if (isdir(file)) bk_usage();
			push(&files, File_normalize(file));
		}
		self.root = bk("root -R");
	}

	if (!self.resolve && !bk_lock()) {
		displayMessage("Could not obtain a read lock for this repo", 1);
	}

	self.filelist = files;

	if (self.resolve) {
		self.root = bk("pwd");
		self.nested = 0;
	}

	self.root = File_normalize(self.root);
	if (isdir(self.root)) chdir(self.root);

	getComponentList();
	getNumFiles();

	// Initialize the ChangeSet template if it exists.
	self.templates{"ChangeSet"} = bk("-R cat BitKeeper/templates/commit");
	if (self.templates{"ChangeSet"} != "") {
		self.templates{"ChangeSet"} .= "\n";
	}

	initMsgs();
	createImages();
	processFiles(self.filelist);
	display_text_sizes(1);
	if (quit) exit();
	readBackupComments();
}
}
