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

Last change on this file since 102 was 102, checked in by Nicholas Riley, 19 years ago

Pester 1.1b3

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