/*

File IKIconPath.m

Release 1.2, 7 June 1994
Copyright (C) 1994 by H. Scott Roy

This code is part of IconKit, a general toolbox for drag-and-drop applications.  IconKit is free for noncommercial use, but costs money for a commercial license.  You should have received a copy of the license agreement with this file.  If not, a copy of the license and the complete source of IconKit can be obtained from the author:

		H. Scott Roy
		2573 Stowe Ct.
		Northbrook, IL  60062-8103
		iconkit@cs.stanford.edu

For your editing convenience, this file is best viewed using an editor that automatically wraps long lines, in a fixed point font at 80 columns, with tabs every 4 spaces.

*/


/* ========================================================================== */


/*

This class provides a matrix of IKCells.  It extends the event handling of an ordinary matrix to allow dragging and editing within individual cells.

*/

#import <appkit/appkit.h>
#import <apps/InterfaceBuilder.h>
#import "iconkit.h"

@implementation IKIconPath


/* ========================================================================== */


/*

Defaults for an IKIconPath are a little different than a standard matrix.  The designated initializers are the same as in the Matrix class.

*/


- initFrame: (const NXRect *) frameRect
{
	if ((self = [self  initFrame: frameRect
							mode: NX_RADIOMODE
							cellClass: [IKCell  class]
							numRows: 1  numCols: 1]) != nil)
	{
		[self  setCellSize: &frameRect->size];
		[self  sizeToCells];
	}
	
	return self;
}


- initFrame: (const NXRect *) frameRect
		mode: (int) aMode
		prototype: aCell
		numRows: (int) rows
		numCols: (int) cols
{
	if ((self = [self  initFrame: frameRect
							mode: aMode
							cellClass: [IKCell  class]
							numRows: rows  numCols: cols]) != nil)
	
			[self  setPrototype: aCell];
			
	return self;
}


- initFrame: (const NXRect *) frameRect
		mode: (int) aMode
		cellClass: class
		numRows: (int) rows
		numCols: (int) cols
{
	NXSize
		zero = { 0, 0 };
	
	if ((self = [super  initFrame: frameRect
							mode: aMode
							cellClass: class
							numRows: rows  numCols: cols]) != nil)

			[[[[[[[self		setBackgroundGray: NX_LTGRAY]
							setCellSize: &zero]
							setIntercell: &zero]
							setAutodisplay: YES]
							setClipping: NO]
							setOpaque: YES]
							setEmptySelectionEnabled: NO];
	
	return self;
}


/* ========================================================================== */


/*

Drawing aspects are set upon awakening to cover the current way that an IKIconPath redraws itself.  Clipping is kept off until a cell editor is actually invoked.

*/


- awake
{
	[super  awake];
	[[[self	setAutodisplay: YES]
			setClipping: NO]
			setCellBackgroundGray: NX_LTGRAY];
	
	return self;
}


/* ========================================================================== */


/*

Mouse events are handled specially to check for editing and dragging.  A click in the title rectangle of an alrady selected and editable cell initiates editing.  A click in a draggable cell initiates dragging.

*/


- mouseDown: (NXEvent *) event
{
	NXPoint
		where = event->location;
	NXRect
		cellFrame;
	int
		hit,
		row,
		col;
	
	[self  convertPoint: &where  fromView: nil];
	cell = [self  getRow: &row  andCol: &col  forPoint: &where];
	[self  getCellFrame: &cellFrame  at: row  : col];
	hit = [cell  hitPart: &where  inRect: &cellFrame];
	
	if ((hit == IK_ICONPART) && [cell  isDraggable] &&
				!(event->flags & (NX_SHIFTMASK | NX_ALTERNATEMASK)))
	{
		if (cell == selectedCell)
				[window  makeFirstResponder: self];		else
				[self  endEditing];
				
		[self  selectCell: cell];
		[cell  dragIcon: event  inRect: &cellFrame  ofView: self];
		
		if (event->data.mouse.click > 1)
				[self  sendDoubleAction];	else
				[self  sendAction];
	}
	
	else if ((hit == IK_TITLEPART) && [cell  isEditable] && [cell  state])
					[cell  editTitle: event  inRect: &cellFrame  ofView: self];
	
	else
	{
		if (cell == selectedCell)
				[window  makeFirstResponder: self];		else
				[self  endEditing];
		
		[super  mouseDown: event];
	}
	
	[window  invalidateCursorRectsForView: self];
	
	return self;
}


/* ========================================================================== */


/*

An IKIconPath contains draggable objects, so we want to delay bringing the containing window forward until mouse-ups.

*/


- (BOOL) shouldDelayWindowOrderingForEvent: (NXEvent *) theEvent;
{
	return YES;
}


/* ========================================================================== */


/*

The methods below let one programmatically invoke editing of a cell.  IKBrowser uses them to automatically edit titles in the browser.

*/


- editCell: theCell
{
	int
		row,
		col;
	
	if ([self  getRow: &row  andCol: &col  ofCell: theCell])
			[self  editCellAt: row  : col];
	
	return self;
}


- editCellAt: (int) row  : (int) col
{
	NXRect
		cellFrame;
	
	[window  disableFlushWindow];
	[self  getCellFrame: &cellFrame  at: row  : col];
	[[self	cellAt: row  : col]
			editTitle: NULL  inRect: &cellFrame  ofView: self];
	
	[[window  reenableFlushWindow]  flushWindow];
	
	return self;
}


- endEditing
{
	NXRect
		editorFrame;
	id
		editor = [window  getFieldEditor: NO  for: self];
	
	if ([cellList  indexOf: [editor  delegate]] != NX_NOT_IN_LIST)
	{
		[editor  getFrame: &editorFrame];
		[window  endEditingFor: self];
		[self  displayFromOpaqueAncestor: &editorFrame  : 1  : NO];
	}
	
	return self;
}


/* ========================================================================== */


/*

The abortEditing method gets called whenever any cell in the matrix changes its title.  That's not really what we want, so instead an IKIconPath ignores abortEditing--effectively refusing to abort.  Editing should be terminated with endEditing.

*/


- abortEditing
{
	return nil;
}


/* ========================================================================== */


/*

The cursor is an I-beam whenever it's over the title area of a selected and editable cell.

*/


- resetCursorRects
{
	NXRect
		title;
	id
		editor = [window  getFieldEditor: NO  for: self],
		editingCell = [editor  delegate],
		selection = [self  getSelectedCells: nil],
		theCell;
	int
		row,
		col,
		i;
	
	i = [selection  count];
	while (i--)
		if ([theCell = [selection  objectAt: i]  isEditable]  &&
				theCell != editingCell)
		{
			[self  getRow: &row  andCol: &col  ofCell: theCell];
			[self  getCellFrame: &title  at: row  : col];
			[theCell  getTitleRect: &title];
			[self  addCursorRect: &title  cursor: NXIBeam];
		}
	
	if ([cellList  indexOf: editingCell] != NX_NOT_IN_LIST)
	{
		[editor  getFrame: &title];
		[self  addCursorRect: &title  cursor: NXIBeam];
	}
	
	[selection  free];
	
	return self;
}


/* ========================================================================== */


/*

The default Matrix implementation of drawCellAt:: is no good, since it potentially clobers any subviews--in our case the editor.  The implementation here relies on Matrix being smart enough to avoid drawing anything outside the rectangle passed to display::.

Try clicking on a cell that's being edited without these methods and see what happens.

*/

- drawCellAt: (int) row  : (int) col
{
	NXRect
		cellFrame;
	
	[window  disableFlushWindow];
	[self  getCellFrame: &cellFrame  at: row  : col];
	[self  displayFromOpaqueAncestor: &cellFrame  : 1  : NO];
	[[window  reenableFlushWindow]  flushWindow];
	
	return self;
}


- drawCellInside: theCell
{
	int
		row,
		col;
	
	[self  getRow: &row  andCol: &col  ofCell: theCell];
	[self  drawCellAt: row  : col];
	
	return self;
}


/* ========================================================================== */


/*

The getSelectedCells: method is broken in two ways: it crashes InterfaceBuilder in test mode, and it only returns the cells when the list is in NX_LISTMODE.  Hence, IKIconPath provides its own implementation to give correct behavior.

*/


- getSelectedCells: selection
{
	int
		i,
		j;
	id
		aCell;
	
	if (selection == nil)
			selection = [[List  alloc]  init];
	
	for (i = 0; i < numRows; i++)
		for (j = 0; j < numCols; j++)
		{
			aCell = [self  cellAt: i  : j];
			if ([aCell  state] != 0) [selection  addObject: aCell];
		}
	
	return selection;
}


/* ========================================================================== */


/*

Resizing messes up editing since we no longer allow editing to abort.  So we shut down any editing before resizing, then restart it afterwards.

*/


- setCellSize: (const NXSize *) size
{
	id
		editingCell = [[window  getFieldEditor: NO  for: self]  delegate];
	
	[self  endEditing];
	[super  setCellSize: size];
	if ([cellList  indexOf: editingCell] != NX_NOT_IN_LIST)
			[self  editCell: editingCell];
	
	return self;
}


- setIntercell: (const NXSize *) size
{
	id
		editingCell = [[window  getFieldEditor: NO  for: self]  delegate];
	
	[self  endEditing];
	[super  setIntercell: size];
	if ([cellList  indexOf: editingCell] != NX_NOT_IN_LIST)
			[self  editCell: editingCell];
	
	return self;
}


- sizeTo: (float) width  : (float) height
{
	id
		editingCell = [[window  getFieldEditor: NO  for: self]  delegate];
	
	[self  endEditing];
	[super  sizeTo: width  : height];
	if ([cellList  indexOf: editingCell] != NX_NOT_IN_LIST)
			[self  editCell: editingCell];
	
	return self;
}


/* ========================================================================== */


/*

The methods below help to manipulate cells by delegate.  The first finds the cell with a given delegate.  The second extracts the delegate in the indicated column.

*/


- cellWithDelegate: delegate
{
	id		theCell;
	int		i;
	
	for (i = 0; i < [cellList  count]; i++)
		if ([theCell = [cellList  objectAt: i]  delegate] == delegate)
				return theCell;
	
	return nil;
}


- objectInColumn: (int) n
{
	return [[self  cellAt: 0  : n]  delegate];
}


/* ========================================================================== */


/*

The stringValue of a cell matrix is the path of the active cell delegate.

*/


- (const char *) stringValue
{
	id
		delegate = [selectedCell  delegate];
	
	return [delegate respondsTo: @selector(path)] ?
			[delegate  path]:
			[super  stringValue];
}


/* ========================================================================== */


/*

When acting as a dragging source, the standard IKIconPath allows all dragging operations.  The programmer should override this method for different behavior.

*/


- (NXDragOperation) draggingSourceOperationMaskForLocal: (BOOL) isLocal
{
	return NX_DragOperationAll;
}


/* ========================================================================== */


/*

When acting as a dragging destination, the standard IKIconPath needs to account for mouse movements between cell boundaries.  The variable 'current' is used to keep track of the cell under the mouse.

*/


- (NXDragOperation) draggingEntered: (id <NXDraggingInfo>) sender
{
	last = current = nil;
	if ([self  enteredNewCell: [sender  draggingLocation]])
	
			return [self  cellEntered: sender];		else
			return NX_DragOperationNone;
}


- (NXDragOperation) draggingUpdated: (id <NXDraggingInfo>) sender
{
	if ([self  enteredNewCell: [sender  draggingLocation]])
	{
		if (last) [self  cellExited: sender];
		return [self  cellEntered: sender];
	}
	
	else return current ?
			[self  cellUpdated: sender]:
			[super  draggingUpdated: sender];
}


- draggingExited: (id <NXDraggingInfo>) sender
{
	last = current;
	[self  cellExited: sender];
	
	return nil;
}


/* ========================================================================== */


/*

This method determines whether the mouse has entered a new cell during a drag operation.

*/


- (BOOL) enteredNewCell: (NXPoint) where
{
	int
		row,
		col;
	id
		new;
	
	[self  convertPoint: &where  fromView: nil];
	new = [self  getRow: &row  andCol: &col  forPoint: &where];
	
	if (new != current)
	{
		last = current; current = new;
		return YES;
	}
	
	else return NO;
}


/* ========================================================================== */


/*

The routines below coordinate dragging with the appropriate cell delegate.  If the delegate indicates that it will accept the drag, the cell is updated to reflect the drag operation.  It reverts to its normal state when the cell is exited. 

*/


- (NXDragOperation) cellEntered: (id <NXDraggingInfo>) sender
{
	id
		delegate = [current  delegate];
	NXDragOperation
		op = NX_DragOperationNone;
	
	if (delegate &&
		[current  isDragAccepting] &&
		[delegate  respondsTo: @selector(draggingEntered:)] &&
		((op = [delegate  draggingEntered: sender]) != NX_DragOperationNone))
	{
			[current  setImage: [delegate  acceptingDragImage]];
			[self  drawCell: current];
	}
		
	else current = nil;
	
	return op;
}


- (NXDragOperation) cellUpdated: (id <NXDraggingInfo>) sender
{
	id
		delegate = [current  delegate];
	
	return [delegate  respondsTo: @selector(draggingUpdated:)] ?
				[delegate  draggingUpdated: sender]:
				[super  draggingUpdated: sender];
}


- cellExited: (id <NXDraggingInfo>) sender
{
	id
		delegate;
	
	if ([delegate = [last  delegate]  respondsTo: @selector(draggingExited)])
			[delegate  draggingExited: sender];
	
	[last  setImage: [delegate  image]];
	[self  drawCell: last];
	
	return self;
}


/* ========================================================================== */


/*

The standard drag protocol messages are simply passed on to the current cell delegate.  The cell image is returned to its normal state.

*/


- (BOOL) prepareForDragOperation: (id <NXDraggingInfo>) sender
{
	id
		delegate = [current  delegate];
	
	[current  setImage: [delegate  image]];
	[self  drawCell: current];

	return [delegate  respondsTo: @selector(prepareForDragOperation:)] ?
					[delegate  prepareForDragOperation: sender]:
					YES;
}


- (BOOL) performDragOperation: (id <NXDraggingInfo>) sender
{
	id
		delegate = [current  delegate];
	
	return [delegate  respondsTo: @selector(performDragOperation:)] ?
					[delegate  performDragOperation: sender]:
					NO;
}


- concludeDragOperation: (id <NXDraggingInfo>) sender
{
	id
		delegate = [current  delegate];
	
	return [delegate  respondsTo: @selector(concludeDragOperation:)] ?
					[delegate  concludeDragOperation: sender]:
					nil;
}


@end
