//=============================================================================
//
//	Copyright (C) 1996,1997,1998 by Paul S. McCarthy and Eric Sunshine.
//		Written by Paul S. McCarthy and Eric Sunshine.
//			    All Rights Reserved.
//
//	This notice may not be removed from this source code.
//
//	This object is included in the MiscKit by permission from the authors
//	and its use is governed by the MiscKit license, found in the file
//	"License.rtf" in the MiscKit distribution.  Please refer to that file
//	for a list of all applicable permissions and restrictions.
//
//=============================================================================
//-----------------------------------------------------------------------------
// MiscTableScrollEdit.M
//
//	Text cell editing support for MiscTableScroll.
//
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// $Id: MiscTableScrollEdit.M,v 1.8 98/03/29 23:56:01 sunshine Exp $
// $Log:	MiscTableScrollEdit.M,v $
// Revision 1.8  98/03/29  23:56:01  sunshine
// v138.1: Now uses NSColor's "system" colors for editing.
// Fixed bug: Was sending NSControlTextDidChangeNotification in
// -textDidBeginEditing: rather than NSControlTextDidBeginEditingNotification.
// 
// Revision 1.7  98/03/23  07:48:00  sunshine
// v134.1: Added -suspendEditing / -resumeEditing.  Eliminated delegate
// -tableScroll:edit:atRow:column: methods.
// 
// Revision 1.6  97/06/22  10:22:48  sunshine
// v127.1: Fixed bug: Wasn't grabbing a copy of cell when initiating editing;
// only referenced it.  In lazy mode, scrolling around would cause the
// contents of the referenced cell to change (even just the inital scroll-to-
// visible), thus causing a bus-load of bugs.  Now grabs copy of cell instead.
// Fixed bug: Keyboard cursor was lagging behind as TAB was used to change
// from cell to cell during editing.
// First-responder is now returned to TableScroll when cell editing ends via
// Return key, rather than reverting uselessly to the window.
//-----------------------------------------------------------------------------
#import "MiscTableScrollPrivate.h"
#import "MiscTableBorder.h"
#import "MiscTableView.h"
extern "Objective-C" {
#import	<AppKit/NSCell.h>
#import <AppKit/NSControl.h>
#import	<AppKit/NSText.h>
}
extern "C" {
#import	<stdlib.h>	// malloc(), free()
}

typedef MiscDelegateFlags DF;

@implementation MiscTableScroll(Edit)

- (BOOL)isEditing { return editInfo.editing; }

//-----------------------------------------------------------------------------
// cellFrameAtRow:column:
//-----------------------------------------------------------------------------
- (NSRect)cellFrameAtRow:(int)row column:(int)col
    {
    return [self convertRect:[tableView cellFrameAtRow:row column:col]
		fromView:tableView];
    }


//-----------------------------------------------------------------------------
// getRow:andCol:forPoint:
//-----------------------------------------------------------------------------
- (BOOL)getRow:(int*)row column:(int*)col forPoint:(NSPoint)point
    {
    return [tableView getRow:row column:col
		forPoint:[tableView convertPoint:point fromView:self]];
    }


//-----------------------------------------------------------------------------
// - controlNotify:with:
//-----------------------------------------------------------------------------
- (void)controlNotify:(NSString*)name with:(NSNotification*)n
    {
    [[NSNotificationCenter defaultCenter]
	postNotificationName:name object:self
	    userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
		[n object], @"NSFieldEditor", 0]];
    }


//-----------------------------------------------------------------------------
// forceEditorDirty:
//	Unlike the NEXTSTEP 3.x Text system which *always* sends 
//	-textWillEnd:, OPENSTEP 4.x only sends -textShouldEndEditing: if the 
//	text was indeed edited.  However, -suspendEditing & -resumeEditing 
//	work by ending the old edit session and beginning a new one (without 
//	the informing the delegate or the cell).  The problem is that by 
//	ending the old edit session, the text system loses the knowledge that 
//	the user had changed the text, and consequently -textShouldEndEditing: 
//	may not be called.  This "hack" works around the problem by forcing 
//	the text system to flag itself as dirty.  (FIXME: The text is always 
//	forced dirty even if the old session was not dirty.  This errs on the 
//	side of safety, though.) 
//-----------------------------------------------------------------------------
- (void)forceEditorDirty:(NSText*)editor
    {
    [editor replaceCharactersInRange:NSMakeRange(0,0) withString:@"x"];
    [editor setSelectedRange:NSMakeRange(0,1)];
    [editor delete:0];
    }


//-----------------------------------------------------------------------------
// setupFieldEditor:
//-----------------------------------------------------------------------------
- (void)setupFieldEditor:(NSText*)editor
    {
    [editor setDrawsBackground:YES];
    [editor setBackgroundColor:[NSColor textBackgroundColor]];
    [editor setTextColor:[NSColor textColor]];
    [editor setNeedsDisplay:YES];
    }


//-----------------------------------------------------------------------------
// resumeEditing
//-----------------------------------------------------------------------------
- (void)resumeEditing
    {
    if (editInfo.editing && --editInfo.suspended == 0)
	{
	int const row = editInfo.row;
	int const col = editInfo.col;

	NSText* const editor = [[self window] fieldEditor:YES forObject:self];
	editInfo.editor = [editor retain];
	id const cell = editInfo.cell;

	NSRect rect = [tableView cellInsideAtRow:row column:col];
	[tableView scrollRectToVisible:rect];

	[cell selectWithFrame:rect inView:tableView editor:editor
		delegate:self start:0 length:0];
	[self setupFieldEditor:editor];
	[self forceEditorDirty:editor];
	[editor selectAll:0];

	[self setClickedRow:row column:col];
	}
    }


//-----------------------------------------------------------------------------
// suspendEditing
//-----------------------------------------------------------------------------
- (void)suspendEditing
    {
    if (editInfo.editing && editInfo.suspended++ == 0)
	{
	id const cell = editInfo.cell;
	NSText* const editor = editInfo.editor;
	NSString* const s = [[[editor string] copy] autorelease];
	[editor setDelegate:0]; // Prevent -textDidEndEditing: invocation.
	[cell endEditing:editor];
	[cell setStringValue:s];
	[editor release];
	editInfo.editor = 0;
	}
    }


//-----------------------------------------------------------------------------
// cleanupEditing
//-----------------------------------------------------------------------------
- (void)cleanupEditing
    {
    [editInfo.cell release];
    editInfo.cell = 0;
    [editInfo.editor release];
    editInfo.editor = 0;
    editInfo.editing = NO;
    editInfo.suspended = 0;
    }


//-----------------------------------------------------------------------------
// finishEditing
//-----------------------------------------------------------------------------
- (BOOL)finishEditing
    {
    NSWindow* win = [self window];
    if ([self isEditing] && [win makeFirstResponder:win])
	[win endEditingFor:editInfo.editor];
    return ![self isEditing];
    }


//-----------------------------------------------------------------------------
// abortEditing
//-----------------------------------------------------------------------------
- (BOOL)abortEditing
    {
    BOOL const rc = editInfo.editing;
    if (rc)
	{
	editInfo.editing = NO;
	int const r = editInfo.row;
	int const c = editInfo.col;

	if (editInfo.editor != 0)
	    [editInfo.cell endEditing:editInfo.editor];
	[self cleanupEditing];

	id d;
	if ((d = [self responsibleDelegate:DF::DEL_ABORT_EDIT_AT]) != 0)
	    [d tableScroll:self abortEditAtRow:r column:c];

	if ((d = [self responsibleDelegate:DF::DEL_DID_EDIT_AT]) != 0)
	    [d tableScroll:self didEdit:NO atRow:r column:c];

	[self drawCellAtRow:r column:c];
	}
    return rc;
    }


//-----------------------------------------------------------------------------
// getNext:editRow:column:
//-----------------------------------------------------------------------------
- (BOOL)getNext:(BOOL)foreward
	editRow:(MiscCoord_P*)p_row
	column:(MiscCoord_P*)p_col
    {
    MiscCoord_P pr = *p_row;
    MiscCoord_P pc = *p_col;

    MiscTableBorder const* const br = rowInfo.border;
    MiscTableBorder const* const bc = colInfo.border;

    MiscCoord_V const v_row = br->physicalToVisual( pr );	// Start pos.
    MiscCoord_V const v_col = bc->physicalToVisual( pc );

    MiscCoord_V vr = v_row;
    MiscCoord_V vc = v_col;

    do  {
	if (foreward)
	    {
	    if (++vc >= num_cols)
		{
		vc = 0;
		if (++vr >= num_rows)
		    vr = 0;
		pr = br->visualToPhysical( vr );
		}
	    }
	else
	    {
	    if (--vc < 0)
		{
		vc = num_cols - 1;
		if (--vr < 0)
		    vr = num_rows - 1;
		pr = br->visualToPhysical( vr );
		}
	    }
	if (vc == v_col && vr == v_row)		// Wrapped to start pos.
	    return NO;
	pc = bc->visualToPhysical( vc );
	}
    while (![self canEdit:0 atRow:pr column:pc]);

    *p_row = pr;
    *p_col = pc;
    return YES;
    }


//-----------------------------------------------------------------------------
// getPreviousEditRow:column:
//-----------------------------------------------------------------------------
- (BOOL)getPreviousEditRow:(MiscCoord_P*)p_row column:(MiscCoord_P*)p_col
    {
    return [self getNext:NO editRow:p_row column:p_col];
    }


//-----------------------------------------------------------------------------
// getNextEditRow:column:
//-----------------------------------------------------------------------------
- (BOOL)getNextEditRow:(MiscCoord_P*)p_row column:(MiscCoord_P*)p_col
    {
    return [self getNext:YES editRow:p_row column:p_col];
    }


//-----------------------------------------------------------------------------
// textDidEndEditing:
//-----------------------------------------------------------------------------
- (void)textDidEndEditing:(NSNotification*)n
    {
    id d;
    NSText* theText = [n object];
    int const why = [[[n userInfo] objectForKey:@"NSTextMovement"] intValue];

    editInfo.editing = NO;
    NSString* s = [[[theText string] copy] autorelease];
    [editInfo.cell endEditing:editInfo.editor];
    int const r = editInfo.row;
    int const c = editInfo.col;
    NSString* t = [self stringValueAtRow:r column:c];
    BOOL changed = ![s isEqualToString:t];
    if (changed)
	{
	if ((d = [self responsibleDelegate:DF::DEL_SET_STRINGVALUE_AT]) != 0)
	    changed = [d tableScroll:self setStringValue:s atRow:r column:c];
	else
	    [[self cellAtRow:r column:c] setStringValue:s];
	}
    [self cleanupEditing];

    if ((d = [self responsibleDelegate:DF::DEL_DID_EDIT_AT]) != 0)
	[d tableScroll:self didEdit:changed atRow:r column:c];

    [self drawCellAtRow:r column:c];

    if (changed)
	{
	if ([self autoSortRows])
	    [self sortRow:r];
	if ([self autoSortColumns])
	    [self sortColumn:c];
	}

    if ((d = [self responsibleDelegate:DF::DEL_TEXT_DID_END]) != 0)
	[self controlNotify:NSControlTextDidEndEditingNotification with:n];
    else
	{
	MiscCoord_P row = (MiscCoord_P)r;
	MiscCoord_P col = (MiscCoord_P)c;

	switch (why)
	    {
	    case NSReturnTextMovement:
		[[self window] makeFirstResponder:tableView];
		[self sendAction];
		break;
    
	    case NSTabTextMovement:
		if ([self getNext:YES editRow:&row column:&col])
		    [self editCellAtRow:row column:col];
		else
		    [[self window] selectNextKeyView:self];
		break;
    
	    case NSBacktabTextMovement:
		if ([self getNext:NO editRow:&row column:&col])
		    [self editCellAtRow:row column:col];
		else
		    [[self window] selectPreviousKeyView:self];
		break;
    
	    // FIXME: CTRL-TAB => nextText, CTRL-SHIFT-TAB => previousText
	    default:
		break;
	    }
	}
    }


//-----------------------------------------------------------------------------
// textDidBeginEditing:
//-----------------------------------------------------------------------------
- (void)textDidBeginEditing:(NSNotification*)n
    {
    [self controlNotify:NSControlTextDidBeginEditingNotification with:n];
    }


//-----------------------------------------------------------------------------
// textDidChange:
//-----------------------------------------------------------------------------
- (void)textDidChange:(NSNotification*)n
    {
    [self controlNotify:NSControlTextDidChangeNotification with:n];
    }


//-----------------------------------------------------------------------------
// textShouldBeginEditing:
//-----------------------------------------------------------------------------
- (BOOL)textShouldBeginEditing:(NSText*)text
    {
    id d = [self responsibleDelegate:DF::DEL_TEXT_WILL_CHANGE];
    if (d != 0)
	return [d control:(NSControl*)self textShouldBeginEditing:text];
    return YES;
    }


//-----------------------------------------------------------------------------
// textShouldEndEditing:
//-----------------------------------------------------------------------------
- (BOOL)textShouldEndEditing:(NSText*)text
    {
    id d = [self responsibleDelegate:DF::DEL_TEXT_WILL_END];
    if (d != 0)
	return [d control:(NSControl*)self textShouldEndEditing:text];
    return YES;
    }


//-----------------------------------------------------------------------------
// edit:atRow:column:
//	NOTE *1*: Must set color *after* cell initiates editing, else cell will
//	install its own colors making text selection invisible.
//-----------------------------------------------------------------------------
- (void)edit:(NSEvent*)ev atRow:(MiscCoord_P)row column:(MiscCoord_P)col
    {
    id d;
    if ((d = [self responsibleDelegate:DF::DEL_WILL_EDIT_AT]) != 0)
	[d tableScroll:self willEditAtRow:row column:col];

    id const cell = [[self cellAtRow:row column:col] copyWithZone:[self zone]];
    id const editor = [[self window] fieldEditor:YES forObject:self];

    editInfo.editing = YES;
    editInfo.row = row;
    editInfo.col = col;
    editInfo.editor = [editor retain];
    editInfo.cell = [cell retain];

    NSRect const rect = [tableView cellInsideAtRow:row column:col];
    [tableView scrollRectToVisible:rect];
    if (ev != 0)
	[cell editWithFrame:rect inView:tableView editor:editor
		delegate:self event:ev];
    else
	[cell selectWithFrame:rect inView:tableView editor:editor
	    delegate:self start:0 length:[[cell stringValue] length]];

    [self setupFieldEditor:editor];		// NOTE *1*
    }


//-----------------------------------------------------------------------------
// canEdit:atRow:column:
//-----------------------------------------------------------------------------
- (BOOL)canEdit:(NSEvent*)ev atRow:(MiscCoord_P)row column:(MiscCoord_P)col
    {
    id d = [self responsibleDelegate:DF::DEL_CAN_EDIT_AT];
    if (d != 0)
	return [d tableScroll:self canEdit:ev atRow:row column:col];

    id const cell = [self cellAtRow:row column:col];
    if ([cell respondsToSelector:@selector(tableScroll:canEdit:atRow:column:)])
	return [cell tableScroll:self canEdit:ev atRow:row column:col];

    return (ev == 0 || [ev clickCount] == 2) && 
		[cell respondsToSelector:@selector(isEditable)] &&
		[cell respondsToSelector:@selector(isEnabled)] &&
		[cell isEditable] && [cell isEnabled];
    }


//-----------------------------------------------------------------------------
// editIfAble:atRow:column:
//-----------------------------------------------------------------------------
- (BOOL)editIfAble:(NSEvent*)ev atRow:(MiscCoord_P)row column:(MiscCoord_P)col
    {
    if ([self canEdit:ev atRow:row column:col])
	{
	[self edit:ev atRow:row column:col];
	return YES;
	}
    return NO;
    }


//-----------------------------------------------------------------------------
// editCellAtRow:column:
//-----------------------------------------------------------------------------
- (void)editCellAtRow:(MiscCoord_P)row column:(MiscCoord_P)col
    {
    [self clearSelection];
    if ([self trackingBy] == MISC_ROW_BORDER)
	{
	[self selectRow:row];
	[self setCursorRow:row];
	}
    else
	{
	[self selectColumn:col];
	[self setCursorColumn:col];
	}
    [self setClickedRow:row column:col];
    [self edit:0 atRow:row column:col];
    }

@end
