File: programming/cocoa/UKDockableView.zip/UKDockableWindow/UKDockableView.m


// =============================================================================
//  UKDockableView.m
//  UKDockableWindow
//
//  Created by Uli Kusterer on Tue Feb 03 2004.
//  Copyright (c) 2004 M. Uli Kusterer. All rights reserved.
//
//  PURPOSE:
//		UKDockableView attempts to provide advanced window management with a
//		simple user interface. Some people prefer applications with one large
//		window that contains everything they work with. Others prefer having
//		lots of small windows they can move around and resize.
//
//		Since in many cases there isn't really any "right way" to do it, this
//		class intends to allow splitting each NSindow into several smaller
//		"windows". These smaller parts can be grabbed and dragged out of the
//		NSWindow they are in. Drop them in another window and they end up there,
//		drop them on the desktop and you get a new NSWindow containing only this
//		one "window".
//
//		This allows "tearing apart" a window and re-assembling it as one sees
//		fit.
//
//	DIRECTIONS:
//		UKDockableView is a container view. For each of the "windows" your
//		application has, you create one UKDockableView that contains the views
//		that should be in that "window".
//		Then distribute those UKDockableViews over all the NSWindows you want
//		the user to initially have. Usually, these are several windows, but
//		there'd still be several UKDockableViews in most NSWindows.
// =============================================================================
 
// -----------------------------------------------------------------------------
//	Headers:
// -----------------------------------------------------------------------------
 
#import "UKDockableView.h"
#import "NSApplicationWindowAtPoint.h"
#import "NSViewViewIntersectingRect.h"
 
#if UKDOCKABLEVIEW_SUPPORT_METAL
#import "NSBezierPathRoundRects.h"
#endif
 
 
// -----------------------------------------------------------------------------
//	Class variables / Globals:
// -----------------------------------------------------------------------------
 
NSWindow*		gUKDockableViewOverlayWindow = nil; // Window to use for dragging around a view.
NSWindow*		gUKDockableViewTargetWindow = nil;  // Window the mouse is over while dragging a view (not counting gUKDockableViewOverlayWindow).
NSPoint			gUKDockableViewDragPosition;		// Mouse position while we're dragging the view.
 
 
@implementation UKDockableView
 
// -----------------------------------------------------------------------------
//	* DESIGNATED INITIALIZER:
// -----------------------------------------------------------------------------
 
-(id)   initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
	{
		titleEdge = NSMinXEdge;
    }
    return self;
}
 
 
// -----------------------------------------------------------------------------
//	calculateTitlebar:content:drawbox:inRect:
//		Calculate some rectangles for the various areas of the view.
//		Except for inRect: rect, all parameters are return values.
// -----------------------------------------------------------------------------
 
-(void)   calculateTitlebar: (NSRect*)titlebar content: (NSRect*)contentbox drawbox: (NSRect*)drawbox
				inRect: (NSRect)rect
{
	*drawbox = rect;
	
	drawbox->origin.x += 0.5; drawbox->origin.y += 0.5;
	drawbox->size.width -= 1; drawbox->size.height -= 1;
	
	NSDivideRect( *drawbox, titlebar, contentbox, UKDOCKABLEVIEW_TITLE_BAR_SIZE, titleEdge );
}
 
 
// -----------------------------------------------------------------------------
//	drawRect:
//		Draw this view's look to make it obvious to the user that it can be
//		dragged, and what controls will be dragged out.
// -----------------------------------------------------------------------------
 
-(void) drawRect:(NSRect)rect
{
	NSRect		drawbox,
				contentbox,
				titlebar;
	#if UKDOCKABLEVIEW_SUPPORT_METAL
	BOOL		roundCorners = ([[self window] styleMask] & NSTexturedBackgroundWindowMask) == NSTexturedBackgroundWindowMask;
	#endif
	
	[self calculateTitlebar: &titlebar content: &contentbox drawbox: &drawbox
				inRect: [self bounds]];
	
	// Content background:
	[[NSColor colorWithCalibratedWhite: 0.5 alpha: 0.1] set];
	#if UKDOCKABLEVIEW_SUPPORT_METAL
	if( roundCorners )
		[NSBezierPath fillRoundRectInRect: drawbox radius: UKDOCKABLEVIEW_CORNER_RADIUS];
	else
	#endif
		[NSBezierPath fillRect: drawbox];
	
	// Title bar:
    [[NSColor colorWithCalibratedRed:0.4 green:0.4 blue:0.6 alpha: 0.1] set];
	#if UKDOCKABLEVIEW_SUPPORT_METAL
	if( roundCorners )
		[NSBezierPath fillRoundRectInRect: titlebar radius: UKDOCKABLEVIEW_CORNER_RADIUS];
	else
	#endif
		[NSBezierPath fillRect: titlebar];
	
	// Titlebar widgets:
	NSImage*	grippy = [NSImage imageNamed: @"grippy"];
	NSPoint		imgpos = titlebar.origin;
	imgpos.y += titlebar.size.height -[grippy size].height;
	[grippy compositeToPoint: imgpos operation: NSCompositeSourceOver];
	
	// Content box:
	[[NSColor grayColor] set];
	#if UKDOCKABLEVIEW_SUPPORT_METAL
	if( roundCorners )
		[NSBezierPath strokeRoundRectInRect: drawbox radius: UKDOCKABLEVIEW_CORNER_RADIUS];
	else
	#endif
		[NSBezierPath strokeRect: drawbox];
}
 
 
// -----------------------------------------------------------------------------
//	mouseDown:
//		Allow dragging the view out of its window.
// -----------------------------------------------------------------------------
 
-(void)		mouseDown: (NSEvent*)evt
{
	NSRect		drawbox,
				contentbox,
				titlebar;
	NSPoint		pos;
	
	[self calculateTitlebar: &titlebar content: &contentbox drawbox: &drawbox
				inRect: [self bounds]];
	
	gUKDockableViewDragPosition = [evt locationInWindow];
	pos = [self convertPoint: gUKDockableViewDragPosition fromView: nil];
	if( (pos.x < titlebar.origin.x)
		|| (pos.y < titlebar.origin.y)
		|| (pos.x > (titlebar.origin.x +titlebar.size.width))
		|| (pos.y > (titlebar.origin.y +titlebar.size.height)) )
		return;
	
	if( !gUKDockableViewOverlayWindow )
	{
		[self lockFocus];
		NSBitmapImageRep*   bir = [[[NSBitmapImageRep alloc] initWithFocusedViewRect: [self bounds]] autorelease];
		[self unlockFocus];
		
		NSImage*			img = [[[NSImage alloc] init] autorelease];
		NSRect				box = [self frame];
		box.origin = [self convertPoint: NSMakePoint(0,0) toView: nil];
		NSPoint		diff = [[self window] contentRectForFrameRect: [[self window] frame]].origin;
		box.origin.x += diff.x;
		box.origin.y += diff.y;
		
		[img addRepresentation: bir];
		
		gUKDockableViewOverlayWindow = [[NSWindow alloc] initWithContentRect: box styleMask: NSBorderlessWindowMask
																backing:NSBackingStoreBuffered defer:YES];
		NSImageView*		imgVw = [[[NSImageView alloc] initWithFrame: [self bounds]] autorelease];
		[imgVw setImage: img];
		[[gUKDockableViewOverlayWindow contentView] addSubview: imgVw];
		
		if( [gUKDockableViewOverlayWindow respondsToSelector: @selector(setAlphaValue:)] )
			[gUKDockableViewOverlayWindow setAlphaValue: 0.8];
		[gUKDockableViewOverlayWindow orderFront: nil];
		
		[[self window] setAcceptsMouseMovedEvents: YES];
	}
}
 
 
// -----------------------------------------------------------------------------
//	mouseDragged:
//		I should probably rename this. This catches mouseDragged: events to the
//		application while the view image is being dragged to move the view.
// -----------------------------------------------------------------------------
 
-(void)		mouseDragged: (NSEvent*)evt
{
	NSPoint		pos,
				globalPos;
	NSRect		winBox;
	
	if( !gUKDockableViewOverlayWindow )
		return;
	
	winBox = [[self window] contentRectForFrameRect: [[self window] frame]];
	pos = [evt locationInWindow];
	globalPos.x = pos.x +winBox.origin.x;
	globalPos.y = pos.y +winBox.origin.y;
	
	if( gUKDockableViewOverlayWindow
		&& gUKDockableViewDragPosition.x != pos.x || gUKDockableViewDragPosition.y != pos.y )
	{
		NSRect				box = [gUKDockableViewOverlayWindow frame];
		
		box.origin.x += pos.x -gUKDockableViewDragPosition.x;
		box.origin.y += pos.y -gUKDockableViewDragPosition.y;
		
		gUKDockableViewTargetWindow = [NSApp windowAtPoint: globalPos ignoreWindow: gUKDockableViewOverlayWindow];
		
		[gUKDockableViewOverlayWindow setFrame: box display: YES];
		
		gUKDockableViewDragPosition = pos;
	}
}
 
 
 
// -----------------------------------------------------------------------------
//	mouseUp:
//		I should probably rename this. This catches mouseUp: events to the
//		application while the view image is being dragged to terminate the drag.
// -----------------------------------------------------------------------------
 
-(void) mouseUp: (NSEvent*)evt
{
	[[self window] setAcceptsMouseMovedEvents: NO];
	
	if( !gUKDockableViewOverlayWindow )
		return;
	
	NSPoint		diff;
	NSRect		newbox = [gUKDockableViewOverlayWindow contentRectForFrameRect: [gUKDockableViewOverlayWindow frame]],
				oldbox = [[self window] contentRectForFrameRect: [[self window] frame]];
	diff = [self convertPoint: NSMakePoint(0,0) toView: nil];
	oldbox.origin.x += diff.x;
	oldbox.origin.y += diff.y;
 
	diff.x = newbox.origin.x -oldbox.origin.x;
	diff.y = newbox.origin.y -oldbox.origin.y;
	
	if( diff.x < 0 )
		diff.x = -diff.x;
	if( diff.y < 0 )
		diff.y = -diff.y;
 
	if( diff.x > 16 || diff.y > 16 )		// User moved it a substantial distance and didn't just mis-click?
	{
		NSWindow*			oldWin = [self window];
		NSWindow*			newWin = nil;
		NSView*				collisionView = nil;
		if( gUKDockableViewTargetWindow )
			newWin = gUKDockableViewTargetWindow;
		else
			newWin = [[NSWindow alloc] initWithContentRect: newbox
												styleMask: [oldWin styleMask]
												backing:NSBackingStoreBuffered defer:YES];
		NSWindowController* wc = [oldWin windowController];
		NSDocument*			doc = [wc document];
		NSRect				newBox = NSMakeRect(0,0,[self frame].size.width,[self frame].size.height);
		NSRect				screenBox = [[newWin screen] frame];
		float				nextRowStartY = -1;
		
		while( (collisionView = [[newWin contentView] subviewIntersectingRect: newBox ignoring: self]) )
		{
			if( nextRowStartY == -1 )
				nextRowStartY = NSMaxY([collisionView frame]);
			newBox.origin.x = NSMaxX([collisionView frame]);
			if( (newBox.origin.x +newBox.size.width) > screenBox.size.width )
			{
				newBox.origin.x = 0;
				newBox.origin.y = nextRowStartY;
				nextRowStartY = -1;
			}
		}
		
		[self retain];
		[self removeFromSuperview];
		[[newWin contentView] addSubview: self];
		[self setFrameOrigin: newBox.origin];
		[self release];
		
		NSSize  oldSuperSize = [[newWin contentView] frame].size,
				minSuperSize = [[newWin contentView] subviewsCombinedSize];
		if( oldSuperSize.width > minSuperSize.width )
			minSuperSize.width = oldSuperSize.width;
		if( oldSuperSize.height > minSuperSize.height )
			minSuperSize.height = oldSuperSize.height;
		
		[newWin setContentSize: minSuperSize];
		
		// If there's a document for the old window, create a new window controller of the same class as the old window's and add it to that document:
		if( !gUKDockableViewTargetWindow )
		{
			if( wc )
			{
				NSWindowController*		myWC = [[[[wc class] alloc] initWithWindow: newWin] retain];
				
				if( doc )
				{
					[doc addWindowController: myWC];
					[myWC synchronizeWindowTitleWithDocumentName];
				}
				else
					[newWin setTitle: [oldWin title]];
				
				if( [[[oldWin contentView] subviews] count] == 0 )
					[wc close];
			}
			else
			{
				[newWin setTitle: [oldWin title]];
				if( [[[oldWin contentView] subviews] count] == 0 )
					[oldWin close];
			}
		}
		else
		{
			if( [[[oldWin contentView] subviews] count] == 0 )
				[oldWin close];
		}
		
		[newWin makeKeyAndOrderFront: nil];
	}
	
	[gUKDockableViewOverlayWindow release];
	gUKDockableViewOverlayWindow = nil;
	gUKDockableViewTargetWindow = nil;
}
 
 
// -----------------------------------------------------------------------------
//	mouseDownCanMoveWindow:
//		Otherwise our brushed-metal window moves when our drag area is clicked.
//
//		I wanted to make this depend on whether the user clicked the content or
//		the drag area of this view, but sadly this is called only once when the
//		view is added to a window.
// -----------------------------------------------------------------------------
 
-(BOOL) mouseDownCanMoveWindow
{
	return NO;
	
	/*NSRect		drawbox,
				contentbox,
				titlebar;
	NSPoint		pos = [[[self window] currentEvent] locationInWindow];
	
	pos = [self convertPoint: pos fromView: nil];
	[self calculateTitlebar: &titlebar content: &contentbox drawbox: &drawbox
				inRect: [self bounds]];
	return( (pos.x < titlebar.origin.x)
			|| (pos.y < titlebar.origin.y)
			|| (pos.x > (titlebar.origin.x +titlebar.size.width))
			|| (pos.y > (titlebar.origin.y +titlebar.size.height)) );*/	
}
 
 
// -----------------------------------------------------------------------------
//	titleEdge:
//		Accessor to find out on which edge this view will draw its dragging
//		area and the little grippy-thingie.
// -----------------------------------------------------------------------------
 
-(NSRectEdge)	titleEdge
{
    return titleEdge;
}
 
// -----------------------------------------------------------------------------
//	setTitleEdge:
//		Mutator to specify on which edge this view will draw its dragging
//		area and the little grippy-thingie. NSMinXEdge (left) is the default,
//		and NSMaxYEdge (top) is a sensible alternative. Avoid the right edge
//		because that's where the scrollbar usually is, and *never* use the
//		bottom edge because it feals really weird to be dragging something by
//		its bottom. Just MHO.
// -----------------------------------------------------------------------------
 
-(void)	setTitleEdge: (NSRectEdge)newTitleEdge
{
	titleEdge = newTitleEdge;
	[self setNeedsDisplay: YES];
}
 
 
 
@end

This code uses the PclZip Zip File reading code, which is subject to the GNU LGPL. It also uses the GeSHi syntax highlighter, subject to the GPL. Ask if you want this for your own web site, it's free.