/* CPDatePicker.j * AppKit * * Created by Alexandre Wilhelm * Copyright 2012 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import "CPControl.j" @import "CPFont.j" @import "CPTextField.j" @import "_CPDatePickerTextField.j" @import "_CPDatePickerCalendar.j" @import @import @import @import @import @class CPStepper @class CPApp @global CPLocaleLanguageCode @global CPDateFormatterShortStyle var CPDatePicker_validateProposedDateValue_timeInterval = 1 << 1; CPSingleDateMode = 0; CPRangeDateMode = 1; CPTextFieldAndStepperDatePickerStyle = 0; CPClockAndCalendarDatePickerStyle = 1; CPTextFieldDatePickerStyle = 2; CPHourMinuteDatePickerElementFlag = 0x000c; CPHourMinuteSecondDatePickerElementFlag = 0x000e; CPTimeZoneDatePickerElementFlag = 0x0010; CPYearMonthDatePickerElementFlag = 0x00c0; CPYearMonthDayDatePickerElementFlag = 0x00e0; CPEraDatePickerElementFlag = 0x0100; /*! @ingroup appkit This control displays a datepicker in a Cappuccino application */ @implementation CPDatePicker : CPControl { BOOL _isBordered @accessors(getter=isBordered, setter=setBordered:); BOOL _isBezeled @accessors(getter=isBezeled, setter=setBezeled:); BOOL _drawsBackground @accessors(property=drawsBackground); CPDate _dateValue @accessors(property=dateValue); CPDate _minDate @accessors(property=minDate); CPDate _maxDate @accessors(property=maxDate); CPFont _textFont @accessors(property=textFont); CPLocale _locale @accessors(property=locale); //CPCalendar _calendar @accessors(property=calendar); CPTimeZone _timeZone @accessors(property=timeZone); id _delegate @accessors(property=delegate); CPInteger _datePickerElements @accessors(property=datePickerElements); CPInteger _datePickerMode @accessors(property=datePickerMode); CPInteger _datePickerStyle @accessors(property=datePickerStyle); CPInteger _timeInterval @accessors(property=timeInterval); BOOL _invokedByUserEvent; unsigned _implementedCDatePickerDelegateMethods; BOOL _isTextual; id _datePickerComponent; } #pragma mark - #pragma mark Theme methods + (CPString)defaultThemeClass { return @"datePicker"; } + (CPDictionary)themeAttributes { return @{ @"bezel-color": [CPColor clearColor], @"border-width" : 1.0, @"border-color": [CPColor clearColor], @"content-inset": CGInsetMakeZero(), @"bezel-inset": CGInsetMakeZero(), @"datepicker-textfield-bezel-color": [CPColor clearColor], @"min-size-datepicker-textfield": CGSizeMakeZero(), @"content-inset-datepicker-textfield": CGInsetMakeZero(), @"content-inset-datepicker-textfield-separator": CGInsetMakeZero(), @"separator-content-inset": CGInsetMakeZero(), @"date-hour-margin": 5.0, @"stepper-margin": 5.0, @"bezel-color-calendar": [CPColor clearColor], @"title-text-color": [CPColor blackColor], @"title-text-shadow-color": [CPColor clearColor], @"title-text-shadow-offset": CGSizeMakeZero(), @"title-font": [CPNull null], @"weekday-text-color": [CPColor blackColor], @"weekday-text-shadow-color": [CPColor clearColor], @"weekday-text-shadow-offset": CGSizeMakeZero(), @"weekday-font": [CPNull null], @"arrow-image-left": [CPNull null], @"arrow-image-right": [CPNull null], @"arrow-image-left-highlighted": [CPNull null], @"arrow-image-right-highlighted": [CPNull null], @"arrow-inset": CGInsetMakeZero(), @"circle-image": [CPNull null], @"circle-image-highlighted": [CPNull null], @"tile-text-color": [CPColor blackColor], @"tile-text-shadow-color": [CPColor clearColor], @"tile-text-shadow-offset": CGSizeMakeZero(), @"tile-font": [CPNull null], @"size-tile": CGSizeMakeZero(), @"size-calendar": CGSizeMakeZero(), @"size-header": CGSizeMakeZero(), @"min-size-calendar": CGSizeMakeZero(), @"max-size-calendar": CGSizeMakeZero(), @"bezel-color-clock": [CPColor clearColor], @"clock-text-color": [CPColor blackColor], @"clock-text-shadow-color": [CPColor clearColor], @"clock-text-shadow-offset": CGSizeMakeZero(), @"clock-font": [CPNull null], @"second-hand-image": [CPNull null], @"hour-hand-image": [CPNull null], @"middle-hand-image": [CPNull null], @"minute-hand-image": [CPNull null], @"size-clock": CGSizeMakeZero(), @"second-hand-size": CGSizeMakeZero(), @"hour-hand-size": CGSizeMakeZero(), @"middle-hand-size": CGSizeMakeZero(), @"minute-hand-size": CGSizeMakeZero(), @"previous-button-size": CGSizeMakeZero(), @"current-button-size": CGSizeMakeZero(), @"next-button-size": CGSizeMakeZero(), @"title-inset": [CPNull null], @"day-label-inset": [CPNull null], @"tile-content-inset": CGInsetMakeZero(), @"tile-margin": [CPNull null], @"tile-inset": CGInsetMakeZero(), @"separator-color": [CPNull null], @"separator-margin-width": 0, @"separator-height": 0, @"bezel-color-calendar-left": [CPNull null], @"bezel-color-calendar-middle": [CPNull null], @"bezel-color-calendar-right": [CPNull null], @"tile-vertical-alignment": CPCenterVerticalTextAlignment, @"tile-text-alignment": CPCenterTextAlignment, @"hour-ampm-margin": 2, @"time-separator-content-inset": CGInsetMakeZero(), @"clock-second-hand-over": NO, @"clock-draws-hours": NO, @"clock-hours-font": [CPNull null], @"clock-hours-text-color": [CPColor clearColor], @"clock-hours-radius": 0, @"calendar-clock-margin": 10, @"clock-only-nib2cib-adjustment-frame": CPRectMakeZero(), @"uses-focus-ring": NO }; } #pragma mark - #pragma mark Binding methods + (Class)_binderClassForBinding:(CPString)theBinding { if (theBinding == CPValueBinding || theBinding == CPMinValueBinding || theBinding == CPMaxValueBinding) return [_CPDatePickerValueBinder class]; return [super _binderClassForBinding:theBinding]; } - (CPString)_replacementKeyPathForBinding:(CPString)aBinding { if (aBinding == CPValueBinding) return @"dateValue"; if (aBinding == CPMinValueBinding) return @"minDate"; if (aBinding == CPMaxValueBinding) return @"maxDate"; return [super _replacementKeyPathForBinding:aBinding]; } #pragma mark - #pragma mark Init methods - (id)initWithFrame:(CGRect)aFrame { if (self = [super initWithFrame:aFrame]) { _drawsBackground = YES; _datePickerStyle = CPTextFieldAndStepperDatePickerStyle; _datePickerMode = CPSingleDateMode; _datePickerElements = CPYearMonthDayDatePickerElementFlag | CPHourMinuteSecondDatePickerElementFlag; _timeInterval = 0; _implementedCDatePickerDelegateMethods = 0; [self setObjectValue:[CPDate date]]; _minDate = [CPDate distantPast]; _maxDate = [CPDate distantFuture]; [self setBezeled:YES]; [self setBordered:YES]; [self _init]; } return self } - (void)_init { if (!_locale) _locale = [CPLocale currentLocale]; _datePickerComponent = nil; [self _createComponents]; [self setNeedsDisplay:YES]; [self setNeedsLayout]; } - (void)_createComponents { _isTextual = (_datePickerStyle == CPTextFieldAndStepperDatePickerStyle) || (_datePickerStyle == CPTextFieldDatePickerStyle); if (_datePickerComponent) { [_datePickerComponent removeFromSuperview]; _datePickerComponent = nil; } _datePickerComponent = [[(_isTextual ? _CPDatePickerTextField : _CPDatePickerCalendar) alloc] initWithFrame:[self bounds] withDatePicker:self]; [_datePickerComponent setDateValue:_dateValue]; [_datePickerComponent setControlSize:[self controlSize]]; [_datePickerComponent setDatePickerElements:_datePickerElements]; // FIXME: Don't know why but next line will cause theme compilation to fail... // Workaround: added "if PLATFORM(DOM)" #if PLATFORM(DOM) [_datePickerComponent setEnabled:[self isEnabled]]; #endif if (_isTextual) // We need to transmit text color to the text field version (Cocoa doesn't permit adapting the calendar view text color) [_datePickerComponent setTextColor:[self textColor]]; [self addSubview:_datePickerComponent]; } #pragma mark - #pragma mark Control Size - (void)setControlSize:(CPControlSize)aControlSize { [super setControlSize:aControlSize]; [_datePickerComponent setControlSize:aControlSize]; if (_isTextual) [self _sizeToControlSize]; } #pragma mark - #pragma mark Delegate methods /*! Set the delegate of the datePicker @param aDelegate delegate of the datePicker */ - (void)setDelegate:(id)aDelegate { _delegate = aDelegate; _implementedCDatePickerDelegateMethods = 0; // Look if the delegate implements or not the delegate methods if ([_delegate respondsToSelector:@selector(datePicker:validateProposedDateValue:timeInterval:)]) _implementedCDatePickerDelegateMethods |= CPDatePicker_validateProposedDateValue_timeInterval; } #pragma mark - #pragma mark Layout method /*! Layout the subviews */ - (void)layoutSubviews { [_datePickerComponent setNeedsLayout]; [_datePickerComponent setNeedsDisplay:YES]; } #pragma mark - #pragma mark Setter /*! Return the objectValue of the datePicker. The objectValue should take the timeZoneEffect */ - (id)objectValue { // TODO : add timeZone effect. How to do it because js ??? return _dateValue } /*! Set the objectValue of the datePicker. It has to be a CPDate @param aDateValue the dateValue */ - (void)setObjectValue:(CPDate)aValue { if (![aValue isKindOfClass:[CPDate class]]) return; [self setDateValue:aValue]; } /* Set the dateValue of the datePicker @param aDateValue the dateValue */ - (void)setDateValue:(CPDate)aDateValue { if (aDateValue == nil) return; _invokedByUserEvent = NO; [self _setDateValue:aDateValue timeInterval:_timeInterval]; } /*! Set the dateValue and the timeInterval. This method checks the min and max date of the datePicker also. It will call the delegate if possible. @param aDateValue the dateValue @param aTimeInterval the timeInterval */ - (void)_setDateValue:(CPDate)aDateValue timeInterval:(CPTimeInterval)aTimeInterval { // Make sure to have a valid date and avoid NaN values if (!isFinite(aDateValue)) { [CPException raise:CPInvalidArgumentException reason:@"aDateValue is not valid"]; return; } if (_minDate) aDateValue = new Date (MAX(aDateValue, _minDate)); if (_maxDate) aDateValue = new Date (MIN(aDateValue, _maxDate)); aTimeInterval = MAX(MIN(aTimeInterval, [_maxDate timeIntervalSinceDate:aDateValue]), [_minDate timeIntervalSinceDate:aDateValue]); if ([aDateValue isEqualToDate:_dateValue] && aTimeInterval == _timeInterval) { [_datePickerComponent setDateValue:_dateValue]; return; } if (_implementedCDatePickerDelegateMethods & CPDatePicker_validateProposedDateValue_timeInterval) { // constrain timeInterval also var aStartDateRef = function(x){if (typeof x == 'undefined') return aDateValue; aDateValue = x;}; var aTimeIntervalRef = function(x){if (typeof x == 'undefined') return aTimeInterval; aTimeInterval = x;}; [_delegate datePicker:self validateProposedDateValue:aStartDateRef timeInterval:aTimeIntervalRef]; } [self willChangeValueForKey:@"objectValue"]; [self willChangeValueForKey:@"dateValue"]; _dateValue = aDateValue; [super setObjectValue:_dateValue]; [self didChangeValueForKey:@"objectValue"]; [self didChangeValueForKey:@"dateValue"]; [self willChangeValueForKey:@"timeInterval"]; _timeInterval = (_datePickerMode == CPSingleDateMode)? 0 : aTimeInterval; [self didChangeValueForKey:@"timeInterval"]; if (_invokedByUserEvent) [self sendAction:[self action] to:[self target]]; [_datePickerComponent setDateValue:_dateValue]; } /*! Set the minDate of the datePicker @param aMinDate the minDate */ - (void)setMinDate:(CPDate)aMinDate { if (_minDate === aMinDate) return; [self willChangeValueForKey:@"minDate"]; _minDate = aMinDate; [self didChangeValueForKey:@"minDate"]; [self _setDateValue:_dateValue timeInterval:_timeInterval]; } /*! Set the maxDate of the datePicker @param aMaxDate the maxDate */ - (void)setMaxDate:(CPDate)aMaxDate { if (_maxDate === aMaxDate) return; [self willChangeValueForKey:@"maxDate"]; _maxDate = aMaxDate; [self didChangeValueForKey:@"maxDate"]; [self _setDateValue:_dateValue timeInterval:_timeInterval]; } /*! Set the syle of the datePicker @param aDatePickerStyle the datePicker style */ - (void)setDatePickerStyle:(CPInteger)aDatePickerStyle { if (_datePickerStyle === aDatePickerStyle) return; _datePickerStyle = aDatePickerStyle; // This is needed in order to specify different theme attributes values for textual / graphical date picker if (_datePickerStyle === CPClockAndCalendarDatePickerStyle) [self setThemeState:CPThemeStateAlternateState]; else [self unsetThemeState:CPThemeStateAlternateState]; // This is needed in order to specify different theme attributes values for with / without stepper textual date picker if (_datePickerStyle === CPTextFieldAndStepperDatePickerStyle) [self setThemeState:CPThemeStateComposedControl]; else [self unsetThemeState:CPThemeStateComposedControl]; [self setControlSize:[self controlSize]]; if (_datePickerComponent) { // We already have a component so we need to update it [_datePickerComponent resignFirstResponder]; [self _createComponents]; } [self setNeedsDisplay:YES]; [self setNeedsLayout]; } /*! Set the elements of the datePicker @param aDatePickerElements the datePicker elements */ - (void)setDatePickerElements:(CPInteger)aDatePickerElements { if (_datePickerElements === aDatePickerElements) return; _datePickerElements = aDatePickerElements; // Notify the component of the new value [_datePickerComponent setDatePickerElements:_datePickerElements]; [self setNeedsDisplay:YES]; [self setNeedsLayout]; } /*! Set the mode of the datePicker @param aDatePickerMode the datePicker mode */ - (void)setDatePickerMode:(CPInteger)aDatePickerMode { if (_datePickerMode === aDatePickerMode) return; _datePickerMode = aDatePickerMode; if (_datePickerMode == CPSingleDateMode) [self _setDateValue:[self dateValue] timeInterval:0]; [self setNeedsDisplay:YES]; [self setNeedsLayout]; } /*! Set the timeInterval of the datePicker @param aTimeInterval the timeInterval of the datePicker */ - (void)setTimeInterval:(CPInteger)aTimeInterval { if (_datePickerMode == CPSingleDateMode) return; [self _setDateValue:[self dateValue] timeInterval:aTimeInterval]; } /*! Set the locale of the datePicker. This update laso the locale of the formatter. @param aLocale the locale */ - (void)setLocale:(CPLocale)aLocale { if (_locale === aLocale) return; _locale = aLocale; if (_formatter) { [self willChangeValueForKey:@"locale"]; [_formatter setLocale:_locale]; [self didChangeValueForKey:@"locale"]; } // This will update the textFields (usefull when changing with a date with pm and am) if (_isTextual) [_datePickerComponent setDateValue:_dateValue]; [self setNeedsDisplay:YES]; [self setNeedsLayout]; } /*! Sets whether the datepicker will have a bezeled border. @param shouldBeBezeled \c YES means the datepicker will draw a bezeled border */ - (void)setBezeled:(BOOL)shouldBeBezeled { if (_isBezeled === shouldBeBezeled) return; _isBezeled = shouldBeBezeled; if (shouldBeBezeled) [self setThemeState:CPThemeStateBezeled]; else [self unsetThemeState:CPThemeStateBezeled]; } /*! Sets whether the datepicker will have a border drawn. (actually it does nothing) @param shouldBeBordered \c YES makes the datepicker draw a border */ - (void)setBordered:(BOOL)shouldBeBordered { if (_isBordered === shouldBeBordered) return; _isBordered = shouldBeBordered; if (shouldBeBordered) [self setThemeState:CPThemeStateBordered]; else [self unsetThemeState:CPThemeStateBordered]; } /*! Sets the font of the control. @param aFont */ - (void)setTextFont:(CPFont)aFont { [self setFont:aFont]; if (_isTextual) [_datePickerComponent setTextFont:aFont]; } /*! Sets the color of the control. @param aColor */ - (void)setTextColor:(CPColor)aColor { [super setTextColor:aColor]; if (_isTextual) [_datePickerComponent setTextColor:aColor]; // REM: in Cocoa, setTextColor has no effect on calendar view } /*! Sets the enabled status of the control. Controls that are not enabled can not be used by the user and obtain the CPThemeStateDisabled theme state. @param a boolean. YES if the control should be enabled, otherwise NO. */ - (void)setEnabled:(BOOL)aBoolean { [super setEnabled:aBoolean]; [_datePickerComponent setEnabled:aBoolean]; if (!aBoolean) [self resignFirstResponder]; } /*! Set the background color of the datePicker @param aColor */ - (void)setBackgroundColor:(CPColor)aColor { _backgroundColor = aColor; [self setNeedsLayout]; } /*! Set the boolean drawsBackgroundColor @param aBoolean */ - (void)setDrawsBackground:(BOOL)aBoolean { if (_drawsBackground === aBoolean) return; [self willChangeValueForKey:@"drawsBackground"]; _drawsBackground = aBoolean; [self didChangeValueForKey:@"drawsBackground"]; [self setNeedsLayout]; } /*! Set the timeZone @param aTimeZone */ - (void)setTimeZone:(CPTimeZone)aTimeZone { if (_timeZone === aTimeZone) return; [self willChangeValueForKey:@"timeZone"]; _timeZone = aTimeZone; [self didChangeValueForKey:@"timeZone"]; [self setNeedsLayout]; [_datePickerComponent setDateValue:_dateValue]; } #pragma mark - #pragma mark First responder methods /*! Return YES if style is set to CPTextFieldAndStepperDatePickerStyle or CPTextFieldDatePickerStyle */ - (BOOL)becomeFirstResponder { if (_isTextual) { if (![super becomeFirstResponder]) return NO; [_datePickerComponent _selectTextFieldWithFlags:[[CPApp currentEvent] modifierFlags]]; return YES; } return NO; } /*! Return YES */ - (BOOL)acceptsFirstResponder { return YES; } /*! Return YES */ - (BOOL)resignFirstResponder { if (_isTextual) [_datePickerComponent resignFirstResponder]; return YES; } #pragma mark - #pragma mark getter /*! Returns \c YES if the textfield is bezeled. */ - (BOOL)isBezeled { return [self hasThemeState:CPThemeStateBezeled]; } /*! Returns \c YES if the textfield has a border. */ - (BOOL)isBordered { return [self hasThemeState:CPThemeStateBordered]; } /*! Returns the font of the control. */ - (CPFont)textFont { return [self font]; } /*! Check if we are in the american format or not. Depending on the locale */ - (BOOL)_isAmericanFormat { return [[_locale objectForKey:CPLocaleCountryCode] isEqualToString:@"US"]; } #pragma mark - #pragma mark Key event /*! Key down event @param anEvent */ - (void)keyDown:(CPEvent)anEvent { if (_isTextual) [_datePickerComponent keyDown:anEvent]; } @end var CPDatePickerModeKey = @"CPDatePickerModeKey", CPIntervalKey = @"CPIntervalKey", CPMinDateKey = @"CPMinDateKey", CPMaxDateKey = @"CPMaxDateKey", CPBackgroundColorKey = @"CPBackgroundColorKey", CPDrawsBackgroundKey = @"CPDrawsBackgroundKey", CPTextFontKey = @"CPTextFontKey", CPDatePickerElementsKey = @"CPDatePickerElementsKey", CPDatePickerStyleKey = @"CPDatePickerStyleKey", CPLocaleKey = @"CPLocaleKey", CPBorderedKey = @"CPBorderedKey", CPDateValueKey = @"CPDateValueKey"; @implementation CPDatePicker (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { self = [super initWithCoder:aCoder]; if (self) { _backgroundColor = [aCoder decodeObjectForKey:CPBackgroundColorKey]; [self setBordered:[aCoder decodeBoolForKey:CPBorderedKey]]; [self setDrawsBackground:[aCoder decodeBoolForKey:CPDrawsBackgroundKey]]; [self setDatePickerElements:[aCoder decodeIntForKey:CPDatePickerElementsKey]]; [self setDatePickerMode:[aCoder decodeIntForKey:CPDatePickerModeKey]]; [self setDatePickerStyle:[aCoder decodeIntForKey:CPDatePickerStyleKey]]; [self setMinDate:[aCoder decodeObjectForKey:CPMinDateKey] || [CPDate distantPast]]; [self setMaxDate:[aCoder decodeObjectForKey:CPMaxDateKey] || [CPDate distantFuture]]; [self setLocale:[aCoder decodeObjectForKey:CPLocaleKey]]; [self _init]; [self setTextFont:[aCoder decodeObjectForKey:CPTextFontKey]]; [self setTimeInterval:[aCoder decodeDoubleForKey:CPIntervalKey]]; [self setDateValue:[aCoder decodeObjectForKey:CPDateValueKey]]; } return self } - (void)encodeWithCoder:(CPCoder)aCoder { // Before encoding, we remove all subviews as we'll recreate them at loading while ([[self subviews] count] > 0) [[[self subviews] lastObject] removeFromSuperview]; [super encodeWithCoder:aCoder]; [aCoder encodeDouble:_timeInterval forKey:CPIntervalKey]; [aCoder encodeInt:_datePickerMode forKey:CPDatePickerModeKey]; [aCoder encodeInt:_datePickerStyle forKey:CPDatePickerStyleKey]; [aCoder encodeInt:_datePickerElements forKey:CPDatePickerElementsKey]; [aCoder encodeObject:_minDate forKey:CPMinDateKey]; [aCoder encodeObject:_maxDate forKey:CPMaxDateKey]; [aCoder encodeObject:_dateValue forKey:CPDateValueKey]; [aCoder encodeObject:_textFont forKey:CPTextFontKey]; [aCoder encodeObject:_locale forKey:CPLocaleKey]; [aCoder encodeObject:_backgroundColor forKey:CPBackgroundColorKey]; [aCoder encodeObject:_drawsBackground forKey:CPDrawsBackgroundKey]; [aCoder encodeObject:_isBordered forKey:CPBorderedKey]; } @end // FIXME: add support for CPEditorRegistrationProtocol as implemented for CPTextField @implementation _CPDatePickerValueBinder : CPBinder { } @end @implementation CPDate (CPDatePickerAdditions) - (int)_daysInMonth { return 32 - new Date(self.getFullYear(), self.getMonth(), 32).getDate(); } - (void)_resetToMidnight { self.setHours(0); self.setMinutes(0); self.setSeconds(0); self.setMilliseconds(0); } - (void)_resetToLastSeconds { self.setHours(23); self.setMinutes(59); self.setSeconds(59); self.setMilliseconds(99); } @end