From f74fff6d9d0c5f0a4d77a26df74efd24ce624352 Mon Sep 17 00:00:00 2001 From: Todd White Date: Wed, 29 Jul 2026 15:09:24 -0400 Subject: [PATCH 1/2] Fix: show the date a picker is set to NSDatePickerCell gave its formatter no format, so the cell had nothing to draw from and fell back to the description of the date object. The picker showed a full date and time whatever its datePickerElements were, in the local time zone rather than the one it was set to, and in a form no locale writes. Build the format from the elements. Each element contributes a letter to a template, and the pattern generator returns the pattern that locale writes those elements in; a build without ICU has no generator and gets a fixed order instead. The format is rebuilt whenever the elements or the locale change. NSCell keeps the text its formatter made at the time the value was set, and -[NSDateFormatter stringForObjectValue:] builds that text from a %-style format rather than from the pattern, so take -stringValue from -stringFromDate: instead. A cell decoded without a formatter gets one, as a cell built in code does. --- Source/NSDatePickerCell.m | 140 +++++++++++++++++++++++++++++++ Tests/gui/NSDatePicker/display.m | 125 +++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 Tests/gui/NSDatePicker/display.m diff --git a/Source/NSDatePickerCell.m b/Source/NSDatePickerCell.m index 5799b30eb9..bd640d3562 100644 --- a/Source/NSDatePickerCell.m +++ b/Source/NSDatePickerCell.m @@ -35,6 +35,10 @@ #import "AppKit/NSDatePickerCell.h" #import "AppKit/NSColor.h" +@interface NSDatePickerCell (Private) +- (void) _updateDateFormat; +@end + @implementation NSDatePickerCell - (void) dealloc @@ -60,11 +64,119 @@ - (id) initTextCell: (NSString*)aString [self setBezeled: YES]; _datePickerElements = NSYearMonthDayDatePickerElementFlag | NSHourMinuteSecondDatePickerElementFlag; + [self _updateDateFormat]; } return self; } +/* The template holds one letter per element the picker shows. The pattern + generator turns it into the pattern the locale writes those elements in. + 'j' stands for the hour in the clock the locale uses, twelve or twenty + four. +*/ +- (NSString *) _dateFormatTemplate +{ + NSMutableString *template = [NSMutableString stringWithCapacity: 8]; + NSDatePickerElementFlags elements = _datePickerElements; + + if (elements & NSEraDatePickerElementFlag) + { + [template appendString: @"G"]; + } + if (elements & NSYearMonthDatePickerElementFlag) + { + [template appendString: @"yM"]; + } + if ((elements & NSYearMonthDayDatePickerElementFlag) + == NSYearMonthDayDatePickerElementFlag) + { + [template appendString: @"d"]; + } + if (elements & NSHourMinuteDatePickerElementFlag) + { + [template appendString: @"jm"]; + } + if ((elements & NSHourMinuteSecondDatePickerElementFlag) + == NSHourMinuteSecondDatePickerElementFlag) + { + [template appendString: @"s"]; + } + if (elements & NSTimeZoneDatePickerElementFlag) + { + [template appendString: @"z"]; + } + + return template; +} + +/* Used when the pattern generator is unavailable, which is the case in a + build without ICU. +*/ +- (NSString *) _fallbackDateFormat +{ + NSMutableString *format = [NSMutableString stringWithCapacity: 24]; + NSDatePickerElementFlags elements = _datePickerElements; + + if (elements & NSYearMonthDatePickerElementFlag) + { + [format appendString: @"y-MM"]; + if ((elements & NSYearMonthDayDatePickerElementFlag) + == NSYearMonthDayDatePickerElementFlag) + { + [format appendString: @"-dd"]; + } + } + if (elements & NSHourMinuteDatePickerElementFlag) + { + if ([format length] > 0) + { + [format appendString: @" "]; + } + [format appendString: @"HH:mm"]; + if ((elements & NSHourMinuteSecondDatePickerElementFlag) + == NSHourMinuteSecondDatePickerElementFlag) + { + [format appendString: @":ss"]; + } + } + if (elements & NSTimeZoneDatePickerElementFlag) + { + [format appendString: @" zzz"]; + } + if (elements & NSEraDatePickerElementFlag) + { + [format appendString: @" G"]; + } + + return format; +} + +- (void) _updateDateFormat +{ + NSDateFormatter *formatter = (NSDateFormatter *)[self formatter]; + NSString *template; + NSString *format = nil; + + if (![formatter isKindOfClass: [NSDateFormatter class]]) + { + return; + } + + template = [self _dateFormatTemplate]; + if ([template length] > 0) + { + format = [NSDateFormatter dateFormatFromTemplate: template + options: 0 + locale: [formatter locale]]; + if (format == nil) + { + format = [self _fallbackDateFormat]; + } + } + [formatter setDateFormat: (format == nil) ? (NSString *)@"" : format]; +} + - (NSColor *) backgroundColor { return _backgroundColor; @@ -93,6 +205,7 @@ - (NSDatePickerElementFlags) datePickerElements - (void) setDatePickerElements: (NSDatePickerElementFlags)flags { _datePickerElements = flags; + [self _updateDateFormat]; } - (NSDatePickerMode) datePickerMode @@ -142,6 +255,25 @@ - (void) setDateValue: (NSDate *)date [self setObjectValue: [self _clampedDate: date]]; } +/* NSCell keeps the text its formatter made when the value was set, which is + stale once the elements or the locale change, and -stringForObjectValue: + builds that text from a %-style format rather than from the pattern. +*/ +- (NSString *) stringValue +{ + NSDateFormatter *formatter = (NSDateFormatter *)[self formatter]; + id value = [self objectValue]; + + if ([value isKindOfClass: [NSDate class]] + && [formatter isKindOfClass: [NSDateFormatter class]] + && [[formatter dateFormat] length] > 0) + { + return [formatter stringFromDate: (NSDate *)value]; + } + + return [super stringValue]; +} + - (id) delegate { return _delegate; @@ -170,6 +302,7 @@ - (NSLocale *) locale - (void) setLocale: (NSLocale *)locale { [[self formatter] setLocale: locale]; + [self _updateDateFormat]; } - (NSDate *) maxDate @@ -242,6 +375,13 @@ - (id) initWithCoder: (NSCoder *)aDecoder { if ((self = [super initWithCoder: aDecoder])) { + if (![[self formatter] isKindOfClass: [NSDateFormatter class]]) + { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + + [self setFormatter: formatter]; + RELEASE(formatter); + } if ([aDecoder allowsKeyedCoding]) { [self setTimeInterval: [aDecoder decodeDoubleForKey: @"NSTimeInterval"]]; diff --git a/Tests/gui/NSDatePicker/display.m b/Tests/gui/NSDatePicker/display.m new file mode 100644 index 0000000000..bd8217fade --- /dev/null +++ b/Tests/gui/NSDatePicker/display.m @@ -0,0 +1,125 @@ +/* The text a date picker shows follows the elements it is configured with, + in the order and the form its locale writes them, and in its own time zone. + The assertions look for the parts of the date rather than a whole string, + so that they hold for the pattern any CLDR version produces. A build + without ICU has no pattern generator and falls back to a fixed order, so + the one assertion that compares two locales is made only when the + generator answers. The picker uses the theme and font backend, so the set + is skipped when the backend is unavailable. +*/ +#include "Testing.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static BOOL +has(NSString *string, NSString *part) +{ + return [string rangeOfString: part].length > 0; +} + +int +main(int argc, char **argv) +{ + CREATE_AUTORELEASE_POOL(arp); + NSDatePicker *dp; + NSString *text; + NSString *american; + NSDate *date; + BOOL generator; + + START_SET("NSDatePicker display") + + NS_DURING + { + [NSApplication sharedApplication]; + } + NS_HANDLER + { + if ([[localException name] isEqualToString: NSInternalInconsistencyException]) + SKIP("It looks like GNUstep backend is not yet installed") + } + NS_ENDHANDLER + + NS_DURING + { + generator = ([NSDateFormatter dateFormatFromTemplate: @"yMd" + options: 0 + locale: [NSLocale localeWithLocaleIdentifier: @"en_US"]] + != nil); + + /* 8 March 2023, 20:26:40 GMT. */ + date = [NSDate dateWithTimeIntervalSinceReferenceDate: 700000000.0]; + dp = AUTORELEASE([[NSDatePicker alloc] + initWithFrame: NSMakeRect(0, 0, 260, 26)]); + [dp setLocale: [NSLocale localeWithLocaleIdentifier: @"en_US"]]; + [dp setTimeZone: [NSTimeZone timeZoneWithName: @"GMT"]]; + [dp setDateValue: date]; + + text = [[dp cell] stringValue]; + PASS(has(text, @"2023") && has(text, @":26:") && has(text, @"40"), + "the default elements show the date and the time to the second"); + PASS(!has(text, @"+0000") && ![text isEqualToString: [date description]], + "the shown text is not the description of the date"); + + [dp setDatePickerElements: NSYearMonthDayDatePickerElementFlag]; + text = [[dp cell] stringValue]; + PASS(has(text, @"2023") && !has(text, @":26"), + "the year-month-day elements drop the time"); + + [dp setDatePickerElements: NSHourMinuteSecondDatePickerElementFlag]; + text = [[dp cell] stringValue]; + PASS(has(text, @":26:") && has(text, @"40") && !has(text, @"2023"), + "the hour-minute-second elements drop the date"); + + [dp setDatePickerElements: NSHourMinuteDatePickerElementFlag]; + text = [[dp cell] stringValue]; + PASS(has(text, @":26") && !has(text, @":26:"), + "the hour-minute elements drop the seconds"); + + [dp setDatePickerElements: NSYearMonthDatePickerElementFlag]; + text = [[dp cell] stringValue]; + PASS(has(text, @"2023") && !has(text, @":"), + "the year-month elements drop the day and the time"); + + /* The time zone the picker is set to decides the hour it shows. */ + [dp setDatePickerElements: NSHourMinuteSecondDatePickerElementFlag]; + text = [[dp cell] stringValue]; + [dp setTimeZone: [NSTimeZone timeZoneForSecondsFromGMT: 3600 * 5]]; + PASS(![[[dp cell] stringValue] isEqualToString: text], + "the shown time follows the time zone of the picker"); + + [dp setTimeZone: [NSTimeZone timeZoneWithName: @"GMT"]]; + [dp setDatePickerElements: NSYearMonthDayDatePickerElementFlag]; + american = [[dp cell] stringValue]; + [dp setLocale: [NSLocale localeWithLocaleIdentifier: @"de_DE"]]; + if (generator) + { + PASS(![[[dp cell] stringValue] isEqualToString: american], + "the shown date follows the locale of the picker"); + } + } + NS_HANDLER + { + if ([[localException name] isEqualToString: NSInternalInconsistencyException] + || [[localException name] isEqualToString: @"NSWindowServerCommunicationException"]) + SKIP("No display available") + else + [localException raise]; + } + NS_ENDHANDLER + + END_SET("NSDatePicker display") + + DESTROY(arp); + return 0; +} From af18db8b63d25fe8d0e19b22ebade26d29fd6078 Mon Sep 17 00:00:00 2001 From: Todd White Date: Thu, 30 Jul 2026 08:57:33 -0400 Subject: [PATCH 2/2] tests: let START_SET manage the autorelease pool START_SET creates an autorelease pool and END_SET releases it, so the pool these tests create around the set is redundant. --- Tests/gui/NSDatePicker/display.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/gui/NSDatePicker/display.m b/Tests/gui/NSDatePicker/display.m index bd8217fade..0b5ced583c 100644 --- a/Tests/gui/NSDatePicker/display.m +++ b/Tests/gui/NSDatePicker/display.m @@ -30,7 +30,6 @@ int main(int argc, char **argv) { - CREATE_AUTORELEASE_POOL(arp); NSDatePicker *dp; NSString *text; NSString *american; @@ -120,6 +119,5 @@ END_SET("NSDatePicker display") - DESTROY(arp); return 0; }