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

Last change on this file since 601 was 601, checked in by Nicholas Riley, 10 years ago

Float vs. double fixes to pacify GCC.

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