source: trunk/Cocoa/Pester/Source/OACalendarView.m @ 516

Last change on this file since 516 was 125, checked in by Nicholas Riley, 18 years ago

OACalendarView.m: Highlight with secondary (unfocused) highlight color
when view is not focused. Override -needsPanelToBecomeKey so keyboard
shortcuts work even if full keyboard navigation disabled. Fixes bug
28.

File size: 35.2 KB
Line 
1// Copyright 2001-2002 Omni Development, Inc.  All rights reserved.
2//
3// This software may only be used and reproduced according to the
4// terms in the file OmniSourceLicense.html, which should be
5// distributed with this project and can also be found at
6// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.
7
8#import "OACalendarView.h"
9#import "NSImage-OAExtensions.h"
10#import "NSCalendarDate-OFExtensions.h"
11
12#import <AppKit/AppKit.h>
13
14// RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniAppKit/Widgets.subproj/OACalendarView.m,v 1.20 2002/12/07 00:23:40 andrew Exp $")
15
16
17/*
18    Some Notes:
19   
20    - Setting the View Size: see the notes in -initWithFrame: for some guidelines for determining what size you will want to give this view. Those notes also give information about font sizes and how they affect us and the size calculations. If you set the view size to a non-optimal size, we won't use all the space.
21   
22    - Dynamically Adjusting the Cell Display: check out the "delegate" method -calendarView:willDisplayCell:forDate: in order to adjust the cell attributes (such as the font color, etc.). Note that if you make any changes which impact the cell size, the calendar is unlikely to draw as desired, so this is mostly useful for color changes. You can also use -calendarView:highlightMaskForVisibleMonth: to get highlighting of certain days. This is more efficient since we need only ask once for the month rather than once for each cell, but it is far less flexible, and currently doesn't allow control over the highlight color used. Also, don't bother to implement both methods: only the former will be used if it is available.
23   
24    - We should have a real delegate instead of treating the target as the delgate.
25   
26    - We could benefit from some more configurability: specify whether or not to draw vertical/horizontal grid lines, grid and border widths, fonts, whether or not to display the top control area, whether or not the user can change the displayed month/year independant of whether they can change the selected date, etc.
27   
28    - We could be more efficient, such as in only calculating things we need. The biggest problem (probably) is that we recalculate everything on every -drawRect:, simply because I didn't see an ideal place to know when we've resized. (With the current implementation, the monthAndYearRect would also need to be recalculated any time the month or year changes, so that the month and year will be correctly centered.)
29*/
30
31
32@interface OACalendarView (Private)
33
34- (NSButton *)_createButtonWithFrame:(NSRect)buttonFrame;
35
36- (void)_calculateSizes;
37- (void)_drawDaysOfMonthInRect:(NSRect)rect;
38- (void)_drawGridInRect:(NSRect)rect;
39
40- (float)_maximumDayOfWeekWidth;
41- (NSSize)_maximumDayOfMonthSize;
42- (float)_minimumColumnWidth;
43- (float)_minimumRowHeight;
44
45- (NSCalendarDate *)_hitDateWithLocation:(NSPoint)targetPoint;
46- (NSCalendarDate *)_hitWeekdayWithLocation:(NSPoint)targetPoint;
47
48@end
49
50
51@implementation OACalendarView
52
53const float OACalendarViewButtonWidth = 15.0;
54const float OACalendarViewButtonHeight = 15.0;
55const float OACalendarViewSpaceBetweenMonthYearAndGrid = 6.0;
56const int OACalendarViewNumDaysPerWeek = 7;
57const int OACalendarViewMaxNumWeeksIntersectedByMonth = 6;
58
59//
60// Init / dealloc
61//
62
63- (id)initWithFrame:(NSRect)frameRect;
64{
65    // The calendar will only resize on certain boundaries. "Ideal" sizes are:
66    //     - width = (multiple of 7) + 1, where multiple >= 22; "minimum" width is 162
67    //     - height = (multiple of 6) + 39, where multiple >= 15; "minimum" height is 129
68   
69    // In reality you can shrink it smaller than the minimums given here, and it tends to look ok for a bit, but this is the "optimum" minimum. But you will want to set your size based on the guidelines above, or the calendar will not actually fill the view exactly.
70
71    // The "minimum" view size comes out to be 162w x 129h. (Where minimum.width = 23 [minimum column width] * 7 [num days per week] + 1.0 [for the side border], and minimum.height = 22 [month/year control area height; includes the space between control area and grid] + 17 [the  grid header height] + (15 [minimum row height] * 6 [max num weeks in month]). [Don't need to allow 1 for the bottom border due to the fact that there's no top border per se.]) (We used to say that the minimum height was 155w x 123h, but that was wrong - we weren't including the grid lines in the row/column sizes.)
72    // These sizes will need to be adjusted if the font changes, grid or border widths change, etc. We use the controlContentFontOfSize:11.0 for the  - if the control content font is changed our calculations will change and the above sizes will be incorrect. Similarly, we use the default NSTextFieldCell font/size for the month/year header, and the default NSTableHeaderCell font/size for the day of week headers; if either of those change, the aove sizes will be incorrect.
73
74    NSDateFormatter *monthAndYearFormatter;
75    int index;
76    NSUserDefaults *defaults;
77    NSArray *shortWeekDays;
78    NSRect buttonFrame;
79    NSButton *button;
80    NSBundle *thisBundle;
81
82    if ([super initWithFrame:frameRect] == nil)
83        return nil;
84   
85    thisBundle = [NSBundle bundleForClass: [OACalendarView class]];
86    monthAndYearTextFieldCell = [[NSTextFieldCell alloc] init];
87    monthAndYearFormatter = [[NSDateFormatter alloc] initWithDateFormat:@"%B %Y" allowNaturalLanguage:NO];
88    [monthAndYearTextFieldCell setFormatter:monthAndYearFormatter];
89    [monthAndYearTextFieldCell setFont: [NSFont boldSystemFontOfSize: [NSFont systemFontSize]]];
90    [monthAndYearFormatter release];
91
92    defaults = [NSUserDefaults standardUserDefaults];
93    shortWeekDays = [defaults objectForKey:NSShortWeekDayNameArray];
94    for (index = 0; index < OACalendarViewNumDaysPerWeek; index++) {
95        dayOfWeekCell[index] = [[NSTableHeaderCell alloc] init];
96        [dayOfWeekCell[index] setAlignment:NSCenterTextAlignment];
97        [dayOfWeekCell[index] setStringValue:[[shortWeekDays objectAtIndex:index] substringToIndex:1]];
98    }
99
100    dayOfMonthCell = [[NSTextFieldCell alloc] init];
101    [dayOfMonthCell setAlignment:NSCenterTextAlignment];
102    [dayOfMonthCell setFont:[NSFont controlContentFontOfSize:11.0]];
103
104    buttons = [[NSMutableArray alloc] initWithCapacity:2];
105
106    monthAndYearView = [[NSView alloc] initWithFrame:NSMakeRect(0.0, 0.0, frameRect.size.width, OACalendarViewButtonHeight + 2)];
107    [monthAndYearView setAutoresizingMask:NSViewWidthSizable];
108
109    // Add left/right buttons
110
111    buttonFrame = NSMakeRect(0.0, 0.0, OACalendarViewButtonWidth, OACalendarViewButtonHeight);
112    button = [self _createButtonWithFrame:buttonFrame];
113    [button setImage:[NSImage imageNamed:@"OALeftArrow" inBundle:thisBundle]];
114    [button setAlternateImage:[NSImage imageNamed:@"OALeftArrowPressed" inBundle:thisBundle]];
115    [button setAction:@selector(previous:)];
116    [button setAutoresizingMask:NSViewMaxXMargin];
117    [monthAndYearView addSubview:button];
118
119    buttonFrame = NSMakeRect(frameRect.size.width - OACalendarViewButtonWidth, 0.0, OACalendarViewButtonWidth, OACalendarViewButtonHeight);
120    button = [self _createButtonWithFrame:buttonFrame];
121    [button setImage:[NSImage imageNamed:@"OARightArrow" inBundle:thisBundle]];
122    [button setAlternateImage:[NSImage imageNamed:@"OARightArrowPressed" inBundle:thisBundle]];
123    [button setAction:@selector(next:)];
124    [button setAutoresizingMask:NSViewMinXMargin];
125    [monthAndYearView addSubview:button];
126
127    [self addSubview:monthAndYearView];
128    [monthAndYearView release];
129
130//[self sizeToFit];
131//NSLog(@"frame: %@", NSStringFromRect([self frame]));
132
133    [self setVisibleMonth:[NSCalendarDate calendarDate]];
134    [self setSelectedDay:[NSCalendarDate calendarDate]];
135   
136    return self;
137}
138
139- (void)dealloc;
140{
141    int index;
142
143    [dayOfMonthCell release];
144
145    for (index = 0; index < OACalendarViewNumDaysPerWeek; index++)
146        [dayOfWeekCell[index] release];
147
148    [monthAndYearTextFieldCell release];
149    [buttons release];
150    [selectedDay release];
151    [visibleMonth release];
152
153    [super dealloc];
154}
155
156
157//
158// NSControl overrides
159//
160
161+ (Class)cellClass;
162{
163    // We need to have an NSActionCell (or subclass of that) to handle the target and action; otherwise, you just can't set those values.
164    return [NSActionCell class];
165}
166
167- (BOOL)mouseDownCanMoveWindow;
168{
169    return YES;
170}
171
172- (void)setEnabled:(BOOL)flag;
173{
174    unsigned int buttonIndex;
175
176    [super setEnabled:flag];
177   
178    buttonIndex = [buttons count];
179    while (buttonIndex--)
180        [[buttons objectAtIndex:buttonIndex] setEnabled:flag];
181}
182
183- (void)sizeToFit;
184{
185    NSSize minimumSize;
186
187    // we need calculateSizes in order to get the monthAndYearRect; would be better to restructure some of that
188    // it would be good to refactor the size calculation (or pass it some parameters) so that we could merely calculate the stuff we need (or have _calculateSizes do all our work, based on the parameters we provide)
189    [self _calculateSizes];
190
191    minimumSize.height = monthAndYearRect.size.height + gridHeaderRect.size.height + ((OACalendarViewMaxNumWeeksIntersectedByMonth * [self _minimumRowHeight]));
192    // This should really check the lengths of the months, and include space for the buttons.
193    minimumSize.width = ([self _minimumColumnWidth] * OACalendarViewNumDaysPerWeek) + 1.0;
194
195    [self setFrameSize:minimumSize];
196    [self setNeedsDisplay:YES];
197}
198
199
200//
201// NSView overrides
202//
203
204- (BOOL)needsPanelToBecomeKey;
205{
206    return YES;
207}
208
209- (BOOL)isFlipped;
210{
211    return YES;
212}
213
214- (void)drawRect:(NSRect)rect;
215{
216    int columnIndex;
217    NSRect tempRect;
218   
219    [self _calculateSizes];
220   
221// for testing, to see if there's anything we're not covering
222//[[NSColor greenColor] set];
223//NSRectFill(gridHeaderAndBodyRect);
224// or...
225//NSRectFill([self bounds]);
226   
227    // draw the month/year
228    [monthAndYearTextFieldCell drawWithFrame:monthAndYearRect inView:self];
229   
230    // draw the grid header
231    tempRect = gridHeaderRect;
232    tempRect.size.width = columnWidth;
233    for (columnIndex = 0; columnIndex < OACalendarViewNumDaysPerWeek; columnIndex++) {
234        [dayOfWeekCell[columnIndex] drawWithFrame:tempRect inView:self];
235        tempRect.origin.x += columnWidth;
236    }
237
238    // draw the grid background
239    [[NSColor controlBackgroundColor] set];
240    NSRectFill(gridBodyRect);
241
242    // fill in the grid
243    [self _drawGridInRect:gridBodyRect];
244    [self _drawDaysOfMonthInRect:gridBodyRect];
245   
246    // draw a border around the whole thing. This ends up drawing over the top and right side borders of the header, but that's ok because we don't want their border, we want ours. Also, it ends up covering any overdraw from selected sundays and saturdays, since the selected day covers the bordering area where vertical grid lines would be (an aesthetic decision because we don't draw vertical grid lines, another aesthetic decision).
247    [[NSColor gridColor] set];
248    NSFrameRect(gridHeaderAndBodyRect);
249
250}
251
252- (void)mouseDown:(NSEvent *)mouseEvent;
253{
254    if ([self isEnabled]) {
255        NSCalendarDate *hitDate;
256        NSPoint location;
257   
258        location = [self convertPoint:[mouseEvent locationInWindow] fromView:nil];
259        hitDate = [self _hitDateWithLocation:location];
260        if (hitDate) {
261            id target = [self target];
262            if (!flags.targetApprovesDateSelection || [target calendarView:self shouldSelectDate:hitDate]) {
263                [self setSelectedDay:hitDate];
264                [self setVisibleMonth:hitDate];
265                if (flags.targetReceivesDismiss && [mouseEvent clickCount] == 2)
266                    [target calendarViewShouldDismiss: target];
267                [self sendAction:[self action] to:target];
268            }
269           
270        } else if (selectionType == OACalendarViewSelectByWeekday) {
271            NSCalendarDate *hitWeekday;
272           
273            hitWeekday = [self _hitWeekdayWithLocation:location];
274            if (hitWeekday) {
275                id target = [self target];
276                if (!flags.targetApprovesDateSelection || [target calendarView:self shouldSelectDate:hitWeekday]) {
277                    [self setSelectedDay:hitWeekday];
278                    [self sendAction:[self action] to: target];
279                    if (flags.targetReceivesDismiss && [mouseEvent clickCount] == 2)
280                        [target calendarViewShouldDismiss: target];
281                }
282            }
283        }
284    }
285}
286
287
288//
289// API
290//
291
292- (NSCalendarDate *)visibleMonth;
293{
294    return visibleMonth;
295}
296
297- (void)setVisibleMonth:(NSCalendarDate *)aDate;
298{
299    [visibleMonth release];
300    visibleMonth = [[aDate firstDayOfMonth] retain];
301    [monthAndYearTextFieldCell setObjectValue:visibleMonth];
302
303    [self updateHighlightMask];
304    [self setNeedsDisplay:YES];
305   
306    if (flags.targetWatchesVisibleMonth)
307        [[self target] calendarView:self didChangeVisibleMonth:visibleMonth];
308}
309
310- (NSCalendarDate *)selectedDay;
311{
312    return selectedDay;
313}
314
315- (void)setSelectedDay:(NSCalendarDate *)newSelectedDay;
316{
317    if (newSelectedDay == selectedDay || [newSelectedDay isEqual:selectedDay])
318        return;
319   
320    [selectedDay release];
321    selectedDay = [newSelectedDay retain];
322    [self setNeedsDisplay:YES];
323}
324
325- (int)dayHighlightMask;
326{
327    return dayHighlightMask;
328}
329
330- (void)setDayHighlightMask:(int)newMask;
331{
332    dayHighlightMask = newMask;
333    [self setNeedsDisplay:YES];
334}
335
336- (void)updateHighlightMask;
337{
338    if (flags.targetProvidesHighlightMask) {
339        int mask;
340        mask = [[self target] calendarView:self highlightMaskForVisibleMonth:visibleMonth];
341        [self setDayHighlightMask:mask];
342    } else
343        [self setDayHighlightMask:0];
344
345    [self setNeedsDisplay:YES];
346}
347
348- (BOOL)showsDaysForOtherMonths;
349{
350    return flags.showsDaysForOtherMonths;
351}
352
353- (void)setShowsDaysForOtherMonths:(BOOL)value;
354{
355    if (value != flags.showsDaysForOtherMonths) {
356        flags.showsDaysForOtherMonths = value;
357
358        [self setNeedsDisplay:YES];
359    }
360}
361
362- (OACalendarViewSelectionType)selectionType;
363{
364    return selectionType;
365}
366
367- (void)setSelectionType:(OACalendarViewSelectionType)value;
368{
369    NSParameterAssert((value == OACalendarViewSelectByDay) || (value == OACalendarViewSelectByWeek) || (value == OACalendarViewSelectByWeekday));
370    if (selectionType != value) {
371        selectionType = value;
372
373        [self setNeedsDisplay:YES];
374    }
375}
376
377- (NSArray *)selectedDays;
378{
379    if (!selectedDay)
380        return nil;
381
382    switch (selectionType) {
383        case OACalendarViewSelectByDay:
384            return [NSArray arrayWithObject:selectedDay];
385            break;
386           
387        case OACalendarViewSelectByWeek:
388            {
389                NSMutableArray *days;
390                NSCalendarDate *day;
391                int index;
392               
393                days = [NSMutableArray arrayWithCapacity:OACalendarViewNumDaysPerWeek];
394                day = [selectedDay dateByAddingYears:0 months:0 days:-[selectedDay dayOfWeek] hours:0 minutes:0 seconds:0];
395                for (index = 0; index < OACalendarViewNumDaysPerWeek; index++) {
396                    NSCalendarDate *nextDay;
397
398                    nextDay = [day dateByAddingYears:0 months:0 days:index hours:0 minutes:0 seconds:0];
399                    if (flags.showsDaysForOtherMonths || [nextDay monthOfYear] == [selectedDay monthOfYear])
400                        [days addObject:nextDay];                   
401                }
402           
403                return days;
404            }           
405            break;
406
407        case OACalendarViewSelectByWeekday:
408            {
409                NSMutableArray *days;
410                NSCalendarDate *day;
411                int index;
412               
413                days = [NSMutableArray arrayWithCapacity:OACalendarViewMaxNumWeeksIntersectedByMonth];
414                day = [selectedDay dateByAddingYears:0 months:0 days:-(([selectedDay weekOfMonth] - 1) * OACalendarViewNumDaysPerWeek) hours:0 minutes:0 seconds:0];
415                for (index = 0; index < OACalendarViewMaxNumWeeksIntersectedByMonth; index++) {
416                    NSCalendarDate *nextDay;
417
418                    nextDay = [day dateByAddingYears:0 months:0 days:(index * OACalendarViewNumDaysPerWeek) hours:0 minutes:0 seconds:0];
419                    if (flags.showsDaysForOtherMonths || [nextDay monthOfYear] == [selectedDay monthOfYear])
420                        [days addObject:nextDay];
421                }
422
423                return days;
424            }
425            break;
426           
427        default:
428            [NSException raise:NSInvalidArgumentException format:@"OACalendarView: Unknown selection type: %d", selectionType];
429            return nil;
430            break;
431    }
432}
433
434
435//
436// Actions
437//
438
439- (IBAction)previous:(id)sender;
440{
441    if ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)
442        [self previousYear: sender];
443    else
444        [self previousMonth: sender];
445}
446
447- (IBAction)next:(id)sender;
448{
449    if ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)
450        [self nextYear: sender];
451    else
452        [self nextMonth: sender];
453}
454
455- (IBAction)previousMonth:(id)sender;
456{
457    NSCalendarDate *newDate;
458
459    newDate = [visibleMonth dateByAddingYears:0 months:-1 days:0 hours:0 minutes:0 seconds:0];
460    [self setVisibleMonth:newDate];
461}
462
463- (IBAction)nextMonth:(id)sender;
464{
465    NSCalendarDate *newDate;
466
467    newDate = [visibleMonth dateByAddingYears:0 months:1 days:0 hours:0 minutes:0 seconds:0];
468    [self setVisibleMonth:newDate];
469}
470
471- (IBAction)previousYear:(id)sender;
472{
473    NSCalendarDate *newDate;
474
475    newDate = [visibleMonth dateByAddingYears:-1 months:0 days:0 hours:0 minutes:0 seconds:0];
476    [self setVisibleMonth:newDate];
477}
478
479- (IBAction)nextYear:(id)sender;
480{
481    NSCalendarDate *newDate;
482
483    newDate = [visibleMonth dateByAddingYears:1 months:0 days:0 hours:0 minutes:0 seconds:0];
484    [self setVisibleMonth:newDate];
485}
486
487- (void)keyDown:(NSEvent *)theEvent;
488{
489    BOOL commandKey = ([theEvent modifierFlags] & NSCommandKeyMask) != 0;
490    BOOL optionKey = ([theEvent modifierFlags] & NSAlternateKeyMask) != 0;
491    NSCalendarDate *newDate = nil;
492    unichar firstCharacter = [[theEvent characters] characterAtIndex: 0];
493    // move by week, or month/year if modified
494    if (firstCharacter == NSUpArrowFunctionKey) {
495        if (commandKey) firstCharacter = NSLeftArrowFunctionKey;
496        else newDate = [selectedDay dateByAddingYears:0 months:0 days:-7 hours:0 minutes:0 seconds:0];
497    } else if (firstCharacter == NSDownArrowFunctionKey) {
498        if (commandKey) firstCharacter = NSRightArrowFunctionKey;
499        else newDate = [selectedDay dateByAddingYears:0 months:0 days:7 hours:0 minutes:0 seconds:0];
500    }
501    // move by day, or month/year if modified
502    if (firstCharacter == NSLeftArrowFunctionKey) {
503        if (commandKey) {
504            if (optionKey)
505                newDate = [selectedDay dateByAddingYears:-1 months:0 days:0 hours:0 minutes:0 seconds:0];
506            else
507                newDate = [selectedDay dateByAddingYears:0 months:-1 days:0 hours:0 minutes:0 seconds:0];
508        } else newDate = [selectedDay dateByAddingYears:0 months:0 days:-1 hours:0 minutes:0 seconds:0];
509    } else if (firstCharacter == NSRightArrowFunctionKey) {
510        if (commandKey) {
511            if (optionKey)
512                newDate = [selectedDay dateByAddingYears:1 months:0 days:0 hours:0 minutes:0 seconds:0];
513            else
514                newDate = [selectedDay dateByAddingYears:0 months:1 days:0 hours:0 minutes:0 seconds:0];
515        } else newDate = [selectedDay dateByAddingYears:0 months:0 days:1 hours:0 minutes:0 seconds:0];
516    } else if (firstCharacter >= '0' && firstCharacter <= '9') {
517        // For consistency with List Manager as documented, reset the typeahead buffer after twice the delay until key repeat (in ticks).
518        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
519        int keyRepeatTicks = [defaults integerForKey: @"InitialKeyRepeat"];
520        NSTimeInterval resetDelay;
521
522        if (keyRepeatTicks == 0) keyRepeatTicks = 35; // default may be missing; if so, set default
523
524        resetDelay = MIN(2.0 / 60.0 * keyRepeatTicks, 2.0);
525
526        if (typed == nil) typed = [[NSMutableString alloc] init];
527        else if (typeSelectResetTime != nil && [typeSelectResetTime compare: [NSDate date]] == NSOrderedAscending)
528            [typed setString: @""];
529        if ([typed length] != 0 || firstCharacter != '0') // don't construct a string 000... because it'll mess up length measurement for deciding whether to select a day
530            CFStringAppendCharacters((CFMutableStringRef)typed, &firstCharacter, 1);
531
532        [typeSelectResetTime release];
533        typeSelectResetTime = [[NSDate dateWithTimeIntervalSinceNow: resetDelay] retain];
534
535        int length = [typed length];
536        if (length > 2) {
537            [typed deleteCharactersInRange: NSMakeRange(0, length - 2)];
538            length = 2;
539        }
540        if (length == 1 || length == 2) {
541            int dayOfMonth = [typed intValue], daysInMonth = [selectedDay numberOfDaysInMonth];
542            if (dayOfMonth >= daysInMonth) {
543                [typed deleteCharactersInRange: NSMakeRange(0, 1)];
544                dayOfMonth = [typed intValue];
545            }
546            if (dayOfMonth > 0)
547                newDate = [selectedDay dateByAddingYears:0 months:0 days:dayOfMonth - [selectedDay dayOfMonth] hours:0 minutes:0 seconds:0];
548        }
549    }
550    if (newDate != nil) {
551        if (flags.targetApprovesDateSelection && ![[self target] calendarView: self shouldSelectDate: newDate])
552            return;
553        if (([selectedDay monthOfYear] != [newDate monthOfYear]) || ([selectedDay yearOfCommonEra] != [newDate yearOfCommonEra]))
554            [self setVisibleMonth: newDate];
555        [self setSelectedDay: newDate];
556        return;
557    }
558    [super keyDown: theEvent];
559}
560
561@end
562
563
564@implementation OACalendarView (Private)
565
566- (NSButton *)_createButtonWithFrame:(NSRect)buttonFrame;
567{
568    NSButton *button;
569   
570    button = [[NSButton alloc] initWithFrame:buttonFrame];
571    [button setBezelStyle:NSShadowlessSquareBezelStyle];
572    [button setBordered:NO];
573    [button setImagePosition:NSImageOnly];
574    [button setTarget:self];
575    [button setContinuous:YES];
576//    [self addSubview:button];
577    [buttons addObject:button];
578    [button release];
579
580    return button;
581}
582
583- (void)setTarget:(id)value;
584{
585    [super setTarget:value];
586    flags.targetProvidesHighlightMask = [value respondsToSelector:@selector(calendarView:highlightMaskForVisibleMonth:)];
587    flags.targetWatchesCellDisplay = [value respondsToSelector:@selector(calendarView:willDisplayCell:forDate:)];
588    flags.targetApprovesDateSelection = [value respondsToSelector:@selector(calendarView:shouldSelectDate:)];
589    flags.targetWatchesVisibleMonth = [value respondsToSelector:@selector(calendarView:didChangeVisibleMonth:)];
590    flags.targetReceivesDismiss = [value respondsToSelector:@selector(calendarViewShouldDismiss:)];
591}
592
593- (void)_calculateSizes;
594{
595    NSSize cellSize;
596    NSRect viewBounds;
597    NSRect topRect;
598    NSRect discardRect;
599    NSRect tempRect;
600
601    viewBounds = [self bounds];
602   
603    // get the grid cell width (subtract 1.0 from the bounds width to allow for the border)
604    columnWidth = floor((viewBounds.size.width - 1.0) / OACalendarViewNumDaysPerWeek);
605    viewBounds.size.width = (columnWidth * OACalendarViewNumDaysPerWeek) + 1.0;
606   
607    // resize the month & year view to be the same width as the grid
608    [monthAndYearView setFrameSize:NSMakeSize(viewBounds.size.width, [monthAndYearView frame].size.height)];
609
610    // get the rect for the month and year text field cell
611    cellSize = [monthAndYearTextFieldCell cellSize];
612    NSDivideRect(viewBounds, &topRect, &gridHeaderAndBodyRect, ceil(cellSize.height + OACalendarViewSpaceBetweenMonthYearAndGrid), NSMinYEdge);
613    NSDivideRect(topRect, &discardRect, &monthAndYearRect, floor((viewBounds.size.width - cellSize.width) / 2), NSMinXEdge);
614    monthAndYearRect.size.width = cellSize.width;
615   
616    tempRect = gridHeaderAndBodyRect;
617    // leave space for a one-pixel border on each side
618    tempRect.size.width -= 2.0;
619    tempRect.origin.x += 1.0;
620    // leave space for a one-pixel border at the bottom (the top already looks fine)
621    tempRect.size.height -= 1.0;
622
623    // get the grid header rect
624    cellSize = [dayOfWeekCell[0] cellSize];
625    NSDivideRect(tempRect, &gridHeaderRect, &gridBodyRect, ceil(cellSize.height), NSMinYEdge);
626   
627    // get the grid row height (add 1.0 to the body height because while we can't actually draw on that extra pixel, our bottom row doesn't have to draw a bottom grid line as there's a border right below us, so we need to account for that, which we do by pretending that next pixel actually does belong to us)
628    rowHeight = floor((gridBodyRect.size.height + 1.0) / OACalendarViewMaxNumWeeksIntersectedByMonth);
629   
630    // get the grid body rect
631    gridBodyRect.size.height = (rowHeight * OACalendarViewMaxNumWeeksIntersectedByMonth) - 1.0;
632   
633    // adjust the header and body rect to account for any adjustment made while calculating even row heights
634    gridHeaderAndBodyRect.size.height = NSMaxY(gridBodyRect) - NSMinY(gridHeaderAndBodyRect) + 1.0;
635}
636
637- (void)_drawDaysOfMonthInRect:(NSRect)rect;
638{
639    NSRect cellFrame;
640    NSRect dayOfMonthFrame;
641    NSRect discardRect;
642    int visibleMonthIndex;
643    NSCalendarDate *thisDay;
644    int index, row, column;
645    NSSize cellSize;
646    BOOL isFirstResponder = ([[self window] firstResponder] == self);
647
648    // the cell is actually one pixel shorter than the row height, because the row height includes the bottom grid line (or the top grid line, depending on which way you prefer to think of it)
649    cellFrame.size.height = rowHeight - 1.0;
650    // the cell would actually be one pixel narrower than the column width but we don't draw vertical grid lines. instead, we want to include the area that would be grid line (were we drawing it) in our cell, because that looks a bit better under the header, which _does_ draw column separators. actually, we want to include the grid line area on _both sides_ or it looks unbalanced, so we actually _add_ one pixel, to cover that. below, our x position as we draw will have to take that into account. note that this means that sunday and saturday overwrite the outside borders, but the outside border is drawn last, so it ends up ok. (if we ever start drawing vertical grid lines, change this to be - 1.0, and adjust the origin appropriately below.)
651    cellFrame.size.width = columnWidth + 1.0;
652
653    cellSize = [dayOfMonthCell cellSize];
654   
655    visibleMonthIndex = [visibleMonth monthOfYear];
656
657    thisDay = [visibleMonth dateByAddingYears:0 months:0 days:-[visibleMonth dayOfWeek] hours:0 minutes:0 seconds:0];
658
659    for (row = column = index = 0; index < OACalendarViewMaxNumWeeksIntersectedByMonth * OACalendarViewNumDaysPerWeek; index++) {
660        NSColor *textColor;
661        BOOL isVisibleMonth;
662
663        // subtract 1.0 from the origin because we're including the area where vertical grid lines would be were we drawing them
664        cellFrame.origin.x = rect.origin.x + (column * columnWidth) - 1.0;
665        cellFrame.origin.y = rect.origin.y + (row * rowHeight);
666
667        [dayOfMonthCell setIntValue:[thisDay dayOfMonth]];
668        isVisibleMonth = ([thisDay monthOfYear] == visibleMonthIndex);
669
670        if (flags.showsDaysForOtherMonths || isVisibleMonth) {
671            if (selectedDay) {
672                BOOL shouldHighlightThisDay = NO;
673
674                // We could just check if thisDay is in [self selectedDays]. However, that makes the selection look somewhat weird when we
675                // are selecting by weekday, showing days for other months, and the visible month is the previous/next from the selected day.
676                // (Some of the weekdays are shown as highlighted, and later ones are not.)
677                // So, we fib a little to make things look better.
678                switch (selectionType) {
679                    case OACalendarViewSelectByDay:
680                        shouldHighlightThisDay = ([selectedDay dayOfCommonEra] == [thisDay dayOfCommonEra]);
681                        break;
682                       
683                    case OACalendarViewSelectByWeek:
684                        shouldHighlightThisDay = [selectedDay isInSameWeekAsDate:thisDay];
685                        break;
686                       
687                    case OACalendarViewSelectByWeekday:
688                        shouldHighlightThisDay = ([selectedDay monthOfYear] == visibleMonthIndex && [selectedDay dayOfWeek] == [thisDay dayOfWeek]);
689                        break;
690                       
691                    default:
692                        [NSException raise:NSInvalidArgumentException format:@"OACalendarView: Unknown selection type: %d", selectionType];
693                        break;
694                }
695               
696                if (shouldHighlightThisDay) {
697                    [(isFirstResponder ? [NSColor selectedControlColor] : [NSColor secondarySelectedControlColor]) set];
698                    NSRectFill(cellFrame);
699                }
700            }
701           
702            if (flags.targetWatchesCellDisplay) {
703                [[self target] calendarView:self willDisplayCell:dayOfMonthCell forDate:thisDay];
704            } else {
705                if ((dayHighlightMask & (1 << index)) == 0) {
706                    textColor = (isVisibleMonth ? [NSColor blackColor] : [NSColor grayColor]);
707                } else {
708                    textColor = [NSColor blueColor];
709                }
710                [dayOfMonthCell setTextColor:textColor];
711            }
712            NSDivideRect(cellFrame, &discardRect, &dayOfMonthFrame, floor((cellFrame.size.height - cellSize.height) / 2.0), NSMinYEdge);
713            [dayOfMonthCell drawWithFrame:dayOfMonthFrame inView:self];
714        }
715       
716        thisDay = [thisDay dateByAddingYears:0 months:0 days:1 hours:0 minutes:0 seconds:0];
717        column++;
718        if (column > OACalendarViewMaxNumWeeksIntersectedByMonth) {
719            column = 0;
720            row++;
721        }
722    }
723}
724
725- (void)_drawGridInRect:(NSRect)rect;
726{
727    NSPoint pointA;
728    NSPoint pointB;
729    int weekIndex;
730   
731    // we will be adding the row height each time, so subtract 1.0 (the grid thickness) from the starting y position (for example, if starting y = 0 and row height = 10, then starting y + row height = 10, so we would draw at pixel 10... which is the 11th pixel. Basically, we subtract 1.0 to make the result zero-based, so that we draw at pixel 10 - 1 = 9, which is the 10th pixel)
732    // add 0.5 to move to the center of the pixel before drawing a line 1.0 pixels thick, centered around 0.0 (which would mean half a pixel above the starting point and half a pixel below - not what we want)
733    // we could just subtract 0.5, but I think this is clearer, and the compiler will optimize it to the appropriate value for us
734    pointA = NSMakePoint(NSMinX(rect), NSMinY(rect) - 1.0 + 0.5);
735    pointB = NSMakePoint(NSMaxX(rect), NSMinY(rect) - 1.0 + 0.5);
736   
737    [[NSColor controlHighlightColor] set];
738    for (weekIndex = 1; weekIndex < OACalendarViewMaxNumWeeksIntersectedByMonth; weekIndex++) {
739        pointA.y += rowHeight;
740        pointB.y += rowHeight;
741        [NSBezierPath strokeLineFromPoint:pointA toPoint:pointB];
742    }
743   
744#if 0
745// we would do this if we wanted to draw columns in the grid
746    {
747        int dayIndex;
748       
749        // see aov for explanation of why we subtract 1.0 and add 0.5 to the x position
750        pointA = NSMakePoint(NSMinX(rect) - 1.0 + 0.5, NSMinY(rect));
751        pointB = NSMakePoint(NSMinX(rect) - 1.0 + 0.5, NSMaxY(rect));
752       
753        for (dayIndex = 1; dayIndex < OACalendarViewNumDaysPerWeek; dayIndex++) {
754            pointA.x += columnWidth;
755            pointB.x += columnWidth;
756            [NSBezierPath strokeLineFromPoint:pointA toPoint:pointB];
757        }
758    }
759#endif
760}
761
762- (float)_maximumDayOfWeekWidth;
763{
764    float maxWidth;
765    int index;
766
767    maxWidth = 0;
768    for (index = 0; index < OACalendarViewNumDaysPerWeek; index++) {
769        NSSize cellSize;
770
771        cellSize = [dayOfWeekCell[index] cellSize];
772        if (maxWidth < cellSize.width)
773            maxWidth = cellSize.width;
774    }
775
776    return ceil(maxWidth);
777}
778
779- (NSSize)_maximumDayOfMonthSize;
780{
781    NSSize maxSize;
782    int index;
783
784    maxSize = NSZeroSize; // I'm sure the height doesn't change, but I need to know the height anyway.
785    for (index = 1; index <= 31; index++) {
786        NSString *str;
787        NSSize cellSize;
788
789        str = [NSString stringWithFormat:@"%d", index];
790        [dayOfMonthCell setStringValue:str];
791        cellSize = [dayOfMonthCell cellSize];
792        if (maxSize.width < cellSize.width)
793            maxSize.width = cellSize.width;
794        if (maxSize.height < cellSize.height)
795            maxSize.height = cellSize.height;
796    }
797
798    maxSize.width = ceil(maxSize.width);
799    maxSize.height = ceil(maxSize.height);
800
801    return maxSize;
802}
803
804- (float)_minimumColumnWidth;
805{
806    float dayOfWeekWidth;
807    float dayOfMonthWidth;
808   
809    dayOfWeekWidth = [self _maximumDayOfWeekWidth];     // we don't have to add 1.0 because the day of week cell whose width is returned here includes it's own border
810    dayOfMonthWidth = [self _maximumDayOfMonthSize].width + 1.0;        // add 1.0 to allow for the grid. We don't actually draw the vertical grid, but we treat it as if there was one (don't respond to clicks "on" the grid, we have a vertical separator in the header, etc.)
811    return (dayOfMonthWidth > dayOfWeekWidth) ? dayOfMonthWidth : dayOfWeekWidth;
812}
813
814- (float)_minimumRowHeight;
815{
816    return [self _maximumDayOfMonthSize].height + 1.0;  // add 1.0 to allow for a bordering grid line
817}
818
819- (NSCalendarDate *)_hitDateWithLocation:(NSPoint)targetPoint;
820{
821    int hitRow, hitColumn;
822    int firstDayOfWeek, targetDayOfMonth;
823    NSPoint offset;
824
825    if (NSPointInRect(targetPoint, gridBodyRect) == NO)
826        return nil;
827
828    firstDayOfWeek = [[visibleMonth firstDayOfMonth] dayOfWeek];
829
830    offset = NSMakePoint(targetPoint.x - gridBodyRect.origin.x, targetPoint.y - gridBodyRect.origin.y);
831    // if they exactly hit the grid between days, treat that as a miss
832    if ((selectionType != OACalendarViewSelectByWeekday) && (((int)offset.y % (int)rowHeight) == 0))
833        return nil;
834    // if they exactly hit the grid between days, treat that as a miss
835    if ((selectionType != OACalendarViewSelectByWeek) && ((int)offset.x % (int)columnWidth) == 0)
836        return nil;
837    hitRow = (int)(offset.y / rowHeight);
838    hitColumn = (int)(offset.x / columnWidth);
839
840    targetDayOfMonth = (hitRow * OACalendarViewNumDaysPerWeek) + hitColumn - firstDayOfWeek + 1;
841    if (!flags.showsDaysForOtherMonths && (targetDayOfMonth < 1 || targetDayOfMonth > [visibleMonth numberOfDaysInMonth]))
842        return nil;
843
844    return [visibleMonth dateByAddingYears:0 months:0 days:targetDayOfMonth-1 hours:0 minutes:0 seconds:0];
845}
846
847- (NSCalendarDate *)_hitWeekdayWithLocation:(NSPoint)targetPoint;
848{
849    int hitDayOfWeek;
850    int firstDayOfWeek, targetDayOfMonth;
851    float offsetX;
852
853    if (NSPointInRect(targetPoint, gridHeaderRect) == NO)
854        return nil;
855   
856    offsetX = targetPoint.x - gridHeaderRect.origin.x;
857    // if they exactly hit a border between weekdays, treat that as a miss (besides being neat in general, this avoids the problem where clicking on the righthand border would result in us incorrectly calculating that the _first_ day of the week was hit)
858    if (((int)offsetX % (int)columnWidth) == 0)
859        return nil;
860   
861    hitDayOfWeek = offsetX / columnWidth;
862
863    firstDayOfWeek = [[visibleMonth firstDayOfMonth] dayOfWeek];
864    if (hitDayOfWeek >= firstDayOfWeek)
865        targetDayOfMonth = hitDayOfWeek - firstDayOfWeek + 1;
866    else
867        targetDayOfMonth = hitDayOfWeek + OACalendarViewNumDaysPerWeek - firstDayOfWeek + 1;
868
869    return [visibleMonth dateByAddingYears:0 months:0 days:targetDayOfMonth-1 hours:0 minutes:0 seconds:0];
870}
871
872@end
Note: See TracBrowser for help on using the repository browser.