// // HistogramView.m // Task Views // // Created by Chris Karr on 3/18/09. // Copyright 2009 __MyCompanyName__. All rights reserved. // #import "HistogramView.h" #import "HistogramBlockView.h" @implementation HistogramView - (id)initWithFrame:(NSRect) frame { if (self = [super initWithFrame:frame]) { attributeLabel = nil; countLabel = nil; bins = [[NSMutableDictionary alloc] init]; blocks = [[NSMutableArray alloc] init]; } return self; } - (void) drawBackground:(NSRect) rect { NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; NSData * color = [defaults valueForKey:@"hist_background"]; if (color == nil) [[NSColor whiteColor] setFill]; else [[NSKeyedUnarchiver unarchiveObjectWithData:color] setFill]; NSBezierPath * path = [NSBezierPath bezierPathWithRect:rect]; [path fill]; } - (void) drawAxes:(NSRect) rect { NSBezierPath * path = [NSBezierPath bezierPath]; [path setLineWidth:1.0]; [path setLineCapStyle:NSSquareLineCapStyle]; [path setLineJoinStyle:NSMiterLineJoinStyle]; [[NSColor blackColor] setStroke]; [path moveToPoint:NSMakePoint(rect.origin.x, rect.size.height)]; [path lineToPoint:NSMakePoint(rect.origin.x, rect.origin.y)]; [path lineToPoint:NSMakePoint(rect.size.width, rect.origin.y)]; [path stroke]; } - (void) drawRect:(NSRect) rect { NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; [self drawBackground:rect]; if (attributeLabel == nil) { attributeLabel = [[NSTextField alloc] init]; [attributeLabel setBezeled:NO]; [attributeLabel setBordered:NO]; [attributeLabel setEditable:NO]; [attributeLabel setDrawsBackground:NO]; [self addSubview:attributeLabel]; [self setNeedsDisplay:YES]; } if ([defaults stringForKey:@"hist_attribute"] != nil) [attributeLabel setStringValue:[NSString stringWithFormat:@"%@ (%d values)", [defaults stringForKey:@"hist_attribute"], categoryCount, nil]]; else [attributeLabel setStringValue:@"No Property Selected"]; [attributeLabel sizeToFit]; if (countLabel == nil) { countLabel = [[NSTextField alloc] init]; [countLabel setBezeled:NO]; [countLabel setBordered:NO]; [countLabel setEditable:NO]; [countLabel setDrawsBackground:NO]; [self addSubview:countLabel]; [countLabel rotateByAngle:-90.0]; [countLabel sizeToFit]; [self setNeedsDisplay:YES]; } [countLabel setStringValue:[NSString stringWithFormat:@"Frequency (max = %d)", maxCategoryCount, nil]]; [countLabel sizeToFit]; NSRect axes = NSMakeRect(0, 0, [self frame].size.width, [self frame].size.height); if (NSEqualRects(rect, [self bounds]) || NSContainsRect(rect, [self bounds])) { NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; NSString * attribute = @" Unknown Property"; if ([defaults stringForKey:@"hist_attribute"] != nil) attribute = [defaults stringForKey:@"hist_attribute"]; CGFloat xOffset = ([self bounds].size.width - [attributeLabel bounds].size.width) / 2; [attributeLabel setFrame:NSMakeRect(xOffset, 10, [attributeLabel bounds].size.width, [attributeLabel bounds].size.height)]; CGFloat yOffset = ([self bounds].size.height - [countLabel bounds].size.height) / 2; [countLabel setFrame:NSMakeRect(10, yOffset, [countLabel bounds].size.width, [countLabel bounds].size.height)]; axes.size.width = axes.size.width - ([countLabel bounds].size.height + 20); axes.origin.x = floor(axes.origin.x + [attributeLabel bounds].size.height + 20) - 0.5; axes.size.height = axes.size.height - ([attributeLabel bounds].size.height + 20); axes.origin.y = floor(axes.origin.y + [attributeLabel bounds].size.height + 20) - 0.5; CGFloat blockWidth = floor((axes.size.width - ([countLabel bounds].size.height + 20)) / categoryCount); CGFloat blockHeight = floor((axes.size.height - ([attributeLabel bounds].size.height + 20)) / maxCategoryCount); if (blockHeight < 1) blockHeight = 1; if (blockWidth < 1) blockWidth = 1; CGFloat x = ceil(axes.origin.x); NSUInteger blockIndex = 0; for (NSString * key in [[bins allKeys] sortedArrayUsingSelector:@selector(compare:)]) { CGFloat y = ceil(axes.origin.y); NSArray * matches = [bins valueForKey:key]; for (NSDictionary * task in matches) { HistogramBlockView * block = [blocks objectAtIndex:blockIndex]; block.task = task; [block setToolTip:[NSString stringWithFormat:@"%@ (%@: %@)", [task valueForKey:@"name"], attribute, key, nil]]; [block setFrame:NSMakeRect(x, y, blockWidth, blockHeight)]; blockIndex = blockIndex + 1; y = y + blockHeight; } x = x + blockWidth; } } [super drawRect:rect]; [self drawAxes:axes]; } - (void) setSelectedItem:(NSDictionary *) task { [tasks setSelectedObjects:[NSArray arrayWithObject:task]]; } - (void) stuffBins { [bins removeAllObjects]; for (NSView * v in blocks) [v removeFromSuperview]; [blocks removeAllObjects]; NSDate * startDate = nil; NSDate * endDate = nil; NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy-MM-dd"]; NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; BOOL ignoreMissing = [defaults boolForKey:@"hist_ignore_missing"]; NSString * key = @" Unknown Property"; if ([defaults stringForKey:@"hist_attribute"] != nil) key = [defaults stringForKey:@"hist_attribute"]; for (NSDictionary * task in [tasks arrangedObjects]) { id value = [task valueForKey:key]; if (value == nil) { if (!ignoreMissing) value = @" Unknown Property"; } if ([value isKindOfClass:[NSDate class]]) { if (startDate == nil) startDate = value; if (endDate == nil) endDate = value; if ([startDate compare:value] == NSOrderedDescending) startDate = value; if ([endDate compare:value] == NSOrderedAscending) endDate = value; value = [dateFormatter stringFromDate:value]; } if (value != nil) { NSMutableArray * matches = [bins valueForKey:[value description]]; if (matches == nil) { matches = [NSMutableArray array]; [bins setValue:matches forKey:[value description]]; } [matches addObject:task]; NSView * v = [[HistogramBlockView alloc] init]; [blocks addObject:v]; [self addSubview:v]; [self setNeedsDisplay:YES]; } } if (startDate != nil && endDate != nil) { NSDate * day = [[NSDate alloc] initWithTimeInterval:(60 * 60 * 24) sinceDate:startDate]; while ([endDate compare:day] == NSOrderedDescending) { NSString * dayString = [dateFormatter stringFromDate:day]; if ([bins valueForKey:dayString] == nil) [bins setValue:[NSArray array] forKey:dayString]; NSDate * oldDay = day; day = [[NSDate alloc] initWithTimeInterval:(60 * 60 * 24) sinceDate:oldDay]; [oldDay release]; } [day release]; } [dateFormatter release]; categoryCount = [[bins allKeys] count]; maxCategoryCount = 0; for (id key in [bins allKeys]) { NSUInteger thisCount = [[bins valueForKey:key] count]; if (thisCount > maxCategoryCount) maxCategoryCount = thisCount; } } - (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:@"hist_background" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"hist_default_color" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"hist_selection_color" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"hist_attribute" options:0 context:NULL]; [defaults addObserver:self forKeyPath:@"hist_ignore_missing" options:0 context:NULL]; [[self window] setAcceptsMouseMovedEvents:YES]; [[self window] setIgnoresMouseEvents:NO]; [self stuffBins]; } - (void) observeValueForKeyPath: (NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context { if ([keyPath isEqual:@"selection"]) { for (HistogramBlockView * v in blocks) { id last = [[tasks selectedObjects] lastObject]; if (last == nil || v.task != last) v.highlighted = [NSNumber numberWithBool:NO]; else v.highlighted = [NSNumber numberWithBool:YES]; [v setNeedsDisplay:YES]; } } else if ([keyPath isEqual:@"arrangedObjects"] || [keyPath isEqual:@"hist_attribute"] || [keyPath isEqual:@"hist_ignore_missing"]) { [self stuffBins]; } [self setNeedsDisplay:YES]; } - (BOOL) acceptsFirstResponder { return YES; } - (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]; } - (void) editTask { [taskManager editTask:self]; } - (void) keyDown:(NSEvent *)theEvent { unsigned short code = [theEvent keyCode]; if (code >= 123 && code <= 126 && [blocks count] > 0) { HistogramBlockView * highlighted = nil; for (HistogramBlockView * v in blocks) { if ([v.highlighted boolValue]) highlighted = v; } if (highlighted != nil) { NSUInteger index = [blocks indexOfObject:highlighted]; if (code == 126) { HistogramBlockView * next = [blocks objectAtIndex:0]; if (index < ([blocks count] - 1)) next = [blocks objectAtIndex:(index + 1)]; [tasks setSelectedObjects:[NSArray arrayWithObject:next.task]]; } else if (code == 125) { HistogramBlockView * next = [blocks objectAtIndex:0]; if (index > 0) next = [blocks objectAtIndex:(index - 1)]; else next = [blocks lastObject]; [tasks setSelectedObjects:[NSArray arrayWithObject:next.task]]; } else { NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy-MM-dd"]; NSDictionary * task = highlighted.task; NSString * attribute = @" Unknown Property"; NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; if ([defaults stringForKey:@"hist_attribute"] != nil) attribute = [defaults stringForKey:@"hist_attribute"]; NSArray * keys = [[bins allKeys] sortedArrayUsingSelector:@selector(compare:)]; NSUInteger x = 0; for (NSUInteger i = 0; i < [keys count]; i++) { id value = [task valueForKey:attribute]; if ([value isKindOfClass:[NSDate class]]) value = [dateFormatter stringFromDate:value]; if (value != nil && [value isEqual:[keys objectAtIndex:i]]) x = i; } NSArray * bar = [bins valueForKey:[keys objectAtIndex:x]]; NSUInteger y = [bar indexOfObject:task]; if (code == 123) { if (x == 0) x = [keys count] - 1; else x = x - 1; while ([[bins valueForKey:[keys objectAtIndex:x]] count] == 0) { if (x == 0) x = [keys count] - 1; else x = x - 1; } } else { if (x < [keys count] - 1) x = x + 1; else x = 0; while ([[bins valueForKey:[keys objectAtIndex:x]] count] == 0) { if (x < [keys count] - 1) x = x + 1; else x = 0; } } NSArray * nextBar = [bins valueForKey:[keys objectAtIndex:x]]; if (y >= [nextBar count] - 1) y = [nextBar count] - 1; [tasks setSelectedObjects:[NSArray arrayWithObject:[nextBar objectAtIndex:y]]]; [dateFormatter release]; } } else { HistogramBlockView * next = [blocks objectAtIndex:0]; [tasks setSelectedObjects:[NSArray arrayWithObject:next.task]]; } } else [super keyDown:theEvent]; } @end