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

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

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

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