ftp.nice.ch/pub/next/developer/resources/palettes/PAScrollViewDeluxe.s.tar.gz#/PAScrollViewDeluxePalette/PAScrollViewDeluxe.m

This is PAScrollViewDeluxe.m in view mode; [Download] [Up]

// Format: 80 columns, tabs = 4 spaces
#import "PAScrollViewDeluxe.h"
#import "Ruler.h"

/******************************************************************************	
	PAScrollViewDeluxe
	
	This object is an enhanced subclass of ScrollView. It adds the following features:
	
	Support for a 'top view', a view at the top of the scroll view that scrolls horizontally with the document but remains static when the document is scrolled vertically. This is useful to add column headers, rulers, etc, to a document. The top view can be added from inside of IB by connecting the topView outlet.
	
	Support for a 'left view', a view to the left of the scroll view that scrolls vertically with the document, but remains static when the document is scrolled horizontally. This is useful for adding rulers, line numbers, etc, to a document. The left view can be added from inside of IB by connecting the leftView outlet.
	
	Automatic support for rulers. If a topView or leftView has not been set, a call to setShowTopView:YES, setShowLeftView:YES, showRulers: or toggleRulers: will instanciate a member of rulerClass (see setRulerClass:). Depending on which ruler is called, the scrollview will try to call setHorizontal or setVertical on the new rulerClass instance. The default class is Ruler (from Draw). A ruler that supports flipping and smooth zooming is forthcoming.
	
	Support for synchronizing other scroll views. Other ScrollViews can be made to scroll, size and zoom with respect to the docView by using the addSyncViews: methods. This is useful for ruler type views that need to exist outside of the scrollers. One synchronized view of each type can be added in IB by connecting a given ScrollView to the syncViews, horizSyncViews or vertSyncViews outlets. Be sure and connect to a ScrollView and not its content view (this can be tricky for ScrollViews without scrollers). Synchronized ScrollViews should not have scrollers of their own (but they can).

	Automatic support for page up/down & left/right buttons. Simply call the method setPageUpDownButtonsVisible:YES or setPageLeftRightButtonsVisible:YES and these are added automatically. They are removed temporarily if the scroller is too small(less than an inch), if there is nothing to be scrolled or if the scrolling area is less than 2 pages.
	
	Automatic support for zooming. Simply call the method setShowZoomButton:YES and a popup list containing zoom values will be added to the horizontal scroller. Zooming is implemented by calling zoomTo:(float)zoomX :(float)zoomY on the docView(and topView, leftView, syncViews, horizSyncViews and vertSyncViews). If the docView does not respond to this method or returns NO from this method, automatic zooming is performed by scaling the clipView by the zoom amount. The 'Set...' item in the zoom popup allows for arbitrary scaling. The 'Fit' menu item calculates the zoom value necessary to fit the docView entirely in the scrollview. The default zooming behavior tends to fail on documents that contain NXImages. Implement zoomTo:: in your custom view to explicitly scale NXImages (returning NO if you don't otherwise handle zoom).
	
	Support for adding arbitrary views to the vertical and horizontal scrollers. This is useful for adding little gadgets like a 'goto page' control inside of the horizontal scroller. The page up/down & left/right buttons as well as the zoom button use this facility. Page left/right is assumed to be the first in the horizScrollerViews list if it exists. The zoom button is next. Other views should be added at [horizScrollerViews count]. Page up/down is assumed to be first in the vertScrollerViews list. Scroller views are temporarily removed in reverse order if the scroller is not long enough to accommodate them.
	
	The PAScrollViewDeluxe palette allows you to create a PAScrollViewDeluxe by command clicking the 'Group in ScrollView' menu item. The code is a complete hack inside of the PAScrollViewDeluxeInspector code (at the bottom) and introduces a bug into IB that you can no longer drag the default TextObject/ScrollView in (it just disapears).

Copyright 1992. Jeff Martin. (jmartin@next.com 415-780-3833)
******************************************************************************/

@implementation PAScrollViewDeluxe

- initFrame:(const NXRect *)rect
{
	char supportNibPath[MAXPATHLEN];
	NXSize defaultRulerSize = { 23, 23 };
	NXRect bogusFrame = {0,0,0,0};
	
	[super initFrame:rect];
	
	// The following nib has the page up/down buttons & tiffs, zoom button and
	//    the zoom panel.
	[[NXBundle bundleForClass:[self class]] getPath:supportNibPath 
		forResource:"PAScrollViewDeluxe" ofType:"nib"];
	[NXApp loadNibFile:supportNibPath owner:self withNames:NO 
		fromZone:[self zone]];
		
	// Hack to work around IB crasher caused by "Group in ScrollView" hack.
	if(rect) NXSetRect(&bogusFrame,0,0, NX_WIDTH(rect)-23, NX_HEIGHT(rect)-23);
	[self setDocView:[[View alloc] initFrame:&bogusFrame]];

	// Set reasonable defaults
	[self setHorizScrollerRequired:YES];
	[self setVertScrollerRequired:YES];
	[self setPageScroll:20];
	[self setPageUpDownButtonsVisible:YES];
	[self setPageLeftRightButtonsVisible:YES];
	[self setZoomButtonVisible:YES];
	[self setBorderType:NX_BEZEL];
	[self setBackgroundColor:NX_COLORLTGRAY];
	[self setRulerClass:[Ruler class]];
	[self setRulerSize:defaultRulerSize];
	[self showRulers:self];
	
	return self;
}

/******************************************************************************	
	topView, setTopView:view

	topView returns the current topView. If none exists, it allocates a view of ruler class, sets it to be the topView and returns it.
	setTopView: places the given view inside the scrollview (inside a clip view, topClip) at the top of the scroll view at its origional hieght but at the width of the docView. It returns the oldTopView.
******************************************************************************/
- topView
{
	// If there isn't a topView, alloc and set rulerClass instance as default
	if(!topView) {
		NXRect rulerFrame = {0,0,0,0};
		rulerFrame.size = [self rulerSize];
		NX_WIDTH(&rulerFrame) = [contentView frameWidth];
		[self setTopView:[[[self rulerClass] alloc] initFrame:&rulerFrame]];
		if([topView respondsTo:@selector(setHorizontal)])
			[topView setHorizontal];

	}
	return topView;
}
- setTopView:view
{
	id old = topView;

	// Set the new topView, its ruler height, and remove it from where it was
	topView = view;
	rulerSize.height = [view frameHeight];
	[topView removeFromSuperview];

	// Install it if need be
	if([self topViewVisible]) 
		{ [self setTopViewVisible:NO]; [self setTopViewVisible:YES]; }
	return old;
}

/******************************************************************************
	(BOOL)topViewVisible, setTopViewVisible:(BOOL)flag
	
	topViewVisible returns whether or not the topView is visible.
	setTopViewVisible: will install the current topView inside a clipView on top of the docView if set to YES and will remove the existing topView if set to NO. Retiles the views, but does not call display.Returns self.
******************************************************************************/
- (BOOL)topViewVisible { return topViewVisible; }
- setTopViewVisible:(BOOL)flag;
{
	// If the new state is equal to the old, return
	if(flag == topViewVisible) return self;
	
	topViewVisible = flag;
	
	// If we are setting topView to visible add it to hierarchy
	if(topViewVisible) {
	
		// Allocate clipView to scroll leftView, and build view hierarchy
		topClip = [[[ClipView alloc] init] setAutosizing:NX_WIDTHSIZABLE];
		[self addSubview:topClip];
		[topClip setDocView:[self topView]];
	}
	else {
		// Remove topView and topClip. Free topClip and set it to NULL.
		[[self topView] removeFromSuperview];
		[topClip removeFromSuperview];
		[topClip free]; topClip = NULL;
	}
	
	// Retile to fix layout of views and synchronize topView to docView
	[self tile];
	if(topViewVisible) [self synchronizeClipView:topClip 
		withClipView:contentView horizontally:YES vertically:NO];
	return self;
}

/******************************************************************************
	showTopView:sender, hideTopView:sender toggleTopView:sender

	These convenience methods simply wrap around setTopViewVisible: and can be set to be called from menus or controls inside of InterfaceBuilder.
******************************************************************************/
- showTopView:sender
{ [self setTopViewVisible:YES]; [self display]; return self; }
- hideTopView:sender
{ [self setTopViewVisible:NO]; [self display]; return self; }
- toggleTopView:sender 
{ [self setTopViewVisible:!topViewVisible]; [self display]; return self; }

/******************************************************************************	
	leftView, setLeftView: - query and set the "left view".

	leftView returns the current leftView. If none exists, it allocates a view of ruler class, sets it to be the leftView and returns it.
	setLeftView: places the given view inside the scrollview (inside a clip view, leftClip) at the left of the scroll view at its origional width but at the height of the docView. It returns the oldLeftView.
******************************************************************************/
- leftView
{
	// If there isn't a leftView, alloc and set rulerClass instance as default
	if(!leftView) {
		NXRect rulerFrame = {0,0,0,0};
		rulerFrame.size = [self rulerSize];
		NX_HEIGHT(&rulerFrame) = [contentView frameHeight];
		[self setLeftView:[[[self rulerClass] alloc] initFrame:&rulerFrame]];
		if([leftView respondsTo:@selector(setVertical)])
			[leftView setVertical];
	}
	return leftView;
}
- setLeftView:view
{
	id old = leftView;

	// Set the new leftView, its ruler Width, and remove it from where it was
	leftView = view;
	rulerSize.width = [view frameWidth];
	[leftView removeFromSuperview];
	
	// Install leftView if necessary
	if([self leftViewVisible])
		{ [self setLeftViewVisible:NO]; [self setLeftViewVisible:YES]; }
	return old;
}


/******************************************************************************
	leftViewVisible, setLeftViewVisible - query & set whether leftView is visible
	
	leftViewVisible returns whether or not the leftView is visible.
	setLeftViewVisible: will install the current leftView inside a clipView on left of the docView if set to YES and will remove the existing leftView if set to NO. Retiles the views, but does not call display. Returns self.
******************************************************************************/
- (BOOL)leftViewVisible { return leftViewVisible; }
- setLeftViewVisible:(BOOL)flag;
{
	// If the new state is equal to the old, return
	if(flag == leftViewVisible) return self;
	
	leftViewVisible = flag;
	
	// If we are setting leftView to visible add it to hierarchy
	if(leftViewVisible) {
	
		// Allocate clipView to scroll leftView, and build view hierarchy
		leftClip = [[[ClipView alloc] init] setAutosizing:NX_WIDTHSIZABLE];
		[self addSubview:leftClip];
		[leftClip setDocView:[self leftView]];
	}
	else {
		// Remove leftView and leftClip. Free leftClip and set it to NULL.
		[[self leftView] removeFromSuperview];
		[leftClip removeFromSuperview];
		[leftClip free]; leftClip = NULL;
	}
	
	// Retile to fix layout of views and synchronize leftView to docView
	[self tile];
	if(leftViewVisible) [self synchronizeClipView:leftClip 
		withClipView:contentView horizontally:NO vertically:YES];
	return self;
}

/******************************************************************************
	showLeftView:, hideLeftView: toggleLeftView:

	These convenience methods simply wrap around setLeftViewVisible: and can be set to be called from menus or controls inside of InterfaceBuilder.
******************************************************************************/
- showLeftView:sender
{ [self setLeftViewVisible:YES]; [self display]; return self; }
- hideLeftView:sender
{ [self setLeftViewVisible:NO]; [self display]; return self; }
- toggleLeftView:sender 
{ [self setLeftViewVisible:!leftViewVisible]; [self display]; return self; }

/******************************************************************************
	showRulers:, hideRuler: toggleRulers:
	
	These are convenience methods for showing/hiding/toggling top and left views as a pair. They wrap around the setTopViewVisible and setLeftViewVisible. These methods can be set to be called from menus or controls inside of InterfaceBuilder. They all return self.
******************************************************************************/
- showRulers:sender 
{
	[window disableDisplay];
	[self setTopViewVisible:YES]; [self setLeftViewVisible:YES];
	[window reenableDisplay]; [self display];
	return self;
}
- hideRulers:sender
{
	[window disableDisplay];
	[self setTopViewVisible:NO]; [self setLeftViewVisible:NO];
	[window reenableDisplay]; [self display];
	return self;
}
- toggleRulers:sender
{
	[window disableDisplay];
	
	// If visible set to not visible and visa-versa.
	[self setTopViewVisible:![self topViewVisible]];
	[self setLeftViewVisible:![self leftViewVisible]];

	[window reenableDisplay];
	[self display];
	return self;
}

/******************************************************************************
	rulerClass, setRulerClass:(Class)class
	
	If the PAScrollView deluxe is asked to show top or left views when none has been set, it attempts to allocate an instance of 'rulerClass' (assumed to be a view). If the instance responds to setHorizontal or setVertical, this will be called.
******************************************************************************/
- (Class)rulerClass { return rulerClass; }
- setRulerClass:(Class)class { rulerClass = class; return self; }

/******************************************************************************
	rulerSize, setRulerSize:(NXSize)size
	
	When PAScrollViewDeluxe allocates a default top/left view, it sets the top one to be of height rulerSize.height and the left one to be of width rulerSize.width. If a topView or leftView are added programatically, the rulerSize.height and rulerSize.width are set respectively.
******************************************************************************/
- (NXSize)rulerSize { return rulerSize; }
- setRulerSize:(NXSize)size { rulerSize = size; return self; }

/******************************************************************************
	syncView, addSyncView:at:, removeSyncView:, removeSyncViewAt:
	horizSyncViews, addHorizSyncView:at:, removeHorizSyncView: & ViewAt:
	vertSyncViews, addVertSyncView:at:, removeVertSyncView: & ViewAt:
		
	syncViews are ScrollViews that are to be scrolled, sized and (optionally)zoomed with respect to the docViews position, size and zoom. horizSyncViews are only affected in the horizontal direction, while vertSyncViews are only affected in the vertical direction. Group a view inside of a ScrollView, disable its horizontal and vertical scrollers, and use one of the addMethods. In IB you can actually set one of each type view by setting the syncViews, horizSyncViews or vertSyncViews outlet to a ScrollView. It will be added to the list when the outlets are set.
	The syncViews, horizSyncViews and vertSyncViews methods return the list of the views that are currently being syncronized in the respective direction(can be NULL if there are none).
	addSyncView:at:, addHorizSyncView:at: and addVertSyncView:at: add scrollviews to be synchronized in the respective direction at the given location in the list(use [[myPASV syncViews] count] to add to end). Returns self.
	removeSyncView:, removeHorizSyncView:, removeVertSyncView: remove the given view from its respective list by calling removeSyncAt: with the view's index.
	removeSyncViewAt:, removeHorizSyncViewAt: and removeVertSyncViewAt: remove ScrollViews from the respective list of syncronized ScrollViews. Returns self.
******************************************************************************/
- syncViews { return syncViews; }
- addSyncView:view at:(int)at
{
	// Only add ScrollViews
	if([view isKindOfClassNamed:"ScrollView"]) {
		if(!syncViews) syncViews = [[List alloc] init];
		[syncViews addObjectIfAbsent:view];
	}
	return self;
}
- removeSyncView:view
{ return [self removeSyncViewAt:[[self syncViews] indexOf:view]]; }
- removeSyncViewAt:(int)at { [syncViews removeObjectAt:at]; return self; }

- horizSyncViews { return horizSyncViews; }
- addHorizSyncView:view at:(int)at
{
	// Only add ScrollViews
	if([view isKindOfClassNamed:"ScrollView"]) {
		if(!horizSyncViews) horizSyncViews = [[List alloc] init];
		[horizSyncViews addObjectIfAbsent:view];
	}
	return self;
}
- removeHorizSyncView:view
{ return [self removeHorizSyncViewAt:[[self horizSyncViews] indexOf:view]]; }
- removeHorizSyncViewAt:(int)at { [horizSyncViews removeObjectAt:at]; return self; }

- vertSyncViews { return vertSyncViews; }
- addVertSyncView:view at:(int)at
{
	// Only add ScrollViews
	if([view isKindOfClassNamed:"ScrollView"]) {
		if(!vertSyncViews) vertSyncViews = [[List alloc] init];
		[vertSyncViews addObjectIfAbsent:view];
	}
	return self;
}
- removeVertSyncView:view
{ return [self removeVertSyncViewAt:[[self vertSyncViews] indexOf:view]]; }
- removeVertSyncViewAt:(int)at { [vertSyncViews removeObjectAt:at]; return self; }

/******************************************************************************
	horizScrollerViews, addHorizScrollerView:at:, removeHorizScrollerView:
	vertScrollerViews, addVertScrollerView:at:, removeVertScrollerView:
		
	ScrollerViews are views embedded inside of the vertical or horizontal scrollers. The are frequently simple controls like a "Goto Page:" control. In fact the page up/down & left/right buttons as well as the zoomButton are horizScrollerViews (assumed to be at 0 and 1 respectively if they exist).When added these views are sized to fit into the scroller(ie, horizontal scroller views are constrained to the horizontal scroller's height).
	The horizScrollerViews and vertScrollerViews methods return the list of the views that are currently in the respective scroller (can be NULL if there are none).
	addHorizScrollerView:at: and addVertScrollerView:at: add a view to their respective list at the given location(use [[myPASV vertSyncViews] count] to add to end). They returns self.
	removeHorizScrollerView: and removeVertScrollerView: removes the given view from the respective scroller list. Returns self.
	removeHorizScrollerViewAt: and removeVertScrollerViewAt: removes the view at the given location from the respective scroller list. Returns self.
******************************************************************************/
- horizScrollerViews { return horizScrollerViews; }
- addHorizScrollerView:view at:(int)at
{
	// Make sure the list exists and add the view
	if(!horizScrollerViews) horizScrollerViews = [[List alloc] init];
	[horizScrollerViews insertObject:view at:at];

	// Add the view to subview list and retile
	[self addSubview:view]; [self tile];
	return self;
}
- removeHorizScrollerView:view
{
	[self removeHorizScrollerViewAt:[[self horizScrollerViews] indexOf:view]];
	return self;
}
- removeHorizScrollerViewAt:(int)at
{
	[[horizScrollerViews removeObjectAt:at] removeFromSuperview];
	[self tile];
	return self;
}

- vertScrollerViews { return vertScrollerViews; }
- addVertScrollerView:view at:(int)at
{
	// Make sure the list exists and add the view.
	if(!vertScrollerViews) vertScrollerViews = [[List alloc] init];
	[vertScrollerViews insertObject:view at:at];

	// Add the view to subview list and retile
	[self addSubview:view]; [self tile];
	return self;
}
- removeVertScrollerView:view
{
	[self removeVertScrollerViewAt:[[self vertScrollerViews] indexOf:view]];
	return self;
}
- removeVertScrollerViewAt:(int)at
{
	[[vertScrollerViews removeObjectAt:at] removeFromSuperview];
	[self tile];
	return self;
}

/******************************************************************************
	pageUpDownButtons, pageLeftRightButtons, zoomButton
	
	These methods return pointers to the matrices that contain the up/down & left/right buttons. They are loaded in at init time from PAScrollViewDeluxe.nib.
******************************************************************************/ 
- pageUpDownButtons { return pageUpDownButtons; }
- pageLeftRightButtons { return pageLeftRightButtons; }
- zoomButton { return zoomButton; }

/******************************************************************************
	pageUpDownButtonsVisible, setPageUpDownButtonsVisible:, needUpDownButtons
	pageLeftRightButtonsVisible, setPageLeftRightButtonsVisible
	needPageLeftRightButtons
	
	These methods query and set whether the respective button set is visible.
	The setButtonsVisible method calls the respective add or remove scrollerView method with the 'at' value equal to zero.
	The needPageButtons methods return whether the page buttons are actually needed (ie, if the docView is smaller than the contentView or the scrollable area is less than 2 pages, the buttons are not needed).
******************************************************************************/ 
- (BOOL)pageUpDownButtonsVisible { return pageUpDownButtonsVisible; }
- setPageUpDownButtonsVisible:(BOOL)flag
{
	// If this is a new state then either add or remove buttons from scroller
	if(pageUpDownButtonsVisible != flag) {
		pageUpDownButtonsVisible = flag;
		if(flag) [self addVertScrollerView:[self pageUpDownButtons] at:0];
		else [self removeVertScrollerView:[self pageUpDownButtons]];
	}
	return self;
}
- (BOOL)needPageUpDownButtons { return (([vScroller perCent] < .5) && 
	([contentView boundsHeight] < [[contentView docView] frameHeight])); }

- (BOOL)pageLeftRightButtonsVisible { return pageLeftRightButtonsVisible; }
- setPageLeftRightButtonsVisible:(BOOL)flag
{
	// If this is a new state then either add or remove buttons from scroller
	if(pageLeftRightButtonsVisible != flag) {
		pageLeftRightButtonsVisible = flag;
		if(flag) [self addHorizScrollerView:[self pageLeftRightButtons] at:0];
		else [self removeHorizScrollerView:[self pageLeftRightButtons]];
	}
	return self;
}
- (BOOL)needPageLeftRightButtons { return (([hScroller perCent] < .5) && 
	([contentView boundsWidth] < [[contentView docView] frameWidth])); }

/******************************************************************************
	pageButton

	This method is the target to all of the page up/down/left/right buttons. Based on the tags, it scrolls the visible rect by its extents minus the page overlap (pageContext) in the respective direction. Returns self.
******************************************************************************/ 
- pageButton:sender
{
	NXRect rect;
	int flipped;
	NXSize pageContextSize = {pageContext, pageContext};
	NXCoord newPageCont;
	
	// Get the docView's visible rect in docView's coords.
	[[self docView] getVisibleRect:&rect];
	[[self docView] convertRectFromSuperview:&rect];

	// Convert the ScrollViews pageContext to docView(to correct for zooming)
	[[self docView] convertSize:&pageContextSize fromView:self];
	newPageCont = pageContextSize.width;
	
	flipped = [[self docView] isFlipped] ? -1 : 1;

	// Move the visible rect in the respective direction (up/down/left/right
	//    respectively). Allow for flippedness and page overlap.
	switch([sender selectedTag]) {
		case 0: NX_Y(&rect) += (NX_HEIGHT(&rect) - newPageCont)*flipped; break;
		case 1: NX_Y(&rect) -= (NX_HEIGHT(&rect) - newPageCont)*flipped; break;
		case 2: NX_X(&rect) -= NX_WIDTH(&rect) - newPageCont; break;
		case 3: NX_X(&rect) += NX_WIDTH(&rect) - newPageCont; break;
	}
	
	// Scroll the new rect to visible.
	[[self docView] scrollRectToVisible:&rect];
	return self;
}

/******************************************************************************
	zoomButtonVisible, setZoomButtonVisible:

	These methods query and set whether the zoom button is visible.
	setZoomButtonVisible: either adds the zoomButton to the vert scroller via - addHorizScrollerView: or removes via removeHorizScrollerView. Returns self.
******************************************************************************/
- (BOOL)zoomButtonVisible { return zoomButtonVisible; }
- setZoomButtonVisible:(BOOL)flag
{
	// If this is a new state then either add or remove button from scroller
	if(zoomButtonVisible != flag) {
		zoomButtonVisible = flag;
		if(zoomButtonVisible) {
			if(pageLeftRightButtonsVisible)
				[self addHorizScrollerView:[self zoomButton] at:1];
			else [self addHorizScrollerView:[self zoomButton] at:0];
		}
		else [self removeHorizScrollerView:[self zoomButton]];
	}
	return self;
}

/******************************************************************************
	zoom:

	This method is called by the zoomButton's popUpList to get the zoom amount. It reads the title and converts it to a scale (special cased for 'Set...' and 'Size To Fit'). Calls zoomTo::.
******************************************************************************/
#define CLAMP(a,x,y) (MAX((x), MIN((y), (a))))
- zoom:sender
{
	float scaleX, scaleY;
	char *title;
	
	// Get the title of the selected menu item
	title = NXCopyStringBuffer([[sender selectedCell] title]);

	// If it is "Fit" (tag == 6) the simply sizeTo:: to get size to fit...
	if([sender selectedTag] == 6) {
		NXRect docViewBounds, contentFrame;
		[[self docView] getBounds:&docViewBounds];
		[contentView getFrame:&contentFrame];
		// Zoom so that docView's bounds == content's frame
		scaleX = NX_WIDTH(&contentFrame)/(NX_WIDTH(&docViewBounds)+1);
		scaleY = NX_HEIGHT(&contentFrame)/(NX_HEIGHT(&docViewBounds)+1);
	}

	// Otherwise if it is "Set..." (tag == 7) then run the panel...
	else if([sender selectedTag] == 7) {
			static char string[32];
			
			[[zoomButton target] removeItem:string];
			[zoomText selectText:self];
			[NXApp runModalFor:zoomPanel];
			[zoomPanel close];
			scaleX = CLAMP([zoomText intValue],10,1600);
			sprintf(string, "%d%%", (int)scaleX);
			scaleX = scaleY = scaleX/100.0;
			
			// Add the value to the button (lazily since there is a problem
			//    doing it interactively, and reset the popUp's action to zoom.
			[zoomButton perform:@selector(setTitle:) with:(id)string 
				afterDelay:0 cancelPrevious:YES];
			[[[zoomButton target] setTarget:self] setAction:@selector(zoom:)];
	}
	
	// Otherwise convert the title from percentage to scale value
	else {
		title[strlen(title)-1] = '\0';
		scaleX = scaleY = atoi(title)/100.0;
	}
	free(title);

	[self zoomTo:scaleX :scaleY];
	return self;
}

// This method is used to stop the zoomPanels modal state.
- stopZoomPanel:sender { [NXApp stopModal]; return self; }

/******************************************************************************
	zoomTo:(float)zoomX :(float)zoomY

	This method tries to call zoomTo:: on the docView and all of the accessory views (topView, leftView, syncViews, horizSyncViews, vertSyncViews) with the given scale (1 is full size). If the views implement zoomTo:: and actually do the zoom, they should return YES. If they just implement zoomTo:: to get notification of a zoom or to scale dependent pieces(like NXImages), they should return NO. If zoomTo:: is not implemented or returns NO, automatic scaling takes place(on the ClipView).
******************************************************************************/
- (BOOL)zoomTo:(float)scaleX :(float)scaleY
{
	NXRect docViewVis, docViewVis2;
	int i,j;

	// Disable display so we don't see scales and scrolls
	[window disableDisplay];

	// Get the current visible docView rect(to preserve current view)
	[[self docView] getVisibleRect:&docViewVis];
	[[self docView] convertRectFromSuperview:&docViewVis];
	
	// Zoom the docView
	if(!([[contentView docView] respondsTo:@selector(zoomTo::)] &&
		[[contentView docView] zoomTo:scaleX :scaleY])) {
		[contentView setDrawSize:[contentView frameWidth] 
			:[contentView frameHeight]];
		[contentView scale:scaleX :scaleY];
		[self reflectScroll:contentView];
	}

	// Zoom the topView
	if(!([topView respondsTo:@selector(zoomTo::)] &&
		[topView zoomTo:scaleX :scaleY])) {
		[topClip setDrawSize:[topClip frameWidth] :[topClip frameHeight]];
		[topClip scale:scaleX :1];
	}
	
	// Zoom the leftView
	if(!([leftView respondsTo:@selector(zoomTo::)] &&
		[leftView zoomTo:scaleX :scaleY])) {
		[leftClip setDrawSize:[leftClip frameWidth] :[leftClip frameHeight]];
		[leftClip scale:1 :scaleY];
	}
	
	// Zoom the synchronized Views.
	for(i=0; i<[syncViews count]; i++) {
		id subviewList = [[syncViews objectAt:i] subviews];
		
		// Look inside each scrollview for its clipviews
		for(j=0; j<[subviewList count]; j++) {
			id view = [subviewList objectAt:j];
			
			// Only zoom the ClipViews of the scrollViews
			if([view isKindOf:[ClipView class]]) {
			
				// If view responds NO to zoomTo: scale the ClipView
				if(!([[view docView] respondsTo:@selector(zoomTo::)] &&
					[[view docView] zoomTo:scaleX :scaleY])) {
					[view setDrawSize:[view frameWidth] :[view frameHeight]];
					[view scale:scaleX :scaleY];
					
					// Only reflect Scroll the docView
					if([view docView] == [[view superview] docView])
						[[view superview] reflectScroll:view];
				}
			}
		}
		[[horizSyncViews objectAt:i] display];
	}
	
	// Zoom the horizontally synchronized Views.
	for(i=0; i<[horizSyncViews count]; i++) {
		id subviewList = [[horizSyncViews objectAt:i] subviews];
		
		// Look inside each scrollview for its clipviews
		for(j=0; j<[subviewList count]; j++) {
			id view = [subviewList objectAt:j];
			
			// Only zoom the ClipViews of the scrollViews
			if([view isKindOf:[ClipView class]]) {
			
				// If view responds NO to zoomTo: scale the ClipView
				if(!([[view docView] respondsTo:@selector(zoomTo::)] &&
					[[view docView] zoomTo:scaleX :1])) {
					[view setDrawSize:[view frameWidth] :[view frameHeight]];
					[view scale:scaleX :1];
					
					// Only reflect Scroll the docView
					if([view docView] == [[view superview] docView])
						[[view superview] reflectScroll:view];
				}
			}
		}
		[[horizSyncViews objectAt:i] display];
	}
	
	// Zoom the vertically synchronized views.
	for(i=0; i<[vertSyncViews count]; i++) {
		id subviewList = [[vertSyncViews objectAt:i] subviews];
		
		// Look inside each scrollview for its clipviews
		for(j=0; j<[subviewList count]; j++) {
			id view = [subviewList objectAt:j];
			
			// Only zoom the ClipViews of the scrollViews
			if([view isKindOf:[ClipView class]]) {
			
				// If view responds NO to zoomTo: scale the ClipView
				if(!([[view docView] respondsTo:@selector(zoomTo::)] &&
					[[view docView] zoomTo:1 :scaleY])) {
					[view setDrawSize:[view frameWidth] :[view frameHeight]];
					[view scale:1 :scaleY];
					
					// Only reflect Scroll the docView
					if([view docView] == [[view superview] docView])
						[[view superview] reflectScroll:view];
				}
			}
		}
		[[vertSyncViews objectAt:i] display];
	}
	
	// Get new visible size and inset old visible rect by the difference
	[[self docView] getVisibleRect:&docViewVis2];
	[[self docView] convertRectFromSuperview:&docViewVis2];
	NXInsetRect(&docViewVis, 
		(NX_WIDTH(&docViewVis) - NX_WIDTH(&docViewVis2))/2, 
		(NX_HEIGHT(&docViewVis) - NX_HEIGHT(&docViewVis2))/2);
		
	// Scroll to new rect (as much to the center of the old rect as possible)
	[[self docView] scrollRectToVisible:&docViewVis];
	
	// Renable display and display final change
	[window reenableDisplay]; [window display];

	return YES;
}	

/******************************************************************************
	scrollClip:to:

	This method is called automatically whenever any of the clipView subviews are scrolled. We intercept it so that we can perform the scroll on all of the dependant views(topView, leftView, syncViews, horizSyncViews & vertSyncViews). Returns Self.
******************************************************************************/
- scrollClip:(ClipView *)clipView to:(const NXPoint *)aPoint
{
	int i,j;
	
	// RawScroll the given clipView to given point
	[clipView rawScroll:aPoint];

	// Scroll the docView
	if(contentView != clipView)
		[self synchronizeClipView:contentView withClipView:clipView
			horizontally:(clipView==topClip) vertically:(clipView==leftClip)];
	
	// Synchronize top ruler if visible and wasn't the given clipView
    if (topViewVisible && (topClip != clipView))
		[self synchronizeClipView:topClip withClipView:contentView
			horizontally:YES vertically:NO];
	
	// Synchronize left ruler if visible and wasn't the given clipView
	if(leftViewVisible && (leftClip != clipView))
		[self synchronizeClipView:leftClip withClipView:contentView
			horizontally:NO vertically:YES];

	// Scroll syncViews
    for(i=0; i<[syncViews count]; i++) {
		id view = [syncViews objectAt:i];
		id subViews = [view subviews];
		
		// RawScroll the ClipView subViews, reflect scroll the contentView
		for(j=0; j<[subViews count]; j++) {
			id subView = [subViews objectAt:j];
			if([subView isKindOf:[ClipView class]]) {
				[self synchronizeClipView:subView withClipView:contentView
					horizontally:YES vertically:YES];
				if([view docView] == [subView docView])
					[view reflectScroll:subView];
			}
		}
    }
	
	// Scroll horizSyncViews
    for(i=0; i<[horizSyncViews count]; i++) {
		id view = [horizSyncViews objectAt:i];
		id subViews = [view subviews];
		
		// horiz RawScroll the ClipView subViews, reflect scroll contentView
		for(j=0; j<[subViews count]; j++) {
			id subView = [subViews objectAt:j];
			if([subView isKindOf:[ClipView class]]) {
				[self synchronizeClipView:subView withClipView:contentView
					horizontally:YES vertically:NO];
				if([view docView] == [subView docView])
					[view reflectScroll:subView];
			}
		}
    }
	
	// Scroll vertSyncViews
    for(i=0; i<[vertSyncViews count]; i++) {
		id view = [vertSyncViews objectAt:i];
		id subViews = [view subviews];
		
		// vert RawScroll the ClipView subViews, reflect scroll the contentView
		for(j=0; j<[subViews count]; j++) {
			id subView = [subViews objectAt:j];
			if([subView isKindOf:[ClipView class]]) {
				[self synchronizeClipView:subView withClipView:contentView
					horizontally:NO vertically:YES];
				if([view docView] == [subView docView])
					[view reflectScroll:subView];
			}
		}
    }

    return self;
}


/******************************************************************************
	synchronizeClipView:withClipView:horizontally:vertically:

	This method sets the first clipView so that it is viewing as much of the same rect that the withClipView is viewing as possible. It figures out this rect in the withClipView, corrects for coordinate system differences and flippedness and does a rawScroll in the given clipView.
	The horizontally and vertically flags allow the synchronization to be constrained to a particular direction.
	This method is called within scrollClip:to: to synchronize the various parts of the ScrollView (rulers, etc). You will probably never call it directly. Returns self.
******************************************************************************/
- synchronizeClipView:newClip withClipView:oldClip
	horizontally:(BOOL)horizSync vertically:(BOOL)vertSync
{
	// Get the offset of the oldClip's origin from its docView's origin 
	float dx = [oldClip boundsX] - [[oldClip docView] frameX];
	float dy = [oldClip boundsY] - [[oldClip docView] frameY];
	NXPoint point;
	
	// Calc new X and Y by adding offset to newClips's docView's origin
	point.x = [[newClip docView] frameX] + dx;
	point.y = [[newClip docView] frameY] + dy;

	// If the flippedness is not the same, take the compliment of y
	if([oldClip isFlipped] != [newClip isFlipped])
		point.y = [[newClip docView] frameY] +
		[[oldClip docView] frameHeight] - (dy + [newClip boundsHeight]);
	
	// Constrain scrolling from given flags
	if(!horizSync) point.x = [newClip boundsX];
	if(!vertSync)  point.y = [newClip boundsY];
	
	// Scroll to new boundsOrigin
	[newClip rawScroll:&point];
	return self;
}
			
			/******************************************************************************
	reflectScroll

	This method is called to adjust the scrollers when there has been a change to the docView (it is called automatically durring autoscroll or when the docView is resized). We override it so that we can add or remove the page buttons if necessary. Returns self.
******************************************************************************/
- reflectScroll:view
{	
	BOOL newUpDown, oldUpDown = [self needPageUpDownButtons];
	BOOL newLeftRight,  oldLeftRight = [self needPageLeftRightButtons];

	[super reflectScroll:contentView];
	
	newUpDown = [self needPageUpDownButtons];
	newLeftRight = [self needPageLeftRightButtons];
	
	// If scroller knobs appeared or disapeared, retile and display scrollers
	if((newUpDown != oldUpDown) || (newLeftRight !=oldLeftRight)) {
		[self tileScrollerViews];
		[hScroller display]; [vScroller display];
		[[self horizScrollerViews] makeObjectsPerform:@selector(display)];
		[[self vertScrollerViews] makeObjectsPerform:@selector(display)];
	}
	return self;
}

/******************************************************************************
	descendantFrameChanged:

	This method is called automatically whenever the document changes its frame size. We override it so that we can grow the dependent views respectively (topView, leftView, syncView, horizSyncView, vertSyncViews).
******************************************************************************/
- descendantFrameChanged:sender
{
	int i;
	
	[super descendantFrameChanged:sender];

	// Size topView to docView's new height
	if(topView) [[self topView] sizeTo:[[self docView] frameWidth] 
			:[[self topView] frameHeight]];
	
	// Size leftView to docView's new height
	if(leftView) [[self leftView] sizeTo:[[self leftView] frameWidth]
		:[[self docView] frameHeight]]; 

	// Size syncViews to docViews new width and hieght
	for(i=0; i<[syncViews count]; i++)
		[[[syncViews objectAt:i] docView] sizeTo:[[self docView] frameWidth]
			:[[self docView] frameHeight]];
			
	// Size horizSyncViews to docViews new width
	for(i=0; i<[horizSyncViews count]; i++)
		[[[horizSyncViews objectAt:i] docView] 
			sizeTo:[[self docView] frameWidth] 
			:[[[horizSyncViews objectAt:i] docView] frameHeight]];
			
	// Size vertSyncViews to docViews new hieght
	for(i=0; i<[vertSyncViews count]; i++)
		[[[vertSyncViews objectAt:i] docView] 
			sizeTo:[[[vertSyncViews objectAt:i] docView] frameWidth] 
			:[[self docView] frameHeight]];
			

	return self;
}

/******************************************************************************
	tile

	This method is where the real work of adding all of the subviews to the scrollview happens. It simply does a divide rect on the various parts and sets the origional parts to the diminished rect and the new parts to the new rect. Does not display. Returns self.
******************************************************************************/
- tile
{	
	[super tile];

	if([self topViewVisible]) {
		NXRect contentFrame = [contentView frame];
		NXRect topClipFrame, cornerFrame;
		
		// Split contentView frame into topView part and contentView part
		NXDivideRect(&contentFrame, &topClipFrame, rulerSize.height, NX_YMIN);
		[contentView setFrame:&contentFrame];
		
		// If Both rulers exist, further divide topFrame for the corner
		if([self leftViewVisible]) NXDivideRect(&topClipFrame, &cornerFrame, 
			rulerSize.width, NX_XMIN);
		[topClip setFrame:&topClipFrame];
		
		// Resize the topView to Min(docViewWidth, contentViewWidth)
		[topView sizeTo:MAX([[self docView] frameWidth], 
			[contentView boundsWidth]) :rulerSize.height]; 
	}
	
	if([self leftViewVisible]) {
		NXRect contentFrame = [contentView frame];
		NXRect leftClipFrame;
		
		// Split contentView frame into leftView part and contentView part
		NXDivideRect(&contentFrame, &leftClipFrame, rulerSize.width, NX_XMIN);
		[contentView setFrame:&contentFrame];
		[leftClip setFrame:&leftClipFrame];
		
		// Resize leftView to the Min(docView height, contentView height)
		[leftView sizeTo:rulerSize.width 
			:MAX([[self docView] frameHeight], [contentView boundsHeight])]; 
	}
	
	// Retile the scroller views
	[self tileScrollerViews];
	
	return self;
}

/******************************************************************************
	tileScrollerViews

	This method tiles the views in the scrollers. Views are added to the scroller in the order they are encountered in their respective list. If they don't leave at least an inch for the scroller if added, they are not added.
	I had to pull this code out of the tile method because the scrollers need to be retiled occasionally after a reflectScroll (to see if the page buttons go away). Since tile indirectly causes a reflectScroll, it cannot be called from within reflectScroll. Returns self.
******************************************************************************/
- tileScrollerViews
{
	NXRect aRect, bRect;
	int i;
	
	// Reset scrollers to their origional sizes
	aRect = bounds;
	if([self borderType] == NX_LINE) NXInsetRect(&aRect, 1, 1);
	else if([self borderType] == NX_BEZEL) NXInsetRect(&aRect, 2, 2);

    if (_sFlags.vScrollerRequired) {
		NXDivideRect(&aRect, &bRect, NX_SCROLLERWIDTH, NX_XMIN);
		[vScroller setFrame:&bRect];
		NXDivideRect(&aRect, &bRect, 1.0, 0);
    }
    if (_sFlags.hScrollerRequired) {
		NXDivideRect(&aRect, &bRect, NX_SCROLLERWIDTH, NX_YMAX);
		[hScroller setFrame:&bRect];
    }

	// Set frames for each horizontal scroller view in order
	for(i=0; i<[horizScrollerViews count]; i++) {
		id hView = [horizScrollerViews objectAt:i];
		NXRect horizRect = [hScroller frame];
		NXRect viewRect  = [hView frame];

		// This metric says that if there would be less than an inch of 
		//    scroller, then don't add this view.
		if((NX_WIDTH(&horizRect) - NX_WIDTH(&viewRect)) > 72) {

			// Stick pageLeftRightButtons on the left (all others on the right)
			if(hView == [self pageLeftRightButtons]) {
			
				// If there is no need for page left & right buttons move them
				if(![self needPageLeftRightButtons])
					{ NX_X(&viewRect)=-1000; NX_Y(&viewRect)=-1000; }
				
				// Otherwise, calculate their rect from the left
				else 
					NXDivideRect(&horizRect,&viewRect,
						NX_WIDTH(&viewRect),NX_XMIN);
			}

			// Stick all normal horiz scroller views to the right
			else
				NXDivideRect(&horizRect,&viewRect,NX_WIDTH(&viewRect),NX_XMAX);

			// Set the scroller frame
			[hScroller setFrame:&horizRect];

			// Adjust the new scroller view frame to fit in scroller and set
			NX_Y(&viewRect)++; NX_HEIGHT(&viewRect) = NX_HEIGHT(&viewRect) - 2;
			[hView setFrame:&viewRect];
		}
		else [hView moveTo:-1000 :-1000];
	}

	// Set frames for each vertical scroller view in order
	for(i=0; i<[vertScrollerViews count]; i++) {
		id vView = [vertScrollerViews objectAt:i];
		NXRect vertRect = [vScroller frame];
		NXRect viewRect = [vView frame];

		// This metric says that if there would be less than an inch of 
		//    scroller, then don't add this view.
		if((NX_HEIGHT(&vertRect) - NX_HEIGHT(&viewRect)) > 72) {

			// Stick pageLeftRightButtons on the left (all others on the right)
			if(vView == [self pageUpDownButtons]) {
			
				// If there is no need for page up & down buttons move them
				if(![self needPageUpDownButtons])
					{ NX_X(&viewRect)=-1000; NX_Y(&viewRect)=-1000; }
					
				// Otherwise, calculate their rect from the bottom
				else NXDivideRect(&vertRect, &viewRect, 
					NX_HEIGHT(&viewRect), NX_YMAX);
			}

			// Stick all normal horiz scroller views at the top
			else 
			   NXDivideRect(&vertRect,&viewRect, NX_HEIGHT(&viewRect),NX_YMIN);

			// Set the scroller frame
			[vScroller setFrame:&vertRect];
			
			// Adjust the new scroller view frame to fit in scroller and set
			NX_X(&viewRect)++; NX_WIDTH(&viewRect) = NX_WIDTH(&vertRect) - 2;
			[vView setFrame:&viewRect];
		}
		else [vView moveTo:-1000 :-1000];
	}
	return self;
}

// Overridden to handle "Size to Fit" zoom setting.
- sizeTo:(NXCoord)width :(NXCoord)height
{
	// Let super do its sizing
	[super sizeTo:width :height];

	// If zoomButton is visible and set to "Fit" (tag 6) do appropriate zoomTo
	if([self zoomButtonVisible] && 
		([[[[self zoomButton] target] itemList] selectedTag] == 6)) {
		NXRect docViewBounds, contentFrame;
		[[self docView] getBounds:&docViewBounds];
		[contentView getFrame:&contentFrame];

		// Zoom so that docView's bounds == content's frame
		[self zoomTo:NX_WIDTH(&contentFrame)/(NX_WIDTH(&docViewBounds)+1)
			:NX_HEIGHT(&contentFrame)/(NX_HEIGHT(&docViewBounds)+1)];
	}

	return self;
}

// Overridden to synchronize all views after addition.
- setDocView:view
{
	NXPoint point = [contentView boundsOrigin];
	id oldDocView = [super setDocView:view];
	[self scrollClip:contentView to:&point];
	return oldDocView;	
}

- write:(NXTypedStream *)stream
{
	[super write:stream];
	
	NXWriteTypes(stream, "@@c@@c#{ff}@@@@@@c@c@c@@",
		&topView,
		&topClip,
		&topViewVisible,
		&leftView,
		&leftClip,
		&leftViewVisible,
		&rulerClass,
		&rulerSize,
		&syncViews,
		&horizSyncViews,
		&vertSyncViews,
		&horizScrollerViews,
		&vertScrollerViews,
		&pageUpDownButtons,
		&pageUpDownButtonsVisible,
		&pageLeftRightButtons,
		&pageLeftRightButtonsVisible,
		&zoomButton,
		&zoomButtonVisible,
		&zoomPanel,
		&zoomText);
	
	return self;
}

- read:(NXTypedStream *)stream
{
	[super read:stream];
	
	NXReadTypes(stream, "@@c@@c#{ff}@@@@@@c@c@c@@",
		&topView,
		&topClip,
		&topViewVisible,
		&leftView,
		&leftClip,
		&leftViewVisible,
		&rulerClass,
		&rulerSize,
		&syncViews,
		&horizSyncViews,
		&vertSyncViews,
		&horizScrollerViews,
		&vertScrollerViews,
		&pageUpDownButtons,
		&pageUpDownButtonsVisible,
		&pageLeftRightButtons,
		&pageLeftRightButtonsVisible,
		&zoomButton,
		&zoomButtonVisible,
		&zoomPanel,
		&zoomText);
	
	return self;
}

// Hack methods to allow each type of sync and scroller view to be added in IB.
- setSyncViews:object
{ [self addSyncView:object at:[[self syncViews] count]];return self;}
- setHorizSyncViews:object
{ [self addHorizSyncView:object at:[[self horizSyncViews] count]];return self;}
- setVertSyncViews:object
{ [self addVertSyncView:object at:[[self vertSyncViews] count]]; return self; }
- setHorizScrollerViews:object
{ [self addHorizScrollerView:object at:[[self horizScrollerViews] count]];
	return self; }
- setVertScrollerViews:object
{ [self addVertScrollerView:object at:[[self vertScrollerViews] count]];
	return self; }

// Interface Builder support
- (const char *)getInspectorClassName { return "PAScrollViewDeluxeInspector"; }

@end

@implementation View(Convenience)
- (NXCoord)frameX { return NX_X(&frame); }
- (NXCoord)frameY { return NX_Y(&frame); }
- (NXPoint)frameOrigin { return frame.origin; }
- (NXCoord)frameWidth { return NX_WIDTH(&frame); }
- (NXCoord)frameHeight { return NX_HEIGHT(&frame); }
- (NXSize)frameSize { return frame.size; }
- (NXRect)frame { return frame; }

- (NXCoord)boundsX { return NX_X(&bounds); }
- (NXCoord)boundsY { return NX_Y(&bounds); }
- (NXPoint)boundsOrigin { return bounds.origin; }
- (NXCoord)boundsWidth { return NX_WIDTH(&bounds); }
- (NXCoord)boundsHeight { return NX_HEIGHT(&bounds); }
- (NXSize)boundsSize { return bounds.size; }
- (NXRect)bounds { return bounds; }
@end

@implementation Scroller(PerCent)
- (float)perCent { return perCent; }
@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.