5 Copyright (c) 2006, Keith Anderson
8 Redistribution and use in source and binary forms, with or without modification,
9 are permitted provided that the following conditions are met:
11 * Redistributions of source code must retain the above copyright notice,
12 this list of conditions and the following disclaimer.
13 * Redistributions in binary form must reproduce the above copyright notice,
14 this list of conditions and the following disclaimer in the documentation
15 and/or other materials provided with the distribution.
16 * Neither the name of keeto.net or Keith Anderson nor the names of its
17 contributors may be used to endorse or promote products derived
18 from this software without specific prior written permission.
20 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
22 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
24 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
25 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
26 OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
28 STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 * Modified to have a glass-style background; this requires the sourceListBackground image.
38 #import "KNShelfSplitView.h"
40 #import <AIUtilities/AIAttributedStringAdditions.h>
41 #import <AIUtilities/AIParagraphStyleAdditions.h>
42 #import <AIUtilities/AIImageAdditions.h>
43 #import <AIUtilities/AIToolbarUtilities.h>
46 #define DEFAULT_SHELF_WIDTH 200
47 #define CONTROL_HEIGHT 22
48 #define BUTTON_WIDTH 30
49 #define THUMB_WIDTH 15
50 #define THUMB_LINE_SPACING 2.0
51 #define RESIZE_BAR_EFFECTIVE_WIDTH 1.0
53 #define CONTROL_PART_NONE 0
54 #define CONTROL_PART_ACTION_BUTTON 1
55 #define CONTROL_PART_CONTEXT_BUTTON 2
56 #define CONTROL_PART_RESIZE_THUMB 3
57 #define CONTROL_PART_RESIZE_BAR 4
59 #define TOOLBAR_TOGGLESHELF_IDENTIFIER @"Toggle Shelf"
60 #define TOGGLESHELF @"Toggle Shelf"
61 @implementation KNShelfSplitView
65 if ((self == [KNShelfSplitView class])) {
66 [self exposeBinding:@"contextButtonMenu"];
71 -(IBAction)toggleShelf:(id)sender
73 #pragma unused(sender)
74 [self setShelfIsVisible: ![self isShelfVisible]];
75 [self setNeedsDisplay: YES];
78 - (id)initWithFrame:(NSRect)aFrame {
79 return [self initWithFrame: aFrame shelfView: nil contentView: nil];
82 -(id)initWithFrame:(NSRect)aFrame shelfView:(NSView *)aShelfView contentView:(NSView *)aContentView{
83 self = [super initWithFrame: aFrame];
86 currentShelfWidth = DEFAULT_SHELF_WIDTH; //change this
89 activeControlPart = CONTROL_PART_NONE;
90 contextButtonMenu = nil;
91 [self recalculateSizes];
94 shelfBackgroundColor = nil;
95 actionButtonImage = nil;
96 contextButtonImage = nil;
98 background = [[NSImage imageNamed:@"sourceListBackground" forClass:[self class]] retain];
99 backgroundSize = [background size];
101 [self setDelegate: nil];
105 [self setShelfView: aShelfView];
106 [self setContentView: aContentView];
113 if( autosaveName ){ [autosaveName release]; }
114 if( contextButtonImage ){ [contextButtonImage release]; }
115 if( actionButtonImage ){ [actionButtonImage release]; }
116 if( shelfBackgroundColor ){ [shelfBackgroundColor release]; }
117 if( contextButtonMenu ){ [contextButtonMenu release]; }
119 [self unbind:@"contextButtonMenu"];
124 -(void)setDelegate:(id)aDelegate{
125 delegate = aDelegate;
127 delegateHasValidateWidth = NO;
130 if( [delegate respondsToSelector:@selector(shelfSplitView:validateWidth:)] ){
131 delegateHasValidateWidth = YES;
140 -(void)setTarget:(id)aTarget{
142 [self recalculateSizes];
149 -(void)setAction:(SEL)aSelector{
151 [self recalculateSizes];
158 -(void)setContextButtonMenu:(NSMenu *)aMenu{
159 if( contextButtonMenu ){
160 [contextButtonMenu autorelease];
161 [[NSNotificationCenter defaultCenter] removeObserver: self];
164 contextButtonMenu = [aMenu retain];
166 if( contextButtonMenu ){
167 [contextButtonMenu setDelegate: self];
168 [[NSNotificationCenter defaultCenter] addObserver: self
169 selector: @selector(didEndContextMenuTracking)
170 name: NSMenuDidEndTrackingNotification
171 object: contextButtonMenu
175 [self recalculateSizes];
178 -(void)didEndContextMenuTracking{
180 [self setNeedsDisplayInRect: controlRect];
183 -(NSMenu *)contextButtonMenu{
184 return contextButtonMenu;
187 -(void)setShelfView:(NSView *)aView{
189 [shelfView removeFromSuperview];
195 [self addSubview: shelfView];
197 [self recalculateSizes];
200 -(NSView *)shelfView{
204 -(void)setContentView:(NSView *)aView{
206 [contentView removeFromSuperview];
212 [self addSubview: contentView];
215 [self recalculateSizes];
218 -(NSView *)contentView{
222 -(void)setShelfWidth:(float)aWidth{
223 float newWidth = aWidth;
226 // The shelf can never be completely closed. We always have at least enough to show our resize thumb, otherwise
227 // if the delegate responds to shelfSplitView:validateWidth:, we use that width as our minimum shelf size
228 float minShelf = THUMB_WIDTH;
229 if( delegateHasValidateWidth ){
230 float requestedWidth = [delegate shelfSplitView:self validateWidth: aWidth];
231 if( requestedWidth > minShelf ){
232 minShelf = requestedWidth;
235 if( minShelf > newWidth ){
239 // The shelf can never be wider than half the entire view
240 float maxShelf = [self frame].size.width / 2;
242 if( newWidth > maxShelf ){
246 currentShelfWidth = newWidth;
248 [self recalculateSizes];
252 return currentShelfWidth;
255 -(void)setAutosaveName:(NSString *)aName{
257 [autosaveName autorelease];
259 autosaveName = [aName retain];
262 -(NSString *)autosaveName{
266 -(void)recalculateSizes{
267 if( isShelfVisible ){
268 controlRect = NSMakeRect( 0, 0, currentShelfWidth, CONTROL_HEIGHT );
270 resizeThumbRect = NSMakeRect( (controlRect.size.width - THUMB_WIDTH), 0, THUMB_WIDTH, CONTROL_HEIGHT );
271 resizeBarRect = NSMakeRect( currentShelfWidth - (RESIZE_BAR_EFFECTIVE_WIDTH / 2), 0, RESIZE_BAR_EFFECTIVE_WIDTH, [self frame].size.height );
273 float availableSpace = controlRect.size.width - THUMB_WIDTH;
275 if( target && action && (availableSpace > BUTTON_WIDTH) ){
276 shouldDrawActionButton = YES;
277 actionButtonRect = NSMakeRect( 0, 0, BUTTON_WIDTH, CONTROL_HEIGHT );
278 availableSpace -= BUTTON_WIDTH;
281 if( contextButtonMenu && [contextButtonMenu numberOfItems] && (availableSpace > BUTTON_WIDTH) ){
282 shouldDrawContextButton = YES;
283 contextButtonRect = NSMakeRect(controlRect.size.width - (THUMB_WIDTH + availableSpace), 0, BUTTON_WIDTH, CONTROL_HEIGHT);
288 [shelfView setFrame: NSMakeRect( 0, CONTROL_HEIGHT + 1, currentShelfWidth, [self bounds].size.height - (CONTROL_HEIGHT + 1) )];
292 float contentViewX = (isShelfVisible ? (currentShelfWidth + 1) : 0);
293 NSRect newRect = NSMakeRect(contentViewX, 0, NSWidth([self bounds]) - contentViewX, NSHeight([self bounds]));
294 if (!NSEqualRects(newRect, [contentView frame])){
295 [contentView setFrame:newRect];
299 [self setNeedsDisplay: YES];
300 [[self window] invalidateCursorRectsForView: self];
304 -(BOOL)isShelfVisible{
305 return isShelfVisible;
308 -(void)setShelfIsVisible:(BOOL)visible{
310 if( isShelfVisible && !visible ){
312 [shelfView removeFromSuperview];
313 } else if( !isShelfVisible && visible ){
314 [self addSubview: shelfView];
319 isShelfVisible = visible;
320 [self recalculateSizes];
323 -(void)setActionButtonImage:(NSImage *)anImage{
324 if( actionButtonImage ){
325 [actionButtonImage autorelease];
328 actionButtonImage = [anImage retain];
330 [self setNeedsDisplayInRect: controlRect];
333 -(NSImage *)actionButtonImage{
334 return actionButtonImage;
337 -(void)setContextButtonImage:(NSImage *)anImage{
338 if( contextButtonImage ){
339 [contextButtonImage autorelease];
342 contextButtonImage = [anImage retain];
344 [self setNeedsDisplayInRect: controlRect];
347 -(NSImage *)contextButtonImage{
348 return contextButtonImage;
351 -(void)setShelfBackgroundColor:(NSColor *)aColor{
352 if( shelfBackgroundColor ){
353 [shelfBackgroundColor autorelease];
356 shelfBackgroundColor = [aColor retain];
357 [self setNeedsDisplay: YES];
360 -(NSColor *)shelfBackgroundColor{
361 return shelfBackgroundColor;
364 -(void)resetCursorRects{
365 [super resetCursorRects];
366 if( isShelfVisible ){
367 [self addCursorRect: resizeThumbRect cursor: [NSCursor resizeLeftRightCursor]];
368 [self addCursorRect: resizeBarRect cursor: [NSCursor resizeLeftRightCursor]];
372 -(void)mouseDown:(NSEvent *)anEvent{
373 BOOL stillMouseDown = YES;
374 NSPoint currentLocation;
376 // determine if we're in a control part we care about
377 currentLocation = [self convertPoint: [anEvent locationInWindow] fromView: nil];
379 if( shouldDrawActionButton && NSPointInRect( currentLocation, actionButtonRect ) ){
380 activeControlPart = CONTROL_PART_ACTION_BUTTON;
382 }else if( shouldDrawContextButton && NSPointInRect( currentLocation, contextButtonRect ) ){
383 activeControlPart = CONTROL_PART_CONTEXT_BUTTON;
386 NSEvent * contextEvent = [NSEvent mouseEventWithType: [anEvent type]
387 location: NSMakePoint( contextButtonRect.origin.x + (contextButtonRect.size.width / 2) , contextButtonRect.origin.y + (contextButtonRect.size.height / 2) )
388 modifierFlags: [anEvent modifierFlags]
389 timestamp: [anEvent timestamp]
390 windowNumber: [anEvent windowNumber]
391 context: [anEvent context]
392 eventNumber: [anEvent eventNumber]
393 clickCount: [anEvent clickCount]
394 pressure: [anEvent pressure]
396 [self setNeedsDisplayInRect: controlRect];
397 [NSMenu popUpContextMenu: contextButtonMenu withEvent: contextEvent forView: self];
398 [super mouseDown: contextEvent];
401 }else if( NSPointInRect( currentLocation, resizeThumbRect ) ){
402 activeControlPart = CONTROL_PART_RESIZE_THUMB;
403 }else if( NSPointInRect( currentLocation, resizeBarRect ) ){
404 activeControlPart = CONTROL_PART_RESIZE_BAR;
406 activeControlPart = CONTROL_PART_NONE;
409 [self setNeedsDisplayInRect: controlRect];
411 if( activeControlPart != CONTROL_PART_NONE ){
412 if([anEvent clickCount] == 2){
413 [self setShelfIsVisible: NO];
415 while( stillMouseDown ){
416 anEvent = [[self window] nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask];
417 currentLocation = [self convertPoint: [anEvent locationInWindow] fromView: nil];
420 if( (activeControlPart == CONTROL_PART_ACTION_BUTTON) && NSPointInRect( currentLocation, actionButtonRect ) ){
422 }else if( (activeControlPart == CONTROL_PART_CONTEXT_BUTTON) && NSPointInRect( currentLocation, contextButtonRect ) ){
426 switch( [anEvent type] ){
427 case NSLeftMouseDragged:
428 if( (activeControlPart == CONTROL_PART_RESIZE_THUMB) || (activeControlPart == CONTROL_PART_RESIZE_BAR) ){
429 [self setShelfWidth: currentLocation.x];
431 [self setNeedsDisplayInRect: controlRect];
437 [self setNeedsDisplayInRect: controlRect];
439 if( (activeControlPart == CONTROL_PART_ACTION_BUTTON) && NSPointInRect( currentLocation, actionButtonRect ) ){
441 if( target && action && [target respondsToSelector:action]){
442 [target performSelector: action withObject: self];
455 [super mouseDown:anEvent];
459 - (void)drawRect:(NSRect)rect {
460 #pragma unused( rect )
462 if( isShelfVisible ){
463 //NSLog(@"Drawing Control( %f, %f) (%f, %f)", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
465 float remainderStart = 0.0;
468 if( shouldDrawActionButton == YES){
469 [self drawControlBackgroundInRect: actionButtonRect
470 active: (activeControlPart == CONTROL_PART_ACTION_BUTTON) && shouldHilite
472 [[NSColor windowFrameColor] set];
473 // NSRectFill( NSMakeRect( (actionButtonRect.origin.x + actionButtonRect.size.width) - 1, 0, 1, controlRect.size.height ) );
474 remainderStart += actionButtonRect.size.width;
476 if( actionButtonImage ){
478 NSRect targetRect = NSMakeRect(actionButtonRect.origin.x,
479 actionButtonRect.origin.y,
480 [actionButtonImage size].width,
481 [actionButtonImage size].height
484 if( targetRect.size.width > actionButtonRect.size.width ){
485 targetRect.size.width = actionButtonRect.size.width;
487 if( targetRect.size.width < actionButtonRect.size.width ){
488 targetRect.origin.x += (actionButtonRect.size.width - targetRect.size.width) / 2.0;
490 if( targetRect.size.height > actionButtonRect.size.height ){
491 targetRect.size.height = actionButtonRect.size.height;
493 if( targetRect.size.height < actionButtonRect.size.height ){
494 targetRect.origin.y += (actionButtonRect.size.height - targetRect.size.height) / 2.0;
497 [actionButtonImage compositeToPoint:NSMakePoint(actionButtonRect.origin.x,
498 actionButtonRect.origin.y) operation:NSCompositeDestinationAtop];
506 if( shouldDrawContextButton ){
507 [self drawControlBackgroundInRect: contextButtonRect
508 active: (activeControlPart == CONTROL_PART_CONTEXT_BUTTON ) && shouldHilite
510 [[NSColor windowFrameColor] set];
511 NSRectFill( NSMakeRect( (contextButtonRect.origin.x + contextButtonRect.size.width) - 1, 0, 1, controlRect.size.height ) );
512 remainderStart += contextButtonRect.size.width;
514 if( contextButtonImage ){
516 NSRect targetRect = NSMakeRect(contextButtonRect.origin.x,
517 contextButtonRect.origin.y,
518 [contextButtonImage size].width,
519 [contextButtonImage size].height
522 if( targetRect.size.width > contextButtonRect.size.width ){
523 targetRect.size.width = contextButtonRect.size.width;
525 if( targetRect.size.width < contextButtonRect.size.width ){
526 targetRect.origin.x += (contextButtonRect.size.width - targetRect.size.width) / 2.0;
528 if( targetRect.size.height > contextButtonRect.size.height ){
529 targetRect.size.height = contextButtonRect.size.height;
531 if( targetRect.size.height < contextButtonRect.size.height ){
532 targetRect.origin.y += (contextButtonRect.size.height - targetRect.size.height) / 2.0;
534 [contextButtonImage drawInRect: targetRect
535 fromRect: NSMakeRect( 0, 0, [contextButtonImage size].width, [contextButtonImage size].height )
536 operation: NSCompositeSourceOver
542 //remainder and thumb
543 [self drawControlBackgroundInRect:NSMakeRect( remainderStart, 0, (controlRect.size.width - remainderStart), controlRect.size.height )
546 [[NSColor windowFrameColor] set];
547 NSRectFill( NSMakeRect( 0, CONTROL_HEIGHT, currentShelfWidth, 1 ) );
549 // Draw our split line
550 [[NSColor windowFrameColor] set];
551 NSRectFill( NSMakeRect( currentShelfWidth, 0, 1, [self frame].size.height ) );
553 // Draw our thumb lines
554 [[NSColor disabledControlTextColor] set];
555 NSRect thumbLineRect = NSMakeRect(
556 resizeThumbRect.origin.x + (resizeThumbRect.size.width - ((2*THUMB_LINE_SPACING) + 3.0)) / 2.0,
557 resizeThumbRect.size.height / 4.0,
559 resizeThumbRect.size.height / 2.0
562 for( i=0; i<3; i++ ){
563 NSRectFill( thumbLineRect );
564 thumbLineRect.origin.x += (1+THUMB_LINE_SPACING);
567 if( shelfBackgroundColor ){
568 [shelfBackgroundColor set];
569 NSRectFill( NSMakeRect( 0, CONTROL_HEIGHT+1, currentShelfWidth, [self frame].size.height ) );
573 if (attributedStringValue && !shouldDrawContextButton && !shouldDrawActionButton) {
574 NSRect textRect = NSMakeRect(6, (NSHeight(controlRect) - stringHeight)/2, NSMinX(resizeThumbRect) - 8, stringHeight);
576 [attributedStringValue drawInRect:textRect];
581 -(void)drawControlBackgroundInRect:(NSRect)aRect active:(BOOL)isActive{
582 //Draw the background, tiling across
583 NSRect sourceRect = NSMakeRect(0, 0, backgroundSize.width, backgroundSize.height);
584 NSRect destRect = NSMakeRect(aRect.origin.x, aRect.origin.y, sourceRect.size.width, aRect.size.height);
586 while ((destRect.origin.x < NSMaxX(aRect)) && destRect.size.width > 0) {
588 if (NSMaxX(destRect) > NSMaxX(aRect)) {
589 destRect.size.width = NSMaxX(aRect) - NSMinX(destRect);
590 sourceRect.size.width = NSWidth(destRect);
593 [background drawInRect:destRect
595 operation:NSCompositeSourceOver
597 destRect.origin.x += destRect.size.width;
601 -(void)setFrame:(NSRect)aRect{
602 [super setFrame: aRect];
603 [self recalculateSizes];
606 #pragma mark Status string
607 - (void)setResizeThumbStringValue:(NSString *)inString
609 if (![inString isEqualToString:stringValue]) {
610 [stringValue release];
611 stringValue = [inString copy];
613 [attributedStringValue release];
615 NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
616 [NSParagraphStyle styleWithAlignment:NSLeftTextAlignment
617 lineBreakMode:NSLineBreakByTruncatingTail], NSParagraphStyleAttributeName,
618 [NSFont systemFontOfSize:[NSFont smallSystemFontSize]], NSFontAttributeName,
621 stringHeight = [NSAttributedString stringHeightForAttributes:attributes];
622 attributedStringValue = [[NSAttributedString alloc] initWithString:stringValue
623 attributes:attributes];
625 attributedStringValue = nil;
627 [self setNeedsDisplay:YES];