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

Last change on this file since 636 was 602, checked in by Nicholas Riley, 15 years ago

Rename variables which shadow stdlib functions (and maxSize).

File size: 35.6 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 dayOfWeek;
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 (dayOfWeek = 0; dayOfWeek < OACalendarViewNumDaysPerWeek; dayOfWeek++) {
101 dayOfWeekCell[dayOfWeek] = [[NSTableHeaderCell alloc] init];
102 [dayOfWeekCell[dayOfWeek] setAlignment:NSCenterTextAlignment];
103 [dayOfWeekCell[dayOfWeek] setStringValue:[[shortWeekDays objectAtIndex:dayOfWeek] 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 dayOfWeek;
148
149 [dayOfMonthCell release];
150
151 for (dayOfWeek = 0; dayOfWeek < OACalendarViewNumDaysPerWeek; dayOfWeek++)
152 [dayOfWeekCell[dayOfWeek] 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 dayOfWeek;
398
399 days = [NSMutableArray arrayWithCapacity:OACalendarViewNumDaysPerWeek];
400 day = [selectedDay dateByAddingYears:0 months:0 days:-[selectedDay dayOfWeek] hours:0 minutes:0 seconds:0];
401 for (dayOfWeek = 0; dayOfWeek < OACalendarViewNumDaysPerWeek; dayOfWeek++) {
402 NSCalendarDate *nextDay;
403
404 nextDay = [day dateByAddingYears:0 months:0 days:dayOfWeek 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 dayOfWeek;
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 (dayOfWeek = 0; dayOfWeek < OACalendarViewMaxNumWeeksIntersectedByMonth; dayOfWeek++) {
422 NSCalendarDate *nextDay;
423
424 nextDay = [day dateByAddingYears:0 months:0 days:(dayOfWeek * 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 cellIndex, 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 = cellIndex = 0; cellIndex < OACalendarViewMaxNumWeeksIntersectedByMonth * OACalendarViewNumDaysPerWeek; cellIndex++) {
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 << cellIndex)) == 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 dayOfWeek;
772
773 maxWidth = 0;
774 for (dayOfWeek = 0; dayOfWeek < OACalendarViewNumDaysPerWeek; dayOfWeek++) {
775 NSSize cellSize;
776
777 cellSize = [dayOfWeekCell[dayOfWeek] cellSize];
778 if (maxWidth < cellSize.width)
779 maxWidth = cellSize.width;
780 }
781
782 return ceilf(maxWidth);
783}
784
785- (NSSize)_maximumDayOfMonthSize;
786{
787 NSSize maximumSize;
788 int dayOfMonth;
789
790 maximumSize = NSZeroSize; // I'm sure the height doesn't change, but I need to know the height anyway.
791 for (dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) {
792 NSString *str;
793 NSSize cellSize;
794
795 str = [NSString stringWithFormat:@"%d", dayOfMonth];
796 [dayOfMonthCell setStringValue:str];
797 cellSize = [dayOfMonthCell cellSize];
798 if (maximumSize.width < cellSize.width)
799 maximumSize.width = cellSize.width;
800 if (maximumSize.height < cellSize.height)
801 maximumSize.height = cellSize.height;
802 }
803
804 maximumSize.width = ceilf(maximumSize.width);
805 maximumSize.height = ceilf(maximumSize.height);
806
807 return maximumSize;
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.