/*

File IKBrowser.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.

*/


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


/*

An IKBrowser works like the Workspace file viewer.  Selections are displayed in an icon path, from which they can be dragged or inspected.

*/

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

@implementation IKBrowser


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


#define max(a,b)	((a) > (b) ? (a):(b))

#define LEFT	0xAC
#define UP		0xAD
#define RIGHT	0xAE
#define DOWN	0xAF

#define BUFSIZE		1000


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


/*

The Matrix getSelectedCells method breaks in InterfaceBuilder test mode.  The IKIconPath class implements a correct version, so the workaround is to use that method whenever running in IB test mode.  Note that it would be undesirable to use the IKIconPath implementation exclusively, since programmers might conceivably create an IKBrowser with a custom matrix class that overrides getSelectedCells:.

*/


static IMP	getSelectedCells;


+ initialize
{
	if (self == [IKBrowser  class])
	{
		getSelectedCells =
				[IKIconPath  instanceMethodFor: @selector(getSelectedCells:)];
	}
	
	return self;
}


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


/*

The IKBrowser is designed to work in conjunction with a ScrollView containing an IKIconPath.  Accordingly, the defaults for an IKBrowser are a little bit different than a normal NXBrowser.

Various aspects of the scrollView and iconPath must be initialized when an IKBrowser awakes.  In particular, the scroller on the scrollView must be set to pass its messages through the browser, so that the browser can detect when it should scroll to a new selection.  The iconPath must also be told to send its double clicks to the browser, so that the browser can treat them just like double clicks in the browser itself.

*/


- initFrame: (const NXRect *) frameRect
{
	[super  initFrame: frameRect];
	[[[[[[[[[self	setHorizontalScrollButtonsEnabled: NO]
					setHorizontalScrollerEnabled: NO]
					setMultipleSelectionEnabled: YES]
					acceptArrowKeys: YES  andSendActionMessages: YES]
					setAutoSynchronize: YES]
					useScrollButtons: YES]
					reuseColumns: YES]
					setCellClass: [IKBrowserCell  class]]
					setTitled: NO];
	
	return self;
}


- awake
{
	id
		scroller;
	
	[self  sizeCells];
	
	if (!scroll)
	{
		scroll = [scroller = [scrollView  horizScroller]  action];
		[[scroller	setAction: @selector(scrolling:)]
					setTarget: self];
	}
	
	[iconPath  setDoubleAction: @selector(doDoubleClick:)];
	[iconPath  setBackgroundGray: NX_LTGRAY];
	[window  makeFirstResponder: self];
	
	return self;
}


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


/*

Goodness!  The awake method gets called quite a bit--three times when unarchiving from a nib file.  Alas, I've not really found a better way.  I had originally intended not to include the awake call in setIconPath: and setScrollView:, but that plan was foiled when I realized InterfaceBuilder doesn't send out any awakeFromNib messages in test mode.

*/


- (const char *) getInspectorClassName
{
     return "IKBrowserInspector";
}


- iconPath			{	return iconPath;	}
- scrollView		{	return scrollView;	}


- setIconPath: theIconPath
{
	iconPath = theIconPath;
	[self  awake];
	
	return self;
}


- setScrollView: theScrollView
{
	scrollView = theScrollView;
	[self  awake];
	
	return self;
}


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


/*

Among other things, the archiving methods allow an IKBrowser to be correctly initialized from an InterfaceBuilder palette.

*/


- read: (NXTypedStream *) stream
{
    [super  read: stream];
	
	iconPath = NXReadObject (stream);
	scrollView = NXReadObject (stream);
	NXReadTypes (stream, "c", &autoSynchronize);
	
	return self;
}


- write: (NXTypedStream *) stream
{
    [super  write: stream];
	
	NXWriteObject (stream, iconPath);
	NXWriteObject (stream, scrollView);
	NXWriteTypes (stream, "c", &autoSynchronize);
    
	return self;
}


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


/*

The icon path should always show an integral number of icons.  Hence, whenever the ScrollView resizes, the cell size must also change.  This code is a slight hack, since I assume that the scrollView never changes size unless the browser does so as well.

*/


- sizeTo: (NXCoord) width  : (NXCoord) height
{
	int
		first = [self  firstVisibleColumn];
	
	[super  sizeTo: width  : height];
	[self  scrollColumnToVisible: first];
	[self  sizeCells];
	
	return self;
}


- sizeCells
{
	NXSize
		size;
	
	[scrollView  getContentSize: &size];
	size.width = floor(size.width / [self  numVisibleColumns]);
	
	[iconPath  setCellSize: &size];
	[self  synchronizePath];	
	[[scrollView	setLineScroll: size.width]
					setPageScroll: 0.0];
	
	return self;
}


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


/*

The iconPath should always reflect the current state of the browser.  The scrollView should display the same number of columns as are visible in the browser, but the iconPath matrix itself is sometimes larger to avoid spurious scrolling.  The first icon visible always corresponds to the first visible column.

*/


- synchronizePath
{
	id
		clipView;
	NXPoint
		origin;
	NXSize
		size,
		contentSize;
	int
		cells,
		n;
	float
		excess;
	
	if (autoSynchronize)
	{
		[scrollView  getContentSize: &contentSize];
		[iconPath  getCellSize: &size];
		
		excess = contentSize.width - size.width * [self  numVisibleColumns];
		cells = [self  lastColumn] + 1;
		n = max(cells, [self  firstVisibleColumn] + [self  numVisibleColumns]);
		
		[iconPath  renewRows: 1  cols: cells];
		[iconPath  sizeTo: n * size.width + excess : size.height];
		[[iconPath  cellAt: 0  : cells - 2]  setBranch: YES];
		[[iconPath  cellAt: 0  : cells - 1]  setBranch: NO];
		[iconPath  selectCellAt: 0  : cells - 1];
		[iconPath  update];
		
		origin.x = floor(size.width) * [self  firstVisibleColumn];
		origin.y = 0.0;
		
		[clipView = [iconPath  superview]  rawScroll: &origin];
		[scrollView  reflectScroll: clipView];
	}
	
	return self;
}


- setAutoSynchronize: (BOOL) flag;
{
	autoSynchronize = flag;
	return self;
}


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


/*

This is the method an IKBrowser calls whenever there is a new selection.  It sends a request to the delegate to install an icon to represent the new selection; then it redraws the affected cell.

*/


- newSelection
{
	if (autoSynchronize)
	{
		if ([delegate  respondsTo: @selector(browser:setColumnIcon:for:)])
				[delegate  browser: self  setColumnIcon:
							[iconPath  selectedCell]  for: [self  lastColumn]];
	}
	
	return self;
}


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


/*

The browser can update itself in three ways: mouse clicks or arrow movements within the browser, clicks inside the icon path, and calls to setPath:.  The beginUpdate and endUpdate methods provide the common glue at the start and end of these operations.

We achieve maximum performance when we disable displaying the icon path until the update is complete, while drawing the browser as things arrive.

*/


- beginUpdate
{
	[window  disableFlushWindow];
	[[iconPath	setAutodisplay: NO]
				endEditing];
	
	return self;
}


- endUpdate
{
	[[iconPath	setAutodisplay: YES]
				displayIfNeeded];
	
	if ([delegate  respondsTo: @selector(browserWillFinishChange:)])
			[delegate  browserWillFinishChange: self];
	
	[[[window	makeFirstResponder: self]
				reenableFlushWindow]
				flushWindow];
	
	if ([delegate  respondsTo: @selector(browserDidChange:)])
			[delegate  browserDidChange: self];
	
	return self;
}


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


/*

The iconPath must synchronize after any event that affects the browser.  The most significant times are when a new column is added and when the number of loaded columns is decreased.  The setLastColumn: method alerts the browser delegate that columns are about to disappear so that it can properly clear out all the cells.

	-WARNING-

It's essentially impossible to clean things up at the right time if the browser doesn't reuse its columns.  Moreover, NXBrowser doesn't provide any way to check whether it reuses its columns or not.  So we disallow reuseColumns: NO.

*/


- addColumn
{
	[[[super	addColumn]
				synchronizePath]
				newSelection];		
	
	if ([self  lastColumn] == 0)
			[iconPath  editCellAt: 0  : 0];
	
	return self;
}


- setLastColumn: (int) n
{
	id
		columnMatrices = [[List  alloc]  init],
		pathCells = [[List  alloc]  init];
	int
		i = n,
		m = [self  lastColumn];
	
	while (i++ < m)
	{
		[columnMatrices  addObject: [self  matrixInColumn: i]];
		[pathCells  addObject: [iconPath  cellAt: 0  : i]];
	}
	
	[[super		setLastColumn: n]
				synchronizePath];
	
	i = [columnMatrices  count];
	while (i--)
	{
		if ([delegate  respondsTo: @selector(browser:emptyMatrix:inColumn:)])
				[delegate  browser: self  emptyMatrix:
						[columnMatrices  objectAt: i]  inColumn: n + i + 1];
		
		if ([delegate  respondsTo: @selector(browser:removeColumnIcon:for:)])
				[delegate  browser: self  removeColumnIcon:
						[pathCells  objectAt: i]  for: n + i + 1];
	}
	
	[columnMatrices  free];
	[pathCells  free];
	
	return self;
}


- reuseColumns: (BOOL) flag
{
	if (flag == YES)
			[super  reuseColumns: flag];
	
	return self;
}


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


/*

Under the right circumstances, the drawing performance of setPath: can be improved by disabling window drawing entirely.  With drawing disabled, one needs to redraw all the visible columns, but one avoids drawing columns and then scrolling them away.

*/


- (BOOL) _disableDrawingForPath: (const char *) path
{
	const unsigned char
		* p = path;
	unsigned char
		currentPath [10000],
		separator [2],
		* q = currentPath;
	int
		common = 0,
		rest = 0;
	
	[self  getPath: currentPath  toColumn: [self  lastColumn]];
	separator[0] = pathSeparator;
	separator[1] = '\0';
	strcat (q, separator);
	
	while (*q  &&  *q == *p)
		if (p++, *q++ == pathSeparator) common++;
	
	while (*p)
		if (*p++ == pathSeparator) rest++;
	
	return rest > [self  numVisibleColumns];
}


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


/*

When setting the path, the iconPath is not synchronized until after the complete path is loaded.  The performance improvement is significant.

*/


- setPath: (const char *) path
{
	int
		disable = [self  _disableDrawingForPath: path],
		n;
	id
		current = nil;
	
	[[self  setAutoSynchronize: NO]  beginUpdate];
	if (disable)
			[window  disableDisplay];
	
	[super	setPath: path];
	if ([self  lastColumn] == [self  selectedColumn]) [self  addColumn];
	[[self  setAutoSynchronize: YES]  synchronizePath];
	
	for (n = 0;  n <= [self  lastColumn];  n++)
	{
		current = [iconPath  cellAt: 0  : n];
		[current  setBranch: YES];
		if ([delegate  respondsTo: @selector(browser:setColumnIcon:for:)])
				[delegate  browser: self  setColumnIcon: current  for: n];
	}
	
	[current  setBranch: NO];
	[iconPath  update];
	if (disable)
	{
		[[scrollView  horizScroller]  update];
		[[window  reenableDisplay]  displayIfNeeded];
	}
	
	[self  endUpdate];
	
	return self;
}


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


/*

The action method below allows controls to set the browser path.  Nothing in the IconKit currently uses it, since requests to set the path typically pass through an IKBrowserManager.

*/


- takePathFrom: sender
{
	[self  setPath: [sender  stringValue]];
	return self;
}


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


/*

The method below determines how the browser repsonds when a column changes.  After the new data is loaded, the column either flushes everything to the right of the changed column or tries to reset itself to the indicated path.  The usual practice is for this path to be the one that was present before the column changed.

*/


- resetColumn: (int) n  usingPath: (const char *) path
{
	[window  disableFlushWindow];
	[self  reloadColumn: n];
	
	if (path) [self  setPath: path];
	else
	
			[[[[[self	beginUpdate]
						setLastColumn: n]
						matrixInColumn: n]
						selectCellAt: -1  : -1]
						endUpdate];
	
	[[window  reenableFlushWindow]  flushWindow];
	return self;
}


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


/*

Scroll messages pass through the IKBrowser so that it can respond accordingly.  When the scroll knob is dragged, the browser waits until the mouseUp event before it shifts to its new location.  The scroller is also reset so that the scrollView exactly encloses the current set of path icons.

Note that the NXBrowser fails to respond to page scrolling.  NeXT should probably fix this at some point.

The iconSelected: message is sent by the iconPath whenever an icon is clicked.  It scrolls the browser to the indicated column and sends out the browser's action message.

*/


- scrolling: sender
{
	if ([sender  hitPart] != NX_KNOB)
	{
		[window  disableFlushWindow];
		[iconPath  setAutodisplay: NO];
		
		[[self	scrollViaScroller: sender]
				synchronizePath];
		
		[iconPath  setAutodisplay: YES];
		[[window  reenableFlushWindow]  flushWindow];
	}
	
	else [scrollView  perform: scroll  with: sender];
	
	return self;
}


- iconSelected: sender
{
	[[[self		setAutoSynchronize: NO]
				beginUpdate]
				setLastColumn: [iconPath  selectedCol]];
	
	[[self matrixInColumn: [self  lastColumn]]  selectCellAt: -1  : -1];
	
	[[[[self	setAutoSynchronize: YES]
				synchronizePath]
				endUpdate]
				sendAction];
	
	return self;
}


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


/*

A selected leaf node gets its own column in an IKBrowser.  AutoSynchronize is disabled to avoid setting the icon path twice.  The delegate must be smart enough to insert the right icon and leave the column empty.

*/


- doClick: sender
{	
	[[self		setAutoSynchronize: NO]
				beginUpdate];
	
	[super  doClick: sender];
	if ([self  lastColumn] == [self  selectedColumn])
			[self  addColumn];
	
	[[[[self	setAutoSynchronize: YES]
				synchronizePath]
				newSelection]
				endUpdate];
	
	return self;
}


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


/*

The IKBrowser seems not to handle the arrow keys well using the standard NXBrowser methods, so keyDown: and keyUp: get new implementations that try to keep everything in sync.  Updating the browser is delayed after up and down arrow keys to allow rapid fire scrolling within browser columns.

*/

- keyDown: (NXEvent *) theEvent
{
	int
		r,
		c = [self  selectedColumn],
		n,
		m;
	id
		matrix;
	BOOL
		clickNow;
	
	switch (theEvent->data.key.charCode)
	{
		case LEFT:
				matrix = [self  matrixInColumn: c - 1];
				r = [matrix  selectedRow];
				clickNow = YES;
				break;
		
		case RIGHT:
				matrix = [self  matrixInColumn: c + 1];
				r = 0;
				clickNow = YES;
				break;
		
		case UP:
				[matrix = [self matrixInColumn: c] getNumRows: &n  numCols: &m];
				r = ([matrix  selectedRow] + n - 1) % n;
				clickNow = NO;
				break;
				
		case DOWN:
				[matrix = [self matrixInColumn: c] getNumRows: &n  numCols: &m];
				r = ([matrix  selectedRow] + 1) % n;
				clickNow = NO;
				break;
		
		default:
				return nil;
				break;
	}
	
	[[matrix  scrollCellToVisible: r  : 0]  selectCellAt: r  : 0];
	if (matrix && ([matrix  selectedRow] != -1) && clickNow)
			[self  doClick: matrix];
	
	return self;
}


- keyUp: (NXEvent *) theEvent
{
	id
		matrix;
	
	if ((theEvent->data.key.charCode == UP) ||
						(theEvent->data.key.charCode == DOWN))
	{
		matrix = [self  matrixInColumn: [self  selectedColumn]];
		if (matrix  && ([matrix  selectedRow] != -1))
				[self  doClick: matrix];
	}
	
	return self;
}


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


/*

The empty extra column that an IKBrowser puts in for a leaf selection messes up the selectAll: message, so that gets fixed here.

*/


- selectAll: sender
{
	int
		r,
		c;
	
	[window  disableFlushWindow];
	[[self  matrixInColumn: [self  lastColumn]]  getNumRows: &r  numCols: &c];
	
	if (r == 0)
			[self  setLastColumn: [self  lastColumn] - 1];
	
	[super  selectAll: sender];
	if ([self  lastColumn] == [self  selectedColumn])
			[self  addColumn];
	
	[window  reenableFlushWindow];
	[window  flushWindow];
	
	return self;
}


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


/*

The method below facilitates finding the selected objects in a browser.  It extracts the selected cell delegates in the indicated column.

*/


- getSelectionInColumn: (int) column
{
	id
		selection = nil,
		matrix = [self  matrixInColumn: column];
	int
		i;
	
	if (matrix != nil)
	{
		selection =
			[NXApp  conformsTo: @protocol(IB)] ?
				getSelectedCells (matrix, @selector(getSelectedCells:), nil):
				[matrix  getSelectedCells: nil];
		
		for (i = 0; i < [selection  count]; i++)
				[selection  replaceObjectAt: i
								with: [[selection  objectAt: i]  delegate]];
	}
	
	return selection;
}


@end
