mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-28 02:48:49 +00:00
Merge branch 'main' into fixed-CPTimeZone-initWithName-does-not-return-the-timeZone-with-the-good-daylight-saving-
This commit is contained in:
8 files changed
+234
-13
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user