| /* |
| * Copyright (C) 2018 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "WKDatePickerViewController.h" |
| |
| #if PLATFORM(WATCHOS) |
| |
| #import <PepperUICore/PUICApplication_Private.h> |
| #import <PepperUICore/PUICPickerView.h> |
| #import <PepperUICore/PUICPickerView_Private.h> |
| #import <PepperUICore/PUICQuickboardViewController_Private.h> |
| #import <PepperUICore/PUICStatusBar_Private.h> |
| #import <PepperUICore/UIDevice+PUICAdditions.h> |
| #import <UIKit/UIGeometry_Private.h> |
| #import <UIKit/UIInterface_Private.h> |
| #import <UIKit/UIResponder_Private.h> |
| #import <UIKit/UIView_Private.h> |
| #import <WebCore/LocalizedStrings.h> |
| #import <wtf/RetainPtr.h> |
| #import <wtf/WeakObjCPtr.h> |
| #import <wtf/text/WTFString.h> |
| |
| static const CGFloat pickerViewHorizontalMargin = 4; |
| static const CGFloat pickerViewBorderRadius = 6; |
| static const CGFloat pickerViewUnfocusedBorderWidth = 1; |
| static const CGFloat pickerViewFocusedBorderWidth = 2; |
| static const CGFloat pickerViewGranularityLabelBorderRadius = 8; |
| static const CGFloat pickerViewGranularityLabelFontSize = 10; |
| static const CGFloat pickerViewGranularityLabelHeight = 16; |
| static const CGFloat pickerViewGranularityLabelHorizontalPadding = 12; |
| static const CGFloat pickerViewGranularityLabelBottomMargin = 2; |
| static const CGFloat datePickerSetButtonTitleFontSize = 16; |
| |
| static CGFloat datePickerSetButtonHeight() |
| { |
| return [[UIDevice currentDevice] puic_deviceVariant] == PUICDeviceVariantCompact ? 32 : 40; |
| } |
| |
| static CGFloat datePickerVerticalMargin() |
| { |
| return [[UIDevice currentDevice] puic_deviceVariant] == PUICDeviceVariantCompact ? 10 : 14; |
| } |
| |
| static NSString *pickerViewStandaloneYearFormat = @"y"; |
| static NSString *pickerViewStandaloneDateFormat = @"dd"; |
| static NSString *pickerViewStandaloneMonthFormat = @"MMM"; |
| static NSString *inputValueDateFormat = @"yyyy-MM-dd"; |
| |
| #pragma mark - WKDatePickerWheelLabel |
| |
| @interface WKDatePickerWheelLabel : UILabel |
| |
| - (BOOL)needsUpdateForIndex:(NSUInteger)index selectedDate:(NSDate *)date; |
| @property (nonatomic) NSUInteger index; |
| @property (nonatomic, copy) NSDate *lastSelectedDate; |
| |
| @end |
| |
| @implementation WKDatePickerWheelLabel { |
| RetainPtr<NSDate> _lastSelectedDate; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame |
| { |
| if (!(self = [super initWithFrame:frame])) |
| return nil; |
| |
| self.index = NSNotFound; |
| self.lastSelectedDate = nil; |
| self.textColor = [UIColor whiteColor]; |
| self.textAlignment = NSTextAlignmentCenter; |
| self.opaque = YES; |
| self.adjustsFontSizeToFitWidth = YES; |
| return self; |
| } |
| |
| - (NSDate *)lastSelectedDate |
| { |
| return _lastSelectedDate.get(); |
| } |
| |
| - (void)setLastSelectedDate:(NSDate *)date |
| { |
| _lastSelectedDate = adoptNS([date copy]); |
| } |
| |
| - (BOOL)needsUpdateForIndex:(NSUInteger)index selectedDate:(NSDate *)date |
| { |
| return self.index != index || ![self.lastSelectedDate isEqualToDate:date]; |
| } |
| |
| @end |
| |
| #pragma mark - WKDatePickerWheel |
| |
| @interface WKDatePickerWheel : PUICPickerView |
| - (instancetype)initWithController:(WKDatePickerViewController *)controller style:(PUICPickerViewStyle)style NS_DESIGNATED_INITIALIZER; |
| @property (nonatomic) BOOL drawsFocusOutline; |
| @property (nonatomic, getter=isChangingValue) BOOL changingValue; |
| @end |
| |
| @interface WKDatePickerViewController () <PUICPickerViewDataSource, PUICPickerViewDelegate> |
| @property (nonatomic, copy) NSDate *date; |
| @property (nonatomic, copy) NSDate *minimumDate; |
| @property (nonatomic, copy) NSDate *maximumDate; |
| - (void)didBeginInteractingWithDatePicker:(WKDatePickerWheel *)datePicker; |
| @end |
| |
| @interface WKDatePickerWheel () <UIGestureRecognizerDelegate> |
| @end |
| |
| @implementation WKDatePickerWheel { |
| WeakObjCPtr<WKDatePickerViewController> _controller; |
| RetainPtr<UILongPressGestureRecognizer> _gesture; |
| BOOL _drawsFocusOutline; |
| } |
| |
| - (instancetype)initWithStyle:(PUICPickerViewStyle)style |
| { |
| return [self initWithController:nil style:style]; |
| } |
| |
| - (instancetype)initWithController:(WKDatePickerViewController *)controller style:(PUICPickerViewStyle)style |
| { |
| if (!(self = [super initWithStyle:style])) |
| return nil; |
| |
| self._continuousCornerRadius = pickerViewBorderRadius; |
| self.layer.borderColor = [UIColor systemGrayColor].CGColor; |
| self.layer.borderWidth = pickerViewUnfocusedBorderWidth; |
| self.drawsFocusOutline = NO; |
| self.changingValue = NO; |
| |
| _controller = controller; |
| _gesture = adoptNS([[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(gestureRecognized:)]); |
| [_gesture setDelegate:self]; |
| [_gesture setMinimumPressDuration:0]; |
| [self addGestureRecognizer:_gesture.get()]; |
| |
| return self; |
| } |
| |
| - (void)gestureRecognized:(UIGestureRecognizer *)gesture |
| { |
| if (gesture.state == UIGestureRecognizerStateBegan) |
| [_controller didBeginInteractingWithDatePicker:self]; |
| } |
| |
| - (void)setDrawsFocusOutline:(BOOL)drawsFocusOutline |
| { |
| if (_drawsFocusOutline == drawsFocusOutline) |
| return; |
| |
| _drawsFocusOutline = drawsFocusOutline; |
| self.layer.borderColor = (_drawsFocusOutline ? [UIColor systemGreenColor] : [UIColor systemGrayColor]).CGColor; |
| self.layer.borderWidth = _drawsFocusOutline ? pickerViewFocusedBorderWidth : pickerViewUnfocusedBorderWidth; |
| } |
| |
| - (BOOL)drawsFocusOutline |
| { |
| return _drawsFocusOutline; |
| } |
| |
| #pragma mark - UIGestureRecognizerDelegate |
| |
| - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer |
| { |
| return YES; |
| } |
| |
| @end |
| |
| struct EraAndYear { |
| NSInteger era; |
| NSInteger year; |
| }; |
| |
| @implementation WKDatePickerViewController { |
| NSInteger _day; |
| NSInteger _month; |
| NSInteger _year; |
| NSInteger _era; |
| |
| RetainPtr<WKDatePickerWheel> _monthPicker; |
| RetainPtr<WKDatePickerWheel> _dayPicker; |
| RetainPtr<WKDatePickerWheel> _yearAndEraPicker; |
| RetainPtr<WKDatePickerWheel> _focusedPicker; |
| RetainPtr<UILabel> _monthLabel; |
| RetainPtr<UILabel> _dayLabel; |
| RetainPtr<UILabel> _yearLabel; |
| RetainPtr<UIButton> _setButton; |
| |
| RetainPtr<NSTimeZone> _timeZoneForDateFormatting; |
| RetainPtr<NSCalendar> _calendar; |
| |
| RetainPtr<NSDate> _minimumDate; |
| RetainPtr<NSDate> _maximumDate; |
| RetainPtr<NSDate> _cachedDate; |
| |
| BOOL _displayCombinedEraAndYear; |
| RetainPtr<NSDateFormatter> _standaloneYearFormatter; |
| RetainPtr<NSDateFormatter> _standaloneDayFormatter; |
| RetainPtr<NSDateFormatter> _standaloneMonthFormatter; |
| RetainPtr<NSDateFormatter> _standaloneInputValueFormatter; |
| |
| RetainPtr<PUICStatusBarGlobalContextViewAssertion> _statusBarAssertion; |
| } |
| |
| @dynamic delegate; |
| |
| - (instancetype)initWithDelegate:(id <WKQuickboardViewControllerDelegate>)delegate |
| { |
| return [super initWithDelegate:delegate]; |
| } |
| |
| - (void)viewDidLoad |
| { |
| [super viewDidLoad]; |
| |
| self.headerView.hidden = YES; |
| |
| NSString *localeIdentifier = [NSLocale currentLocale].localeIdentifier; |
| _displayCombinedEraAndYear = [localeIdentifier containsString:@"calendar=japanese"] || [localeIdentifier isEqualToString:@"ja_JP_TRADITIONAL"]; |
| _timeZoneForDateFormatting = [NSTimeZone localTimeZone]; |
| _calendar = [NSCalendar currentCalendar]; |
| [_calendar setTimeZone:_timeZoneForDateFormatting.get()]; |
| |
| _standaloneYearFormatter = adoptNS([[NSDateFormatter alloc] init]); |
| [_standaloneYearFormatter setDateFormat:pickerViewStandaloneYearFormat]; |
| [_standaloneYearFormatter setLocale:NSLocale.currentLocale]; |
| [_standaloneYearFormatter setCalendar:_calendar.get()]; |
| [_standaloneYearFormatter setTimeZone:_timeZoneForDateFormatting.get()]; |
| |
| _standaloneDayFormatter = adoptNS([[NSDateFormatter alloc] init]); |
| [_standaloneDayFormatter setDateFormat:[NSDateFormatter dateFormatFromTemplate:pickerViewStandaloneDateFormat options:0 locale:NSLocale.currentLocale]]; |
| [_standaloneDayFormatter setCalendar:_calendar.get()]; |
| [_standaloneDayFormatter setTimeZone:_timeZoneForDateFormatting.get()]; |
| |
| _standaloneMonthFormatter = adoptNS([[NSDateFormatter alloc] init]); |
| [_standaloneMonthFormatter setDateFormat:[NSDateFormatter dateFormatFromTemplate:pickerViewStandaloneMonthFormat options:0 locale:NSLocale.currentLocale]]; |
| [_standaloneMonthFormatter setCalendar:_calendar.get()]; |
| [_standaloneMonthFormatter setTimeZone:_timeZoneForDateFormatting.get()]; |
| |
| _standaloneInputValueFormatter = adoptNS([[NSDateFormatter alloc] init]); |
| [_standaloneInputValueFormatter setDateFormat:inputValueDateFormat]; |
| [_standaloneInputValueFormatter setCalendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]]; |
| [_standaloneInputValueFormatter setTimeZone:_timeZoneForDateFormatting.get()]; |
| |
| self.minimumDate = self.defaultMinimumDate; |
| self.maximumDate = self.defaultMaximumDate; |
| self.date = [self _dateFromInitialText] ?: [NSDate date]; |
| |
| _monthPicker = adoptNS([[WKDatePickerWheel alloc] initWithController:self style:PUICPickerViewStyleList]); |
| [self _configurePickerView:_monthPicker.get()]; |
| _monthLabel = [self _createAndConfigureGranularityLabelWithText:WebCore::datePickerMonthLabelTitle()]; |
| |
| _dayPicker = adoptNS([[WKDatePickerWheel alloc] initWithController:self style:PUICPickerViewStyleList]); |
| [self _configurePickerView:_dayPicker.get()]; |
| _dayLabel = [self _createAndConfigureGranularityLabelWithText:WebCore::datePickerDayLabelTitle()]; |
| |
| _yearAndEraPicker = adoptNS([[WKDatePickerWheel alloc] initWithController:self style:PUICPickerViewStyleList]); |
| [self _configurePickerView:_yearAndEraPicker.get()]; |
| _yearLabel = [self _createAndConfigureGranularityLabelWithText:WebCore::datePickerYearLabelTitle()]; |
| |
| [self _updateSelectedPickerViewIndices]; |
| |
| [_dayPicker setDrawsFocusOutline:YES]; |
| [_dayLabel setHidden:NO]; |
| _focusedPicker = _dayPicker; |
| |
| _setButton = [UIButton buttonWithType:UIButtonTypeSystem]; |
| [_setButton setTitle:WebCore::datePickerSetButtonTitle() forState:UIControlStateNormal]; |
| [_setButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; |
| [_setButton setBackgroundColor:[UIColor systemGreenColor]]; |
| [_setButton _setContinuousCornerRadius:datePickerSetButtonHeight() / 2]; |
| [_setButton titleLabel].font = [UIFont systemFontOfSize:datePickerSetButtonTitleFontSize]; |
| [_setButton addTarget:self action:@selector(_setButtonPressed) forControlEvents:UIControlEventTouchUpInside]; |
| [self.contentView addSubview:_setButton.get()]; |
| |
| self.view.opaque = YES; |
| self.view.backgroundColor = [UIColor blackColor]; |
| } |
| |
| - (BOOL)prefersStatusBarHidden |
| { |
| return NO; |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated |
| { |
| [super viewWillAppear:animated]; |
| |
| _statusBarAssertion = [[PUICApplication sharedPUICApplication] _takeStatusBarGlobalContextAssertionAnimated:NO]; |
| [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleStatusBarNavigation) name:PUICStatusBarNavigationBackButtonPressedNotification object:nil]; |
| [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleStatusBarNavigation) name:PUICStatusBarTitleTappedNotification object:nil]; |
| |
| configureStatusBarForController(self, self.delegate); |
| } |
| |
| - (void)viewDidAppear:(BOOL)animated |
| { |
| [super viewDidAppear:animated]; |
| [self becomeFirstResponder]; |
| } |
| |
| - (void)viewDidDisappear:(BOOL)animated |
| { |
| [super viewDidDisappear:animated]; |
| |
| _statusBarAssertion = nil; |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| } |
| |
| - (void)_handleStatusBarNavigation |
| { |
| [self.delegate quickboardInputCancelled:self]; |
| } |
| |
| - (void)viewWillLayoutSubviews |
| { |
| [super viewWillLayoutSubviews]; |
| |
| auto viewBounds = self.view.bounds; |
| auto width = CGRectGetWidth(viewBounds); |
| auto height = CGRectGetHeight(viewBounds); |
| |
| [_setButton setFrame:CGRectMake(0, height - datePickerSetButtonHeight(), width, datePickerSetButtonHeight())]; |
| |
| [_monthLabel sizeToFit]; |
| [_dayLabel sizeToFit]; |
| [_yearLabel sizeToFit]; |
| |
| CGRect pickerLayoutFrame = CGRectMake(0, CGRectGetMaxY(self.headerView.frame), width, CGRectGetMinY([_setButton frame]) - CGRectGetMaxY(self.headerView.frame)); |
| pickerLayoutFrame = UIRectInsetEdges(pickerLayoutFrame, UIRectEdgeTop | UIRectEdgeBottom, datePickerVerticalMargin()); |
| CGFloat pickerViewLayoutOriginY = CGRectGetMinY(pickerLayoutFrame) + pickerViewGranularityLabelHeight + pickerViewGranularityLabelBottomMargin; |
| CGFloat pickerViewColumnWidth = CGRectGetWidth(pickerLayoutFrame) / 3; |
| CGFloat pickerViewLayoutHeight = CGRectGetHeight(pickerLayoutFrame) - pickerViewGranularityLabelHeight - pickerViewGranularityLabelBottomMargin; |
| |
| // FIXME: Respect locale and interface direction when mapping picker views and labels to left, middle and right positions. |
| WKDatePickerWheel *leftPicker = _monthPicker.get(); |
| WKDatePickerWheel *middlePicker = _dayPicker.get(); |
| WKDatePickerWheel *rightPicker = _yearAndEraPicker.get(); |
| UILabel *leftLabel = _monthLabel.get(); |
| UILabel *middleLabel = _dayLabel.get(); |
| UILabel *rightLabel = _yearLabel.get(); |
| |
| leftPicker.frame = UIRectInsetEdges(CGRectMake(0, pickerViewLayoutOriginY, pickerViewColumnWidth, pickerViewLayoutHeight), UIRectEdgeRight, pickerViewHorizontalMargin); |
| leftLabel.frame = UIRectInsetEdges(CGRectMake(0, CGRectGetMinY(pickerLayoutFrame), CGRectGetWidth(leftLabel.bounds), pickerViewGranularityLabelHeight), UIRectEdgeRight, -pickerViewGranularityLabelHorizontalPadding); |
| |
| middlePicker.frame = UIRectInsetEdges(CGRectMake(pickerViewColumnWidth, pickerViewLayoutOriginY, pickerViewColumnWidth, pickerViewLayoutHeight), UIRectEdgeRight | UIRectEdgeLeft, pickerViewHorizontalMargin / 2); |
| middleLabel.frame = UIRectInsetEdges(CGRectMake((CGRectGetWidth(pickerLayoutFrame) - CGRectGetWidth(middleLabel.bounds)) / 2, CGRectGetMinY(pickerLayoutFrame), CGRectGetWidth(middleLabel.bounds), pickerViewGranularityLabelHeight), UIRectEdgeRight | UIRectEdgeLeft, -pickerViewGranularityLabelHorizontalPadding / 2); |
| |
| rightPicker.frame = UIRectInsetEdges(CGRectMake(2 * pickerViewColumnWidth, pickerViewLayoutOriginY, pickerViewColumnWidth, pickerViewLayoutHeight), UIRectEdgeLeft, pickerViewHorizontalMargin); |
| rightLabel.frame = UIRectInsetEdges(CGRectMake(CGRectGetWidth(pickerLayoutFrame) - CGRectGetWidth(rightLabel.bounds), CGRectGetMinY(pickerLayoutFrame), CGRectGetWidth(rightLabel.bounds), pickerViewGranularityLabelHeight), UIRectEdgeLeft, -pickerViewGranularityLabelHorizontalPadding); |
| } |
| |
| - (BOOL)becomeFirstResponder |
| { |
| return [_focusedPicker becomeFirstResponder]; |
| } |
| |
| - (NSDate *)defaultMinimumDate |
| { |
| auto components = adoptNS([[NSDateComponents alloc] init]); |
| [components setDay:1]; |
| [components setMonth:1]; |
| [components setYear:1]; |
| NSDate *minDateInGregorianCalendar = [[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian] dateFromComponents:components.get()]; |
| NSDate *minDateInCurrentCalendar = nil; |
| if (![_calendar rangeOfUnit:NSCalendarUnitYear startDate:&minDateInCurrentCalendar interval:nil forDate:minDateInGregorianCalendar]) { |
| // Fall back to the Gregorian calendar date if the year containing the min date couldn't be computed using the current calendar. |
| ASSERT_NOT_REACHED(); |
| return minDateInGregorianCalendar; |
| } |
| |
| if ([_calendar compareDate:minDateInCurrentCalendar toDate:minDateInGregorianCalendar toUnitGranularity:NSCalendarUnitDay] == NSOrderedAscending) |
| minDateInCurrentCalendar = [_calendar dateByAddingUnit:NSCalendarUnitYear value:1 toDate:minDateInCurrentCalendar options:0]; |
| |
| return minDateInCurrentCalendar; |
| } |
| |
| - (NSDate *)defaultMaximumDate |
| { |
| auto components = adoptNS([[NSDateComponents alloc] init]); |
| [components setDay:1]; |
| [components setMonth:1]; |
| [components setYear:10000]; |
| NSDate *dayAfterMaxDateInGregorianCalendar = [[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian] dateFromComponents:components.get()]; |
| NSDate *dayAfterMaxDateInCurrentCalendar = nil; |
| NSDate *dayAfterMaxDate = nil; |
| if ([_calendar rangeOfUnit:NSCalendarUnitYear startDate:&dayAfterMaxDateInCurrentCalendar interval:nil forDate:dayAfterMaxDateInGregorianCalendar]) |
| dayAfterMaxDate = dayAfterMaxDateInCurrentCalendar; |
| else { |
| // Fall back to the Gregorian calendar date if the year containing the max date couldn't be computed using the current calendar. |
| ASSERT_NOT_REACHED(); |
| dayAfterMaxDate = dayAfterMaxDateInGregorianCalendar; |
| } |
| return [_calendar dateByAddingUnit:NSCalendarUnitDay value:-1 toDate:dayAfterMaxDate options:0]; |
| } |
| |
| - (NSString *)_valueForInput |
| { |
| return [_standaloneInputValueFormatter stringFromDate:self.date]; |
| } |
| |
| - (NSDate *)_dateFromInitialText |
| { |
| return [_standaloneInputValueFormatter dateFromString:[self.delegate initialValueForViewController:self]]; |
| } |
| |
| - (void)_setButtonPressed |
| { |
| [self _canonicalizeAndUpdateSelectedDate]; |
| |
| auto attributedStringValue = adoptNS([[NSAttributedString alloc] initWithString:[self _valueForInput]]); |
| [self.delegate quickboard:self textEntered:attributedStringValue.get()]; |
| } |
| |
| - (void)_updateSelectedPickerViewIndices |
| { |
| [_monthPicker setSelectedIndex:[self _indexFromMonth:_month]]; |
| [_dayPicker setSelectedIndex:[self _indexFromDay:_day]]; |
| [_yearAndEraPicker setSelectedIndex:[self _indexFromYear:_year era:_era]]; |
| } |
| |
| - (void)_configurePickerView:(WKDatePickerWheel *)picker |
| { |
| picker.dataSource = self; |
| picker.delegate = self; |
| [self.contentView addSubview:picker]; |
| } |
| |
| - (void)setMinimumDate:(NSDate *)date |
| { |
| if ([_minimumDate isEqualToDate:date]) |
| return; |
| |
| _minimumDate = adoptNS([date copy]); |
| [_monthPicker reloadData]; |
| [_yearAndEraPicker reloadData]; |
| [_dayPicker reloadData]; |
| } |
| |
| - (NSDate *)minimumDate |
| { |
| return _minimumDate.get(); |
| } |
| |
| - (void)setMaximumDate:(NSDate *)date |
| { |
| if ([_minimumDate isEqualToDate:date]) |
| return; |
| |
| _maximumDate = adoptNS([date copy]); |
| [_monthPicker reloadData]; |
| [_yearAndEraPicker reloadData]; |
| [_dayPicker reloadData]; |
| } |
| |
| - (NSDate *)maximumDate |
| { |
| return _maximumDate.get(); |
| } |
| |
| - (void)setDate:(NSDate *)date |
| { |
| if ([self.date isEqualToDate:date]) |
| return; |
| |
| if ([date compare:_minimumDate.get()] == NSOrderedAscending) |
| date = _minimumDate.get(); |
| |
| if ([date compare:_maximumDate.get()] == NSOrderedDescending) |
| date = _maximumDate.get(); |
| |
| [self setDateFromComponents:[_calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear | NSCalendarUnitEra fromDate:date]]; |
| } |
| |
| - (void)setDateFromComponents:(NSDateComponents *)components |
| { |
| [self setDay:components.day month:components.month year:components.year era:components.era]; |
| } |
| |
| - (void)setDay:(NSInteger)day month:(NSInteger)month year:(NSInteger)year era:(NSInteger)era |
| { |
| _day = day; |
| _month = month; |
| _year = year; |
| _era = era; |
| _cachedDate = nil; |
| } |
| |
| - (NSDate *)date |
| { |
| if (!_cachedDate) |
| _cachedDate = [self _dateComponentForDay:_day month:_month year:_year era:_era].date; |
| return _cachedDate.get(); |
| } |
| |
| - (NSDateComponents *)_dateComponentForDay:(NSInteger)day month:(NSInteger)month year:(NSInteger)year era:(NSInteger)era |
| { |
| NSDateComponents *dateComponents = [[[NSDateComponents alloc] init] autorelease]; |
| dateComponents.day = day; |
| dateComponents.month = month; |
| dateComponents.year = year; |
| dateComponents.era = era; |
| dateComponents.calendar = _calendar.get(); |
| return dateComponents; |
| } |
| |
| - (void)_adjustDateToValidDateIfNecessary |
| { |
| NSDateComponents *adjustedComponents = [self _dateComponentForDay:_day month:_month year:_year era:_era]; |
| BOOL didAdjust = NO; |
| while (![adjustedComponents isValidDateInCalendar:_calendar.get()]) { |
| if (adjustedComponents.day <= 1) { |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| --adjustedComponents.day; |
| didAdjust = YES; |
| } |
| |
| [self setDateFromComponents:adjustedComponents]; |
| |
| if (didAdjust) |
| [self _updateSelectedPickerViewIndices]; |
| } |
| |
| - (RetainPtr<UILabel>)_createAndConfigureGranularityLabelWithText:(NSString *)localizedText |
| { |
| auto label = adoptNS([[UILabel alloc] init]); |
| [label setText:localizedText]; |
| [label setTextAlignment:NSTextAlignmentCenter]; |
| [label setFont:[UIFont systemFontOfSize:pickerViewGranularityLabelFontSize weight:UIFontWeightHeavy]]; |
| [label layer].backgroundColor = [UIColor systemGreenColor].CGColor; |
| [label _setContinuousCornerRadius:pickerViewGranularityLabelBorderRadius]; |
| [label setTextColor:[UIColor blackColor]]; |
| [label setHidden:YES]; |
| [self.contentView addSubview:label.get()]; |
| return label; |
| } |
| |
| - (void)_canonicalizeAndUpdateSelectedDate |
| { |
| auto eraAndYear = [self _eraAndYearFromIndex:[_yearAndEraPicker selectedIndex]]; |
| [self setDay:[self _dayFromIndex:[_dayPicker selectedIndex]] month:[self _monthFromIndex:[_monthPicker selectedIndex]] year:eraAndYear.year era:eraAndYear.era]; |
| [self _adjustDateToValidDateIfNecessary]; |
| [_dayPicker reloadData]; |
| } |
| |
| #pragma mark - PUICPickerViewDataSource |
| |
| - (NSInteger)numberOfItemsInPickerView:(WKDatePickerWheel *)pickerView |
| { |
| if (pickerView == _monthPicker) |
| return [_calendar rangeOfUnit:NSCalendarUnitMonth inUnit:NSCalendarUnitYear forDate:self.date].length; |
| |
| if (pickerView == _dayPicker) |
| return [_calendar rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:self.date].length; |
| |
| if (pickerView == _yearAndEraPicker) { |
| NSDateComponents *yearAndEraComponents = [_calendar components:NSCalendarUnitYear | NSCalendarUnitEra fromDate:self.maximumDate]; |
| return 1 + [self _indexFromYear:yearAndEraComponents.year era:yearAndEraComponents.era]; |
| } |
| |
| return 0; |
| } |
| |
| - (UIView *)pickerView:(WKDatePickerWheel *)pickerView viewForItemAtIndex:(NSInteger)index |
| { |
| auto label = retainPtr((WKDatePickerWheelLabel *)[pickerView dequeueReusableItemView]) ?: adoptNS([[WKDatePickerWheelLabel alloc] init]); |
| if (![label needsUpdateForIndex:index selectedDate:self.date]) |
| return label.autorelease(); |
| |
| if (pickerView == _monthPicker) { |
| NSDateComponents *dateComponentsAtIndex = [self _dateComponentForDay:NSNotFound month:[self _monthFromIndex:index] year:_year era:_era]; |
| [label setText:[_standaloneMonthFormatter stringFromDate:dateComponentsAtIndex.date].localizedUppercaseString]; |
| } |
| |
| if (pickerView == _dayPicker) { |
| NSDateComponents *dateComponentsAtIndex = [self _dateComponentForDay:[self _dayFromIndex:index] month:_month year:_year era:_era]; |
| [label setText:[_standaloneDayFormatter stringFromDate:dateComponentsAtIndex.date]]; |
| } |
| |
| if (pickerView == _yearAndEraPicker) { |
| auto eraAndYear = [self _eraAndYearFromIndex:index]; |
| NSDate *dateForEraAndYear = [self _dateComponentForDay:NSNotFound month:NSNotFound year:eraAndYear.year era:eraAndYear.era].date; |
| NSString *eraPrefix = _displayCombinedEraAndYear ? [[[_calendar eraSymbols] objectAtIndex:eraAndYear.era] substringToIndex:1] : @""; |
| [label setText:[eraPrefix stringByAppendingString:[_standaloneYearFormatter stringFromDate:dateForEraAndYear]]]; |
| } |
| |
| [label setIndex:index]; |
| [label setLastSelectedDate:self.date]; |
| return label.autorelease(); |
| } |
| |
| - (void)didBeginInteractingWithDatePicker:(WKDatePickerWheel *)datePicker |
| { |
| if (datePicker == _focusedPicker) |
| return; |
| |
| [_monthPicker setDrawsFocusOutline:datePicker == _monthPicker]; |
| [_monthLabel setHidden:![_monthPicker drawsFocusOutline]]; |
| [_dayPicker setDrawsFocusOutline:datePicker == _dayPicker]; |
| [_dayLabel setHidden:![_dayPicker drawsFocusOutline]]; |
| [_yearAndEraPicker setDrawsFocusOutline:datePicker == _yearAndEraPicker]; |
| [_yearLabel setHidden:![_yearAndEraPicker drawsFocusOutline]]; |
| |
| _focusedPicker = datePicker; |
| [self becomeFirstResponder]; |
| } |
| |
| - (void)pickerView:(WKDatePickerWheel *)pickerView didSelectItemAtIndex:(NSInteger)index |
| { |
| } |
| |
| - (void)pickerViewWillBeginSelection:(WKDatePickerWheel *)pickerView |
| { |
| if (pickerView == _monthPicker) |
| [_monthPicker setChangingValue:YES]; |
| else if (pickerView == _yearAndEraPicker) |
| [_yearAndEraPicker setChangingValue:YES]; |
| else if (pickerView == _dayPicker) |
| [_dayPicker setChangingValue:YES]; |
| } |
| |
| - (void)pickerViewDidEndSelection:(WKDatePickerWheel *)pickerView |
| { |
| if (pickerView == _monthPicker) |
| [_monthPicker setChangingValue:NO]; |
| else if (pickerView == _yearAndEraPicker) |
| [_yearAndEraPicker setChangingValue:NO]; |
| else if (pickerView == _dayPicker) |
| [_dayPicker setChangingValue:NO]; |
| else if (![_monthPicker isChangingValue] && ![_yearAndEraPicker isChangingValue] && ![_dayPicker isChangingValue]) |
| [self _canonicalizeAndUpdateSelectedDate]; |
| } |
| |
| #pragma mark - Conversion between picker view indices and era, year, month, day |
| |
| - (NSInteger)_dayFromIndex:(NSUInteger)dayIndex |
| { |
| return dayIndex + 1; |
| } |
| |
| - (EraAndYear)_eraAndYearFromIndex:(NSUInteger)yearIndex |
| { |
| NSDate *dateAtYearIndex = [_calendar dateByAddingUnit:NSCalendarUnitYear value:yearIndex toDate:self.minimumDate options:0]; |
| NSDateComponents *components = [_calendar components:NSCalendarUnitYear | NSCalendarUnitEra fromDate:dateAtYearIndex]; |
| return { [components era], [components year] }; |
| } |
| |
| - (NSInteger)_monthFromIndex:(NSUInteger)monthIndex |
| { |
| return monthIndex + 1; |
| } |
| |
| - (NSUInteger)_indexFromDay:(NSInteger)day |
| { |
| return day - 1; |
| } |
| |
| - (NSUInteger)_indexFromYear:(NSInteger)year era:(NSInteger)era |
| { |
| NSDate *dateForEraAndYear = [self _dateComponentForDay:NSNotFound month:NSNotFound year:year era:era].date; |
| NSDateComponents *components = [_calendar components:NSCalendarUnitYear fromDate:self.minimumDate toDate:dateForEraAndYear options:0]; |
| return components.year; |
| } |
| |
| - (NSUInteger)_indexFromMonth:(NSInteger)month |
| { |
| return month - 1; |
| } |
| |
| @end |
| |
| |
| #endif |