//=============================================================================
//
//		Copyright (C) 1995-1997 by Paul S. McCarthy and Eric Sunshine.
//				Written by Paul S. McCarthy and Eric Sunshine.
//							All Rights Reserved.
//
//		This notice may not be removed from this source code.
//
//		This object is included in the MiscKit by permission from the authors
//		and its use is governed by the MiscKit license, found in the file
//		"License.rtf" in the MiscKit distribution.	Please refer to that file
//		for a list of all applicable permissions and restrictions.
//		
//=============================================================================
//-----------------------------------------------------------------------------
// MiscBorderView.M
//
//		View class for the row/column borders on a MiscTableView.
//		Supports resizing, dragging.
//
// NOTE: *COORDS*
//		COORDINATE SYSTEM RULES:
//
//		1) The BorderView and the TableView must *NOT* be scaled or rotated.
//				There is no reason to do so, and supporting this unnecessary
//				capability is complicated and slow.	 The entire TableScroll
//				can be scaled and/or rotated, but the BorderView and
//				TableView subviews should not be scaled and/or rotated
//				relative to the TableScroll.
//
//		2) This means that the only coordinate transformations that are
//				necessary when converting coordinates between the BorderView,
//				TableView and/or TableScroll are simple translations.
//
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// $Id: MiscBorderView.M,v 1.21 97/06/10 05:40:55 sunshine Exp $
// $Log:		MiscBorderView.M,v $
//	Revision 1.21  97/06/10	 05:40:55  sunshine
//	v127: Uses new MiscAssert.
//	
//	Revision 1.20  97/02/11	 19:13:58  sunshine
//	v123: reflectSelection --> selectionChanged
//	Now checks isAutodisplay in -selectionChanged.
//	
//	Revision 1.19  96/12/30	 16:19:08  zarnuk
//	-frameHeight now returns the appropriate component of the frame.
//	Added -setFrameHeight:.
//	Cleaned up some code in -toggleEvent.
//-----------------------------------------------------------------------------
#import "MiscBorderView.h"
#import "MiscAssert.h"
#import "MiscBorderCell.h"
#import "MiscGeometry.h"
#import "MiscHighlightTracker.h"
#import "MiscListTracker.h"
#import "MiscRadioTracker.h"
#import "MiscTableBorder.h"
#import "MiscTableScrollPrivate.h"
#import "MiscTableView.h"
#import "MiscTableWell.h"
#import <misckit/MiscTableScroll.h>
#import <misckit/MiscTableTypes.h>
#import <new.h>

extern "Objective-C" {
#import <appkit/Application.h>
#import <appkit/ClipView.h>
#import <appkit/Font.h>
#import <appkit/NXBitmapImageRep.h>
#import <appkit/NXCursor.h>
#import <appkit/NXImage.h>
#import <appkit/Text.h>
#import <appkit/Window.h>
#import <appkit/graphics.h>
#import <appkit/timer.h>
#import <objc/NXBundle.h>
#import <dpsclient/dpsNeXT.h>
#import <dpsclient/wraps.h>
}

extern "C" {
#import <float.h>
#import <limits.h>
#import <math.h>
#import <string.h>		// memset().
#import <stdio.h>		// FILENAME_MAX
}

static NXCoord MIN_TOGGLE_WIDTH = 5;
static NXCoord TOGGLE_WIDTH		= 5;

#define MISC_RESIZE_EPSILON		4
#define MISC_FRAME_HEIGHT		18
#define MISC_FRAME_WIDTH		46

static NXCursor* horzCursor		= 0;
static NXCursor* vertCursor		= 0;
static NXCursor* dragCursor		= 0;
static NXCursor* reverseCursor	= 0;

static NXImage* sortAscendImage = 0;
static NXImage* sortAscendHImage		= 0;
static NXImage* sortDescendImage		= 0;
static NXImage* sortDescendHImage		= 0;

static inline MiscPixels dmin( MiscPixels a, MiscPixels b )
										{ return (a < b ? a : b); }
static inline MiscPixels dmax( MiscPixels a, MiscPixels b )
										{ return (a > b ? a : b); }


//----------------------------------------------------------------------------
// startTimer
//----------------------------------------------------------------------------
static inline void startTimer( NXTrackingTimer*& timer )
	{
	if (timer == 0)
		timer = NXBeginTimer( 0, 0.1, 0.1 );
	}


//----------------------------------------------------------------------------
// stopTimer
//----------------------------------------------------------------------------
static inline void stopTimer( NXTrackingTimer*& timer )
	{
	if (timer)
		{
		NXEndTimer( timer );
		timer = 0;
		}
	}


//=============================================================================
// IMPLEMENTATION
//=============================================================================
@implementation MiscBorderView

//-----------------------------------------------------------------------------
// + findImageNamed:
//-----------------------------------------------------------------------------
+ (NXImage*)findImageNamed:(char const*)name
	{
	NXImage* image = [NXImage findImageNamed:name];
	if (image == 0)
		{
		char path[ FILENAME_MAX + 1 ];
		[[NXBundle bundleForClass:self]
				getPath:path forResource:name ofType:"tiff"];
		image = [[NXImage alloc] initFromFile:path];
		[image setName:name];
		}
	return image;
	}


//-----------------------------------------------------------------------------
// + cursorFromImageName:
//		NOTE: Cannot use [self zone] in a class (factory) method
//		like this one, hence no allocFromZone:.
//-----------------------------------------------------------------------------
+ (NXCursor*)cursorFromImageName:(char const*)name
	{
	NXImage* image = [self findImageNamed:name];
	NXPoint const hot = { 8, 8 };
	NXCursor* cursor = [[NXCursor alloc] initFromImage:image];
	[cursor setHotSpot:&hot];
	return cursor;
	}


//-----------------------------------------------------------------------------
// + initialize
//-----------------------------------------------------------------------------
+ (id)initialize
	{
	if (horzCursor == 0)
		{
		horzCursor = [self cursorFromImageName:"MiscHorzResizeCursor"];
		vertCursor = [self cursorFromImageName:"MiscVertResizeCursor"];
		dragCursor = [self cursorFromImageName:"MiscSlotDragCursor"	 ];
		reverseCursor = [self cursorFromImageName:"MiscReverseCursor"];

		NXPoint const hot = { 4, 0 };
		[reverseCursor setHotSpot:&hot];

		sortAscendImage	  = [self findImageNamed:"MiscSortAscend"  ];
		sortAscendHImage  = [self findImageNamed:"MiscSortAscendH" ];
		sortDescendImage  = [self findImageNamed:"MiscSortDescend" ];
		sortDescendHImage = [self findImageNamed:"MiscSortDescendH"];

		NXSize sz;
		[sortAscendImage getSize:&sz];
		TOGGLE_WIDTH = sz.width;
		if (TOGGLE_WIDTH < MIN_TOGGLE_WIDTH)
			TOGGLE_WIDTH = MIN_TOGGLE_WIDTH;
		}
	return self;
	}


//-----------------------------------------------------------------------------
// TYPE VARIATIONS
//-----------------------------------------------------------------------------

- (MiscPixels)frameHeight
	{
	NXRect rect;
	[self getFrame:&rect];
	return (MiscPixels) (isHorz ? rect.size.height : rect.size.width);
	}

- (void)setFrameHeight:(MiscPixels)x
	{
	NXRect rect;
	[self getFrame:&rect];
	if (isHorz)
		rect.size.height = x;
	else
		rect.size.width = x;
	[self setFrame:&rect];
	}

- (NXCursor*)cursor
		{ return isHorz ? horzCursor : vertCursor; }
- (MiscBorderType)borderType
		{ return isHorz ? MISC_COL_BORDER : MISC_ROW_BORDER; }
- (MiscTableBorder*)otherBorder
		{ return [scroll border:MISC_OTHER_BORDER([self borderType])]; }
- (void)clearOtherBorder { [self otherBorder]->selectNone(); }
- (id)cellAt:(MiscCoord_P)row :(MiscCoord_P)col
	{ return isHorz ? [scroll cellAt:row:col] : [scroll cellAt:col:row]; }

- (void)convertPointToScroll:(MiscPoint_O*)p_o
	{ NXPoint p = *p_o; [scroll convertPoint:&p fromView:self]; *p_o = p; }
- (void)convertOriginToScroll:(MiscRect_O*)r_o
	{ NXPoint p = *r_o; [scroll convertPoint:&p fromView:self]; *r_o = p; }


//-----------------------------------------------------------------------------
// - changeFrameIfNeeded
//		FIXME: The frame updates need to be untangled.	All requests should
//		go through the table-scroll object.	 Implementations should only
//		update themselves, not other objects.
//-----------------------------------------------------------------------------
- (void)changeFrameIfNeeded
	{
	MiscRect_O r( isHorz, frame );
	MiscPixels const my_width = r.getWidth_O();
	MiscPixels const i_width = info->totalSize();
	if (my_width != i_width)
		{
		[window invalidateCursorRectsForView:self];
		r.setWidth_O( i_width );
		[self sizeTo:r.getWidth() :r.getHeight()];
		[[scroll docView] adjustSize];
		}
	}


//-----------------------------------------------------------------------------
// - setPos:width:
//-----------------------------------------------------------------------------
- (void)setPos:(MiscCoord_V)pos width:(MiscPixels)size
	{
	MiscCoord_P pPos = info->visualToPhysical(pos);
	[scroll border:[self borderType] setSlot:pPos size:(NXCoord)size];
	[window invalidateCursorRectsForView:self];
	[scroll update];
	}


//-----------------------------------------------------------------------------
// - initFrame:scroll:info:type:
//-----------------------------------------------------------------------------
- (id)initFrame:(NXRect const*)frameRect
		 scroll:(MiscTableScroll*)i_scroll
		   info:(MiscTableBorder*)i_info
		   type:(MiscBorderViewType)type
	{
	NXZone* const z = [self zone];

	[super initFrame:0];
	[self setFlipped:YES];
	[self setOpaque:YES];
	[self setClipping:NO];

	togglePos = -1;
	isHorz = (type == MISC_COL_BORDER_VIEW);
	scroll = i_scroll;
	info = i_info;
	theCell = [[MiscBorderCell allocFromZone:z] initTextCell:"Kilroy"];

	oldSel = new( NXZoneMalloc(z,sizeof(*oldSel)) ) MiscSparseSet;
	[self setSelectionMode:[scroll selectionMode]];

	MiscRect_O myFrame( isHorz );
	if (frameRect != 0)
		myFrame = frameRect->origin;
	myFrame.setWidth_O( i_info->totalSize() );
	myFrame.setHeight_O( (isHorz ? MISC_FRAME_HEIGHT : MISC_FRAME_WIDTH) );
	NXRect r = myFrame;
	[self setFrame:&r];
	return self;
	}


//-----------------------------------------------------------------------------
// - free
//-----------------------------------------------------------------------------
- (id)free
	{
	[theCell free];
	[tracker free];
	if (oldSel != 0)
		{
		oldSel->MiscSparseSet::~MiscSparseSet();
		NXZoneFree( [self zone], oldSel );
		}
	return [super free];
	}



//=============================================================================
// CONVERSIONS
//=============================================================================
//-----------------------------------------------------------------------------
// - range::fromRect:
//
//		Returns by reference the range of visual slots contained in rect.
//		rMin is inclusive, rMax is exclusive.
//-----------------------------------------------------------------------------
- (void)range:(MiscCoord_V*)rMin :(MiscCoord_V*)rMax
	fromRect:(NXRect const*)rect
	{
	MiscRect_O r( isHorz, *rect );
	*rMin = info->visualForOffset( r.getX_O() );
	*rMax = info->visualForOffset( r.getMaxX_O() ) + 1;
	}


//-----------------------------------------------------------------------------
// - rect:forPos:
//-----------------------------------------------------------------------------
- (void)rect:(NXRect*)nxrect forPos:(MiscCoord_V)pos
	{
	NXRect nxBounds;
	[self getBounds:&nxBounds];
	MiscRect_O r( isHorz, nxBounds );
	r.setX_O( info->getOffset(pos) );
	r.setWidth_O( info->effectiveSize(pos) );
	*nxrect = r;
	}



//=============================================================================
// DRAWING
//=============================================================================
//-----------------------------------------------------------------------------
// - getVisibleRange::
//
//		Returns by reference the range of visual slots contained in the visible
//		rectangle.	rMin is inclusive, rMax is exclusive.
//-----------------------------------------------------------------------------
- (void)getVisibleRange:(MiscCoord_V*)rMin :(MiscCoord_V*)rMax
	{
	NXRect visRect;
	[superview getDocVisibleRect:&visRect];
	[self range:rMin:rMax fromRect:&visRect];
	}


//-----------------------------------------------------------------------------
// drewPos:
//
//		Keeps the old selection sets up to date whenever we draw a cell.  This
//		way -selectionChanged has valid data to work from.
//-----------------------------------------------------------------------------
- (void)drewPos:(MiscCoord_V)slot
	{
	BOOL isSelected = info->isSelected( slot );
	if (isSelected != oldSel->contains(slot))
		{
		if (isSelected)
			oldSel->add( slot );
		else
			oldSel->remove( slot );
		}
	}


//-----------------------------------------------------------------------------
// - drawPos:inRect:controlView:
//-----------------------------------------------------------------------------
- (void)drawPos:(MiscCoord_V)pos inRect:(NXRect const*)rect
	controlView:(View*)controlView
	{
	[theCell setStringValue:info->getTitle(pos)];
	[theCell setState:info->isSelected(pos)];

	NXImage* img = 0;
	if ([scroll autoSortSlots:MISC_OTHER_BORDER([self borderType])])
		if (info->isSortable(pos))
			if (info->getSortDirection(pos) == MISC_SORT_DESCENDING)
				img = togglePos == pos ? sortDescendHImage : sortDescendImage;
			else
				img = togglePos == pos ? sortAscendHImage : sortAscendImage;
	[theCell setImage:img];

	[theCell drawSelf:rect inView:controlView];
	}


//-----------------------------------------------------------------------------
// - drawPos:updateSel:
//-----------------------------------------------------------------------------
- (void)drawPos:(MiscCoord_V)pos updateSel:(BOOL)updateSel
	{
	if (0 <= pos && pos < info->count())
		{
		NXRect rect;
		[self rect:&rect forPos:pos];
		[self drawPos:pos inRect:&rect controlView:self];
		if (updateSel)
			[self drewPos:pos];
		}
	}


//-----------------------------------------------------------------------------
// - drawPos:
//-----------------------------------------------------------------------------
- (void)drawPos:(MiscCoord_V)pos
	{
	[self drawPos:pos updateSel:YES];
	}


//-----------------------------------------------------------------------------
// - drawRect:
//-----------------------------------------------------------------------------
- (void)drawRect:(NXRect const*)rect
	{
	MiscCoord_V pos_min;
	MiscCoord_V pos_max;
	[self range:&pos_min:&pos_max fromRect:rect];
	for (MiscCoord_V pos = pos_min;	 pos < pos_max;	 pos++)
		[self drawPos:pos];
	}


//-----------------------------------------------------------------------------
// - drawSelf::
//-----------------------------------------------------------------------------
- (id)drawSelf:(NXRect const*)rects :(int)nrects
	{
	if (nrects == 1)
		[self drawRect:rects];
	else if (nrects == 3)
		{
		[self drawRect:++rects];
		[self drawRect:++rects];
		}
	return self;
	}


//-----------------------------------------------------------------------------
// - drawSlot:	[public]
//-----------------------------------------------------------------------------
- (void)drawSlot:(MiscCoord_V)n
	{
	MiscCoord_V rMin, rMax;
	[self getVisibleRange:&rMin:&rMax];
	if (rMin <= n && n < rMax)
		{
		if ([self isAutodisplay] && [self canDraw])
			{
			[self lockFocus];
			[self drawPos:n];
			[self unlockFocus];
			}
		else
			{
			[self setNeedsDisplay:YES];
			}
		}
	}



//=============================================================================
// SELECTION
//=============================================================================
//-----------------------------------------------------------------------------
// - setSelectionMode:
//-----------------------------------------------------------------------------
- (void)setSelectionMode:(MiscSelectionMode)mode
	{
	NXZone* const z = [self zone];
	if (tracker)
		[tracker free];
	switch (mode)
		{
		case MISC_LIST_MODE:
			tracker = [MiscListTracker allocFromZone:z];
			break;
		case MISC_RADIO_MODE:
			tracker = [MiscRadioTracker allocFromZone:z];
			break;
		case MISC_HIGHLIGHT_MODE:
			tracker = [MiscHighlightTracker allocFromZone:z];
			break;
		}
	[tracker initBorder:info];
	}


//-----------------------------------------------------------------------------
// - selectionChanged
//-----------------------------------------------------------------------------
- (void)selectionChanged
	{
	MiscSparseSet const& newSel = info->selectionSet();

	if ([self isAutodisplay] && [self canDraw])
		{
		MiscCoord_V rMin, rMax;
		[self getVisibleRange:&rMin:&rMax];
	
		int locked = 0;
		for (MiscCoord_V i = rMin;	i < rMax;  i++)
			{
			if (oldSel->contains(i) != newSel.contains(i))
				{
				if (!locked)
					{
					locked = 1;
					[window disableFlushWindow];
					[self lockFocus];
					}
				[self drawPos:i updateSel:NO];
				}
			}
		if (locked)
			{
			[self unlockFocus];
			[[window reenableFlushWindow] flushWindow];
			}
		}
	else
		[self setNeedsDisplay:YES];

	*oldSel = newSel;
	}


//-----------------------------------------------------------------------------
// - resetSelection
//-----------------------------------------------------------------------------
- (void)resetSelection
	{
	*oldSel = info->selectionSet();
	}


//-----------------------------------------------------------------------------
// - selectPos:
//-----------------------------------------------------------------------------
- (void)selectPos:(MiscCoord_V)pos
	{
	info->selectOne( pos );
	[scroll selectionChanged];
	}



//=============================================================================
// CURSOR MANAGEMENT
//=============================================================================
//-----------------------------------------------------------------------------
// - toggleRect:forPos:visible:
//-----------------------------------------------------------------------------
- (BOOL)toggleRect:(NXRect*)nxrect forPos:(MiscCoord_V)pos
	visible:(NXRect const*)vis
	{
	MiscPixels const pos_width = info->effectiveSize(pos);
	if (pos_width > 0)
		{
		NXCoord const w = TOGGLE_WIDTH + MISC_RESIZE_EPSILON;
		[self rect:nxrect forPos:pos];
		nxrect->origin.x += nxrect->size.width - w;
		nxrect->size.width = w;
		if (NXIntersectionRect( vis, nxrect ) != 0)
			return YES;
		}
	return NO;
	}


//-----------------------------------------------------------------------------
// - resizeRect:forPos:visible:
//-----------------------------------------------------------------------------
- (BOOL)resizeRect:(NXRect*)nxrect forPos:(MiscCoord_V)pos
	visible:(NXRect const*)vis
	{
	MiscPixels const pos_width = info->effectiveSize(pos);
	if (pos_width > 0)
		{
		MiscPixels const pos_offset = info->getOffset(pos);
		MiscPixels const min_x = pos_offset + pos_width - MISC_RESIZE_EPSILON;
		MiscRect_O r( isHorz, bounds );
		r.setX_O( min_x );
		r.setWidth_O( MISC_RESIZE_EPSILON );
		*nxrect = r;
		if (NXIntersectionRect( vis, nxrect ) != 0)
			return YES;
		}
	return NO;
	}


//-----------------------------------------------------------------------------
// - resetCursorRects
//-----------------------------------------------------------------------------
- (id)resetCursorRects
	{
	BOOL const draggable = info->isDraggable();
	BOOL const sizeable = info->isSizeable();
	NXRect vis;
	[self getVisibleRect:&vis];

	if (draggable && !info->isModifierDrag())
		[self addCursorRect:&vis cursor:dragCursor];

	if ([scroll autoSortSlots:MISC_OTHER_BORDER([self borderType])])
		{
		MiscCoord_V min_pos, max_pos;
		[self getVisibleRange:&min_pos:&max_pos];
		int const count = info->count();
		if (0 <= min_pos && min_pos < count)
			{
			NXRect rect;
			int const lim = (max_pos <= count) ? max_pos : count;
			for (MiscCoord_V pos = min_pos; pos < lim; pos++)
				if (info->isSortable(pos) &&
					[self toggleRect:&rect forPos:pos visible:&vis])
					[self addCursorRect:&rect cursor:reverseCursor];
			}
		}

	if (sizeable)
		{
		NXCursor* const resizeCursor = [self cursor];
		MiscCoord_V min_pos, max_pos;
		[self getVisibleRange:&min_pos:&max_pos];
		int const count = info->count();
		if (0 <= min_pos && min_pos < count)
			{
			NXRect rect;
			int const lim = (max_pos <= count) ? max_pos : count;
			for (MiscCoord_V pos = min_pos; pos < lim; pos++)
				if (info->isSizeable(pos) &&
					[self resizeRect:&rect forPos:pos visible:&vis])
					[self addCursorRect:&rect cursor:resizeCursor];
			}
		}

	return self;
	}



//=============================================================================
// MOUSE-TRACKING
//=============================================================================
//-----------------------------------------------------------------------------
// - posForMousePt:
//-----------------------------------------------------------------------------
- (MiscCoord_V)posForMousePt:(NXPoint const*)p
	{
	MiscPixels pix = MiscPoint_O( isHorz, *p ).getX_O();
	MiscRect_O r( isHorz, bounds );
	return (pix < r.getMaxX_O() ? info->visualForOffset( pix ) : info->count());
	}



//=============================================================================
// RESIZING
//=============================================================================
//-----------------------------------------------------------------------------
// - resizeEvent:x:deltaX:minX:maxX:
//-----------------------------------------------------------------------------
- (int)resizeEvent:(NXEvent const*)p
		x:(MiscPixels)x
		deltaX:(MiscPixels)deltaX
		minX:(MiscPixels)minX
		maxX:(MiscPixels)maxX
	{
	NXRect nxDocFrame;	[scroll getDocClipFrame:&nxDocFrame];
	NXRect nxClipFrame; [superview getFrame:&nxClipFrame];
	MiscRect_O docFrame( isHorz, nxDocFrame );
	MiscRect_O clipFrame( isHorz, nxClipFrame );
	NXCoord const minDrawX = (NXCoord)clipFrame.getX_O();	 // scroll coords
	NXCoord const maxDrawX = (NXCoord)clipFrame.getMaxX_O(); // scroll coords

	x += deltaX;

	NXCoord draw_x;				// scroll coords
	MiscRect_O line( isHorz );
	line.setX_O( x - 1 );
	line.setY_O( 0 );
	line.setWidth_O( 2 );
	line.setHeight_O( clipFrame.getHeight_O() + docFrame.getHeight_O() - 1 );
	[self convertOriginToScroll:&line];
	draw_x = line.getX_O();

	BOOL did_scroll = NO;
	BOOL in_bounds = (minDrawX <= draw_x && draw_x <= maxDrawX);

	[scroll lockFocus];
	PSsetinstance(YES);
	NXSetColor( NX_COLORBLACK );

	if (in_bounds)
		{
		NXRect r = line;
		NXRectFill( &r );
		}

	NXTrackingTimer* timer = 0;
	startTimer( timer );
	NXEvent lastEvent = *p;

	while (1)
		{
		p = [NXApp getNextEvent:NX_MOUSEUPMASK |
								NX_MOUSEDRAGGEDMASK |
								NX_TIMERMASK ];
		if (p == 0 || p->type == NX_MOUSEUP)
			break;
		if (p->type == NX_TIMER)
			{
			NXPoint mousePt = lastEvent.location;
			[scroll convertPoint:&mousePt fromView:0];
			MiscPixels const mousePtX = MiscPoint_O( isHorz,mousePt ).getX_O();
			if (mousePtX < minDrawX || maxDrawX < mousePtX)
				{
				PSsetinstance(NO);
				[self autoscroll:&lastEvent];
				PSsetinstance(YES);
				did_scroll = YES;
				}
			}
		else
			lastEvent = *p;

		NXPoint new_loc = lastEvent.location;
		[self convertPoint:&new_loc fromView:0];
		MiscPixels mouseX = MiscPoint_O( isHorz, new_loc ).getX_O();
		MiscPixels new_x = (mouseX + deltaX);
		if (new_x < minX)
			new_x = minX;
		else if (new_x >= maxX)
			new_x = maxX - 1;

		BOOL const did_move = (new_x != x);
		if (did_move || did_scroll)
			{
			if (in_bounds && !did_scroll)
				PShideinstance( line.getX(), line.getY(),
								line.getWidth(), line.getHeight() );
			if (did_move)
				{
				x = new_x;
				line.setX_O( x - 1 );
				line.setY_O( 0 );
				[self convertOriginToScroll:&line];
				draw_x = line.getX_O();
				in_bounds = (minDrawX <= draw_x && draw_x <= maxDrawX);
				}

			if (in_bounds)
				{
				NXRect r = line;
				NXRectFill( &r );
				}
			did_scroll = NO;
			}
		}

	stopTimer( timer );

	if (in_bounds)
		PShideinstance( line.getX(), line.getY(),
								line.getWidth(), line.getHeight() );

	PSsetinstance(NO);
	[scroll unlockFocus];

	x -= deltaX;
	return x;
	}


//-----------------------------------------------------------------------------
// - resizeEvent:inPos:atX:deltaX:finalWidth:
//-----------------------------------------------------------------------------
- (BOOL)resizeEvent:(NXEvent const*)p
		 inPos:(MiscCoord_V)pos
		 atX:(MiscPixels)x_origin
		 deltaX:(MiscPixels)delta_x
		 finalWidth:(MiscPixels*)finalWidth
	{
	MiscPixels const org_x = info->getOffset(pos);
	MiscPixels const min_x = org_x + info->effectiveMinSize(pos);
	MiscPixels const max_x = org_x + info->effectiveMaxSize(pos) + 1;
	MiscPixels const curr_x =
		[self resizeEvent:p x:x_origin deltaX:delta_x minX:min_x maxX:max_x];
	MiscPixels final_delta = curr_x - x_origin;
	if (final_delta != 0)
		{
		*finalWidth = info->effectiveSize(pos) + final_delta;
		return YES;
		}

	return NO;
	}


//-----------------------------------------------------------------------------
// - inResizeZone:forPos:atX:deltaX:
//-----------------------------------------------------------------------------
- (BOOL)inResizeZone:(NXPoint const*)pt
		forPos:(MiscCoord_V*)pos
		atX:(MiscPixels*)pos_x
		deltaX:(MiscPixels*)delta_x
	{
	if (info->isSizeable())
		{
		MiscCoord_V const plim = info->count();
		MiscPixels x = MiscPoint_O( isHorz, *pt ).getX_O();
		MiscCoord_V p = info->visualForOffset(x);
		if (0 <= p && p < plim)
			{
			MiscPixels max_x = info->getOffset(p) + info->effectiveSize(p);
			MiscPixels delta = max_x - x;
		
			if (0 <= delta && delta <= MISC_RESIZE_EPSILON)
				{
				do { p++; } while (p < plim && info->effectiveSize(p) <= 0);
				p--;
				if (0 <= p && p < plim && info->isSizeable(p))
					{
					*pos = p;
					*pos_x = x;
					*delta_x = delta;
					return YES;
					}
				}
			}
		}

	return NO;
	}


//-----------------------------------------------------------------------------
// - adjustSize
//-----------------------------------------------------------------------------
- (void)adjustSize
	{
	[self changeFrameIfNeeded];
	}



//=============================================================================
// DRAGGING
//=============================================================================
//-----------------------------------------------------------------------------
// draw_view
//		Draw a view hierarchy without calling lockFocus so that it can be
//		drawn onto a different window.	Used for drawing the drag cache
//		window.
//-----------------------------------------------------------------------------
static void draw_view( View* v, NXRect const* r )
	{
	// FIXME: maybe clip, maybe manually transform graphics state coord matrix.
	[v drawSelf:r:1];
	List* subs = [v subviews];
	unsigned int lim = (subs ? [subs count] : 0);
	for (unsigned int i = 0; i < lim; i++)
		{
		View* sub = (View*) [subs objectAt:i];
		NXRect frame;
		[sub getFrame:&frame];
		if (NXIntersectionRect( r, &frame ) != 0)
			{
			[sub convertRect:&frame fromView:v];
			draw_view( sub, &frame );
			}
		}
	}


//-----------------------------------------------------------------------------
// - dragCacheForPos:
//
// NOTE *COORDS*
//		The TableScroll, BorderView, and TableView all share the the same 
//		scaling and rotation.  Also, the coordinate system of the TableView 
//		is always anchored at the same "X" offset as the BorderView, so that 
//		(orientation corrected) "X" coordinates and widths can be exchanged 
//		between the two views without further adjustments.	
//-----------------------------------------------------------------------------
- (NXImage*)dragCacheForPos:(MiscCoord_V)pos
	{
	NXImage* cache = [[NXImage allocFromZone:[self zone]] init];
	MiscTableView* tableView = (MiscTableView*) [scroll docView];

	NXRect nxBorder;	[self rect:&nxBorder forPos:pos];
	NXRect nxTable;		[tableView getVisibleRect:&nxTable];

	MiscRect_O oBorder( isHorz, nxBorder );
	MiscRect_O oTable( isHorz, nxTable );

	MiscSize_O oCache( isHorz );
	oCache.setWidth_O( oBorder.getWidth_O() );
	oCache.setHeight_O( oBorder.getHeight_O() + oTable.getHeight_O() );
	NXSize nxCache = oCache;
	[cache setSize:&nxCache];

	if (oTable.getX_O() <= oBorder.getX_O() &&			// Is entire slot
		oBorder.getMaxX_O() <= oTable.getMaxX_O())		// visible?
		{
		MiscRect_O oScroll( isHorz );
		oScroll.setX_O( oBorder.getX_O() );
		oScroll.setY_O( oBorder.getY_O() );
		oScroll.setWidth_O( oBorder.getWidth_O() );
		oScroll.setHeight_O( oCache.getHeight_O() );
		NXRect nxScroll = oScroll;

		[scroll convertRect:&nxScroll fromView:self];

		[scroll lockFocus];
		NXBitmapImageRep* rep = [[NXBitmapImageRep allocFromZone:[self zone]]
										initData:0 fromRect:&nxScroll];
		[scroll unlockFocus];
		[cache useRepresentation:rep];
		}
	else
		{
		[cache setFlipped:YES];
		[cache lockFocus];

		oTable.setX_O( oBorder.getX_O() );				// NOTE *COORDS*
		oTable.setWidth_O( oBorder.getWidth_O() );
		nxTable = oTable;

		MiscPoint_O oDelta( isHorz, nxTable.origin );
		oDelta.setY_O( oDelta.getY_O() - oBorder.getHeight_O() );
		NXPoint delta = oDelta;

		View* focusView = [NXApp focusView];

		[focusView translate:-delta.x:-delta.y];
		draw_view( tableView, &nxTable );
		[focusView translate:delta.x:delta.y];

		[focusView translate:-nxBorder.origin.x:-nxBorder.origin.y];
		draw_view( self, &nxBorder );
		[focusView translate:nxBorder.origin.x:nxBorder.origin.y];

		[cache unlockFocus];
		}

	return cache;
	}


//-----------------------------------------------------------------------------
// - getVisibleCacheMin:max:
//-----------------------------------------------------------------------------
- (NXImage*)getVisibleCacheMin:(MiscPixels*)pMin max:(MiscPixels*)pMax
	{
	NXRect nxDoc;		[[scroll docView] getVisibleRect:&nxDoc];
	NXRect nxVis;		[self getVisibleRect:&nxVis];
	MiscRect_O rDoc( isHorz );
	MiscRect_O rVis( isHorz, nxVis );
	*pMin = rVis.getX_O();
	*pMax = rVis.getMaxX_O();

	[scroll convertRect:&nxDoc fromView:[scroll docView]];
	[scroll convertRect:&nxVis fromView:self];
	rDoc = nxDoc;
	rVis = nxVis;

	MiscRect_O r( isHorz );
	r.setX_O( rVis.getX_O() );
	r.setY_O( rVis.getY_O() );
	r.setWidth_O( rVis.getWidth_O() );
	r.setHeight_O( rVis.getHeight_O() + rDoc.getHeight_O() );

	NXRect nxr = r;
	[scroll lockFocus];
	NXBitmapImageRep* rep = [[NXBitmapImageRep allocFromZone:[self zone]]
								initData:0 fromRect:&nxr];
	[scroll unlockFocus];
	NXImage* cache = [[NXImage allocFromZone:[self zone]] initSize:&nxr.size];
	[cache useRepresentation:rep];
	return cache;
	}


//-----------------------------------------------------------------------------
// - setWells::forPos:
//-----------------------------------------------------------------------------
- (void)setWells:(MiscTableWell**)w1 :(MiscTableWell**)w2
	forPos:(MiscCoord_V)pos
	{
	NXZone* const z = [self zone];
	MiscTableView* doc = [scroll docView];
	NXRect nxDoc;		[doc getVisibleRect:&nxDoc];
	NXRect nxClip;		[superview getFrame:&nxClip];
	MiscRect_O rDoc( isHorz, nxDoc );
	MiscRect_O rClip( isHorz, nxClip );

	MiscRect_O r( isHorz );
	r.setX_O( info->getOffset(pos) );
	r.setWidth_O( info->effectiveSize(pos) );
	r.setHeight_O( rClip.getHeight_O() );

	NXRect nxr = r;
	*w1 = [[MiscTableWell allocFromZone:z] initFrame:&nxr];
	[self addSubview:*w1];

	r.setY_O( rDoc.getY_O() );
	r.setHeight_O( rDoc.getHeight_O() );
	nxr = r;
	*w2 = [[MiscTableWell allocFromZone:z] initFrame:&nxr];
	[doc addSubview:*w2];

	[*w1 display];
	[*w2 display];
	}


//-----------------------------------------------------------------------------
// - clearWells::
//-----------------------------------------------------------------------------
- (void)clearWells:(MiscTableWell**)w1 :(MiscTableWell**)w2
	{
	[*w1 removeFromSuperview];
	[*w2 removeFromSuperview];
	[*w1 free];
	[*w2 free];
	*w1 = 0;
	*w2 = 0;
	// NOTE: Does not need display here.  Everything will get displayed later.
	}


//-----------------------------------------------------------------------------
// - offsetFromEvent:
//-----------------------------------------------------------------------------
- (NXCoord)offsetFromEvent:(NXEvent const*)ev
	{
	NXPoint mLoc = ev->location;
	[self convertPoint:&mLoc fromView:0];
	return (NXCoord) MiscPoint_O( isHorz, mLoc ).getX_O();
	}


//-----------------------------------------------------------------------------
// - calcDrop::
//-----------------------------------------------------------------------------
- (MiscCoord_V)calcDrop :(MiscCoord_V)fromPos
						:(NXPoint const*)mouseDownPt
						:(NXPoint const*)mouseUpPt
	{
	MiscCoord_V toPos = fromPos;
	MiscPixels const start_pos = MiscPoint_O( isHorz, *mouseDownPt ).getX_O();
	MiscPixels const end_pos   = MiscPoint_O( isHorz, *mouseUpPt   ).getX_O();
	MiscPixels const delta_pos = (end_pos - start_pos);

	MiscPixels const SLOP = 4;
	if (delta_pos < -SLOP || SLOP < delta_pos)
		{
		MiscPixels const start_ofs = info->getOffset( fromPos );
		MiscPixels drop_pos = start_ofs + delta_pos;
		if (delta_pos < 0)
			drop_pos += SLOP;
		else
			drop_pos += info->effectiveSize(fromPos) - SLOP;
		toPos = info->visualForOffset( drop_pos );
		if (toPos < 0)
			toPos = 0;
		}

	return toPos;
	}


//-----------------------------------------------------------------------------
// - dragEvent:inPos:
//-----------------------------------------------------------------------------
- (MiscCoord_V)dragEvent:(NXEvent*)event inPos:(MiscCoord_V)pos
	{
	int const WANTED = (NX_MOUSEUPMASK | NX_MOUSEDRAGGEDMASK | NX_TIMERMASK);
	NXPoint mouseDownPt = event->location;

	[self convertPoint:&mouseDownPt fromView:0];
	NXPoint mouseUpPt = mouseDownPt;

	[scroll disableCursor];
	[window flushWindow];
	[window disableFlushWindow];

	NXImage* dragCache = [self dragCacheForPos:pos];

	MiscTableWell *w1, *w2;
	[self setWells:&w1 :&w2 forPos:pos];

	MiscPixels pMin,pMax;
	NXImage* visCache = [self getVisibleCacheMin:&pMin max:&pMax];

	NXSize nxSize;
	NXPoint nxPoint;
	NXRect nxRect;

	[dragCache getSize:&nxSize];
	MiscSize_O size( isHorz, nxSize );
	MiscPixels pLoc = info->getOffset( pos );
	MiscPixels delta = MiscPixels( [self offsetFromEvent:event] - pLoc );

	NXTrackingTimer* timer = 0;
	NXEvent lastEvent = *event;
	[scroll lockFocus];
	while (1)
		{
		MiscPoint_O pt( isHorz );
		MiscRect_O rDrag( isHorz );
		MiscPixels const w = size.getWidth_O();
		MiscPixels const dw =
			dmin( dmin( dmin(w, pMax - pMin), pMax - pLoc), pLoc + w - pMin );
		BOOL const shouldDraw = (dw > 0.0);
		if (shouldDraw)
			{
			if (isHorz)
				{
				pt.setX( dmax( pLoc, pMin ) );
				pt.setY( size.getHeight_O() );
				if (pLoc < pMin)
					rDrag.setX( pMin - pLoc );
				}
			else
				{
				pt.setX( 0 );
				pt.setY( dmax( pLoc, pMin ) + dw );
				if (pLoc + w >= pMax)
					rDrag.setY( pLoc + w - pMax );
				}
			[self convertPointToScroll:&pt];
			nxPoint = pt;
			rDrag.setWidth_O( dw );
			rDrag.setHeight_O( size.getHeight_O() );
			nxRect = rDrag;
			[dragCache composite:NX_COPY fromRect:&nxRect toPoint:&nxPoint];
			}
		[[window reenableFlushWindow] flushWindow];

		event = [NXApp getNextEvent:WANTED];

		[window disableFlushWindow];
		if (shouldDraw)
			{
			NXSize s; [visCache getSize:&s];
			MiscPixels xTarg;
			if (isHorz)
				{
				xTarg = (pLoc < pMin ? 0 : pLoc - pMin);
				}
			else
				{
				if (pLoc < pMin)
					xTarg = MiscPixels(s.height) - dw;
				else if (pLoc < pMax)
					xTarg = pMax - pLoc - dw;
				else
					xTarg = 0;
				}
			MiscRect_O rVis( isHorz );
			rVis.setX_O( xTarg );
			rVis.setWidth_O( rDrag.getWidth_O() );
			rVis.setHeight_O( rDrag.getHeight_O() );
			nxRect = rVis;
			nxPoint = pt;
			[visCache composite:NX_COPY fromRect:&nxRect toPoint:&nxPoint];
			}

		if (event == 0)
			break;
		if (event->type == NX_MOUSEUP)
			{
			mouseUpPt = event->location;
			[self convertPoint:&mouseUpPt fromView:0];
			break;
			}
		if (event->type != NX_TIMER)
			lastEvent = *event;

		MiscPixels mLoc = MiscPixels( [self offsetFromEvent:&lastEvent] );
		if ((mLoc < pMin && pMin > 0.0) ||
			(mLoc > pMax && pMax < MiscRect_O( isHorz, bounds ).getMaxX_O()))
			{
			[self autoscroll:&lastEvent];
			[visCache free];
			visCache = [self getVisibleCacheMin:&pMin max:&pMax];
			mLoc = MiscPixels( [self offsetFromEvent:&lastEvent] );
			startTimer( timer );
			}
		else
			{
			stopTimer( timer );
			}

		pLoc = mLoc - delta;
		if (pLoc < pMin - size.getWidth_O())
			pLoc = pMin - size.getWidth_O();
		else if (pLoc > pMax)
			pLoc = pMax;
		}

	stopTimer( timer );
	[scroll unlockFocus];
	[self clearWells:&w1 :&w2];
	[visCache free];
	[dragCache free];

	MiscCoord_V const toPos = [self calcDrop:pos :&mouseDownPt :&mouseUpPt];

	if (toPos != pos)
		{
		BOOL const was_auto = [scroll isAutodisplay];
		[scroll setAutodisplay:NO];
		[scroll border:[self borderType] moveSlotFrom:pos to:toPos];
		[window invalidateCursorRectsForView:self];
		[scroll border:[self borderType] slotDraggedFrom:pos to:toPos];
		[scroll reenableCursor];
		[scroll update];
		[scroll setAutodisplay:was_auto];		// Will display if needed.
		}
	else
		{
		// Need to redisplay the slot that the wells were covering.
		MiscBorderType const b = [self borderType];
		int const phys_pos = [scroll border:b slotAtPosition:toPos];
		[scroll border:b drawSlotTitle:phys_pos];
		[scroll border:b drawSlot:phys_pos];
		[scroll reenableCursor];
		}

	[[window reenableFlushWindow] flushWindow];
	return toPos;
	}


//-----------------------------------------------------------------------------
// - awaitDragEvent:inPos:
//-----------------------------------------------------------------------------
- (MiscCoord_V)awaitDragEvent:(NXEvent const*)event inPos:(MiscCoord_V)pos
	{
	MiscCoord_V toPos = pos;
	int const WANTED = (NX_MOUSEUPMASK | NX_MOUSEDRAGGEDMASK);
	float const FOREVER = FLT_MAX;
	float const SLOP = 2.0;
	NXEvent mouseDown = *event;
	for (;;)
		{
		NXEvent peek;
		NXEvent* p = [NXApp peekNextEvent:WANTED into:&peek waitFor:FOREVER
							threshold:NX_MODALRESPTHRESHOLD];
		if (p == 0 || p->type == NX_MOUSEUP)
			break;
		else if (p->location.x > mouseDown.location.x + SLOP ||
				 p->location.y > mouseDown.location.y + SLOP ||
				 p->location.x < mouseDown.location.x - SLOP ||
				 p->location.y < mouseDown.location.y - SLOP)
			{
			toPos = [self dragEvent:&mouseDown inPos:pos];
			break;
			}
		else
			[NXApp getNextEvent:NX_MOUSEDRAGGEDMASK];
		}
	return toPos;
	}
	


//=============================================================================
// TOGGLE SORT DIRECTION
//=============================================================================
//-----------------------------------------------------------------------------
// - toggleRect:forPos:
//-----------------------------------------------------------------------------
- (void)toggleRect:(NXRect*)r forPos:(MiscCoord_V)pos
	{
	[self rect:r forPos:pos];
	NXCoord const w = TOGGLE_WIDTH + MISC_RESIZE_EPSILON;
	r->origin.x = floor( r->origin.x + r->size.width - w );
	r->size.width = w;
	}


//-----------------------------------------------------------------------------
// - inToggleZone:forPos:
//-----------------------------------------------------------------------------
- (BOOL)inToggleZone:(NXPoint const*)pt
		forPos:(MiscCoord_V)pos
	{
	if ([scroll autoSortSlots:MISC_OTHER_BORDER([self borderType])] &&
		info->isSortable(pos))
		{
		NXRect r;
		[self toggleRect:&r forPos:pos];
		return r.origin.x <= pt->x && pt->x <= (r.origin.x + r.size.width);
		}
	return NO;
	}


//-----------------------------------------------------------------------------
// - toggleEvent:forPos:
//-----------------------------------------------------------------------------
- (void)toggleEvent:(NXEvent const*)p forPos:(MiscCoord_V)pos
	{
	NXRect toggleZone;
	BOOL was_in_zone = YES;
	BOOL is_in_zone = YES;

	[self toggleRect:&toggleZone forPos:pos];

	togglePos = pos;
	[self lockFocus];
	[self drawPos:pos];
	[window flushWindow];
	DPSFlush();

	while (1)
		{
		p = [NXApp getNextEvent:NX_MOUSEUPMASK |
								NX_MOUSEDRAGGEDMASK ];

		if (p == 0)
			break;

		NXPoint new_loc = p->location;
		[self convertPoint:&new_loc fromView:0];
		is_in_zone = NXPointInRect( &new_loc, &toggleZone );
		if (was_in_zone != is_in_zone)
			{
			was_in_zone = is_in_zone;
			togglePos = (is_in_zone ? pos : -1);
			[self drawPos:pos];
			[window flushWindow];
			DPSFlush();
			}

		if (p->type == NX_MOUSEUP)
			break;
		}

	togglePos = -1;

	[self unlockFocus];

	if (is_in_zone)
		{
		BOOL const was_auto = [scroll isAutodisplay];
		[scroll setAutodisplay:NO];
		MiscBorderType const bt = [self borderType];
		MiscSortDirection dir = info->getSortDirection( pos );
		dir = MISC_OTHER_DIRECTION( dir );
		MiscCoord_P p_pos = info->visualToPhysical( pos );
		[scroll border:bt setSlot:p_pos sortDirection:dir];
		[scroll border:bt slotSortReversed:p_pos];
		[scroll update];
		[scroll setAutodisplay:was_auto];
		}
	}



//=============================================================================
// SELECTION
//=============================================================================
//-----------------------------------------------------------------------------
// - adjustCursor:
//-----------------------------------------------------------------------------
- (void)adjustCursor:(MiscCoord_V)pos
	{
	if (pos < 0)
		pos = 0;
	else if (pos >= info->count())
		pos = info->count() - 1;
	[scroll border:[self borderType] setCursor:info->visualToPhysical(pos)];
	}


//-----------------------------------------------------------------------------
// - selectionEvent:fromPos:
//-----------------------------------------------------------------------------
- (void)selectionEvent:(NXEvent const*)p fromPos:(MiscCoord_V)pos
	{
	BOOL doubleClicked = (p->data.mouse.click > 1);

	[scroll disableCursor];
	[self clearOtherBorder];
	[tracker mouseDown:p atPos:pos];
	[scroll selectionChanged];

	NXTrackingTimer* timer = 0;
	startTimer( timer );
	NXEvent lastEvent = *p;

	while (1)
		{
		p = [NXApp getNextEvent:NX_MOUSEUPMASK |
								NX_MOUSEDRAGGEDMASK |
								NX_TIMERMASK ];

		if (p == 0 || p->type == NX_MOUSEUP)
			break;
		else if (p->type == NX_TIMER)
			[self autoscroll:&lastEvent];
		else
			lastEvent = *p;

		NXPoint new_loc = lastEvent.location;
		[self convertPoint:&new_loc fromView:0];
		MiscCoord_V new_pos = [self posForMousePt:&new_loc];
		if (new_pos != pos)
			{
			pos = new_pos;
			[tracker mouseDragged:p atPos:pos];
			[scroll selectionChanged];
			}
		}

	stopTimer( timer );

	[tracker mouseUp:p atPos:pos];
	[scroll selectionChanged];
	[self adjustCursor:pos];
	[scroll reenableCursor];
	[[self window] flushWindow];

	if ([scroll isEnabled])
		{
		[scroll sendAction];
		if (doubleClicked)
			[scroll sendDoubleAction];
		}
	}



//=============================================================================
// MOUSE-EVENTS
//=============================================================================
//-----------------------------------------------------------------------------
// - acceptsFirstMouse
//-----------------------------------------------------------------------------
- (BOOL)acceptsFirstMouse
	{
	return YES;
	}


//-----------------------------------------------------------------------------
// - mouseDown:
//-----------------------------------------------------------------------------
- (id)mouseDown:(NXEvent*)p
	{
	[scroll abortEditing];		// Force cell-editing to end.

	NXPoint evpt = p->location;
	[self convertPoint:&evpt fromView:0];
	MiscCoord_V pos = [self posForMousePt:&evpt];
	int old_mask = [window addToEventMask:NX_MOUSEDRAGGEDMASK];
	MiscPixels x, delta_x;

	if ([self inResizeZone:&evpt forPos:&pos atX:&x deltaX:&delta_x])
		{
		MiscPixels finalWidth;
		BOOL doit;
		doit = [self resizeEvent:p inPos:pos atX:x deltaX:delta_x
				finalWidth:&finalWidth];
		if (doit)
			{
			[window disableFlushWindow];
			[self setPos:pos width:finalWidth];
			[scroll border:[self borderType] slotResized:pos];
			[[window reenableFlushWindow] flushWindow];
			}
		}
	else if ([self inToggleZone:&evpt forPos:pos])
		{
		[self toggleEvent:p forPos:pos];
		}
	else if (info->isDraggable() &&
		(info->isModifierDrag() == ((p->flags & NX_COMMANDMASK) != 0)))
		{
		[self awaitDragEvent:p inPos:pos];
		}
	else
		{
		MiscAssert( tracker != 0 );
		[self selectionEvent:p fromPos:pos];
		}

	[window setEventMask:old_mask];
	return self;
	}

@end
