/*

File IKShelf.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 specializes the IKIconPath to conveniently set things up as needed for a shelf.  In particular, its cells are initialized to act as locked containers, its mode is set to highlight, and it automatically adds new cells when its size changes.

*/

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

@implementation IKShelf


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


/*

An IKShelf needs access to the Sheaf image so that it can put something in its cells for the user to see in InterfaceBuilder.

*/

static NXImage		* folder = nil;
static NXSize		defaultCellSize = { 70.0, 74.0 };


+ initialize
{
    if (self == [IKShelf  class])
	{
		IKInitIDpboardType();
		folder = [NXImage  findImageNamed: "Folder"];
	}
	
	return self;
}


- (const char *) getInspectorClassName
{
	return ([NXApp  currentEvent]->flags & NX_ALTERNATEMASK) ?
			[super getInspectorClassName]:
			"IKShelfInspector";
}


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


/*

The default initFrame: sets up a prototype IKCell that's appropriate for a shelf.  It also does a little snooping and adds an appropriate image and title if it decides its running as part of the InterfaceBuilder palette.

*/

- initFrame: (const NXRect *) frameRect
{
	if ((self = [self  initFrame: frameRect
							mode: NX_HIGHLIGHTMODE
							cellClass: [IKCell  class]
							numRows: 0  numCols: 0]) != nil)
	{
		[self  setPrototype:
					
					[[[[[IKCell		alloc]
									init]
									setLocked: YES]
									setReallyLocked: NO]
									setContainer: YES]];
		
		if ([NXApp  conformsTo: @protocol(IB)])
				[[[self  prototype]  setTitle: "IconKit"]  setImage: folder];
		
		[[[self		fillWithCells]
					sizeCellsToFit]
					sizeToCells];
	}
	
	return self;
}


- initFrame: (const NXRect *) frameRect
		mode: (int) aMode
		cellClass: class
		numRows: (int) rows
		numCols: (int) cols
{
	if ((self = [super  initFrame: frameRect
							mode: aMode
							cellClass: class
							numRows: rows  numCols: cols]) != nil)
	{
		users = [[IKAnnouncer  alloc]  initOwner: self];
		objectToPaste = nil;
		lastIsEmptyContainer = NO;
		dynamic = YES;
		
		[[[[self	setAutosizeCells: NO]
					setClassToHold: ""]
					setCellSize: &defaultCellSize]
					sizeCellsToFit];
	}
	
	return self;
}


- free
{
	[self  unregisterDraggedTypes];
	[users  announce: @selector(willFree:)];
	[users  free];
	
	return [super  free];
}


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


/*

Here are the archiving methods.  Archiving allows an IKShelf to be used on an InterfaceBuilder palette.

*/

- read: (NXTypedStream *) stream
{
	char
		* name;
	
    [super  read: stream];
	
	users = [[IKAnnouncer  alloc]  initOwner: self];
	NXReadTypes (stream, "*c", &name, &dynamic);
	[self  setPrototype: NXReadObject (stream)];
	[self  setClassToHold: name];
	free(name);
	
	return self;
}


- write: (NXTypedStream *) stream
{
    [super  write: stream];
	
	NXWriteTypes (stream, "*c", &classToHold, &dynamic);
	NXWriteObject (stream, [self  prototype]);
    
	return self;
}


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


/*

Here are the IKDependency methods.  They all simply get forwarded to the users object.

*/


- addUser: who			{		return [users  addUser: who];			}
- addListener: who		{		return [users  addListener: who];		}
- removeUser: who		{		return [users  removeUser: who];		}
- removeListener: who	{		return [users  removeListener: who];	}


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


/*

Here are the methods to adjust the properties of the shelf.  The delegate message returns the delegate of the currently selected cell.

*/

- (BOOL) isDynamic				{	return dynamic;						}
- (const char *) classToHold	{	return classToHold;					}
- setDynamic: (BOOL) flag		{	dynamic = flag; return self;		}


- setClassToHold: (const char *) name
{
	NXAtom
		* pasteTypes;
	int
		n = 0;
	id
		class;
	
	if (classToHold) free(classToHold);
	classToHold = name ? NXCopyStringBuffer(name) : NULL;
	class = classToHold ? objc_lookUpClass(name) : nil;
	pasteTypes =
			classToHold == NULL ? NULL:
			[class respondsTo: @selector(pasteTypes)] ? [class  pasteTypes]:
			classToHold[0] == '\0' ? IKIDPasteTypes():
			NULL;
	
	if (pasteTypes != NULL)
	{
		while (pasteTypes[n] != NULL) n++;
		[self  unregisterDraggedTypes];
		[self  registerForDraggedTypes: pasteTypes  count: n];
	}
		
	return self;
}


- delegate
{
	return [selectedCell  delegate];
}


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


/*

Here are methods to automatically resize and add cells to the shelf.  The fillWithCells method fills the shelf with as many cells of the current size as it can.  The sizeCellsToFit method adjusts the intercell spacing so that the cells exactly fill the frame.

When dynamic sizing is active, the shelf calls both these methods whenever its size changes.  The net effect is that the shelf will always contain the maximum number of cells of the original size.  If the cells have been set to autoSize, dynamic sizing does nothing.

*/


- sizeTo: (NXCoord) width  : (NXCoord) height
{
	[super  sizeTo: width  : height];
	if (dynamic)
			[[self  fillWithCells]  sizeCellsToFit];
	
	return self;
}


- fillWithCells
{
	int
		rows = frame.size.height / cellSize.height,
		cols = frame.size.width  / cellSize.width;
		
	[self  renewRows: rows  cols: cols];
	[self  sizeCellsToFit];
	return self;
}


- sizeCellsToFit
{
	NXSize
		gap;
	
	gap.height = numRows > 1 ?
		(frame.size.height - cellSize.height * numRows) / (numRows - 1) : 0.0;
	gap.width  = numCols > 1 ?
		(frame.size.width  - cellSize.width  * numCols) / (numCols - 1) : 0.0;
	
	[self  setIntercell: &gap];
	return self;
}


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


/*

The standard matrix highlighting mode seems somewhat strange, in that it leaves the final cell selected.  This code makes sure that cells act like buttons when a shelf is in NX_HIGHLIGHTMODE.

*/


- mouseDown: (NXEvent *) event
{
	[super  mouseDown: event];
	if ([self  mode] == NX_HIGHLIGHTMODE)
	{
		[self  drawCell: [selectedCell  setState: 0]];
		[self  selectCellAt: -1  : -1];
	}
	
	return self;
}


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


/*

The method below determine how an empty container responds to drags.  The first two should give an appropriate icon and title; the last should determine the allowable dragging operations.  The default implementations use the dragged image and take the title from the pasteboard.

*/


- (NXDragOperation) draggingOperation: (id <NXDraggingInfo>) sender
{
	id
		delegates = [users  usersAndListeners],
		who;
	int
		i = [delegates  count];
	
	while (i--)
	{
		who = [delegates  objectAt: i];
		if ([who  respondsTo: @selector(shelf:willDrag:from:)])
				return [who  shelf: self  isDragging: objectToPaste
								from: sender];
	}
	
	return	objectToPaste != nil ?
				NX_DragOperationAll:
				NX_DragOperationNone;
}


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


/*

The object on the pasteboard is read once when dragging enters the shelf.

*/


- (NXDragOperation) draggingEntered: (id <NXDraggingInfo>) sender
{
	id
		class;
	
	[users  announce: @selector(shelf:dragWillEnter:)  with: sender];
	lastIsEmptyContainer = NO;
	class = classToHold ? objc_lookUpClass(classToHold) : nil;
	
	objectToPaste =
			[class  respondsTo: @selector(readFromPasteboard:)] ?
					[class  readFromPasteboard: [sender  draggingPasteboard]]:
					IKReadID ([sender  draggingPasteboard]);
	
	[objectToPaste  addUser: self];
	
	return [super  draggingEntered: sender];
}


- draggingExited: (id <NXDraggingInfo>) sender
{
	[users  announce: @selector(shelf:dragWillExit:)  with: sender];
	[objectToPaste  removeUser: self];
	return [super  draggingExited: sender];
}


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


/*

When a new cell is entered, the shelf checks to see whether the current cell is an empty container.  If so it checks to see what operation to perform and puts an appropriate title and icon in the container.  When the cell is exited, the title and icon are erased.

*/


- (NXDragOperation) cellEntered: (id <NXDraggingInfo>) sender
{
	NXDragOperation		op;
	
	if ([current  isEmptyContainer] &&
				((op = [self  draggingOperation: sender]) !=
											 NX_DragOperationNone))
	{
		[[current  setGhosted: YES]  setDelegate: objectToPaste];
		lastIsEmptyContainer = YES;
		
		return op;
	}
	
	else return [super  cellEntered: sender];
}


- (NXDragOperation) cellUpdated: (id <NXDraggingInfo>) sender
{
	return lastIsEmptyContainer ?
				[self  draggingOperation: sender]:
				[super  cellUpdated: sender];
}


- cellExited: (id <NXDraggingInfo>) sender
{
	if (lastIsEmptyContainer)
	{
		[[last  setGhosted: NO]  setDelegate: nil];
		lastIsEmptyContainer = NO;
		
		return self;
	}
	
	else return [super  cellExited: sender];
}


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


/*

The shelf processes the standard drag protocol messages itself whenever the destination is an empty container cell.

*/


- (BOOL) prepareForDragOperation: (id <NXDraggingInfo>) sender
{
	return !lastIsEmptyContainer ?
				[super  prepareForDragOperation: sender]:
				YES;
}


- (BOOL) performDragOperation: (id <NXDraggingInfo>) sender
{
	[objectToPaste  removeUser: self];
	objectToPaste = nil;
	if (lastIsEmptyContainer)
	{
		[current  setGhosted: NO];
		[self  drawCell: current];
	}
	
	return !lastIsEmptyContainer ?
				[super  performDragOperation: sender]:
				YES;
}


- concludeDragOperation: (id <NXDraggingInfo>) sender
{
	[users  announce:  @selector(shelf:dragWillComplete:)];
	
	return !lastIsEmptyContainer ?
				[super  concludeDragOperation: sender]:
				nil;
}


@end
