// // TagCloudView.m // Task Views // // Created by Chris Karr on 3/3/09. // Copyright 2009 Chris J. Karr. All rights reserved. // #import "TagCloudView.h" #import "TagButton.h" #import "NSButton+TextColor.h" NSInteger areaSort (id one, id two, void * reverse) { NSRect oneRect = [one rectValue]; NSRect twoRect = [two rectValue]; double oneArea = oneRect.size.width * oneRect.size.height; double twoArea = twoRect.size.width * twoRect.size.height; if (oneArea > twoArea) return NSOrderedAscending; return NSOrderedDescending; } @implementation TagCloudView - (id) initWithFrame:(NSRect) frame { self = [super initWithFrame:frame]; if (self) { textViews = [[NSMutableArray alloc] init]; textDefs = [[NSArray alloc] init]; lastView = nil; lastClick = nil; } return self; } - (BOOL) acceptsFirstResponder { return YES; } - (void) refreshTags { NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; NSScanner * scanner = [[NSScanner alloc] init]; NSColor * color = [NSColor lightGrayColor]; NSData * colorData = [defaults valueForKey:@"tag_default_color"]; NSString * colorField = [defaults valueForKey:@"tag_color_attribute"]; if (colorField == nil) colorField = @"name"; NSDictionary * mappings = [defaults objectForKey:@"color_mappings"]; if (mappings == nil) mappings = [NSDictionary dictionary]; NSArray * colorStrings = [mappings allKeys]; if (colorData != nil) color = [NSKeyedUnarchiver unarchiveObjectWithData:colorData]; for (TagButton * field in textViews) { [field setTitle:[field.task valueForKey:@"name"]]; [field setMenu:[self menu]]; if (field.color == nil) { NSObject * value = [field.task valueForKey:colorField]; if (value != nil) { NSString * superString = [value description]; NSColor * myColor = nil; for (NSString * match in colorStrings) { if (myColor == nil) { if ([superString rangeOfString:match].location != NSNotFound) myColor = [NSKeyedUnarchiver unarchiveObjectWithData:[mappings valueForKey:match]]; } } if (myColor != nil) field.color = myColor; } if (field.color == nil) field.color = color; } [field setTextColor:field.color]; [field setBordered:NO]; NSString * key = [defaults valueForKey:@"tag_size_attribute"]; NSObject * value = [field.task valueForKey:key]; NSNumber * base = nil; if ([value isKindOfClass:[NSNumber class]]) base = (NSNumber *) value; else if ([value isKindOfClass:[NSDate class]]) { NSTimeInterval dateDelta = ((([((NSDate *) value) timeIntervalSinceNow] / 60) / 60) / 24); base = [NSNumber numberWithDouble:(0 - dateDelta)]; } else { double d; if (![scanner scanDouble:&d]) d = 1; base = [NSNumber numberWithDouble:d]; } NSNumber * multiplier = [defaults valueForKey:@"tag_size_multiplier"]; if (multiplier == nil) multiplier = [NSNumber numberWithInteger:0]; NSNumber * adjustment = [defaults valueForKey:@"tag_size_adjustment"]; if (adjustment == nil) adjustment = [NSNumber numberWithInteger:0]; float baseFloat = [base floatValue]; if ([defaults boolForKey:@"tag_size_ln"]) { float absLog = log(abs(baseFloat)); if (baseFloat < 0) baseFloat = 0 - absLog; else baseFloat = absLog; } float size = (baseFloat * [multiplier floatValue]) + [adjustment floatValue]; if (size < 0) size = 0; size = 6 + floorf(size); NSFont * font = [NSFont fontWithName:@"Helvetica" size:size]; if (![font isEqual:[field font]]) { finishRefresh = YES; [field setFont:font]; } [field sizeToFit]; [field setAction:@selector(click:)]; [field setTarget:self]; [field setNeedsDisplay:YES]; } [scanner release]; [self setNeedsDisplay:YES]; } - (void) refresh:(NSTimer *) theTimer { if (finishRefresh) { [self refreshTags]; finishRefresh = NO; } } - (void) click:(id) sender { if ([sender isMemberOfClass:[TagButton class]]) { TagButton * button = (TagButton *) sender; if (sender == lastClick) { [taskManager editTask:sender]; } else { if (lastClick != nil) [lastClick setTextColor:((TagButton *) lastClick).color]; [button setTextColor:[NSColor whiteColor]]; [tasks setSelectedObjects:[NSArray array]]; [tasks setSelectedObjects:[NSArray arrayWithObject:button.task]]; lastClick = sender; } } } - (void) setTextDefinitions:(NSArray *) defs { lastClick = nil; [textDefs release]; textDefs = [defs retain]; NSEnumerator * viewIter = [textViews objectEnumerator]; NSView * view; while (view = [viewIter nextObject]) [view removeFromSuperview]; [textViews removeAllObjects]; NSEnumerator * defIter = [textDefs objectEnumerator]; NSDictionary * def = nil; srand([[NSDate date] timeIntervalSince1970]); while (def = [defIter nextObject]) { TagButton * text = [[TagButton alloc] init]; [text setHidden:YES]; [[text cell] setShowsStateBy:NSNoCellMask]; [[text cell] setHighlightsBy:NSNoCellMask]; text.task = def; [textViews addObject:text]; if (lastView == nil) [self addSubview:text]; else [self addSubview:text positioned:NSWindowBelow relativeTo:lastView]; lastView = text; [text release]; } [self refreshTags]; } - (void) awakeFromNib { [[self window] setAcceptsMouseMovedEvents:YES]; [tasks addObserver:self forKeyPath:@"arrangedObjects" options:0 context:NULL]; [tasks addObserver:self forKeyPath:@"selection" options:0 context:NULL]; NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; [defaults addObserver:self forKeyPath:@"tag_size_ln" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"tag_size_attribute" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"tag_size_multiplier" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"tag_size_adjustment" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"tag_default_color" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"tag_color_attribute" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"color_mappings" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"cloud_background" options:0 context:NULL]; timer = [[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(refresh:) userInfo:nil repeats:YES] retain]; } - (void) observeValueForKeyPath: (NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context { if (lastClick != nil) [((TagButton *) lastClick) setTextColor:((TagButton *) lastClick).color]; lastClick = nil; if ([keyPath isEqual:@"arrangedObjects"]) [self setTextDefinitions:[tasks arrangedObjects]]; else if ([keyPath isEqual:@"selection"]) { NSArray * selectedTasks = [tasks selectedObjects]; if ([selectedTasks count] > 0) { for (TagButton * field in textViews) { if (![field isHidden] && [field.task isEqualToDictionary:[[tasks selectedObjects] lastObject]]) { [field setTextColor:[NSColor whiteColor]]; lastClick = field; } } } } else [self setTextDefinitions:[tasks arrangedObjects]]; } - (IBAction) refreshView:(id) sender { [self setTextDefinitions:[tasks arrangedObjects]]; } - (NSValue *) bestRectInValues:(NSArray *) values forSize:(NSSize) size { if ([values count] == 0) return nil; for (NSValue * val in [values sortedArrayUsingFunction:areaSort context:NULL]) { NSRect rect = [val rectValue]; if (rect.size.width >= size.width && rect.size.height >= size.height) return val; } return nil; } - (NSArray *) shatterRect:(NSRect) container withRect:(NSRect) hole { NSMutableArray * remainders = [NSMutableArray array]; NSSize wide = NSMakeSize(container.size.width, ((container.size.height - hole.size.height) / 2)); NSSize tall = NSMakeSize(((container.size.width - hole.size.width) / 2), container.size.height); if ((wide.width * wide.height) < (tall.width * tall.height)) // Use tall blocks { NSRect left = NSMakeRect(container.origin.x, container.origin.y, tall.width, tall.height); NSRect right = NSMakeRect((container.origin.x + container.size.width) - tall.width, container.origin.y, tall.width, tall.height); NSRect bottom = NSMakeRect(container.origin.x + tall.width, container.origin.y, hole.size.width, ((container.size.height - hole.size.height) / 2)); NSRect top = NSMakeRect(container.origin.x + tall.width, hole.origin.y + hole.size.height, hole.size.width, ((container.size.height - hole.size.height) / 2)); if (left.size.width > 5 && left.size.height > 5) [remainders addObject:[NSValue valueWithRect:left]]; if (right.size.width > 5 && right.size.height > 5) [remainders addObject:[NSValue valueWithRect:right]]; if (top.size.width > 5 && top.size.height > 5) [remainders addObject:[NSValue valueWithRect:top]]; if (bottom.size.width > 5 && bottom.size.height > 5) [remainders addObject:[NSValue valueWithRect:bottom]]; } else { NSRect bottom = NSMakeRect(container.origin.x, container.origin.y, wide.width, wide.height); NSRect top = NSMakeRect(container.origin.x, hole.origin.y + hole.size.height, wide.width, wide.height); NSRect left = NSMakeRect(container.origin.x, container.origin.y + wide.height, ((container.size.width - hole.size.width) / 2), hole.size.height); NSRect right = NSMakeRect(hole.origin.x + hole.size.width, left.origin.y, ((container.size.width - hole.size.width) / 2), hole.size.height); [remainders addObject:[NSValue valueWithRect:left]]; [remainders addObject:[NSValue valueWithRect:right]]; [remainders addObject:[NSValue valueWithRect:top]]; [remainders addObject:[NSValue valueWithRect:bottom]]; } return remainders; } - (void) drawBox:(NSRect) rect { NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; NSData * color = [defaults valueForKey:@"cloud_background"]; if (color == nil) [[NSColor blackColor] setFill]; else [[NSKeyedUnarchiver unarchiveObjectWithData:color] setFill]; NSBezierPath * path = [NSBezierPath bezierPathWithRect:rect]; [path fill]; } - (void) drawRect:(NSRect) rect { NSSize size = [self bounds].size; [self drawBox:rect]; if (size.width == rect.size.width && size.height == rect.size.height) { [self willChangeValueForKey:@"tasksVisible"]; NSMutableArray * rects = [NSMutableArray arrayWithObject:[NSValue valueWithRect:[self bounds]]]; NSEnumerator * textIter = [textViews objectEnumerator]; NSTextField * text = nil; tasksVisible = 0; while (text = [textIter nextObject]) { [text setHidden:YES]; [text sizeToFit]; NSSize size = [text frame].size; NSValue * bestValue = [self bestRectInValues:rects forSize:size]; if (bestValue != nil) { [text setHidden:NO]; NSRect bestRect = [bestValue rectValue]; NSPoint center = NSMakePoint((bestRect.origin.x + (bestRect.size.width / 2)), (bestRect.origin.y + (bestRect.size.height / 2))); [text setFrameOrigin:NSMakePoint(center.x - (size.width / 2), center.y - (size.height / 2))]; NSArray * remainders = [self shatterRect:bestRect withRect:[text frame]]; [rects removeObject:bestValue]; [rects addObjectsFromArray:remainders]; tasksVisible = tasksVisible + 1; } } [self didChangeValueForKey:@"tasksVisible"]; [[self window] update]; } [super drawRect:rect]; } - (NSNumber *) tasksVisible { return [NSNumber numberWithUnsignedInteger:tasksVisible]; } - (IBAction) settings:(id) sender { [NSApp beginSheet:settings modalForWindow:[self window] modalDelegate:self didEndSelector:nil contextInfo:NULL]; } - (IBAction) closeSettings:(id) sender; { [NSApp endSheet:settings]; [settings orderOut:self]; } - (IBAction) addColor:(id) sender { NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; NSString * string = [substring stringValue]; NSColor * color = [colorWell color]; NSDictionary * mappings = [defaults objectForKey:@"color_mappings"]; NSMutableDictionary * newMappings = [NSMutableDictionary dictionaryWithDictionary:mappings]; [newMappings setValue:[NSKeyedArchiver archivedDataWithRootObject:color] forKey:string]; [defaults setObject:newMappings forKey:@"color_mappings"]; } @end