/* MiscTreeBrowserMatrix
 *
 */

// IMPORTS

#import <math.h>
#import <dpsclient/wraps.h>
#import <appkit/publicWraps.h>	// NXBeep()
#import <appkit/nextstd.h>		// NXLogError()
#import <appkit/graphics.h>
#import <appkit/Application.h>
#import <appkit/TextField.h>
#import <appkit/TextFieldCell.h>
#import <appkit/Text.h>

#import <misckit/MiscTreeBrowser.h>
#import <misckit/MiscTreeBrowserCell.h>
#import <misckit/MiscTreeBrowserMatrix.h>
#import <misckit/MiscTreeBrowserProtocols.h>
#import <misckit/TB_images.h>

// DEFINES

#define RETURN	13
#define ESCAPE	27
#define DELETE	127

#define LEFTARROW	172
#define RIGHTARROW	174
#define UPARROW		173
#define DOWNARROW	175

#define NODE_INDENT_WIDTH	10.0
#define NODE_XMARGIN		8.0

#define MTB_DRAG_PBOARD_STR	"MTBDragPboard"

// MACROS

#define MiscEqualPoint(P1,P2) (((P1)->x == (P2)->x) && ((P1)->y == (P2)->y))

// CLASS LOCAL DATA

static NXAtom MTBDragPboardType = NULL;
static NXPoint mouseOffset = {0.0, 0.0};

static Pasteboard *_dragPBoard = nil;
static int _dragEventMask;
static NXEvent _dragMousedownEvent;

static NXPoint _dragImagePoint, _oldDragImagePoint;

// IMPLEMENTATION

@implementation MiscTreeBrowserMatrix

+ initialize
{
	if (self == [MiscTreeBrowserMatrix class])
	{
		MTBDragPboardType = NXUniqueString(MTB_DRAG_PBOARD_STR);
	}

	return self;
}

// PRIVATE METHODS

- (int)_indexOfNode:aNode
/*"
Returns index of cell containing aNode, or -1 if none does.
"*/
{
	unsigned int row, cellCount = [self cellCount];

	for (row = 0; row < cellCount; row++)
	{
		id aCell = [self cellAt:row :0];

		if ([aCell node] == aNode)
		{
			return row;
		}
	}

	return -1;					// no cells contain aNode
}

- _cellForNode:aNode
/*"
Returns the cell in the receiver which contains aNode, or nil if none do.
"*/
{
	int idx = [self _indexOfNode:aNode];

	return (idx >= 0) ? [self cellAt:idx :0] : nil;
}

- _placementImage
/*"
Returns the (static) placement image.  See #{-drawSelf::}.
"*/
{
	static NXImage *dImage = nil;
	if (!dImage)
	{
		dImage = _MISC_TBDrag_tiff_fn();
	}

	return dImage;
}

- _treeBrowser
/*"
Returns our controlling MiscTreeBrowser 
"*/
{
	return [superview superview];
}

- _editField
/*"
Return the textfield used for editing cells.
"*/
{
	if (!editField)
	{
		NXRect aFrame = { 0.0, 0.0, 0.0, 0.0 };

		aFrame.size = cellSize;
		editField = [[TextField alloc] initFrame:&aFrame];
		if (font)
		{
			[editField setFont:font];
		}
		[editField setEditable:YES];
		[editField setBezeled:YES];
		[editField setTextDelegate:self];
	}

	return editField;
}

// cell dragging private methods

- _autoscroll:(BOOL)performScroll
/*"
If performScroll is YES and the current mouse location is outside the frame,
scroll the matrix in the direction of the mouse. Used delayed perform to call #{-_autoscroll:YES} again in 100 milliseconds. This non-modal scrolling loop is terminated by calling #{-_autoscroll:NO} (in #{-draggingEntered:) and ....
Returns self.
"*/
{
	if (performScroll)
	{
		NXEvent *theEvent = [NXApp currentEvent];
		NXPoint mouse = theEvent->location;
		NXRect visibleRect, frameRect;
		BOOL isVis;

		NXPing();
		if (!(theEvent->type == NX_MOUSEDRAGGED))
		{
			return self;
		}
		isVis= [self getVisibleRect:&visibleRect];
		[self convertPoint:&mouse fromView:nil];
		[self getFrame:&frameRect];
		[self getCellSize:&cellSize];

		if ((mouse.x >= visibleRect.origin.x) &&
			(mouse.x <= (visibleRect.origin.x + visibleRect.size.width)))
		{
			const NXCoord scrollScale = 2.0;
			NXCoord delta = 0.0;

			if (mouse.y > (visibleRect.origin.y + visibleRect.size.height))
			{					// scroll up
				delta = mouse.y - (visibleRect.origin.y +
								   visibleRect.size.height);
			}
			else if (mouse.y < visibleRect.origin.y)
			{					// scroll down
				delta =  - (visibleRect.origin.y - mouse.y);
			}
			if (delta != 0.0)
			{
				visibleRect.origin.y += delta * scrollScale;
				[window disableFlushWindow];
				[self scrollRectToVisible:&visibleRect];
				[window reenableFlushWindow];
				[window flushWindow];
				NXPing();
			}
		}
								// set up another scroll event in 100 ms
		[self perform:@selector(_autoscroll:)
			  with:(id)YES
			  afterDelay:50
			  cancelPrevious:YES];
	}

	return self;
}

- _dragCell
/*"
Return cell dragged from, or nil of not dragging.
"*/
{
	return (dragCellRow >= 0) ? [self cellAt:dragCellRow :0] : nil;
}

- (BOOL)_isDraggingCell
/*"
Return YES if MiscTreeBrowserMatrix is dragging.
"*/
{
	return (dragCellRow >= 0) ? YES : NO;
}

- _clearDragging
/*"
Mark MiscTreeBrowserMatrix as no longer dragging.  Switches off autoscrolling,
for if the user moused-up outside of a window
"*/
{
	lastPoint.x = lastPoint.y = -1.0;
	dragCellRow = -1;
	toCellRow = -1;
	toIndentLevel = -1;
	[self perform:@selector(_autoscroll:)
		  with:(id)NO
		  afterDelay:1
		  cancelPrevious:YES];

	return self;
}

- _setDragging:(int)rowNum
/*"
Mark MiscTreeBrowserMatrix as dragging; save rowNum of cell dragged from.
"*/
{
	dragCellRow = rowNum;
	
	return self;
}

- _toggleCellOpen
/*"
Toggle the selected cell open or closed.
"*/
{
	id node = [[self selectedCell] node];

	if ([[self _treeBrowser] nodeIsOpen:node])
	{
		[[self _treeBrowser] closeNode:node];
	}
	else
	{
		[[self _treeBrowser] openNode:node];
	}

	return self;
}

- _init
/*"
Initialise instance variables
"*/
{
	textCell = [[TextFieldCell alloc] init];
	nodeIndentWidth = NODE_INDENT_WIDTH;
	nodeXMargin = NODE_XMARGIN;
	[self _clearDragging];
	[self registerForDraggedTypes:&MTBDragPboardType count:1];

	return self;
}

// PUBLIC METHODS

- initFrame:(const NXRect *)frRect 
	   mode:(int)aMode
  cellClass:classId
	numRows:(int)r
	numCols:(int)c
/*"
Initialise a MiscTreeBrowserMatrix instance.  aMode should be MX_TRACKMODE to allow node dragging.
"*/
{
	[super initFrame:frRect
		   mode:aMode			// NX_TRACKMODE for dragging...
		   cellClass:classId
		   numRows:r numCols:c];
	[self _init];

	return self;
}

- initFrame:(const NXRect *)frameRect 
	   mode:(int)aMode
  prototype:aCell
	numRows:(int)r
	numCols:(int)c
/*"
Initialise a MiscTreeBrowserMatrix instance.  aMode should be MX_TRACKMODE to allow node dragging.
"*/
{
	[super initFrame:frameRect mode:aMode prototype:aCell numRows:r numCols:c];
	[self _init];

	return self;
}

- sharedTextCell
/*"
Returns the text cell to be used for writing the text in a cell.  Used by MiscTreeBrowserCell instances.
"*/
{
	return textCell;
}

- setFont:aFont
/*"
Set the matrix font to aFont.
"*/
{
	[[self _editField] setFont:aFont];
	[[self sharedTextCell] setFont:aFont];
	[super setFont:aFont];

	return self;
}

- sendDoubleAction
/*"
If the user double-clicks on a cell's title, edit the title, otherwise invoke %{-sendDoubleAction} on the cell.
"*/
{
	NXEvent *theEvent = [NXApp currentEvent];
	int row, col;
	NXRect cellFrame, titleFrame;
	NXPoint mousePt = theEvent->location;
	id scell = [self selectedCell];

	[self convertPoint:&mousePt fromView:nil];
	[self getRow:&row andCol:&col ofCell:scell];
	[self getCellFrame:&cellFrame at:row :col];

								// get frames for checking pointer location
	titleFrame = cellFrame;
	[scell getTitleRect:&titleFrame inView:self];

	if ([self mouse:&mousePt inRect:&titleFrame])
	{							// double-click on text; edit it
		[self editCell:scell];
	}
	else
	{							// perform double-click action
		[scell sendDoubleAction];
	}

	return self;
}

- (NXColor)cellColor
{
	return [[self _treeBrowser] backgroundColor];
}

- (NXCoord)nodeIndentWidth
{
	return nodeIndentWidth;
}

- (NXCoord)nodeXMargin
{
	return nodeXMargin;
}

- (NXCoord)nodeXOffsetForIndent:(int)indent
{
	return ((float)indent * [self nodeIndentWidth]) + [self nodeXMargin];
}

- (int)indentLevelFromXOffset:(NXCoord)xOffset
{
	return (int)((xOffset - [self nodeXMargin]) / [self nodeIndentWidth]);
}


// CELL SELECTION

- selectCell:aCell
/*"
Select aCell.
"*/
{
	int row, col;

	if ([self getRow:&row andCol:&col ofCell:aCell])
	{
		return [self selectCellAt:row :col];
	}
	return nil;
}

- selectCellAt:(int)row :(int)col
/*"
Select cell at row,col - deselect all others.  Returns self.
"*/
{
	id aCell = [self cellAt:row :col];

	if (aCell)
	{
		[super selectCellAt:row :col];
		[self lockFocus];
		[self highlightCellAt:row :0 lit:YES];
		[self unlockFocus];
		return self;
	}

	return nil;
}

// DRAWING

- drawSelf:(const NXRect *)rects :(int)count
/*"
Draws the matrix, and if the user is dragging a node over the matrix, draws
the placement icon on top of it.
"*/
{			   
    [super drawSelf:rects :count];

	if ([self _isDraggingCell] && (toCellRow > 0))
	{							// draw drag image
		NXImage *placementImage = [self _placementImage];
		
		[placementImage composite:NX_SOVER toPoint:&_dragImagePoint];
	}

    return self;
}

// DRAGGING

// _dragToPoint:moveNode macros

#define DR_SETROWLEVEL(ROW,LEVEL) toCellRow = (ROW); toIndentLevel = (LEVEL)

#define DR_SUCCEED(LEVEL,PARENT,AFTER) \
	{ if (performMove) \
	  { \
	   [[self _treeBrowser] addNode:dragNode toParent:(PARENT) after:(AFTER)];\
	  } \
	  DR_SETROWLEVEL(row,LEVEL); return self; }

#define DR_FAIL(RETVAL)			return (RETVAL)


- _dragToPoint:(const NXPoint *)currentPoint moveNode:(BOOL)performMove
/*"
Given a cell from which a node is being dragged and the current drag point, determine which cell/node pair the node is being dragged to, and whether the node is to be added as a child of the node, or a child of one of its ancestors.  Translate this to an indent level, and store in the #int pointed to by pIndentLevel.  If performMove is YES, message the controlling MiscTreeBrowser to perform the move.
"*/
{
	int row, col;
	int aboveIndentLevel, belowIndentLevel, pointerIndentLevel;
	MiscTreeBrowserCell *overCell, *aboveCell, *belowCell;
	MiscTreeBrowserCell *fromCell = [self _dragCell];
	id<MiscTreeBrowserNodes> aboveNode, belowNode, dragNode, dragParent;
	NXRect cellFrame;
	NXCoord xOffset, yOffset;
	
	overCell = [self getRow:&row andCol:&col forPoint:currentPoint];
	if (!overCell || !fromCell)
	{							// not over a cell; exit here
		DR_FAIL(nil);
	}

	dragNode = [fromCell node];
	dragParent = [dragNode parent];

	if (performMove && dragParent && ![dragParent canRemoveChild:dragNode])
	{							// cannot remove dragged node from parent;
		NXBeep();				// if trying to move, complain
		performMove = NO;		// and do not attempt move
	}
								// get details of cell the pointer is in
	[self getCellFrame:&cellFrame at:row :col];
	yOffset =  currentPoint->y - cellFrame.origin.y;
	xOffset =  currentPoint->x - cellFrame.origin.x;
								// find which cell pointer is "between"
	if (row == dragCellRow)
	{
		NXCoord margin = cellFrame.size.height / 4.0;

		if ((yOffset >= margin) &&
			(yOffset <= (cellFrame.size.height - margin)))
		{						// pointer on home cell; do no more
			DR_SETROWLEVEL(row + 1,[overCell indentLevel]);

			return self;
		}
	}
	if (yOffset > (cellFrame.size.height / 2.0))
	{
		row++;
	}

	// row now indicates bottom row of the cells the pointer is "between"
	aboveCell = [self cellAt:(row - 1) :0];
	belowCell = [self cellAt:row :0];

	aboveNode = [aboveCell node];
	belowNode = [belowCell node];

	dragNode = [fromCell node];
	pointerIndentLevel = [self indentLevelFromXOffset:xOffset];
	aboveIndentLevel = [aboveCell indentLevel];

	if ([aboveNode parent] == [belowNode parent])
	{							// nodes are siblings (possibly roots)
		id parent = [aboveNode parent];
		BOOL asChild = ([aboveNode canAddChild:dragNode]) ? YES : NO;
		BOOL asSibling = (!parent || [parent canAddChild:dragNode
											 after:aboveNode]) ? YES : NO;

		if (belowCell == fromCell)
		{						// between dragCell and one above
			asSibling = NO;
		}
		else if (aboveCell == fromCell)
		{						// between dragCell and one below
			asSibling = asChild  = NO;
		}

		if (asSibling && asChild)
		{	// can be either sib or child, use pointerIntentLevel to decide
			if (pointerIndentLevel > aboveIndentLevel)
			{
				asSibling = NO;
			}
			else
			{
				asChild = NO;
			}
		}

		// only one of asSibling and asChild is now YES
		if (asSibling)
		{
			DR_SUCCEED(aboveIndentLevel,parent,aboveNode);
		}
		else if (asChild)
		{
			DR_SUCCEED(aboveIndentLevel +1,aboveNode,MTB_LASTCHILD(aboveNode));
		}
	}
	else if ([belowNode parent] == aboveNode)
	{							// nodes are parent(above) / child(below)
		BOOL asChild = ([aboveNode canAddChild:dragNode]) ? YES : NO;

		if ((belowCell != fromCell) && (aboveCell != fromCell) && asChild)
		{
			DR_SUCCEED(aboveIndentLevel + 1,aboveNode,nil);
		}
	}
	else if (aboveIndentLevel > (belowIndentLevel = [belowCell indentLevel]))
	{							// above/below not related
		int indentLevel = (pointerIndentLevel < belowIndentLevel)
			? belowIndentLevel
			: pointerIndentLevel;

		if (indentLevel > aboveIndentLevel)
		{
			if ((aboveNode != dragNode) && [aboveNode canAddChild:dragNode])
			{
				DR_SUCCEED(aboveIndentLevel + 1, aboveNode,
						   MTB_LASTCHILD(aboveNode));
			}
		}
		else
		{
			id sibling = [aboveCell nodeAtIndentLevel:pointerIndentLevel];
			id parent = sibling ? [sibling parent] : nil;

			if (!parent || [parent canAddChild:dragNode after:sibling])
			{
				DR_SUCCEED(indentLevel,parent,sibling);
			}
		}
	}
	else
	{
		NXLogError("Unknown drag state!\n");
	}

	DR_FAIL(nil);
}

- (NXDragOperation) draggingSourceOperationMaskForLocal:(BOOL)isLocal
/*"
Allows only local drags.
"*/
{
    return (isLocal) ? NX_DragOperationGeneric : NX_DragOperationNone;
}

// EVENT HANDLING

- mouseDown:(NXEvent *)theEvent
/*"
If already editing in another cell, end editing, and record startPoint.
"*/
{
	int row, col;
	MiscTreeBrowserCell *aCell;
	NXPoint currentPoint = theEvent->location;

//	printf("mdown\n");

	[self convertPoint:&currentPoint fromView:nil];
	aCell = [self getRow:&row andCol:&col forPoint:&currentPoint];

    _dragEventMask = [window addToEventMask:NX_MOUSEDRAGGEDMASK];
    [window addToEventMask:NX_MOUSEUPMASK];

	if (theEvent->data.mouse.click == 1)
	{
		// if we are already editing a (different) cell, end editing.
		if (editCell && (editCell != aCell))
		{
			id editor = [[editField window] getFieldEditor:NO for:editField];

			[self textDidEnd:editor endChar:NX_RETURN];
		}
								// record where clicked (to check if dragging)
		lastPoint = currentPoint;
		_dragMousedownEvent = *theEvent;
	}
	else
	{
		if (_dragPBoard)
		{
			_dragPBoard = [_dragPBoard free];
		}
	}

	return self;
}

- mouseDragged:(NXEvent *)theEvent
/*"
The user is dragging aCell, and the pointer is at currentPoint.  Work out what to draw, where...  To avoid unneccessary flicker during redraw, the image is only drawn if it has moved.  The point that the image was last drawn at is stored in %lastPoint.
"*/
{
	NXPoint currentPoint = theEvent->location;

//	printf("mdr\n");

	[self convertPoint:&currentPoint fromView:nil];

	if ((fabs(currentPoint.x - lastPoint.x) +
		 fabs(currentPoint.y - lastPoint.y) <= 4.0))
	{							// allow a few pixels "wobble" before dragging
		return self;
	}
	else
	{							// start dragging node
		MiscTreeBrowserCell *fromCell;
		NXImage *image;
		NXSize size;
		NXPoint imagePoint;
		int row, col;
								// restore old eventMask
		[window setEventMask:_dragEventMask];
								// get drag row and image
		fromCell = [self getRow:&row andCol:&col forPoint:&currentPoint];
		image = [fromCell dragImage];
		if (!image)
		{						// pointer is outside of view; do not drag
			return self;
		}
		[self _setDragging:row];
								// create drag pasteboard
		_dragPBoard = [Pasteboard newName:NXDragPboard];
		[_dragPBoard declareTypes:&MTBDragPboardType num:1 owner:self];

								// centre drag image on pointer
		[image getSize:&size];
		imagePoint.x = lastPoint.x - (size.width / 2.0);
		imagePoint.y = lastPoint.y + (size.height / 2.0);

		[self dragImage:image
			  at:&imagePoint 
			  offset:&mouseOffset 
			  event:&_dragMousedownEvent
			  pasteboard:_dragPBoard
			  source:self
			  slideBack:NO];
	}

	return self;
}

- (NXDragOperation)draggingEntered:(id <NXDraggingInfo>)sender
{
//	printf("dent\n");
								// switch off autoscrolling
	[self perform:@selector(_autoscroll:)
		  with:(id)NO
		  afterDelay:1
		  cancelPrevious:YES];

	return NX_DragOperationGeneric;
}

- draggingExited:(id <NXDraggingInfo>)sender
{
//	printf("dex\n");

	toCellRow = -1;				// clear drag image
	[self display];
								// switch on autoscrolling
	[self _autoscroll:YES];

	return self;
}

- (BOOL)prepareForDragOperation:(id <NXDraggingInfo>)sender
{
//	printf("dprep\n");

	return YES;
}

- (BOOL)performDragOperation:(id <NXDraggingInfo>)sender
{
	NXPoint currentPoint;

//	printf("dperf\n");

	currentPoint = [sender draggingLocation];
	[self convertPoint:&currentPoint fromView:nil];

	return [self _dragToPoint:&currentPoint moveNode:YES] ? YES : NO;
}

- concludeDragOperation:(id <NXDraggingInfo>)sender
{
	
	[self _clearDragging];		// clean up dragging state (if any)
	[self display];

	return self;
}

- (NXDragOperation)draggingUpdated:(id <NXDraggingInfo>)sender
{
	NXImage *placementImage = [self _placementImage];
	MiscTreeBrowserCell *fromCell = [self _dragCell];
	NXRect cellFrame;
	NXSize placementImageSize;
	NXPoint currentPoint;
	int oldIndentLevel, oldCellRow;

	oldIndentLevel = toIndentLevel;
	oldCellRow = toCellRow;
	_oldDragImagePoint = _dragImagePoint;

	currentPoint = [sender draggingLocation];
	[self convertPoint:&currentPoint fromView:nil];
	[self _dragToPoint:&currentPoint moveNode:NO];

	if (toCellRow < 0)
	{							// pointer is not over a cell in the matrix
//		printf("not over cell\n");

		return NX_DragOperationNone;
	}
								// calculate where to draw image
	[placementImage getSize:&placementImageSize];
	[self getCellFrame:&cellFrame at:toCellRow :0];
	_dragImagePoint.x = cellFrame.origin.x +
		[self nodeXOffsetForIndent:toIndentLevel];

	if ((toCellRow == dragCellRow + 1) &&
		(toIndentLevel == [fromCell indentLevel]))
	{
								// ...superimpose drag image in Cell button.
		_dragImagePoint.y = cellFrame.origin.y -
			((cellFrame.size.height - placementImageSize.height) / 2.0);
	}
	else
	{							// calculate Y for image (on cell lower edge)
		_dragImagePoint.y = cellFrame.origin.y + (placementImageSize.height / 2.0);
	}

	if (!MiscEqualPoint(&_oldDragImagePoint, &_dragImagePoint))
	{
		[self display];
	}

	return NX_DragOperationGeneric;
}

- mouseUp:(NXEvent *)theEvent
/*"
Handles 'mouseUp' event...
"*/
{
	NXPoint currentPoint = theEvent->location;

//	printf("mup\n");

	[self convertPoint:&currentPoint fromView:nil];

	if (theEvent->data.mouse.click == 0)
	{							// drag'n'drop outside view - ignore
		[self display];			// needed?
		return self;
	}
	else if (theEvent->data.mouse.click == 1)
	{							// single click
		MiscTreeBrowserCell *fromCell;
		NXRect cellFrame;
		int row, col;

		fromCell = [self getRow:&row andCol:&col forPoint:&currentPoint];
		[self getCellFrame:&cellFrame at:row :col];
		[fromCell getOpenerRect:&cellFrame inView:self];
		cellFrame.origin.y -= cellFrame.size.height; // frame is flipped

		if ([self mouse:&currentPoint inRect:&cellFrame])
		{
			[self perform:@selector(_toggleCellOpen)
				  with:nil
				  afterDelay:1
				  cancelPrevious:YES];
		}
		[self selectCell:fromCell];
	}
	else if (theEvent->data.mouse.click == 2)
	{							// double-click
		MiscTreeBrowserCell *fromCell;
		int row, col;

		fromCell = [self getRow:&row andCol:&col forPoint:&currentPoint];
//		[self selectCell:fromCell];
		[self sendDoubleAction];
	}

	[self _clearDragging];		// clean up dragging state (if any)

	return self;
}

- (BOOL)acceptsFirstResponder
/*"
Returns YES - matrix allows use of arrow keys etc.
"*/
{
	return YES;
}

- keyDown:(NXEvent *)theEvent
/*"
Does nothing, but eliminated "no responder" beeps
"*/
{
	return self;
}

- keyUp:(NXEvent *)theEvent
/*"
Handle keystrokes; ignore them if the user is editing a cell, otherwise:
Backspace - delete selected node, Return - create sibling to selected node,
Up/DownArrow - move selection up/down a cell. Left/RightArrow - move
out of/into node hierarchy.
"*/
{
	unsigned short keypress = theEvent->data.key.charCode;
	int sRow;
	id sCell;

	if (wasEditing)
	{							// ignore keystrokes whilst editing cell
		if (!editCell)
		{						// skip 'Return' KeyUp at end of edit
			wasEditing = NO;
		}
		return self;
	}

	switch (keypress)
	{
	case DELETE:
		{
			int srow = [self selectedRow];
			int count = [self cellCount];

			if (srow >= (count - 1))
			{
				[self selectCellAt:(srow - 1) :0];
			}
			else
			{
				[self selectCellAt:srow :0];
			}
			[[self _treeBrowser] removeNodeAt:srow];
		}
		break;

	case RETURN:
		{
			id scell = [self selectedCell];
			id node = [scell node];
			id newNode = [[[node class] alloc] init];
			id tb = [self _treeBrowser];
			id ncell;

			if ([scell isOpen] && ([node childCount] > 0))
			{
				[tb addNode:newNode toParent:node after:nil];
			}
			else
			{
				[tb addNode:newNode toParent:[node parent] after:node];
			}
			ncell = [self _cellForNode:newNode];
			[self selectCell:ncell];
			[self editCell:ncell];
		}
		break;

	case UPARROW:
		// select next cell up, or last cell if at top or none selected
		sRow = [self selectedRow];
		sRow = ((sRow <= 0) ? numRows : sRow) - 1;
		[self selectCellAt:sRow : 0];
		[self scrollCellToVisible:sRow :0];
		break;

	case DOWNARROW:
		// select next cell down, or first cell if at end or none selected
		sRow = [self selectedRow] + 1;
		sRow = (sRow >= numRows) ?  0 : sRow;
		[self selectCellAt:sRow : 0];
		[self scrollCellToVisible:sRow :0];
		break;

	case LEFTARROW:
		// if cell is open, close it.  If it is not open, and selected cell is
		// not a root cell, select the parent cell
		sCell = [self selectedCell];
		if ([sCell isOpen])
		{
			[self _toggleCellOpen];
		}
		else
		{
			id parent = [[sCell node] parent];

			if (parent)
			{
				sRow = [self selectedRow] - 1;
				while ((sRow > 0) && ([[self cellAt:sRow :0] node] != parent))
				{
					sRow--;
				}
				[self selectCellAt:sRow : 0];
				[self scrollCellToVisible:sRow :0];
			}
		}
		break;

	case RIGHTARROW:
		// if cell is not open, open it.  If it is open and has children,
		// select first child
		sCell = [self selectedCell];
		if ([sCell isOpen])
		{
			if (![sCell isLeaf])
			{
				sRow = [self selectedRow] + 1;
				[self selectCellAt:sRow :0];
				[self scrollCellToVisible:sRow :0];
			}
		}
		else
		{
			[self _toggleCellOpen];
		}
		break;
	}

	return self;
}

// text (field editor) delegate methods

- textDidEnd:sender endChar:(unsigned short)reason
/*"
Ends editing in the editCell, removes it from the view and checks
whyEnd.  If Escape was pressed, abandon the edit, otherwise set the
selected cell stringValue to the editCell text.
"*/
{
	int row,  col;
	
	if (reason != ESCAPE)
	{
		[editCell setStringValue:[editField stringValue]];
	}
	[editField removeFromSuperview];

	[self getRow:&row andCol:&col ofCell:editCell];
	[editCell setEditing:NO];
	
	[self selectCell:editCell];
	[self drawCellAt:(row - 1) :0];
	[self drawCellAt:row :0];
	[self drawCellAt:(row + 1) :0];

	editCell = nil;
	[[self window] perform:@selector(makeFirstResponder:)
				   with:self
				   afterDelay:1
				   cancelPrevious:YES];

	return self;
}

- editCell:aCell
/*"
Start user editing of a cell; plonk the editcell on top of the aCell,
fill it with aCell title , select the text and make the edit cell
the first responder.  Returns self, or nil if there is some problem;
like aCell not existing.
"*/
{
	if ((aCell == [self selectedCell]) || ([self selectCell:aCell] == aCell))
	{
		int row, col;
		NXRect textFrame;
		id editor = [self _editField];

		editCell = aCell;
		wasEditing = YES;

		[self getRow:&row andCol:&col ofCell:editCell];
		[self getCellFrame:&textFrame at:row :col];
		[editCell getTitleRect:&textFrame inView:self];
		[editCell setEditing:YES];
		[self drawCell:editCell];
		[editor setStringValue:[editCell stringValue]];
		textFrame.size.height += 5.0;
		textFrame.origin.y -= 3.0;
		textFrame.origin.x -= 2.0;
		[editor setFrame:&textFrame];
		[self addSubview:editor];

		[[self window] makeFirstResponder:editor];
		[editor display];
		[editor selectText:self];

		return self;
	}

	return nil;
}

- (BOOL)isEditing
/*"
Returns YES if the user is editing the title of a cell.
"*/
{
	return editCell ? YES : NO;
}

- endEditing
/*"
If the user is editing a cell, force an end to the editing
(accepting whatever changed have been made).  Return self.
"*/
{
	if ([self isEditing])
	{
		id editor = [[editField window] getFieldEditor:NO for:editField];
		id scell = [self selectedCell];

		[self textDidEnd:editor endChar:NX_RETURN];
		[self selectCell:scell];
	}

	return self;
}

// MATRIX EXTENSIONS

- scrollNodeToVisible:aNode
/*"
If aNode is held in a cell in the receiver and the receiver is in a ScrollView,
scroll the cell into view.

#{See also:  - scrollCellToVisible::(Matrix)}
"*/
{
	int row = [self _indexOfNode:aNode];

	if (row != -1)
	{
		[self scrollCellToVisible:row :0];
	}

	return self;
}

- selectNode:aNode
/*"
If aNode is held in a cell in the receiver, select that cell.

#{See also:  - selectCell:}
"*/
{
	return [self selectCell:[self _cellForNode:aNode]];
}

@end
