# -*-Tcl-*- nowrap
# ###################################################################
# 
# FILE: "cardBrowser.tcl"
#                                last update: 27/08/2004 21:06:49
# Author: Joachim Kock
# E-mail: <kock@math.uqam.ca>
# 
# Description: Card Browser is a generic interface to data structures
# that can be reasonably interpreted as a collection of cards.  Typical
# examples are simple databases like an address book, and the main
# application of Card Browser is to interface Apple's Addressbook.
# 
# Other examples provided are:  
# 
#  -- access to a tab-separated database, exemplified by the notorious 
#     Whisky Distilleries and Country-codes files distributed with Alpha
#  -- interface to bibtex data files
#  -- interface to the arXiv preprint server on the internet
#  -- iTunes Library browser
#  -- Safari bookmarks browser
#  -- a Card Browser Tutorial 'hypercard stack'
#  -- a file browser
# 
# 
# The interface is a window consisting of a narrow column on the left and a
# bigger area on the right.  In the column you can scroll through a lists
# of card titles (either all cards or cards that match a performed search).
# The selected item is shown in the area on the right.  (In other words,
# same interface as we know from Apple's Addressbook, and from many other
# applications.)
# 
# Depending on what sort of data is represented, there is support for
# searching, editing, creating and deleting cards, or performing other
# operations related to the data of the card.
# 
# 
# 
# The Card Browser takes care of all the interface details, making it
# easy to write a new client.  A client needs to provide 
# 
#  -- a namespace, say ::Foo
#  -- an array  ::Foo::staticCardParameters  with entries
#     keys, autokeys, headKeys, listWidth, cardWidth, cardHeight
#  -- the following procs:
#         ::Foo::find <record>
#         ::Foo::getRecord <uid>
#         ::Foo::setRecord <uid> <record>
#         ::Foo::deleteRecord <uid>
#         ::Foo::newRecord <record>
#         ::Foo::printCard <uid>
#         ::Foo::init
#         ::Foo::properClose
# 
# Here staticCardParameters(keys) is a list of keys, like for example
# {Firstname Lastname Address City Phone}.  This is the only mandatory
# entry in the array.  If the other entries are not provided, the Card
# Browser just uses some standard values.  ('autokeys' are keys like
# ModificationDate that are not supposed to be set by the user; 'headKeys'
# are those few ones that should be listed in the column on the left of 
# the Card Browser.)
# 
# For the procs, only [find] and [getRecord] are mandatory.  The argument
# to [find] is a keyed list, and it should return a list of uids matching
# that (partial) record.  [getRecord] returns a keyed list.  The other 
# procs have functionality you would expect from their names.
# 
# The [printCard] proc can be provided to provide complete control over
# the layout of the card representing the record.  
# 
# 
# 
# 
# The [init] proc can be provided if the client needs some sort of
# initialisation before it can be used, like for example connetion to a SQL
# server, or loading a complete database into memory.
### There should be a notion of initialisation: happens when the client
# window is created.  Then there should be a close hook in Card mode,
# to the effect of closing the client again.
# 
# At initialisation, some variables can be filled in, and so on...
# [properClose] is to clean up when the window is closed.  For example,
# to close a connection, to free some memory, or something of the sort.


# 
# The clients will typically have a core part which provides only command-line
# interface to the database, and a interface layer where the interaction with
# the cardWiewer is defined.  This is where cards are defined and those global
# arrays set up


# --------------------------------------------------------------------
# Definition of Card mode
# --------------------------------------------------------------------

alpha::package require Alpha 8.0b11
alpha::package require AlphaTcl 8.0b7

namespace eval Card {}
proc cardBrowser.tcl {} {}

alpha::mode Card 0.1 cardBrowser.tcl  {} {} {
#     addMenu cardMenu "Card" Card
}

# # --------------------------------------------------------------------
# # Card Browser Menu
# # --------------------------------------------------------------------
# # set cardMenu "Card"
# 
# 
# proc cardMenu {} {}
# 
# # Tell Alpha what procedures to use to build all menus, submenus.
# 
# menu::buildProc cardMenu Card::buildMenu
# 
# set Card::menuItems [list \
#     "find"                  {Card::windowForFind}    \
#     "findAnywhere"          {Card::findAnywhere}     \
#     "searchWithinResults"   {Card::windowForFindWithin}    \
#     "edit"                  {Card::windowForEditRecord}   \
#     "new"                   {Card::windowForNewRecord}    \
#     "save"                  {Card::save}    \
#     "delete"                {Card::delete}  \
#     "putAddressInClipboard" {Card::putAddressInClipboard}   \
#     "removeFromSelection"   {Card::removeFromSelection}  \
#     ]
# 
# 
# proc Card::buildMenu {} {
#     global cardMenu
#     variable menuItems
#     set submenus ""
#     set itemList [list ]
#     foreach {item proc} $menuItems { 
# 	lappend itemList $item 
#     }
#     return [list build $itemList "Card::menuProc -M Card" $submenus $cardMenu]
# }
# 
# # # Now we actually build the Card Browser Menu.
# # menu::buildSome cardMenu
# 
# 
# proc Card::menuProc {menuName itemName} {
#     set client [clientOfWindow]
#     variable menuItems
#     [dict get $menuItems $itemName]
# }


# --------------------------------------------------------------------
# Hooks and key bindings
# --------------------------------------------------------------------

hook::register characterInsertedHook Card::handleTyping "Card"

proc Card::handleTyping { win pos char } {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    if { $D(currentFunction) == "browse" } {
	if { [string is alpha $char] } {
	    # Use the typed letter to scroll.  If several letters are
	    # typed without pause, then use them all.  So we have to
	    # have a short-term memory D(pat)
	    append D(pat) [::caseInsense $char]
	    append pat $D(pat) *
	    # Clear the short-term memory after 2 seconds:
	    after 2000 "set ::${client}::_dynamicCardParameters(pat) {}"
	    # Look for an item starting with the typed letters:
	    set index [lsearch -glob $D(entryList) $pat]
	    if { $index != -1 } {
		displayItem $client $index
	    } else {
		displayListAndCard $client
	    }
	} else {	    
	    # For browser windows we just refresh and give a message.
	    displayListAndCard $client
	    status::msg "Press Ctrl-P E to edit"
	}
	
    } else {	
	# For all editable windows, preserve whitespace.
	set end [pos::lineEnd $pos]
	set res [search -f 1 -n -l $end "   " $pos]
	if { [string length $res] } {
	    goto [lindex $res 0]
	    if { [string length $char] } {
		deleteChar
	    } else {
		insertText " "
	    }	
	    goto $pos
	}
    }
}


hook::register closeHook Card::properClose "Card"

proc Card::properClose { name } {
    set client [clientOfWindow $name]
    unset -nocomplain ${client}::_dynamicCardParameters
    unset -nocomplain ${client}::_cache
    if { [namespace eval $client {info procs properClose}] == "properClose" } {
	${client}::properClose $name
    }
}

Bind 0x24 Card::carriageReturn "Card"
Bind 0x24 <s> Card::searchWithinResultsWithTheseCriteria "Card"
Bind Up Card::upArrow "Card"
Bind 'k' <z> Card::upArrow "Card"
Bind Down Card::downArrow "Card"
Bind 'j' <z> Card::downArrow "Card"

Bind 0x31 Card::space cardbrowse

Bind 0x33 Card::removeFromSelection "cardbrowse"

Bind 0x30  Card::nextTab "Card"


Bind 'p' <z> {prefixChar CardBrowser}

Bind 'e' <P> {Card::windowForEditRecord} Card
Bind 'n' <P> {Card::windowForNewRecord} Card
Bind 's' <P> {Card::save} Card
Bind 'f' <P> {Card::windowForFind } Card
Bind 'd' <P> {Card::delete} Card

Bind 'z' <P> {Card::separateWindowForCard} cardbrowse

# The CardBrowser mechanism maintains a big array for each client,
# whose content reflects the current state: which list of cards is
# being displayed? which card? which state is the scroll mechanism
# in?   Which sort of view are we in ('find', 'edit', 'browse', 'new')?
# (As well as some auxililary variables: the length of the sublists
# how many lines are hidden, and such...  Here is the complete list:
# 
# currentFunction        can be 'browse', 'find', 'edit', or 'new'
# matchingUIDs           list of all records involved (and whose titles
#                        are being displayed in the list on the left)
# entryList              the headers for these items
# len                    These two lists are obviously of the same length $len
# subMatchingUIDs        the sublist which is currently visible
# matchIndex             the index in $matchingUIDs corresponding to:
# currentCard            the card being displayed on the right
# hiddenEntries          the number of entries in matchingUIDs that are
#                        before the displayed ones
# selectedLine           the line number (relative to the window) of the
#                        highlighted item 


# Most cardBrowser procs take at least one argument, namely the namespace
# of the database --- every client has a unique namespace.  This argument
# must always be fully qualified.




# --------------------------------------------------------------------
# Create editable windows (for edit, find, newRecord).
# --------------------------------------------------------------------
# These windows do not have a scrollable list at the left.


# This proc is no longer used by [find]
proc Card::setupInputWin { client {rec ""}} {
    set client [qualifyName $client]
    upvar #0 ${client}::staticCardParameters S
    # If an input template exists, then use it.
    # Otherwise create a template from the keys list.
    if { [info exists S(inputTemplate)] } {
	# See in the file edit.tcl how this could be done
    } else {
	# Prepare the window contents:
	set content ""
	set keyLength [expr {[longestEntry $S(keys)] + 2}]
	foreach key $S(keys) {
	    append content "[format "%*s: " $keyLength $key]"
	    catch { append content [keylget rec $key]  } 
	    append content "\r"
	}
    }
    writeToWindow $client $content
    # (whereby that window is brought to front)
    win::setInfo [windowOfClient $client] bindtags [list $client cardedit Card]
    goto [minPos]
    nextTab
}

proc Card::windowForEditRecord { } {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    setupInputWin $client [${client}::getRecord $D(currentCard)]
    set D(currentFunction) "edit"
    status::msg "Edit card.  Press Ctrl-P S to save."
}

proc Card::windowForNewRecord { } {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    setupInputWin $client
    set D(currentFunction) "new"
    status::msg "Fill in the data, press Ctrl-P S to save."
}

proc Card::windowForFind { {client ""} } {
    if { [string length $client] } {
	set client [qualifyName $client]
    } else {
	set client [clientOfWindow]
    }
    upvar #0 ${client}::_dynamicCardParameters D
    upvar #0 ${client}::staticCardParameters S
    # If an search template exists, then use it.
    # Otherwise create a template from the keys list.
    if { [info exists S(searchTemplate)] } {
	set content [file::readAll $S(searchTemplate) macroman]
	# for some stupid reason, [read] always translates \r to \n...
	regsub -all {[\n]} $content \r content
    } else {
	# Prepare the window contents:
	set content ""
	set keyLength [expr {[longestEntry $S(keys)] + 2}]
	foreach key $S(keys) {
	    append content "[format "%*s: " $keyLength $key]"
	    append content "\r"
	}
    }
    writeToWindow $client $content
    # (whereby that window is brought to front)
    win::setInfo [windowOfClient $client] bindtags [list $client cardedit Card]
    goto [minPos]
    nextTab
    set D(currentFunction) "find"
    status::msg "Enter search criteria, and press <return> to search."
}

proc Card::windowForFindWithin { {client ""} } {
    if { [string length $client] } {
	set client [qualifyName $client]
    } else {
	set client [clientOfWindow]
    }
    windowForFind $client
    upvar #0 ${client}::_dynamicCardParameters D
    set D(currentFunction) "findwithin"
    status::msg "Enter search criteria, and press <return> to search within previous results."
    
}

# Get the data from an input window
# ---------------------------------
# The result is a keyed list.  By definition, a text chunk is interpreted 
# as a key if it is between linestart (modulo whitespace) and a colon.  The
# value is then taken to be all the following text up to the next linebreak 
# preceeding a key marker.
proc Card::getDataFromWindow { } {
    set win [win::Current]
    set client [clientOfWindow]
    upvar #0 ::${client}::staticCardParameters S
    # If an input template exists, then use it for scanning.
    # Otherwise create a regexp from the keys list.
    set content [getText -w $win [minPos] [maxPos -w $win]]
    unset -nocomplain rec
    
    # List of all field markers:
    set L [regexp -all -indices -inline -- {(?:^|[\r\n])\s*([^:\r\n]+)([ ]?:[ ]*)} $content]
    # This is a list of length 3n, in which we could do 
    # "foreach {fullMatch key colonAndWhiteSpace} $L"
    # Now we will rather shift the list a little bit
    lvarpop L
    lappend L [list [maxPos -w $win] [maxPos -w $win]]
    foreach {keyPos colonString next} $L {
	set keyPos0 [lindex $keyPos 0]
	set keyPos1 [lindex $keyPos 1]
	set valPos0 [lindex $colonString 1]
	set valPos1 [lindex $next 0]
	set key [string range $content $keyPos0 $keyPos1]
	set key [string trim $key]
	set value [string range $content $valPos0 $valPos1]
	set value [string trim $value]
	if { [string length $value] } {
	    if { [ string equal $value "NULL" ] } {
		set value ""
	    }
	    keylset rec $key $value
	}
    }
    if { ![info exists rec] } {
	# No search criteria were given.  In this case we use the empty record.
	set rec ""
	# (Typically the client's [find] function will handle this by returning
	# the list of all records.)
    }
    return $rec    
}



# --------------------------------------------------------------------
# Save and delete
# --------------------------------------------------------------------

proc Card::saveNewRecord { client } {
    set client [qualifyName $client]
    upvar #0 ${client}::_dynamicCardParameters D
    set rec [getDataFromWindow]
    set uid [${client}::newRecord $rec]
    displayCollection $client [list $uid]
}

proc Card::saveRecord { client } {
    set client [qualifyName $client]
    upvar #0 ${client}::_dynamicCardParameters D
    set record [getDataFromWindow]
    set uid $D(currentCard)
    
    # The following chunk is just to avoid writing something
    # that already exists
    set oldRecord [${client}::getRecord $uid]
    foreach field [keylkeys record] {
	if { [lcontain [keylkeys oldRecord] $field] &&
	[string equal [keylget record $field] [keylget oldRecord $field]] } {
	    keyldel record $field
	}
    }
    if { [llength $record] } {
	if { [catch { ${client}::setRecord $uid $record } err] } {
	    error "Error saving record: $err"
	} 
	# Just in case item head changed:
	reloadCurrentCard $client
	status::msg "Record saved"
    } else {
	alertnote "No changes"
    }
}

proc Card::reloadCurrentCard { client } {
    set client [qualifyName $client]
    upvar #0 ${client}::_dynamicCardParameters D
    set uid $D(currentCard)
    cacheUpdate $client $uid
    lvarpop D(entryList) $D(matchIndex)
    lvarpush D(entryList) [_headKeyIncludingSpacePadding $client $uid] $D(matchIndex)
#     displayItem $client $D(matchIndex)  
    displayListAndCard $client
}

proc Card::save { } {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    switch -exact -- $D(currentFunction) {
	edit {
	    saveRecord $client
	}
	new {
	    saveNewRecord $client
	}
	default {
	    status::msg "Nothing to save"
	}	
    }
}


proc Card::delete {} {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    set uid $D(currentCard)
    switch -- [askyesno "Delete $uid?"] {
	"yes" {
	    # continue
	}
	"no" {
	    status::errorMsg "Cancelled."
	    return ;# superfluous
	}
    }
    if { [catch { ${client}::deleteRecord $uid } err] } {
	error "Error: could not delete record: $err"
    } else {
	status::msg "Record deleted"
    }
    # Now we should just show the same collection as before,
    # except for the deleted one.
    removeFromSelection
    status::msg "Record deleted"
}


# Remove the current item from the selection
proc Card::removeFromSelection {} {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    upvar #0 ::${client}::_cache C
    set oldUID [lvarpop D(matchingUIDs) $D(matchIndex)]
    array unset C($oldUID)
    set D(len) [llength $D(matchingUIDs)]
    set item [lvarpop D(entryList) $D(matchIndex)]
    set item [string trimright $item]
    incr D(matchIndex) -1
    incr D(selectedLine) -1
    if { $D(matchIndex) == [expr $D(len) -1] } {
	showNeighbour 0
    } else {
	showNeighbour +1
    }
    status::msg "Item \"$item\" removed from selection."
}


# --------------------------------------------------------------------
# Search
# --------------------------------------------------------------------

# Acquire the search criteria from the input window and call the 
# clients [find] proc.  The result is a list of matching UIDs which
# we pass on to the display department...
proc Card::searchWithTheseCriteria { } {
    set client [clientOfWindow]
    upvar #0 ::${client}::staticCardParameters S
    # The record from the search window is already reduced: 
    # no empty-string valued fields
    set record [getDataFromWindow]
#     # We don't want to search for empty strings, since anyway they will
#     # macth naything...
#     foreach key $S(keys) {
# 	if { [string is space [keylget record $key]] } {
# 	    keyldel record $key
# 	} elseif { [string equal [keylget record $key] "NULL"] } {
# 	    keylset record $key ""
# 	}
#     }
    if { [catch {set L [${client}::find $record]} err] } {
	alertnote $err
	return
    }
    displayCollection $client $L -sort
}

# Acquire the search criteria from the input window and call the 
# clients [find] proc.  The result is a list of matching UIDs which
# we pass on to the display department...
proc Card::searchWithinResultsWithTheseCriteria { } {
    set client [clientOfWindow]
    upvar #0 ::${client}::staticCardParameters S
    upvar #0 ::${client}::_cache C
    # The record from the search window is already reduced: 
    # no empty-string valued fields
    set record [getDataFromWindow]
    
    set res [list ]
    foreach uid [array names C] {
	set match 1
	foreach key [keylkeys record] {
	    # SHOULD BE MORE CAREFUL WITH SUB KEYS HERE
	    if { [catch { keylget C($uid) $key } val] ||
	    ![regexp -nocase -- [keylget record $key] $val] } {
		set match 0
		break
	    }
	}
	if { $match } {
	    lappend res $uid
	} else {
	    continue
	}
    }
    
    if { [llength $res] } {
	displayCollection $client $res -sort
    } else {
	alertnote "No matches"
	return
    }
}

proc Card::findAnywhere {} {
    set client [clientOfWindow]
    if { [catch { set str [prompt "Enter search string " ""] } ] } {
	return
    }
    if { [catch {set res [${client}::findAnywhere $str]} err] } {
	alertnote $err
	return
    }
    displayCollection $client $res -sort
}

# --------------------------------------------------------------------
# Display procs
# --------------------------------------------------------------------

# displayCollection
#        Specify a list of UIDs (in D(matchingUIDs)), put all records into
#        the cache, prepare the entryList.  If the -sort flag is given (as
#        third argument), these two lists are sorted according to the items 
#        in the entrylist.  Finally call the display proc [displayItem].
# displayItem
#        Within the given collection (D(matchingUIDs)), go to a given item
#        (calling [displayListAndCard]).
# printCurrentCard
#        Format the card D(currentCard) from the record, return the whole 
#        text.  (This proc is only called from [displayListAndCard].)
# displayListAndCard
#        Assemble the window content from entryList and card, and write to
#        the screen.  The entryList was prepared in advance, currentCard is 
#        set, and the necessary parameters for the scroll mechanism are also 
#        set.  Call [writeToWindow]:
# writeToWindow
#        This proc just receives a black box of data and writes it to the 
#        window.  Well, it's most important task is actually to ensure there 
#        is a window, and create it and set it up in the right way if there 
#        isn't.
# highlightItem
#        sets the selection (according to the dynamic parameters).
#        
       




# The optional third argument can either be a matchIndex (indicating which
# element in the collection should be highlighted), or it can be the flag
# -sort which indicates that the collection should be sorted according to
# head key.  The two cases are exclusive: it doesn't make sense to indicate
# an index at the same time as sorting...  If no matchIndex argument is
# given, 0 is assumed.
proc Card::displayCollection { client collection args } {
    if { ![llength $collection] } {
	status::msg "No records"
	return
    }
    set client [qualifyName $client]
    upvar #0 ${client}::_dynamicCardParameters D
    cacheErase $client
    set D(matchingUIDs) [list ]
    set D(entryList) [list ]
    set matchIndex 0
    
    # First case: there is a -sort argument:
    if { [llength $args] && [lindex $args 0] == "-sort" } {
	set guideList [list ]
	foreach uid $collection {
	    cacheUpdate $client $uid
	    lappend guideList [list \
	      [_headKeyIncludingSpacePadding $client $uid] $uid]
	}
	foreach pair [lsort -dictionary -index 0 $guideList] {
	    lappend D(entryList) [lindex $pair 0]
	    lappend D(matchingUIDs) [lindex $pair 1]
	}
    } else {
	# Second case: there is a matchIndex argument:   
	if { [llength $args] && [string is integer [lindex $args 0]] } {
	    set matchIndex [lindex $args 0]
	} 

	set D(matchingUIDs) $collection
	foreach uid $collection {
	    cacheUpdate $client $uid
	    lappend D(entryList) [_headKeyIncludingSpacePadding $client $uid]
	}
    }
    set D(len) [llength $D(matchingUIDs)]
    displayItem $client $matchIndex
}


# Find out what should go in the item list
proc Card::_headKeyIncludingSpacePadding { client uid } {
    upvar #0 ${client}::staticCardParameters S
    upvar #0 ${client}::_dynamicCardParameters D
    upvar #0 ${client}::_cache C
    set entry ""
    if { [info exists S(headKeys)] } {
	foreach key $S(headKeys) {
	    catch { append entry "[keylget C($uid) $key] " }
	}
    } else {
	# If the entry S(headKeys) does not exist, just use the
	# first key in the S(keys) list:
	catch { append entry "[keylget C($uid) [lindex $S(keys) 0]] " }
    }
    return [format "%-*.*s" $S(listWidth) $S(listWidth) $entry]
}


# The client argument MUST be a fully qualified namespace
proc Card::displayItem { client {matchIndex 0} } {
    upvar #0 ${client}::staticCardParameters S
    upvar #0 ${client}::_dynamicCardParameters D
    if { $matchIndex < 0 } {
	error "matchIndex out of range"
    } elseif { $matchIndex < $S(cardHeight) } {
	set D(hiddenEntries) 0
    } elseif { $matchIndex > [expr {$D(len) - $S(cardHeight)}] } {
	set D(hiddenEntries) [expr {$D(len) - $S(cardHeight)}]
    } else {
	# Position the match near 1/3 of the visible range
	set D(hiddenEntries) [expr {$matchIndex - $S(cardHeight)/3}]
    }
    
    set D(subMatchingUIDs) [lrange $D(entryList) \
      $D(hiddenEntries) \
      [expr $D(hiddenEntries) + $S(cardHeight) - 1]]
    set D(selectedLine) [expr {$matchIndex - $D(hiddenEntries) + 1}]
    set D(matchIndex) $matchIndex
    displayListAndCard $client
    
    status::msg "Number [expr $D(matchIndex) + 1] out of $D(len) matches. \
      Use arrow keys to scroll."
}


# Only prepare the card part of the view, not the list
# In this proc we are going to read in variables whose names we
# don't know in advance.  Hence we have to be careful not to have
# a clash with the local variables.  For this reason they all
# start with underscore...  (As an example of a clash, in the
# Bibtex stack one of the keys is called "key", which is also the
# name of a local variable (were it not for the underscore.)
proc Card::printCurrentCard { _client } {
    set _client [qualifyName $_client]
    upvar #0 ${_client}::staticCardParameters _S
    upvar #0 ${_client}::_dynamicCardParameters _D
    
    # If the client has its own printCard proc then use it:
    if { [namespace eval $_client {info procs printCard}] == "printCard" } {
	return [${_client}::printCard $_D(currentCard)]
    }
	
    # Otherwise, retrieve the data and fill it into the layout:
    set record [${_client}::getRecord $_D(currentCard)]
    foreach _key $_S(keys) {
	set $_key ""
	catch { set $_key [keylget record $_key] }
	# Example:
	# set First [keylget record First]
    }
    
    # Is there a layout file to use?
    if { [info exists _S(layoutFile)] } {
	set _layout [file::readAll $_S(layoutFile)]
	# then fill in the card:
	set _res [subst -nocommands $_layout]
	regsub -all \13 $_res \r _res
	return [string trimright $_res]
    } else {
	# otherwise make an ad hoc format based on the keys:
	set _res "\r"
	set _keyLength [expr {[longestEntry $_S(keys)] + 2}]
	foreach _key $_S(keys) {
	    append _res "[format "%*s: " $_keyLength $_key]"
	    catch { append _res [keylget record $_key]  } 
	    append _res "\r"
	}
	return $_res
    }
}



# Assemble the window content from entryList and card, and write to the 
# screen.  The entryList was prepared in advance, currentCard is set,
# and the necessary parameters for the scroll mechanism are also set.
# 
# The client argument MUST be a fully qualified namespace
proc Card::displayListAndCard { client } {
    upvar #0 ${client}::staticCardParameters S
    upvar #0 ${client}::_dynamicCardParameters D

    set D(currentCard) [lindex $D(matchingUIDs) $D(matchIndex)]
    set L $D(subMatchingUIDs)
    set C [split [printCurrentCard $client] "\r\n"]
    # make the item list longer by whitespace if necessary:
    while { [llength $L] < [llength $C] || [llength $L] < $S(cardHeight) } {
	lappend L [string repeat " " $S(listWidth)]
    }
    set content ""
    foreach Lline $L Cline $C {
	append content $Lline "  |  " $Cline \r
    }
    set content [string trimright $content]

    set win [windowOfClient $client]
    writeToWindow $client $content
    set D(currentFunction) "browse"
    win::setInfo $win bindtags [list $client cardbrowse Card]
    status::msg "Number [expr $D(matchIndex) + 1] out of $D(len) matches"
    highLightItem $client  
}

# This proc just receives a black box of data and writes it to the window.
# Well, it's most important task is actually to ensure there is a window,
# and create it and set it up in the right way if there isn't.
# 
# If a window name is specified then it is assumed to be a simple window
# without scroll list.  
# 
# The client argument MUST be a fully qualified namespace
proc Card::writeToWindow { client content {win ""} } {
    upvar #0 ${client}::staticCardParameters S
    if { [string length $win] } {
	# If a win name was specified, then we don't use a scroll list
	set numCols [expr $S(cardWidth) + 1]
	set center 0
    } else {
	# No win name specified, so it's a standard browse window
	set win [windowOfClient $client]
	set numCols [expr $S(listWidth) + 5 + $S(cardWidth) + 1]
	set center 1
    }
    
    if { ![lcontain [winNames] $win] } {
	if { [namespace eval $client {info procs init}] == "init" } {
	    # PERHAPS THIS IS TOO DRASTIC: IT MIGHT JUST BE A SEPARATE
	    # WINDOW FOR A CARD, AND THAT SHOULD NOT TRIGGER AN [init]
	    # STATEMENT, WHICH IS FACT MIGHT ERASE PART OF THE CURRENT
	    # STATE...
	    ${client}::init
	}
	set width [::winZoom::winWidth 12 $numCols]
	set height [::winZoom::winHeight 12 $S(cardHeight)]
	if { $center } {
	    set voffset 160
	    set hoffset [expr (1024 - $width ) / 2]
	} else {
	    set voffset 0
	    set hoffset 0
	}
    
	foreach key $S(keys) {
	    # Currently Alpha's keyword coloring mechanism thinks that
	    # colons are always part of words...  Hence we have to add the 
	    # colon:
	    lappend keywords "[lvarpop key end]:"
	    foreach k $key {
		lappend keywords $k
	    }
	}
	colorTagKeywords -k blue "fields" $keywords
	regModeKeywords -k red "Card" [list ]
	
	# 	win::setInitialConfig $win wordbreak {\w} window
	win::setInitialConfig $win colortags fields window
	new -n $win -shell 1 -font Monaco -fontsize 12 \
	  -g $hoffset $voffset $width $height -mode "Card"
	# The calling proc is reponsible for setting bindtags.
	# (Typically these should be: $client cardbrowse Card.)
    }
    bringToFront $win
    replaceText -w $win [minPos] [maxPos -w $win] $content
    hyperize $client
}


# This client argument MUST be a fully qualified namespace
proc Card::highLightItem { client } {
    upvar #0 ${client}::staticCardParameters S
    upvar #0 ${client}::_dynamicCardParameters D
    set win [windowOfClient $client]
    set p0 [pos::fromRowCol -w $win $D(selectedLine) 0]
    set p1 [pos::math -w $win $p0 + $S(listWidth) + 2]
    select -w $win $p0 $p1
}






# Some cache procs.  The original intention was that all cache
# access should go through such procs, but it seems unnecessarily
# complicated, and it is nice to have direct access to the array...

proc Card::cacheErase { client {uid ""} } {
    upvar #0 ${client}::_cache C
    if { [string length $uid] } {
	unset C($uid)
    } else {
	array unset C
    }
}

proc Card::cacheUpdate { client uid } {
    upvar #0 ${client}::_cache C
    set C($uid) [${client}::getRecord $uid]
}

proc Card::sortResults { client fieldToSortBy } {
    upvar #0 ${client}::_dynamicCardParameters D
    upvar #0 ${client}::_cache C
    set guideList [list ]
    foreach uid $D(matchingUIDs) item $D(entryList) {
	if { [catch {set leader [keylget C($uid) $fieldToSortBy]}] } {
	    set leader ""
	}
	lappend guideList [list $leader $uid $item]
    }
    set D(matchingUIDs) [list ]
    set D(entryList) [list ]
    foreach triple [lsort -index 0 $guideList] {
	lappend D(matchingUIDs) [lindex $triple 1]
	lappend D(entryList) [lindex $triple 2]
    }
    displayItem $client 0
}






# This proc moves the selection in the item list.
# Cannot just use [displayItem], because we want stable scrolling...
proc Card::showNeighbour { dir } {
    set client [clientOfWindow]
    upvar #0 ${client}::staticCardParameters S
    upvar #0 ${client}::_dynamicCardParameters D
    
    if { $dir == "+1" && \
      $D(matchIndex) == [expr $D(len) -1] } {
	status::msg "No more cards"
	return
    }
    if { $dir == "-1" && $D(matchIndex) == 0 } {
	status::msg "No previous cards"
	return
    }
    
    set scroll [expr 2 * $S(cardHeight) / 3]
    if { $dir == "+1" && $D(selectedLine) == $S(cardHeight) } {
	set postHiddenEntries \
	  [expr $D(len) - $D(hiddenEntries) - $S(cardHeight)]
	if { $postHiddenEntries < $scroll} {
	    set scroll $postHiddenEntries 
	}
	incr D(hiddenEntries) [expr $dir * $scroll]
	incr D(selectedLine) [expr (-1) * $dir * $scroll]

    }
    if { $dir == "-1" && $D(selectedLine) == 1 } {
	if { $D(hiddenEntries) < $scroll} {
	    set scroll $D(hiddenEntries) 
	}
	incr D(hiddenEntries) [expr $dir * $scroll]
# 	set D(subMatchingUIDs) [lrange $D(entryList) $D(hiddenEntries) \
# 	  [expr $D(hiddenEntries) + $S(cardHeight) - 1]]
	incr D(selectedLine) [expr (-1) * $dir * $scroll]
    }
    set D(subMatchingUIDs) [lrange $D(entryList) $D(hiddenEntries) \
      [expr $D(hiddenEntries) + $S(cardHeight) - 1]]
    
    incr D(selectedLine) $dir
    incr D(matchIndex) $dir
    displayListAndCard $client
}


# --------------------------------------------------------------------
# Navigation and key presses
# --------------------------------------------------------------------

proc Card::carriageReturn {} {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    switch -exact -- $D(currentFunction) {
	browse {
	    status::msg "Return key not bound"
	}
	find {
	    searchWithTheseCriteria
	}
	findwithin {
	    searchWithinResultsWithTheseCriteria
	}
	edit - new {
	    nextTab
	    status::msg "Press Ctrl-P S for save"
	}
	default {
	    insertText "\r"
	}	
    }
}


proc Card::isListAndCardView { } {
    # Here perhaps we ought to maintain a global variable instead,
    # because the following ad hoc check breaks down if the dividing
    # thing is out of order, and this can easily happen if the user
    # messes around with the window.
    set client [clientOfWindow]
    set listWidth [set ${client}::staticCardParameters(listWidth)]
    if { [lookAt [expr $listWidth + 2]] == "|"} {
	return 1
    } else {
	return 0
    } 
}

proc Card::isInList {} {
    set client [clientOfWindow]
    set listWidth [set ${client}::staticCardParameters(listWidth)]
    set col [lindex [pos::toRowChar [getPos]] 1]
    if { $col > [expr $listWidth + 2] } {
	return 0
    } else {
	return 1
    }
}

proc Card::downArrow {} {
    if { [isListAndCardView] && [isInList] } {
	showNeighbour "+1"
    } else {
	nextLine
    }
}

proc Card::upArrow {} {
    if { [isListAndCardView] && [isInList] } {
	showNeighbour "-1"
    } else {
	previousLine
   }
}


# Spacebar is bound to this next-item proc only in 'cardbrowse':
proc Card::space {} {
    if { [isListAndCardView] && [isInList] } {
	showNeighbour "+1"
    } else {
	insertText " "
    }
}

proc Card::nextTab {} {
    set client [clientOfWindow]
    set start [getPos]
    if { [isListAndCardView] } {
	if { [isInList] } {
	    set start [set ${client}::staticCardParameters(listWidth)]
	}
	set pat {.*\|[^:]+:[ ]{1,3}}
    } else {
	set pat {[^:]+:[ ]{1,3}}
    }
    set posPair [search -n -f 1 -r 1 $pat $start]
    if { [string length $posPair] } {
	set p0 [lindex $posPair 1]
	set posPair [search -n -f 1 -r 1 {[ \t]*(\r|\n|$)} $p0]
	if { [string length $posPair] } {
	    set p1 [lindex $posPair 0]
	} else {	    
	    set p1 [maxPos]
	}
	select $p0 $p1
    } else {
	if { [isListAndCardView] } {	    
	    highLightItem $client
	} else {
	    goto [minPos]
	}	
    }
}

proc Card::separateWindowForCard { } {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    append winName $client ": " [lindex $D(entryList) $D(matchIndex)]
    writeToWindow $client [printCurrentCard $client] $winName
}


# --------------------------------------------------------------------
# Miscellaneous auxiliary procs
# --------------------------------------------------------------------


# These two procs are just to translate back and forth between
# client names and window names.  The rule is:
# 
# The window name is derived directly from the namespace of the
# client, except if it is a child of the ::Card:: namespace, in
# which case ::Card:: is not displayed.  Examples:
#     Namespace        Window Name
#     ::Foo            "* Foo Card Browser *"
#     ::Card::bla      "* bla Card Browser *"
# 
proc Card::windowOfClient { client } {
    regsub {^(::)?(Card::)?} $client "" client
    return "* $client Card Browser *"
}

proc Card::clientOfWindow { {win ""} } {
    if { ![string length $win] } {
	set win [win::Current]
    }
    if { [win::getMode $win] == "Card" &&
      [regexp -- {^\* (.+) Card Browser \*$} $win "" client] } {
	if { [namespace exists ::Card::$client] } {
	    return ::Card::$client
	} elseif { [namespace exists ::$client] } {
	    return ::$client
	}
    } 
    # else
    error "Not a card browser window"
}

proc Card::qualifyName { {name } } {
    regsub {^::} $name "" name
    if { [namespace exists ::Card::$name] } {
	return ::Card::$name
    } elseif { [namespace exists ::$name] } {
	return ::$name
    }
} 



# Convenience proc (to avoid that clients look directly in 
# _dynamicCardParameters).
proc Card::currentCard { client } {
    set client [qualifyName $client]
    upvar #0 ${client}::_dynamicCardParameters D
    return $D(currentCard)
}



# Return the length of the longest entry in list $L
proc longestEntry { L } {
    set maxl 0
    foreach entry $L {
	if { [string length $entry] > $maxl } {
	    set maxl [string length $entry]
	}
    }
    return $maxl
}


# --------------------------------------------------------------------
# Bells and whistles
# --------------------------------------------------------------------

proc regexpFromList { L } {
    foreach entry $L {
	lappend quotedL [quote::Regfind $entry]
    }
    if { [llength $quotedL] == 1 } {
	return [lindex $quotedL 0]
    }
    return "(?:[join $quotedL |])"
}


proc Card::hyperize { {client ""} } {
    if { ![string length $client] } {
	set client [clientOfWindow]
    }
    upvar #0 ::${client}::staticCardParameters S
 
    set urlPat {(http://|www\.)[-\w~/\.\?=#]+}
    set emailPat {[-\.\w]+@[-\.\w]+\.[a-zA-Z]+}
    set phonePat {(?:Phone|Mobile)[ \t]*:[ \t]*([^\r\n]+)}
    if { [info exists S(phoneNames)] } {
	set phonePat [::regexpFromList $S(phoneNames)]
	append phonePat {[ \t]*:[ \t]*([^\r\n]+)}
    }
    set hyperPat "(?:${urlPat}|${emailPat}|${phonePat})"

    catch { ::removeColorEscapes }
    set start [minPos]
    while {![catch {search -f 1 -r 1 $hyperPat $start} res]} {
	set link [eval ::getText $res]
	if { [regexp -- $urlPat $link] } {
	    ::text::hyper [lindex $res 0] [lindex $res 1] "::url::execute $link"
	} elseif { [regexp -- $emailPat $link] } {   
	    ::text::hyper [lindex $res 0] [lindex $res 1] "::Card::sendEmail $link"
	} elseif { [regexp -indices -- $phonePat $link dummy ph] } {
	    set start [::pos::math [lindex $res 0] + [lindex $ph 0]]
	    # Don't want trailing whitespace in the link:
	    regexp -indices {\s*$} $link ws
	    set end [::pos::math [lindex $res 0] + [lindex $ws 0]]
	    if { [::pos::compare $start < $end] } {
		set link [::getText $start $end]
		::text::hyper $start $end "::DTMF::dialNumber \"$link\""
	    }
	}	
	set start [lindex $res 1]
    }
    refresh
}

proc Card::sendEmail { email } {
    set client [clientOfWindow]
    upvar #0 ::${client}::_dynamicCardParameters D
    set record [${client}::getRecord $D(currentCard)]
    set To ""
    catch { append To "[keylget record First] " }
    catch { append To "[keylget record Last] " }    
    if { [string length $To] } {
	append To <$email>
    } else {
	set To $email
    } 
    ::mailNewMsg $To
}

set winZoom::colWidth(48) 27
set winZoom::rowHeight(48) 72

proc Card::displayHuge { str } {
    set str "\r  [string trim $str]  \r"
    set numCols [string length $str]
    set width [::winZoom::winWidth 48 $numCols]
    set height [winZoom::winHeight 48 3]
    set hoffset [expr (1024 - $width ) / 2]
    
    new -n Telephone -g $hoffset 440 $width $height \
      -shell 1 -fontsize 48 -info $str
}

proc Card::run { client } {
    if { [namespace eval $client {info procs run}] == "run" } {
	${client}::run
    } else {
	windowForFind $client
    }
}


