Merge branch 'main' into fixed-CPTimeZone-initWithName-does-not-return-the-timeZone-with-the-good-daylight-saving-

This commit is contained in:
daboe01 authored and GitHub committed 2025-07-05 12:58:01 +02:00
commit c41307dc37
8 files changed
+234 -13

No files matched your search

+3 -2
View File
@@ -808,7 +808,8 @@ var CPObjectControllerContentKey = @"CPObjectControllerCo
var value = [self _controllerMarkerForValues:values];
[_cachedValues setObject:value forKey:theKeyPath];
return value;
// Apple's implementation returns nil instead of CPNullMarker
return value === CPNullMarker ? nil : value;
}
else
return values;
@@ -948,4 +949,4 @@ var CPManagedProxyEntityNameKey = @"CPManagedProxyEntity
[aCoder encodeObject:[self fetchPredicate] forKey:CPManagedProxyFetchPredicateKey];
}
@end
@end
+96
View File
@@ -127,16 +127,112 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
@protocol CPOutlineViewDataSource <CPObject>
@optional
/*!
@abstract Invoked when a drag operation concludes over the outline view.
@discussion The data source should incorporate the data from the dragging pasteboard and update its data model.
@param anOutlineView The outline view that is the destination of the drop.
@param info An object that contains information about the dragging session.
@param anItem The item that is the proposed parent for the dropped data. If anItem is nil, the data is to be dropped at the root level.
@param anIndex The index at which to drop the data among the item's children. If you want to drop on anItem, this will be CPOutlineViewDropOnItemIndex (-1).
@return YES if the drop was successful; otherwise, NO.
*/
- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id /*<CPDraggingInfo>*/)info item:(id)anItem childIndex:(CPInteger)anIndex;
/*!
@abstract Asks the data source whether to defer displaying the children of a given item.
@discussion This method is useful for implementing lazy loading of outline view data. Returning NO prevents the outline view from querying for children of anItem, even if it is expandable.
@param anOutlineView The outline view that sent the message.
@param anItem The item being considered for expansion.
@return YES to allow the outline view to query for children of anItem; otherwise, NO. The default is YES.
*/
- (BOOL)outlineView:(CPOutlineView)anOutlineView shouldDeferDisplayingChildrenOfItem:(id)anItem;
/*!
@abstract Invoked when a drag should begin.
@discussion The data source should write the representation of the specified items to the pasteboard.
@param anOutlineView The outline view that is the source of the drag.
@param items An array of items to be dragged.
@param pboard The pasteboard to which the data for the dragged items should be written.
@return YES if the drag should begin; NO to prevent the drag.
*/
- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)items toPasteboard:(CPPasteboard)pboard;
/*!
@abstract Used for promised-file dragging.
@discussion When a promised-file drag is dropped, this method is invoked to ask the data source to create the files at the specified destination and return their names.
@param anOutlineView The outline view that was the source of the drag.
@param dropDestination The URL of the directory where the files should be created.
@param items The items that were dragged, representing the promised files.
@return An array of strings containing the names of the files that were created.
*/
- (CPArray)outlineView:(CPOutlineView)anOutlineView namesOfPromisedFilesDroppedAtDestination:(CPURL)dropDestination forDraggedItems:(CPArray)items;
/*!
@abstract Invoked to determine if a drop is allowed at a specified location.
@discussion This method is called repeatedly while the user drags over the outline view. It should return the drag operation that should be performed.
@param anOutlineView The outline view that is the destination of the drag.
@param info An object that contains information about the dragging session.
@param anItem The item that is the proposed parent for the dropped data.
@param anIndex The index at which to drop the data among the item's children. If you want to drop on anItem, this will be CPOutlineViewDropOnItemIndex (-1).
@return A CPDragOperation value that indicates the type of operation to perform.
*/
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id /*<CPDraggingInfo>*/)info proposedItem:(id)anItem proposedChildIndex:(CPInteger)anIndex;
/*!
@abstract Invoked to determine if a drop is allowed at a specified row.
@discussion This is a legacy method from CPTableView. It is recommended to implement outlineView:validateDrop:proposedItem:proposedChildIndex: instead for more precise control in an outline view.
@param anOutlineView The outline view that is the destination of the drag.
@param info An object that contains information about the dragging session.
@param theRow The proposed row for the drop.
@param theOperation The proposed drop operation (CPTableViewDropOn or CPTableViewDropAbove).
@return A CPDragOperation value that indicates the type of operation to perform.
*/
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id /*<CPDraggingInfo>*/)info proposedRow:(int)theRow proposedDropOperation:(CPTableViewDropOperation)theOperation;
/*!
@abstract Used for state preservation.
@discussion This method is called to convert a persistent, serializable object back into a model item.
@param anOutlineView The outline view requesting the item.
@param anObject The persistent object used to identify the model item.
@return The model item corresponding to anObject, or nil if it cannot be found.
*/
- (id)outlineView:(CPOutlineView)anOutlineView itemForPersistentObject:(id)anObject;
/*!
@abstract Returns the data object to be displayed for a given item and column.
@discussion This method is called by the outline view to get the value for each cell. It is required for cell-based outline views.
@param anOutlineView The outline view that sent the message.
@param aTableColumn The column for which the value is requested.
@param anItem The item for the row being displayed.
@return The data object (e.g., a CPString) for the specified item and column.
*/
- (id)outlineView:(CPOutlineView)anOutlineView objectValueforTableColumn:(CPTableColumn)aTableColumn byItem:(id)anItem;
/*!
@abstract Used for state preservation.
@discussion This method is called to convert a model item into a persistent, serializable object (e.g., a string identifier) that can be saved.
@param anOutlineView The outline view requesting the persistent object.
@param anItem The item to be converted.
@return A serializable object that persistently identifies anItem.
*/
- (id)outlineView:(CPOutlineView)anOutlineView persistentObjectForItem:(id)anItem;
/*!
@abstract Sets the data object for a given item and column.
@discussion This method is called when the user edits a cell's value. The data source should update its model with the new value.
@param anOutlineView The outline view that sent the message.
@param anObject The new value.
@param aTableColumn The column that was edited.
@param anItem The item whose value was edited.
*/
- (void)outlineView:(CPOutlineView)anOutlineView setObjectValue:(id)anObject forTableColumn:(CPTableColumn)aTableColumn byItem:(id)anItem;
/*!
@abstract Notifies the data source that the sort descriptors have changed.
@discussion This method is called after the user clicks a column header to change the sort order. The data source should re-sort its data based on the outline view's new 'sortDescriptors' property and then call `reloadData`.
@param anOutlineView The outline view that sent the message.
@param oldDescriptors The previous sort descriptors.
*/
- (void)outlineView:(CPOutlineView)anOutlineView sortDescriptorsDidChange:(CPArray)oldDescriptors;
@end
+1 -1
View File
@@ -1472,7 +1472,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
![self isBezeled] &&
(lineBreakMode === CPLineBreakByWordWrapping || lineBreakMode === CPLineBreakByCharWrapping))
{
textSize = [text sizeWithFont:font inWidth:textSize.width] + 1;
textSize = [text sizeWithFont:font inWidth:textSize.width];
}
else
{
+12
View File
@@ -1291,6 +1291,18 @@ var _separatorsCharacterSet = nil;
if (!aString)
return [[CPDate alloc] initWithTimeIntervalSinceReferenceDate:-31622400];
// Special case for ISO8601 format, which is poorly handled by the generic parser.
if (aFormat === @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") {
var d = new Date(aString);
if (d && !isNaN(d.getTime())) {
// The native JS Date object handles ISO 8601 strings correctly (as UTC).
// We can create a CPDate directly from the resulting timestamp.
var kCFAbsoluteTimeIntervalSince1970 = 978307200.0;
var interval = (d.getTime() / 1000.0) - kCFAbsoluteTimeIntervalSince1970;
return [CPDate dateWithTimeIntervalSinceReferenceDate:interval];
}
}
if (aFormat == nil)
return nil;
+58 -7
View File
@@ -25,6 +25,7 @@
@import "CPLocale.j"
@class CPData
@class CPNotificationCenter
CPTimeZoneNameStyleStandard = 0;
CPTimeZoneNameStyleShortStandard = 1;
@@ -46,13 +47,63 @@ var abbreviationDictionary,
function abbreviationForDate(date)
{
// Sometimes the browsers will give unmatched timezones in other languages so try one more thing
// Take the locale string for english in the US. It has the following format (9/21/2021, 2:40:44 PM Central European Summer Time)
// Remove the date and time with this regex and take first letter in each word in the time zone name.
// This is not 100% but is better than nothing as JAvascript has no good support for this.
var abbreviation = date.toLocaleString('en-US', {timeZoneName : 'long'}).replace(/^([0]?\d|[1][0-2])\/((?:[0]?|[1-2])\d|[3][0-1])\/([2][01]|[1][6-9])\d{2}(,?\s*([0]?\d|[1][0-2])(\:[0-5]\d){1,2})*\s*([aApP][mM]{0,2})?\s*/, "").split(" ").map(function(l) { return l[0]}).join("");
// Strategy 1: Parse date.toString() as it's more reliable than toLocaleString.
// Format is usually: "Day Mon dd yyyy hh:mm:ss GMT+XXXX (Time Zone Name)"
var dateString = date.toString();
return abbreviation;
// Check for a long name within parentheses, e.g., (Pacific Daylight Time)
var longNameMatch = dateString.match(/\(([^)]+)\)/);
if (longNameMatch) {
var timeZoneComponent = longNameMatch[1];
// If the component is already a known abbreviation (e.g., "EST"), return it.
if ([abbreviationDictionary objectForKey:timeZoneComponent]) {
return timeZoneComponent;
}
// If it's a long name (e.g., "Eastern Daylight Time"), create an acronym.
if (timeZoneComponent.indexOf(' ') > -1) {
var generatedAbbr = timeZoneComponent.split(' ').map(function(word) { return word[0]; }).join('');
if ([abbreviationDictionary objectForKey:generatedAbbr]) {
return generatedAbbr;
}
}
}
// Strategy 2: If string parsing fails (e.g., for "GMT-04:00"), use the modern and reliable Intl API.
try {
var ianaName = new Intl.DateTimeFormat().resolvedOptions().timeZone;
var currentOffset = -date.getTimezoneOffset(); // in minutes
var keys = [abbreviationDictionary keyEnumerator],
key;
// Find all abbreviations for the current IANA time zone.
var possibleAbbrs = [];
while (key = [keys nextObject]) {
if ([abbreviationDictionary valueForKey:key] === ianaName) {
possibleAbbrs.push(key);
}
}
// If more than one (e.g., standard and daylight), use the current offset to find the right one.
for (var i = 0; i < possibleAbbrs.length; i++) {
var abbr = possibleAbbrs[i];
if ([timeDifferenceFromUTC valueForKey:abbr] === currentOffset) {
return abbr;
}
}
// If offset matching fails but we have a unique IANA match, use it as a best guess.
if (possibleAbbrs.length > 0) {
return possibleAbbrs[0];
}
} catch (e) {
// Intl API not supported, or it failed. We cannot proceed with this strategy.
}
// Return nil if no valid abbreviation could be determined.
return nil;
}
function _abbreviationForNameAndDate(tzName, date)
@@ -474,7 +525,7 @@ function _abbreviationForNameAndDate(tzName, date)
systemTimeZone = [self timeZoneWithAbbreviation:abbreviation];
[[CPNotification defaultCenter] postNotificationName:CPSystemTimeZoneDidChangeNotification object:systemTimeZone];
[[CPNotificationCenter defaultCenter] postNotificationName:CPSystemTimeZoneDidChangeNotification object:systemTimeZone];
}
/*! Return the systemTimeZone
+21
View File
@@ -0,0 +1,21 @@
@import <AppKit/AppKit.j>
@implementation CPObjectControllerTest : OJTestCase
{
}
- (void)setUp
{
}
- (void)testObjectControllerSelectionWithNil
{
var controller = [[CPObjectController alloc] init];
[controller setContent:@{@"name": @"Martin"}];
[self assert:@"Martin" equals:[[controller selection] valueForKeyPath:@"name"] message:@"name equals 'Martin'"];
[self assert:nil equals:[[controller selection] valueForKeyPath:@"lastName"] message:@"name equals 'nil'"];
[self assert:@"Martin" equals:[controller valueForKeyPath:@"selection.name"] message:@"selection.name equals 'Martin'"];
[self assert:nil equals:[controller valueForKeyPath:@"selection.lastName"] message:@"selection.name equals 'nil'"];
}
@end
+35
View File
@@ -1245,6 +1245,41 @@
[self assert:result equals:nil];
}
- (void)testDateFromStringISO8601
{
// This test verifies the fix for issue #2537.
// The parser should correctly handle the ISO 8601 format produced by Date.toISOString().
var dateFormatter = [[CPDateFormatter alloc] init];
[dateFormatter setLocale:[[CPLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
// Test with a fixed date string.
var fixedISOString = @"2017-03-31T16:40:15.768Z";
var parsedFixedDate = [dateFormatter dateFromString:fixedISOString];
// Create the expected date using a known-good method (JS Date parsing) to avoid relying on
// the code under test. We create it from a time interval, which supports millisecond precision.
var expectedTimestamp = new Date(fixedISOString).getTime() / 1000.0;
var expectedFixedDate = [CPDate dateWithTimeIntervalSince1970:expectedTimestamp];
// Now assert that the formatter produced the same date.
// isEqualToDate: compares the underlying time interval, which is what we want.
[self assertTrue:[parsedFixedDate isEqualToDate:expectedFixedDate] message:@"DateFormatter should correctly parse a fixed ISO 8601 string."];
// Test with a dynamically generated date string.
var liveJSDate = new Date();
var liveISOString = liveJSDate.toISOString();
var parsedLiveDate = [dateFormatter dateFromString:liveISOString];
// The fix converts from a JS Date, so we can compare time intervals for accuracy.
var liveJSDateTime = liveJSDate.getTime();
var parsedLiveDateTime = [parsedLiveDate timeIntervalSince1970] * 1000;
// We allow a small tolerance for floating point rounding.
[self assertTrue:Math.abs(liveJSDateTime - parsedLiveDateTime) < 1.0 message:@"DateFormatter should correctly parse a dynamically generated ISO 8601 string."];
}
- (void)testGetObjectValueAcceptsNilErrorDescription
{
var date = nil,
+8 -3
View File
@@ -260,10 +260,15 @@
- (void)testAbbreviationWithDate
{
var timeZone = [CPTimeZone localTimeZone],
abbreviation = [timeZone abbreviationForDate:_date],
expected = _date.toLocaleString('en-US', {timeZoneName : 'long'}).replace(/^([0]?\d|[1][0-2])\/((?:[0]?|[1-2])\d|[3][0-1])\/([2][01]|[1][6-9])\d{2}(,?\s*([0]?\d|[1][0-2])(\:[0-5]\d){1,2})*\s*([aApP][mM]{0,2})?\s*/, "").split(" ").map(function(l) { return l[0]}).join("");
abbreviation = [timeZone abbreviationForDate:_date];
[self assert:abbreviation equals:expected];
// With the new robust implementation, we can't easily predict the exact
// abbreviation string without re-implementing the parsing logic.
// Instead, we verify that the returned abbreviation is valid and exists
// in the known dictionary, which is sufficient.
[self assert:(abbreviation !== nil) equals:YES];
var knownAbbreviations = [CPTimeZone abbreviationDictionary];
[self assert:[knownAbbreviations containsKey:abbreviation] equals:YES];
}
- (void)testAbbreviationWithNilDate