#==============================================================================
# Contains the implementation of interactive cell editing in tablelist widgets.
#
# Structure of the module:
#   - Private procedure implementing the tablelist editcell subcommand
#   - Private procedures used in bindings related to interactive cell editing
#   - Private helper procedures related to interactive cell editing
#
# Copyright (c) 2003  Csaba Nemethi (E-mail: csaba.nemethi@t-online.de)
#==============================================================================

#
# Private procedure implementing the tablelist editcell subcommand
# ================================================================
#

#------------------------------------------------------------------------------
# tablelist::editcellSubCmd
#
# This procedure is invoked to process the tablelist editcell subcommand.
# charPos stands for the character position component of the index in the body
# text widget of the character underneath the mouse cursor if this command was
# invoked by clicking mouse button 1 in the body of the tablelist widget.
#------------------------------------------------------------------------------
proc tablelist::editcellSubCmd {win row col restore {charPos -1}} {
    upvar ::tablelist::ns${win}::data data

    if {$data(isDisabled) || $data($col-hide) ||
	![isCellEditable $win $row $col]} {
	return ""
    }
    if {$data(editRow) == $row && $data(editCol) == $col} {
	return ""
    }
    if {$data(editRow) >= 0 && ![finishEditing $win]} {
	return ""
    }

    #
    # Replace the cell contents between the two tabs with an embedded frame
    #
    set w $data(body)
    set item [lindex $data(itemList) $row]
    set key [lindex $item end]
    findCellTabs $win [expr {$row + 1}].0 $col tabIdx1 tabIdx2
    $w delete $tabIdx1+1c $tabIdx2
    set pixels [lindex $data(colList) [expr {2*$col}]]
    if {$pixels == 0} {				;# convention: dynamic width
	set pixels $data($col-width)
    }
    set f $data(bodyFr)
    frame $f -borderwidth 0 -colormap . -container 0 -height 0 \
	     -highlightthickness 0 -relief flat -takefocus 0 \
	     -width [expr {$pixels + $data($col-delta) + 6}]
    $w window create $tabIdx1+1c -padx -3 -pady -2 -stretch 1 -window $f

    #
    # Create an entry widget as a child of the above frame
    #
    set e $data(bodyFrEnt)
    set alignment [lindex $data(colList) [expr {2*$col + 1}]]
    entry $e -borderwidth 2 -font [cellFont $win $key $col] \
	     -highlightthickness 0 -justify $alignment \
	     -relief ridge -takefocus 0
    place $e -relheight 1.0 -relwidth 1.0

    #
    # Restore or initialize some of the entry's data
    #
    if {$restore} {
	restoreEntryData $win
    } else {
	set data(canceled) 0
	set text [lindex $item $col]
	if {[lindex $data(fmtCmdFlagList) $col]} {
	    set text [uplevel #0 $data($col-formatcommand) [list $text]]
	}
	$e insert 0 $text
	if {[string compare $data(-editstartcommand) ""] != 0} {
	    set text [uplevel #0 $data(-editstartcommand) \
		      [list $win $row $col $text]]
	}
	if {$data(canceled)} {
	    destroy $f
	    doCellConfig $row $col $win -text [lindex $item $col]
	    return ""
	}

	$e delete 0 end
	$e insert 0 $text
	if {$charPos >= 0} {
	    #
	    # Determine the position of the insertion cursor
	    #
	    set image [cellcgetSubCmd $win $row $col -image]
	    if {[string compare $alignment right] == 0} {
		scan $tabIdx2 %d.%d line tabCharIdx2
		set entryIdx [expr {[$e index end] - $tabCharIdx2 + $charPos}]
		if {[string compare $image ""] != 0} {
		    incr entryIdx 2
		}
	    } else {
		scan $tabIdx1 %d.%d line tabCharIdx1
		set entryIdx [expr {$charPos - $tabCharIdx1 - 1}]
		if {[string compare $image ""] != 0} {
		    incr entryIdx -2
		}
	    }
	    $e icursor $entryIdx
	} else {
	    $e icursor end
	    $e selection range 0 end
	}

	seecellSubCmd $win $row $col
	focus $e
	set data(rejected) 0
	set data(origEditText) $text
    }

    #
    # Define some bindings for the entry
    #
    bind $e <Control-i>        [list tablelist::entryInsert     $e \t]
    bind $e <Control-j>        [list tablelist::entryInsert     $e \n]
    bind $e <Control-Return>   [list tablelist::entryInsert     $e \n]
    bind $e <Control-KP_Enter> [list tablelist::entryInsert     $e \n]
    bind $e <Escape>	       [list tablelist::abortEditing    $win]
    bind $e <Return>	       [list tablelist::finishEditing   $win]
    bind $e <KP_Enter>	       [list tablelist::finishEditing   $win]
    bind $e <Tab>	       [list tablelist::moveToNext      $win $row $col]
    bind $e <Shift-Tab>	       [list tablelist::moveToPrev      $win $row $col]
    bind $e <<PrevWindow>>     [list tablelist::moveToPrev      $win $row $col]
    bind $e <Alt-Left>         [list tablelist::moveLeft        $win]
    bind $e <Meta-Left>        [list tablelist::moveLeft        $win]
    bind $e <Alt-Right>        [list tablelist::moveRight       $win]
    bind $e <Meta-Right>       [list tablelist::moveRight       $win]
    bind $e <Up>	       [list tablelist::moveOneLineUp   $win $row]
    bind $e <Down>	       [list tablelist::moveOneLineDown $win $row]
    bind $e <Prior>            [list tablelist::moveOnePageUp   $win]
    bind $e <Next>             [list tablelist::moveOnePageDown $win]
    bind $e <Control-Home>     [list tablelist::moveToNext      $win 0 -1]
    bind $e <Control-End>      [list tablelist::moveToPrev      $win 0  0]
    bind $e <Control-Tab> \
	    [list mwutil::generateEvent $e Tablelist <Tab>]
    bind $e <Meta-Tab> \
	    [list mwutil::generateEvent $e Tablelist <Tab>]
    bind $e <Control-Shift-Tab> \
	    [list mwutil::generateEvent $e Tablelist <Shift-Tab>]
    bind $e <Meta-Shift-Tab> \
	    [list mwutil::generateEvent $e Tablelist <Shift-Tab>]
    bind $e <Destroy> \
	    [list array set tablelist::ns${win}::data {editRow -1 editCol -1}]

    #
    # Define some emacs-like key bindings for the entry
    #
    bind $e <Meta-b> {
	if {!$tk_strictMotif} {
	    eval [bind %W <Alt-Left>]
	}
    }
    bind $e <Meta-f> {
	if {!$tk_strictMotif} {
	    eval [bind %W <Alt-Right>]
	}
    }
    bind $e <Control-p> {
	if {!$tk_strictMotif} {
	    eval [bind %W <Up>]
	}
    }
    bind $e <Control-n> {
	if {!$tk_strictMotif} {
	    eval [bind %W <Down>]
	}
    }
    bind $e <Meta-less> {
	if {!$tk_strictMotif} {
	    eval [bind %W <Control-Home>]
	}
    }
    bind $e <Meta-greater> {
	if {!$tk_strictMotif} {
	    eval [bind %W <Control-End>]
	}
    }

    #
    # Propagate the mousewheel events to the tablelist's body
    #
    bind $e <MouseWheel> [list tablelist::genMouseWheelEvent $w %D]
    foreach event {<Button-4> <Button-5>} {
	bind $e $event [list event generate $w $event]
    }

    set data(editKey) $key
    set data(editRow) $row
    set data(editCol) $col
    return ""
}

#
# Private procedures used in bindings related to interactive cell editing
# =======================================================================
#

#------------------------------------------------------------------------------
# tablelist::entryInsert
#
# Inserts the string str into the entry widget w at the point of the
# insertion cursor.
#------------------------------------------------------------------------------
proc tablelist::entryInsert {w str} {
    if {[string compare [info procs ::tkEntryInsert] ::tkEntryInsert] == 0} {
	tkEntryInsert $w $str
    } else {
	tk::EntryInsert $w $str
    }
}

#------------------------------------------------------------------------------
# tablelist::abortEditing
#
# Aborts the interactive cell editing and restores the cell's contents after
# destroying the embedded entry widget.
#------------------------------------------------------------------------------
proc tablelist::abortEditing win {
    upvar ::tablelist::ns${win}::data data

    set row $data(editRow)
    set col $data(editCol)

    set item [lindex $data(itemList) $row]
    destroy $data(bodyFr)
    doCellConfig $row $col $win -text [lindex $item $col]
    focus $data(body)
}

#------------------------------------------------------------------------------
# tablelist::finishEditing
#
# Invokes the command specified by the -editendcommand option if needed, and
# updates the element just edited after destroying the embedded entry widget if
# the latter's contents was not rejected.  Returns 1 on normal termination and
# 0 otherwise.
#------------------------------------------------------------------------------
proc tablelist::finishEditing win {
    upvar ::tablelist::ns${win}::data data

    set row $data(editRow)
    set col $data(editCol)

    set text [$data(bodyFrEnt) get]
    if {[string compare $text $data(origEditText)] == 0} {
	set item [lindex $data(itemList) $row]
	set text [lindex $item $col]
    } elseif {[string compare $data(-editendcommand) ""] != 0} {
	set text [uplevel #0 $data(-editendcommand) [list $win $row $col $text]]
    }

    if {$data(rejected)} {
	seecellSubCmd $win $row $col
	focus $data(bodyFrEnt)
	set data(rejected) 0
	return 0
    } else {
	destroy $data(bodyFr)
	doCellConfig $row $col $win -text $text
	focus $data(body)
	return 1
    }
}

#------------------------------------------------------------------------------
# tablelist::moveToNext
#
# Moves the embedded entry widget into the next editable cell different from
# the one indicated by the given row and column, if there is such a cell.
#------------------------------------------------------------------------------
proc tablelist::moveToNext {win row col} {
    upvar ::tablelist::ns${win}::data data

    set oldRow $row
    set oldCol $col

    while 1 {
	incr col
	if {$col > $data(lastCol)} {
	    incr row
	    if {$row > $data(lastRow)} {
		set row 0
	    }
	    set col 0
	}

	if {$row == $oldRow && $col == $oldCol} {
	    return -code break ""
	} elseif {!$data($col-hide) && [isCellEditable $win $row $col]} {
	    editcellSubCmd $win $row $col 0
	    return -code break ""
	}
    }
}

#------------------------------------------------------------------------------
# tablelist::moveToPrev
#
# Moves the embedded entry widget into the previous editable cell different
# from the one indicated by the given row and column, if there is such a cell.
#------------------------------------------------------------------------------
proc tablelist::moveToPrev {win row col} {
    upvar ::tablelist::ns${win}::data data

    set oldRow $row
    set oldCol $col

    while 1 {
	incr col -1
	if {$col < 0} {
	    incr row -1
	    if {$row < 0} {
		set row $data(lastRow)
	    }
	    set col $data(lastCol)
	}

	if {$row == $oldRow && $col == $oldCol} {
	    return -code break ""
	} elseif {!$data($col-hide) && [isCellEditable $win $row $col]} {
	    editcellSubCmd $win $row $col 0
	    return -code break ""
	}
    }
}

#------------------------------------------------------------------------------
# tablelist::moveLeft
#
# Moves the embedded entry widget into the previous editable cell of the
# current row if the cell being edited is not the first editable one within
# that row.  Otherwise sets the insertion cursor to the beginning of the entry
# and clears the selection in it.
#------------------------------------------------------------------------------
proc tablelist::moveLeft win {
    upvar ::tablelist::ns${win}::data data

    set row $data(editRow)
    set col $data(editCol)

    while 1 {
	incr col -1
	if {$col < 0} {
	    #
	    # On Windows the "event generate" command does not behave
	    # as expected if a Tk version older than 8.2.2 is used.
	    #
	    if {[string compare $::tk_patchLevel 8.2.2] < 0} {
		tkEntrySetCursor $data(bodyFrEnt) 0
	    } else {
		event generate $data(bodyFrEnt) <Home>
	    }
	    return -code break ""
	} elseif {!$data($col-hide) && [isCellEditable $win $row $col]} {
	    editcellSubCmd $win $row $col 0
	    return -code break ""
	}
    }
}

#------------------------------------------------------------------------------
# tablelist::moveRight
#
# Moves the embedded entry widget into the next editable cell of the current
# row if the cell being edited is not the last editable one within that row.
# Otherwise sets the insertion cursor to the end of the entry and clears the
# selection in it.
#------------------------------------------------------------------------------
proc tablelist::moveRight win  {
    upvar ::tablelist::ns${win}::data data

    set row $data(editRow)
    set col $data(editCol)

    while 1 {
	incr col
	if {$col > $data(lastCol)} {
	    #
	    # On Windows the "event generate" command does not behave
	    # as expected if a Tk version older than 8.2.2 is used.
	    #
	    if {[string compare $::tk_patchLevel 8.2.2] < 0} {
		tkEntrySetCursor $data(bodyFrEnt) end
	    } else {
		event generate $data(bodyFrEnt) <End>
	    }
	    return -code break ""
	} elseif {!$data($col-hide) && [isCellEditable $win $row $col]} {
	    editcellSubCmd $win $row $col 0
	    return -code break ""
	}
    }
}

#------------------------------------------------------------------------------
# tablelist::moveOneLineUp
#
# Moves the embedded entry widget into the last editable cell that is situated
# in the current column and has a row index less than the given one, if there
# is such a cell.
#------------------------------------------------------------------------------
proc tablelist::moveOneLineUp {win row} {
    upvar ::tablelist::ns${win}::data data

    set col $data(editCol)

    while 1 {
	incr row -1
	if {$row < 0} {
	    return 0
	} elseif {[isCellEditable $win $row $col]} {
	    editcellSubCmd $win $row $col 0
	    return 1
	}
    }
}

#------------------------------------------------------------------------------
# tablelist::moveOneLineDown
#
# Moves the embedded entry widget into the first editable cell that is situated
# in the current column and has a row index greater than the given one, if
# there is such a cell.
#------------------------------------------------------------------------------
proc tablelist::moveOneLineDown {win row} {
    upvar ::tablelist::ns${win}::data data

    set col $data(editCol)

    while 1 {
	incr row
	if {$row > $data(lastRow)} {
	    return 0
	} elseif {[isCellEditable $win $row $col]} {
	    editcellSubCmd $win $row $col 0
	    return 1
	}
    }
}

#------------------------------------------------------------------------------
# tablelist::moveOnePageUp
#
# Moves the embedded entry widget up by one page within the current column if
# the cell being edited is not the first editable one within that column.
#------------------------------------------------------------------------------
proc tablelist::moveOnePageUp win {
    upvar ::tablelist::ns${win}::data data

    #
    # Check whether there is any editable cell
    # above the current one, in the same column
    #
    set row $data(editRow)
    set col $data(editCol)
    while 1 {
	incr row -1
	if {$row < 0} {
	    return ""
	} elseif {[isCellEditable $win $row $col]} {
	    break
	}
    }

    #
    # Scroll up the view by one page and get the corresponding row index
    #
    set row $data(editRow)
    seeSubCmd $win $row
    set bbox [bboxSubCmd $win $row]
    yviewSubCmd $win {scroll -1 pages}
    set newRow [rowIndex $win @0,[lindex $bbox 1] 0]

    if {$newRow < $row} {
	if {![moveOneLineUp $win [expr {$newRow + 1}]]} {
	    moveOneLineDown $win $newRow
	}
    } else {
	moveOneLineDown $win -1
    }
}

#------------------------------------------------------------------------------
# tablelist::moveOnePageDown
#
# Moves the embedded entry widget down by one page within the current column if
# the cell being edited is not the last editable one within that column.
#------------------------------------------------------------------------------
proc tablelist::moveOnePageDown win {
    upvar ::tablelist::ns${win}::data data

    #
    # Check whether there is any editable cell
    # below the current one, in the same column
    #
    set row $data(editRow)
    set col $data(editCol)
    while 1 {
	incr row
	if {$row > $data(lastRow)} {
	    return ""
	} elseif {[isCellEditable $win $row $col]} {
	    break
	}
    }

    #
    # Scroll down the view by one page and get the corresponding row index
    #
    set row $data(editRow)
    seeSubCmd $win $row
    set bbox [bboxSubCmd $win $row]
    yviewSubCmd $win {scroll 1 pages}
    set newRow [rowIndex $win @0,[lindex $bbox 1] 0]

    if {$newRow > $row} {
	if {![moveOneLineDown $win [expr {$newRow - 1}]]} {
	    moveOneLineUp $win $newRow
	}
    } else {
	moveOneLineUp $win $data(itemCount)
    }
}

#------------------------------------------------------------------------------
# tablelist::genMouseWheelEvent
#
# Generates a <MouseWheel> event with the given delta on the widget w.
#------------------------------------------------------------------------------
proc tablelist::genMouseWheelEvent {w delta} {
    set focus [focus -displayof $w]
    focus $w
    event generate $w <MouseWheel> -delta $delta
    focus $focus
}

#
# Private helper procedures related to interactive cell editing
# =============================================================
#

#------------------------------------------------------------------------------
# tablelist::saveEntryData
#
# Saves some data of the entry widget associated with the tablelist win.
#------------------------------------------------------------------------------
proc tablelist::saveEntryData win {
    upvar ::tablelist::ns${win}::data data

    #
    # Configuration options
    #
    set w $data(bodyFrEnt)
    foreach configSet [$w configure] {
	if {[llength $configSet] != 2} {
	    set opt [lindex $configSet 0]
	    set data(entry$opt) [lindex $configSet 4]
	}
    }

    #
    # Widget callbacks
    #
    if {[info exists ::wcb::version]} {
	foreach when {before after} {
	    foreach opt {insert delete motion} {
		set data(entryCb-$when-$opt) [::wcb::callback $w $when $opt]
	    }
	}
    }

    #
    # Other data
    #
    set data(entryText) [$w get]
    set data(entryPos)  [$w index insert]
    if {[set data(entryHadSel) [$w selection present]]} {
	set data(entrySelFrom) [$w index sel.first]
	set data(entrySelTo)   [$w index sel.last]
    }
    set data(entryHadFocus) \
	[expr {[string compare [focus -lastfor $w] $w] == 0}]
}

#------------------------------------------------------------------------------
# tablelist::restoreEntryData
#
# Saves some data of the entry widget associated with the tablelist win.
#------------------------------------------------------------------------------
proc tablelist::restoreEntryData win {
    upvar ::tablelist::ns${win}::data data

    #
    # Configuration options
    #
    set w $data(bodyFrEnt)
    foreach name [array names data entry-*] {
	set opt [string range $name 5 end]
	$w configure $opt $data($name)
    }

    #
    # Widget callbacks
    #
    if {[info exists ::wcb::version]} {
	foreach when {before after} {
	    foreach opt {insert delete motion} {
		eval [list ::wcb::callback $w $when $opt] \
		     $data(entryCb-$when-$opt)
	    }
	}
    }

    #
    # Other data
    #
    $w insert 0 $data(entryText)
    $w icursor $data(entryPos)
    if {$data(entryHadSel)} {
	$w selection range $data(entrySelFrom) $data(entrySelTo)
    }
    if {$data(entryHadFocus)} {
	focus $w
    }
}
