
/*
	Copyright 1993  Jeremy Slade.

	You are free to use all or any parts of the Locus project
	however you wish, just give credit where credit is due.
	The author (Jeremy Slade) shall not be held responsible
	for any damages that result out of use or misuse of any
	part of this project.

*/

/*
	Project: Locus
	
	Class: GroupBrowserMatrix
	
	Description: See GroupBrowserMatrix.h
	
	Original Author: Jeremy Slade
	
	Revision History:
		Created
			V.101	JGS Wed Feb  3 23:47:02 GMT-0700 1993

*/

#import "GroupBrowserMatrix.h"

#import "Dragging.h"
#import "FolderController.h"
#import "Globals.h"
#import "Group.h"
#import "ItemCell.h"
#import "ItemList.h"

#import <dpsclient/psops.h>
#import <dpsclient/wraps.h>


@implementation GroupBrowserMatrix


// -------------------------------------------------------------------------
//   Stuff used by mouseDown for control-dragging 
// -------------------------------------------------------------------------


// Cache windows shared by all GroupBrowserMatrices
static id matrixCache, cellCache;

#define MOVE_MASK NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK

#define startTimer(timer) if (!timer) timer = NXBeginTimer(NULL, 0.1, 0.01);

#define stopTimer(timer) if (timer) { \
    NXEndTimer(timer); \
    timer = NULL; \
}


// -------------------------------------------------------------------------
//   Creating, initializing methods
// -------------------------------------------------------------------------


+ initialize
{
	[self setVersion:GroupBrowserMatrix_VERSION];
	return ( self );
}



// -------------------------------------------------------------------------
//   Loading the Group
// -------------------------------------------------------------------------


- setGroup:group
/*
	'Loads' the Group (a subclass of List) by replacing our cellList with it.  Also resets selectedCell, etc.   Returns the old cellList.
*/
{
	id oldList = cellList;
	NXSize size;
	DrawInfo drawInfo;
		
	cellList = group;

	selectedCell = nil;
	selectedRow = -1;
	selectedCol = -1;
	
	numRows = [cellList count];
	numCols = 1;
	
	[group getDrawInfo:&drawInfo]; [ItemCell setDrawInfo:&drawInfo];
	[[group objectAt:0] calcCellSize:&size];
	size.width = bounds.size.width;
	[[self setCellSize:&size] sizeToCells];
	
	return ( oldList );
}



// -------------------------------------------------------------------------
//   Responding to mouseDown events
// -------------------------------------------------------------------------


- mouseDown:(NXEvent *)theEvent
/*
	On the first click, and if no meta keys are used, it determines which cell was clicked and sends it a startTrackingAt:inView: message.  This makes it possible for the cells to track the mouse even when in NX_LISTMODE.
	If the user control-clicks a cell, it allows them to drag the cell to a new location.
*/
{
	NXPoint pt;
	int row, col;
	NXRect cellFrame;
	id clickedCell;
	
	// Allow cells to track mouseDown events
	if ( !theEvent->flags ) {
		pt = theEvent->location;
		[self convertPoint:&pt fromView:nil];
		[self getRow:&row andCol:&col forPoint:&pt];
		clickedCell = [self cellAt:row :0];
		[self selectCell:clickedCell];
		[self getCellFrame:&cellFrame at:row :0];
		if ( [clickedCell mouseDownAt:&pt frame:&cellFrame inView:self] ) {
			[self drawCellInside:clickedCell];
			return ( self );
		}
	}
	
	// Check for control-click
	if ( (theEvent->flags & NX_CONTROLMASK)
			&& !(theEvent->flags & NX_SHIFTMASK) ) {
		[self controlDragMouse:theEvent];
		return ( self );
	}
	
	// Check for alternate-click
	if ( (theEvent->flags & NX_ALTERNATEMASK) ) {
		[self alternateDragMouse:theEvent];
		return ( self );
	}
		
	// Just act like normal
	return ( [super mouseDown:theEvent] );
}



- controlDragMouse:(NXEvent *)theEvent
/*
	Called when the user Control-clicks on a cell.  This allows the cells to be reordered by dragging them to a new position.
*/
{
	NXPoint pt;
	int row, col, newRow;
	NXRect cellFrame, visibleRect, cellCacheBounds;
	int eventMask;
	float	dy;
	NXEvent	*event = NULL, peek;
	NXTrackingTimer *timer = NULL;
	BOOL scrolled = NO;
	id group;

	// Prepare the cell and matrix cache windows
	[self setupCacheWindows];
	
	// Tell the window to receive mouse-dragged events
	eventMask = [window addToEventMask:NX_MOUSEDRAGGEDMASK];
	
	// Find the cell that got clicked and select it
	pt = theEvent->location;
	[self convertPoint:&pt fromView:nil]; //Convert from window's base coords
	[self getRow:&row andCol:&col forPoint:&pt];
	activeCell = [self cellAt:row :0];
	[self selectCell:activeCell];
	[self getCellFrame:&cellFrame at:row :0];
	
	// Do whatever's required for a single-click
	[self sendAction];
	
	// draw a 'well' in place of the selected cell (see drawSelf::)
	[window disableFlushWindow];
	[self lockFocus];
	[[self drawSelf:&cellFrame :1] unlockFocus];
	[window reenableFlushWindow];
	
	// Copy what's currently visible into the matrix cache
	[[matrixCache contentView] lockFocus];
	[self getVisibleRect:&visibleRect];
	[self convertRect:&visibleRect toView:nil];
	PScomposite ( NX_X(&visibleRect), NX_Y(&visibleRect),
		NX_WIDTH(&visibleRect), NX_HEIGHT(&visibleRect),
		[window gState], 0.0, NX_HEIGHT(&visibleRect), NX_COPY );
	[[matrixCache contentView] unlockFocus];

	// Image the cell into its cache
	[[cellCache contentView] lockFocus];
	[[cellCache contentView] getBounds:&cellCacheBounds];
	[activeCell drawSelf:&cellCacheBounds inView:[cellCache contentView]];
	[[cellCache contentView] unlockFocus];
	
  	// save the mouse's location relative to the cell's origin
	dy = pt.y - cellFrame.origin.y;
	
	// All further drawing will be done in self
	[self lockFocus];
	
	event = theEvent;
	while ( event->type != NX_MOUSEUP ) { // Loop until mouse-up event
	
		// Erase the active cell using inmage in matrix cache
		[self getVisibleRect:&visibleRect];
		PScomposite ( NX_X(&cellFrame), NX_HEIGHT(&visibleRect) -
			NX_Y(&cellFrame) + NX_Y(&visibleRect) -
		    NX_HEIGHT(&cellFrame), NX_WIDTH(&cellFrame),
		    NX_HEIGHT(&cellFrame), [matrixCache gState],
		    NX_X(&cellFrame), NX_Y(&cellFrame) + NX_HEIGHT(&cellFrame),
		    NX_COPY);
			
		// Move the active cell
		pt = event->location;
		[self convertPoint:&pt fromView:nil];
		cellFrame.origin.y = pt.y - dy;
		
		// Constrain the cell's location to our bounds
		if ( NX_Y(&cellFrame) < NX_X(&bounds) )
		    cellFrame.origin.y = NX_X(&bounds);
		else if ( NX_MAXY(&cellFrame) > NX_MAXY(&bounds) )
		    cellFrame.origin.y = NX_HEIGHT(&bounds) - NX_HEIGHT(&cellFrame);

		// Make sure the cell will be entirely visible in its new location
		if ( !NXContainsRect(&visibleRect,&cellFrame) && mFlags.autoscroll ) {
		
		    [window disableFlushWindow];
		    [self scrollRectToVisible:&cellFrame];
		    [window reenableFlushWindow];

			// Copy the new image to the matrix cache
			[[matrixCache contentView] lockFocus];
			[self getVisibleRect:&visibleRect];
			[self convertRect:&visibleRect toView:nil];
			PScomposite ( NX_X(&visibleRect), NX_Y(&visibleRect),
				NX_WIDTH(&visibleRect), NX_HEIGHT(&visibleRect),
				[window gState], 0.0, NX_HEIGHT(&visibleRect), NX_COPY );
			[[matrixCache contentView] unlockFocus];

			// Note that we scrolled and start generating timer events
			// for autoscrolling
			scrolled = YES;
			startTimer ( timer );
		} else {
			// No scrolling, so stop any timer
			stopTimer ( timer );
		}
	
		// Composite the active cell's image on top
		PScomposite(0.0, 0.0, NX_WIDTH(&cellFrame), NX_HEIGHT(&cellFrame),
			[cellCache gState], NX_X(&cellFrame),
			NX_Y(&cellFrame) + NX_HEIGHT(&cellFrame), NX_COPY);
		
		// Now flush the display
		[window flushWindow];
	
		// If we autoscrolled, flush any lingering window server events to
		// make the scrolling smooth
		if ( scrolled ) {
			NXPing();
			scrolled = NO;
		}
	
		// Save the location, just in case we need it again
		pt = event->location;
	
		if ( ![NXApp peekNextEvent:MOVE_MASK into:&peek] ) {
			// No mouseMoved, mouseUp event avail, so take mouseMoved,
			// mouseUp, or timer
			event = [NXApp getNextEvent:MOVE_MASK|NX_TIMERMASK];
		} else {
			// get the mouseMoved or mouseUp event in the queue
			event = [NXApp getNextEvent:MOVE_MASK];
		}
	
		// If a timer event, mouse location isn't valid, so set it
		if ( event->type == NX_TIMER )
			event->location = pt;
	}
	
	// mouseUp, so stop any timer and unlock focus
	stopTimer ( timer );
	[self unlockFocus];
	
	// Find the cell under under the mouse's location
	pt = event->location;
	[self convertPoint:&pt fromView:nil];
	if ( ![self getRow:&newRow andCol:&col forPoint:&pt] ) {
		// Mouse is out of bounds, so find the cell the active cell covers
		[self getRow:&newRow andCol:&col forPoint:&cellFrame.origin];
	}
	
	// we need to shuffle cells if the active cell's going to a new location
	if ( newRow != row ) {
		// No autodisplay while we move cells around
		[self setAutodisplay:NO];

		// Move the item to it's new location within the group
		group = cellList;
		[group setChanged:YES];
		[group removeObject:activeCell];
		[group insertObject:activeCell at:newRow];
		
		// if the active cell is selected, note its new row
		if ( [activeCell state] )
			selectedRow = newRow;
			
		// make sure the active cell's visible if we're autoscrolling
		if ( mFlags.autoscroll ) {
			[self scrollCellToVisible:newRow :0];
		}
		
		// size to cells after all this shuffling and turn autodisplay back on
		[[self sizeToCells] setAutodisplay:YES];
		
	}
	
	// No longer dragging the cell
	activeCell = nil;
	
	// Now redraw ourself
	[self display];
	
	// set the event mask mask to normal
	[window setEventMask:eventMask];
	
	return ( self );
}



- alternateDragMouse:(NXEvent *)theEvent
/*
	Sent when the user alternate-clicks on a cell.  This allows the selected items to be dragged out of this window to another folder or another application.
*/
{
	id clickedCell;
	char *files = NULL, *buf;
	NXPoint pt, offs = { 24,24 };
	int row, col;
	NXStream *stream;
	int len, mlen;
	int i, count, k;
	NXImage *img;
	id	pboard;	

	// Determine which cell the click happened in
	pt = theEvent->location;
	[self convertPoint:&pt fromView:nil];
	[self getRow:&row andCol:&col forPoint:&pt];
	clickedCell = [self cellAt:row :0];
	
	// Make sure this cell is selected, then redraw
	[clickedCell setState:1];
	[self drawCellInside:clickedCell];
	
	// behave as a single click
	if ( !(theEvent->flags & NX_SHIFTMASK ) ) [self sendAction];
	
	// Get list of all selected items, and put them into
	// tab-separated string
	stream = NXOpenMemory ( NULL, 0, NX_WRITEONLY );
	if ( !stream ) { // Couldn't open stream
		NXBeep();
		return ( self );
	}
	
	k = 0; // This is the first item in the list
	for ( i=0, count=[cellList count]; i<count; i++ ) {
		if ( [[cellList objectAt:i] state] ) {
			if ( k>0 ) NXPutc ( stream, '\t' );
			NXPrintf ( stream, "%s", [[cellList objectAt:i] path] );
			k++;
		}
	}
	
	// Get pointer to list
	NXGetMemoryBuffer ( stream, &buf, &len, &mlen );
	if ( buf ) files = NXCopyStringBuffer ( buf );
	
	// Close the stream
	NXCloseMemory ( stream, NX_FREEBUFFER );
	
	// Now drag the files
	if ( files ) {
		
		// Initiate a dragging sesssion
		pt.x -= 24.0;	// Set pt to be center of icon
		pt.y += 24.0;	
		if ( k > 1 )
			img = [NXImage findImageNamed:"multiple"]; // Multiple Items
		else
			img = [[self selectedCell] image]; // Single Item
		pboard = [Pasteboard newName:NXDragPboard];
		[pboard declareTypes:dragInTypes num:DRAGINTYPES owner:self];
		[pboard writeType:NXFilenamePboardType data:files length:len];
		[self dragImage:img at:&pt offset:&offs
			event:theEvent pasteboard:pboard source:self slideBack:YES];
		
	}
	
	if ( files ) NX_FREE ( files );
	return ( self );
}



// -------------------------------------------------------------------------
//   Making selections
// -------------------------------------------------------------------------


- selectAll:sender
{
	id ret = [super selectAll:sender];
	[self determineSelectionCount];
	return ( ret );
}


- selectCellAt:(int)row :(int)col
{	
	id ret = [super selectCellAt:row :col];
	[self determineSelectionCount];
	return ( ret );
}


- selectCell:aCell
{
	id ret = [super selectCell:aCell];
	[self determineSelectionCount];
	return ( ret );
}


- selectedCell
/*
	Returns the currently selected cell.  If there isn't one selected, or there is more than one, returns nil. Updates selectionCount to hold the number of cells selected
*/
{
	// Returns selectedCell only if a single cell is selected
	if ( selectionCount == 1 ) return ( selectedCell );
		else return ( nil );
}


- determineSelectionCount
{
	register int i, count;
	
	// Reset selectionCount
	selectionCount = 0;
	
	// Get the number of selected cells
	for ( i=0, count=[self cellCount]; i<count; i++ )
		if ( [[self cellAt:i :0] state] ) selectionCount++;

	return ( self );
}


- selection
/*
	Acts like selectedCell, but if multiple items are selected, it returns the selection list ( see -selectionList)
*/
{
	static id list = nil;
	
	if ( list ) {
		[list free];
		list = nil;
	}
	if ( selectionCount == 1 ) return ( selectedCell );
		else return ( [(list = [self selectionList]) count] ? list : nil );
}


- selectionList
/*
	Create an ItemList object and fill it with the items that are selected.  Caller must free.
*/
{
	id theList = [[ItemList alloc] initCount:selectionCount], item;
	int i, count;
	for ( i=0, count=[self cellCount]; i<count; i++ )
		if ( [(item = [self cellAt:i :0]) state] ) [theList addObject:item];
	return ( theList );
}


- (int)selectionCount
{
	return ( selectionCount );
}



// -------------------------------------------------------------------------
//  Drawing the matrix
// -------------------------------------------------------------------------


- drawSelf:(NXRect *)rects :(int)count
/*
	Draws a 'well' in place of the cell being dragged
*/
{
	int row, col;
	NXRect cellBorder;
	int sides[] = {	NX_XMIN, NX_YMIN, NX_XMAX, NX_YMAX,
				NX_XMIN, NX_YMIN};
	float grays[] = {NX_DKGRAY, NX_DKGRAY, NX_WHITE,
				NX_WHITE, NX_BLACK, NX_BLACK};
			   
	[super drawSelf:rects :count];
    
	// draw a "well" if the user's dragging a cell
	if ( activeCell ) {
		//get the cell's frame
		[self getRow:&row andCol:&col ofCell:activeCell];
		[self getCellFrame:&cellBorder at:row :col];
      
		// draw the well
		if ( NXIntersectsRect ( &cellBorder, &(rects[0] ) ) ) {
			NXDrawTiledRects ( &cellBorder, (NXRect *)0, sides, grays, 6 );
			PSsetgray ( 0.17 );
			NXRectFill ( &cellBorder );
		}
	}
    
	// Flush windowserver events to smooth scrolling
	NXPing();

	return ( self );
}



- scrollCellToVisible:(int)row :(int)col
/*
	Override default scrolling behavior to make it scroll the cell to the middle of the visible rectangle
*/
{
	NXRect visibleRect, cellFrame;
	
	[self getVisibleRect:&visibleRect];
	[self getCellFrame:&cellFrame at:row :col];
	
	if ( !NXContainsRect ( &visibleRect, &cellFrame ) ) {
		cellFrame.origin.y -= NX_HEIGHT(&visibleRect) / 2;
		cellFrame.size.height += NX_HEIGHT(&visibleRect);
	}
	
	return ( [self scrollRectToVisible:&cellFrame] );
}



- sizeToCells
{
	NXRect newFrame;
	
	[super sizeToCells];
	
	// If in a ClipView, don't size any smaller than ClipView's width
	if ( [superview isKindOf:[ClipView class]] ) {
		[superview getDocVisibleRect:&newFrame];
		newFrame.origin = frame.origin;
		newFrame.size.height = frame.size.height;
		newFrame.size.width = (newFrame.size.width > frame.size.width)
			? newFrame.size.width : frame.size.width;
		[self setFrame:&newFrame];
	}
	
	return ( self );
}



// -------------------------------------------------------------------------
//   Cache windows used to speed drawing
// -------------------------------------------------------------------------


- setupCacheWindows
/*
	Create and size the cache windows
*/
{
	NXRect visibleRect;
	
	// Create the matrix cache window
	[self getVisibleRect:&visibleRect];
	matrixCache = [self sizeCacheWindow:matrixCache to:&visibleRect.size];
	
	// Create the cell cache window
	cellCache = [self sizeCacheWindow:cellCache to:&cellSize];
	
	return ( self );
}



- sizeCacheWindow:cacheWindow to:(NXSize *)windowSize
/*
	Create ( if cahceWindow = nil ) and size the specified cache window
*/
{
	NXRect cacheFrame;
	
	if ( !cacheWindow ) {
	
		// Create the cache window if it doesn't exist
		cacheFrame.origin.x = cacheFrame.origin.y = 0.0;
		cacheFrame.size = *windowSize;
		cacheWindow = [[[Window alloc] initContent:&cacheFrame
			style:NX_PLAINSTYLE
			backing:NX_RETAINED
			buttonMask:0
			defer:NO] reenableDisplay];
		// Flip the contentView since we are flipped
		[[cacheWindow contentView] setFlipped:YES];
		
	} else {
	
		// Make sure the cache window's the right size
		[cacheWindow getFrame:&cacheFrame];
		if ( NX_WIDTH(&cacheFrame) != windowSize->width || NX_HEIGHT(&cacheFrame) != windowSize->height )
			[cacheWindow sizeWindow:windowSize->width :windowSize->height];
			
	}
	
	return ( cacheWindow );
}



// -------------------------------------------------------------------------
//   NXDraggingSource Protocol
// -------------------------------------------------------------------------


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



@end

