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.
34 #import "KNShelfSplitView.h"
36 #import <AIUtilities/AIAttributedStringAdditions.h>
37 #import <AIUtilities/AIParagraphStyleAdditions.h>
38 #import <AIUtilities/AIImageAdditions.h>
40 #define DEFAULT_SHELF_WIDTH 200
41 #define CONTROL_HEIGHT 22
42 #define BUTTON_WIDTH 30
43 #define THUMB_WIDTH 15
44 #define THUMB_LINE_SPACING 2.0
45 #define RESIZE_BAR_EFFECTIVE_WIDTH 1.0
47 #define CONTROL_PART_NONE 0
48 #define CONTROL_PART_ACTION_BUTTON 1
49 #define CONTROL_PART_CONTEXT_BUTTON 2
50 #define CONTROL_PART_RESIZE_THUMB 3
51 #define CONTROL_PART_RESIZE_BAR 4
53 @implementation KNShelfSplitView
55 -(IBAction)toggleShelf:(id)sender{
56 #pragma unused(sender)
57 [self setShelfIsVisible: ![self isShelfVisible]];
58 [self setNeedsDisplay: YES];
61 - (id)initWithFrame:(NSRect)aFrame {
62 return [self initWithFrame: aFrame shelfView: nil contentView: nil];
65 -(id)initWithFrame:(NSRect)aFrame shelfView:(NSView *)aShelfView contentView:(NSView *)aContentView{
66 self = [super initWithFrame: aFrame];
69 currentShelfWidth = DEFAULT_SHELF_WIDTH;
72 activeControlPart = CONTROL_PART_NONE;
73 contextButtonMenu = nil;
74 [self recalculateSizes];
77 shelfBackgroundColor = nil;
78 actionButtonImage = nil;
79 contextButtonImage = nil;
81 background = [[NSImage imageNamed:@"sourceListBackground" forClass:[self class]] retain];
82 backgroundSize = [background size];
84 [self setDelegate: nil];
88 [self setShelfView: aShelfView];
89 [self setContentView: aContentView];
95 if( autosaveName ){ [autosaveName release]; }
96 if( contextButtonImage ){ [contextButtonImage release]; }
97 if( actionButtonImage ){ [actionButtonImage release]; }
98 if( shelfBackgroundColor ){ [shelfBackgroundColor release]; }
99 if( contextButtonMenu ){ [contextButtonMenu release]; }
103 -(void)setDelegate:(id)aDelegate{
104 delegate = aDelegate;
106 delegateHasValidateWidth = NO;
109 if( [delegate respondsToSelector:@selector(shelfSplitView:validateWidth:)] ){
110 delegateHasValidateWidth = YES;
119 -(void)setTarget:(id)aTarget{
121 [self recalculateSizes];
128 -(void)setAction:(SEL)aSelector{
130 [self recalculateSizes];
137 -(void)setContextButtonMenu:(NSMenu *)aMenu{
138 if( contextButtonMenu ){
139 [contextButtonMenu autorelease];
140 [[NSNotificationCenter defaultCenter] removeObserver: self];
143 contextButtonMenu = [aMenu retain];
145 if( contextButtonMenu ){
146 [contextButtonMenu setDelegate: self];
147 [[NSNotificationCenter defaultCenter] addObserver: self
148 selector: @selector(didEndContextMenuTracking)
149 name: NSMenuDidEndTrackingNotification
150 object: contextButtonMenu
153 [self recalculateSizes];
156 -(void)didEndContextMenuTracking{
158 [self setNeedsDisplayInRect: controlRect];
161 -(NSMenu *)contextButtonMenu{
162 return contextButtonMenu;
165 -(void)setShelfView:(NSView *)aView{
167 [shelfView removeFromSuperview];
173 [self addSubview: shelfView];
175 [self recalculateSizes];
178 -(NSView *)shelfView{
182 -(void)setContentView:(NSView *)aView{
184 [contentView removeFromSuperview];
190 [self addSubview: contentView];
193 [self recalculateSizes];
196 -(NSView *)contentView{
200 -(void)setShelfWidth:(float)aWidth{
201 float newWidth = aWidth;
204 // The shelf can never be completely closed. We always have at least enough to show our resize thumb, otherwise
205 // if the delegate responds to shelfSplitView:validateWidth:, we use that width as our minimum shelf size
206 float minShelf = THUMB_WIDTH;
207 if( delegateHasValidateWidth ){
208 float requestedWidth = [delegate shelfSplitView:self validateWidth: aWidth];
209 if( requestedWidth > minShelf ){
210 minShelf = requestedWidth;
213 if( minShelf > newWidth ){
217 // The shelf can never be wider than half the entire view
218 float maxShelf = [self frame].size.width / 2;
220 if( newWidth > maxShelf ){
224 currentShelfWidth = newWidth;
226 [self recalculateSizes];
230 return currentShelfWidth;
233 -(void)setAutosaveName:(NSString *)aName{
235 [autosaveName autorelease];
237 autosaveName = [aName retain];
240 -(NSString *)autosaveName{
244 -(void)recalculateSizes{
245 shouldDrawActionButton = NO;
246 shouldDrawContextButton = NO;
248 if( isShelfVisible ){
249 controlRect = NSMakeRect( 0, 0, currentShelfWidth, CONTROL_HEIGHT );
251 resizeThumbRect = NSMakeRect( (controlRect.size.width - THUMB_WIDTH), 0, THUMB_WIDTH, CONTROL_HEIGHT );
252 resizeBarRect = NSMakeRect( currentShelfWidth - (RESIZE_BAR_EFFECTIVE_WIDTH / 2), 0, RESIZE_BAR_EFFECTIVE_WIDTH, [self frame].size.height );
254 float availableSpace = controlRect.size.width - THUMB_WIDTH;
256 if( target && action && (availableSpace > BUTTON_WIDTH) ){
257 shouldDrawActionButton = YES;
258 actionButtonRect = NSMakeRect( 0, 0, BUTTON_WIDTH, CONTROL_HEIGHT );
259 availableSpace -= BUTTON_WIDTH;
262 if( contextButtonMenu && (availableSpace > BUTTON_WIDTH) ){
263 shouldDrawContextButton = YES;
264 contextButtonRect = NSMakeRect(controlRect.size.width - (THUMB_WIDTH + availableSpace), 0, BUTTON_WIDTH, CONTROL_HEIGHT);
269 [shelfView setFrame: NSMakeRect( 0, CONTROL_HEIGHT + 1, currentShelfWidth, [self bounds].size.height - (CONTROL_HEIGHT + 1) )];
273 float contentViewX = (isShelfVisible ? currentShelfWidth : 0);
274 NSRect newRect = NSMakeRect( contentViewX + 1, 0, [self bounds].size.width - (contentViewX + 1), [self bounds].size.height);
275 if( ! NSEqualRects(newRect, [contentView frame]) ){
276 [contentView setFrame: newRect];
280 [self setNeedsDisplay: YES];
281 [[self window] invalidateCursorRectsForView: self];
285 -(BOOL)isShelfVisible{
286 return isShelfVisible;
289 -(void)setShelfIsVisible:(BOOL)visible{
291 if( isShelfVisible ){
293 [shelfView removeFromSuperview];
295 [self addSubview: shelfView];
300 isShelfVisible = visible;
301 [self recalculateSizes];
304 -(void)setActionButtonImage:(NSImage *)anImage{
305 if( actionButtonImage ){
306 [actionButtonImage autorelease];
309 actionButtonImage = [anImage retain];
311 [self setNeedsDisplayInRect: controlRect];
314 -(NSImage *)actionButtonImage{
315 return actionButtonImage;
318 -(void)setContextButtonImage:(NSImage *)anImage{
319 if( contextButtonImage ){
320 [contextButtonImage autorelease];
323 contextButtonImage = [anImage retain];
325 [self setNeedsDisplayInRect: controlRect];
328 -(NSImage *)contextButtonImage{
329 return contextButtonImage;
332 -(void)setShelfBackgroundColor:(NSColor *)aColor{
333 if( shelfBackgroundColor ){
334 [shelfBackgroundColor autorelease];
337 shelfBackgroundColor = [aColor retain];
338 [self setNeedsDisplay: YES];
341 -(NSColor *)shelfBackgroundColor{
342 return shelfBackgroundColor;
345 -(void)resetCursorRects{
346 [super resetCursorRects];
347 if( isShelfVisible ){
348 [self addCursorRect: resizeThumbRect cursor: [NSCursor resizeLeftRightCursor]];
349 [self addCursorRect: resizeBarRect cursor: [NSCursor resizeLeftRightCursor]];
353 -(void)mouseDown:(NSEvent *)anEvent{
354 BOOL stillMouseDown = YES;
355 NSPoint currentLocation;
357 // determine if we're in a control part we care about
358 currentLocation = [self convertPoint: [anEvent locationInWindow] fromView: nil];
360 if( shouldDrawActionButton && NSPointInRect( currentLocation, actionButtonRect ) ){
361 activeControlPart = CONTROL_PART_ACTION_BUTTON;
363 }else if( shouldDrawContextButton && NSPointInRect( currentLocation, contextButtonRect ) ){
364 activeControlPart = CONTROL_PART_CONTEXT_BUTTON;
367 NSEvent * contextEvent = [NSEvent mouseEventWithType: [anEvent type]
368 location: NSMakePoint( contextButtonRect.origin.x + (contextButtonRect.size.width / 2) , contextButtonRect.origin.y + (contextButtonRect.size.height / 2) )
369 modifierFlags: [anEvent modifierFlags]
370 timestamp: [anEvent timestamp]
371 windowNumber: [anEvent windowNumber]
372 context: [anEvent context]
373 eventNumber: [anEvent eventNumber]
374 clickCount: [anEvent clickCount]
375 pressure: [anEvent pressure]
377 [self setNeedsDisplayInRect: controlRect];
378 [NSMenu popUpContextMenu: contextButtonMenu withEvent: contextEvent forView: self];
379 [super mouseDown: contextEvent];
382 }else if( NSPointInRect( currentLocation, resizeThumbRect ) ){
383 activeControlPart = CONTROL_PART_RESIZE_THUMB;
384 }else if( NSPointInRect( currentLocation, resizeBarRect ) ){
385 activeControlPart = CONTROL_PART_RESIZE_BAR;
387 activeControlPart = CONTROL_PART_NONE;
390 [self setNeedsDisplayInRect: controlRect];
392 if( activeControlPart != CONTROL_PART_NONE ){
393 while( stillMouseDown ){
394 anEvent = [[self window] nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask];
395 currentLocation = [self convertPoint: [anEvent locationInWindow] fromView: nil];
398 if( (activeControlPart == CONTROL_PART_ACTION_BUTTON) && NSPointInRect( currentLocation, actionButtonRect ) ){
400 }else if( (activeControlPart == CONTROL_PART_CONTEXT_BUTTON) && NSPointInRect( currentLocation, contextButtonRect ) ){
404 switch( [anEvent type] ){
405 case NSLeftMouseDragged:
406 if( (activeControlPart == CONTROL_PART_RESIZE_THUMB) || (activeControlPart == CONTROL_PART_RESIZE_BAR) ){
407 [self setShelfWidth: currentLocation.x];
409 [self setNeedsDisplayInRect: controlRect];
415 [self setNeedsDisplayInRect: controlRect];
417 if( (activeControlPart == CONTROL_PART_ACTION_BUTTON) && NSPointInRect( currentLocation, actionButtonRect ) ){
419 if( target && action && [target respondsToSelector:action]){
420 [target performSelector: action withObject: self];
432 [super mouseDown:anEvent];
436 - (void)drawRect:(NSRect)rect {
437 #pragma unused( rect )
439 if( isShelfVisible ){
440 //NSLog(@"Drawing Control( %f, %f) (%f, %f)", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
442 float remainderStart = 0.0;
445 if( shouldDrawActionButton ){
446 [self drawControlBackgroundInRect: actionButtonRect
447 active: (activeControlPart == CONTROL_PART_ACTION_BUTTON) && shouldHilite
449 [[NSColor windowFrameColor] set];
450 NSRectFill( NSMakeRect( (actionButtonRect.origin.x + actionButtonRect.size.width) - 1, 0, 1, controlRect.size.height ) );
451 remainderStart += actionButtonRect.size.width;
453 if( actionButtonImage ){
454 NSRect targetRect = NSMakeRect(actionButtonRect.origin.x,
455 actionButtonRect.origin.y,
456 [actionButtonImage size].width,
457 [actionButtonImage size].height
460 if( targetRect.size.width > actionButtonRect.size.width ){
461 targetRect.size.width = actionButtonRect.size.width;
463 if( targetRect.size.width < actionButtonRect.size.width ){
464 targetRect.origin.x += (actionButtonRect.size.width - targetRect.size.width) / 2.0;
466 if( targetRect.size.height > actionButtonRect.size.height ){
467 targetRect.size.height = actionButtonRect.size.height;
469 if( targetRect.size.height < actionButtonRect.size.height ){
470 targetRect.origin.y += (actionButtonRect.size.height - targetRect.size.height) / 2.0;
473 [actionButtonImage drawInRect: targetRect
474 fromRect: NSMakeRect( 0, 0, [actionButtonImage size].width, [actionButtonImage size].height )
475 operation: NSCompositeSourceOver
482 if( shouldDrawContextButton ){
483 [self drawControlBackgroundInRect: contextButtonRect
484 active: (activeControlPart == CONTROL_PART_CONTEXT_BUTTON ) && shouldHilite
486 [[NSColor windowFrameColor] set];
487 NSRectFill( NSMakeRect( (contextButtonRect.origin.x + contextButtonRect.size.width) - 1, 0, 1, controlRect.size.height ) );
488 remainderStart += contextButtonRect.size.width;
490 if( contextButtonImage ){
491 NSRect targetRect = NSMakeRect(contextButtonRect.origin.x,
492 contextButtonRect.origin.y,
493 [contextButtonImage size].width,
494 [contextButtonImage size].height
497 if( targetRect.size.width > contextButtonRect.size.width ){
498 targetRect.size.width = contextButtonRect.size.width;
500 if( targetRect.size.width < contextButtonRect.size.width ){
501 targetRect.origin.x += (contextButtonRect.size.width - targetRect.size.width) / 2.0;
503 if( targetRect.size.height > contextButtonRect.size.height ){
504 targetRect.size.height = contextButtonRect.size.height;
506 if( targetRect.size.height < contextButtonRect.size.height ){
507 targetRect.origin.y += (contextButtonRect.size.height - targetRect.size.height) / 2.0;
509 [contextButtonImage drawInRect: targetRect
510 fromRect: NSMakeRect( 0, 0, [contextButtonImage size].width, [contextButtonImage size].height )
511 operation: NSCompositeSourceOver
517 //remainder and thumb
518 [self drawControlBackgroundInRect: NSMakeRect( remainderStart, 0, (controlRect.size.width - remainderStart), controlRect.size.height )
522 [[NSColor windowFrameColor] set];
523 NSRectFill( NSMakeRect( 0, CONTROL_HEIGHT, currentShelfWidth, 1 ) );
525 // Draw our split line
526 [[NSColor windowFrameColor] set];
527 NSRectFill( NSMakeRect( currentShelfWidth, 0, 1, [self frame].size.height ) );
529 // Draw our thumb lines
530 [[NSColor disabledControlTextColor] set];
531 NSRect thumbLineRect = NSMakeRect(
532 resizeThumbRect.origin.x + (resizeThumbRect.size.width - ((2*THUMB_LINE_SPACING) + 3.0)) / 2.0,
533 resizeThumbRect.size.height / 4.0,
535 resizeThumbRect.size.height / 2.0
538 for( i=0; i<3; i++ ){
539 NSRectFill( thumbLineRect );
540 thumbLineRect.origin.x += (1+THUMB_LINE_SPACING);
543 if( shelfBackgroundColor ){
544 [shelfBackgroundColor set];
545 NSRectFill( NSMakeRect( 0, CONTROL_HEIGHT+1, currentShelfWidth, [self frame].size.height ) );
549 if (attributedStringValue && !shouldDrawContextButton && !shouldDrawActionButton) {
550 NSRect textRect = NSMakeRect(6, (NSHeight(controlRect) - stringHeight)/2, NSMinX(resizeThumbRect) - 8, stringHeight);
552 [attributedStringValue drawInRect:textRect];
557 //This glass implementation is actually broken but works for Adium -- a proper one would take the width of aRect into account.
558 -(void)drawControlBackgroundInRect:(NSRect)aRect active:(BOOL)isActive{
559 NSRect frame = [self frame];
561 //Draw the background, tiling across
562 NSRect sourceRect = NSMakeRect(0, 0, backgroundSize.width, backgroundSize.height);
563 NSRect destRect = NSMakeRect(frame.origin.x, frame.origin.y, sourceRect.size.width, aRect.size.height);
565 while ((destRect.origin.x < NSMaxX(frame)) && destRect.size.width > 0) {
567 if (NSMaxX(destRect) > NSMaxX(frame)) {
568 sourceRect.size.width = NSWidth(destRect);
571 [background drawInRect:destRect
573 operation:NSCompositeSourceOver
575 destRect.origin.x += destRect.size.width;
579 -(void)setFrame:(NSRect)aRect{
580 [super setFrame: aRect];
581 [self recalculateSizes];
584 #pragma mark Status string
585 - (void)setResizeThumbStringValue:(NSString *)inString
587 if (![inString isEqualToString:stringValue]) {
588 [stringValue release];
589 stringValue = [inString copy];
591 [attributedStringValue release];
593 NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
594 [NSParagraphStyle styleWithAlignment:NSLeftTextAlignment
595 lineBreakMode:NSLineBreakByTruncatingTail], NSParagraphStyleAttributeName,
596 [NSFont systemFontOfSize:[NSFont smallSystemFontSize]], NSFontAttributeName,
599 stringHeight = [NSAttributedString stringHeightForAttributes:attributes];
600 attributedStringValue = [[NSAttributedString alloc] initWithString:stringValue
601 attributes:attributes];
603 attributedStringValue = nil;
605 [self setNeedsDisplay:YES];