mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-03 13:49:45 +00:00
Foundation: Add CPProgress and cancellable item provider loading
Partial implementation, gaps and impossibilities noted internally as TODO comments. * CPProgress.j: Introduce CPProgress, modelled on NSProgress. - Count units of work, with fraction completed, finished and indeterminate states. - Compose progress trees through explicit children and the current progress object. - Cancel, pause and resume, with handlers, propagating to children. - Announce unit-count changes through key-value observing across the tree. * CPError.j: Add CPUserCancelledError. * CPItemProvider.j: - Return a CPProgress from each load, counting bytes read for files and aborting the read on cancellation. - Pass file contents to CPData as a binary string rather than a byte array. - Implement -canLoadObjectOfClass: and -loadObjectOfClass:completionHandler:. - Add the CPString (CPItemProvider) reading category. * CPString+Encoding.j: Decode data held as a binary string through a Uint8Array, without building a byte array. * Foundation.j: Import CPProgress.j. * CPProgressTest.j: Add unit tests covering counts, children, the current progress object, cancellation, pausing, observation and user information. * CPItemProviderTest.j: Add unit tests covering read progress, cancellation, completion and string loading.
This commit is contained in:
1 parent
40c5c466c1
commit
021e9935aa
7 files changed
+1275
-34
No files matched your search
@@ -40,6 +40,8 @@ CPStringEncodingErrorKey = @"CPStringEncodingErrorKey";
|
||||
CPURLErrorKey = kCFErrorURLKey;
|
||||
CPFilePathErrorKey = kCFErrorFilePathKey;
|
||||
|
||||
CPUserCancelledError = 3072;
|
||||
|
||||
/*!
|
||||
@class CPError
|
||||
@ingroup foundation
|
||||
|
||||
+221
-29
@@ -25,8 +25,10 @@
|
||||
@import "CPData.j"
|
||||
@import "CPDictionary.j"
|
||||
@import "CPError.j"
|
||||
@import "CPProgress.j"
|
||||
@import "CPRunLoop.j"
|
||||
@import "CPString.j"
|
||||
@import "CPString+Encoding.j"
|
||||
|
||||
@global FileReader
|
||||
|
||||
@@ -36,18 +38,35 @@ CPItemProviderUnknownError = -1;
|
||||
CPItemProviderItemUnavailableError = -1000;
|
||||
CPItemProviderUnexpectedValueClassError = -1100;
|
||||
|
||||
var CPItemProviderChunkSize = 32768;
|
||||
|
||||
/*
|
||||
Returns a binary string for an array buffer: one character for each byte, of the same value.
|
||||
The buffer is converted in chunks, because a very long argument list exceeds the JavaScript
|
||||
engine's limit.
|
||||
|
||||
CPData holds a binary string compactly, one byte for each character in current JavaScript
|
||||
engines. Its byte array would hold one JavaScript number for each byte, many times the size,
|
||||
because CFData stores bytes that way. File contents are given to CPData as a binary string.
|
||||
*/
|
||||
var CPItemProviderBinaryStringFromArrayBuffer = function(anArrayBuffer)
|
||||
{
|
||||
var bytes = new Uint8Array(anArrayBuffer),
|
||||
chunks = [];
|
||||
|
||||
for (var i = 0, count = bytes.length; i < count; i += CPItemProviderChunkSize)
|
||||
chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + CPItemProviderChunkSize)));
|
||||
|
||||
return chunks.join("");
|
||||
};
|
||||
|
||||
/*
|
||||
TODO: Gaps against NSItemProvider that can be filled:
|
||||
|
||||
- -loadDataRepresentationForTypeIdentifier:completionHandler: returns an NSProgress in Cocoa,
|
||||
which reports reading progress and cancels the load. Foundation has no CPProgress yet.
|
||||
Cancelling a file read would call FileReader's abort().
|
||||
- Object loading: -canLoadObjectOfClass:, -loadObjectOfClass:completionHandler:,
|
||||
-registerObject:visibility:, -initWithObject: and -initWithItem:typeIdentifier:, with the
|
||||
reading and writing protocols. Loading a CPString this way could decode the browser's
|
||||
buffer directly with TextDecoder.
|
||||
- Files are read into a plain JavaScript array with one number per byte, because CFData
|
||||
stores bytes that way. A large file needs many times its own size in memory.
|
||||
- Writing: -registerObject:visibility:, -registerObjectOfClass:visibility:loadHandler:,
|
||||
-initWithObject: and -initWithItem:typeIdentifier:, with a writing protocol.
|
||||
- Loading a large file still holds it several times over at its peak: the browser's
|
||||
buffer, the binary string in CPData, and the decoded text.
|
||||
*/
|
||||
|
||||
/*!
|
||||
@@ -73,15 +92,21 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
- Representations have no visibility. A page is one process, so Cocoa's visibility levels
|
||||
have no meaning.
|
||||
|
||||
Each load returns a CPProgress. For a file, it counts the bytes read. Cancelling it stops
|
||||
the read, and the completion handler then receives a CPUserCancelledError.
|
||||
|
||||
A completion handler receives the data, or nil and an error:
|
||||
|
||||
<pre>
|
||||
[anItemProvider loadDataRepresentationForTypeIdentifier:@"image/*" completionHandler:function(data, error)
|
||||
var progress = [anItemProvider loadDataRepresentationForTypeIdentifier:@"image/*" completionHandler:function(data, error)
|
||||
{
|
||||
if (data)
|
||||
[self useData:data];
|
||||
}];
|
||||
</pre>
|
||||
|
||||
An object of a class that adopts the item provider reading methods, such as CPString, can
|
||||
be loaded with -loadObjectOfClass:completionHandler:.
|
||||
*/
|
||||
@implementation CPItemProvider : CPObject
|
||||
{
|
||||
@@ -128,7 +153,7 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
[itemProvider setSuggestedName:aFile.webkitRelativePath || aFile.name];
|
||||
[itemProvider registerDataRepresentationForTypeIdentifier:typeIdentifier loadHandler:function(completionHandler)
|
||||
{
|
||||
[CPItemProvider _readFile:aFile completionHandler:completionHandler];
|
||||
return [CPItemProvider _readFile:aFile completionHandler:completionHandler];
|
||||
}];
|
||||
|
||||
return itemProvider;
|
||||
@@ -151,22 +176,37 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
|
||||
/*!
|
||||
Reads a browser File object with FileReader, and calls \c aCompletionHandler with its
|
||||
contents as data, or with nil and an error.
|
||||
contents as data, or with nil and an error. Returns a progress object that counts the bytes
|
||||
read. Cancelling it aborts the read.
|
||||
@ignore
|
||||
*/
|
||||
+ (void)_readFile:(File)aFile completionHandler:(Function)aCompletionHandler
|
||||
+ (CPProgress)_readFile:(File)aFile completionHandler:(Function)aCompletionHandler
|
||||
{
|
||||
var progress = [CPProgress discreteProgressWithTotalUnitCount:MAX(aFile.size || 0, 1)];
|
||||
|
||||
[progress setKind:CPProgressKindFile];
|
||||
[progress setFileOperationKind:CPProgressFileOperationKindReceiving];
|
||||
|
||||
if (typeof FileReader === "undefined")
|
||||
{
|
||||
aCompletionHandler(nil, [self _errorWithCode:CPItemProviderItemUnavailableError description:@"Files cannot be read in this environment."]);
|
||||
return;
|
||||
return progress;
|
||||
}
|
||||
|
||||
var reader = new FileReader();
|
||||
|
||||
reader.onprogress = function(anEvent)
|
||||
{
|
||||
if (anEvent.lengthComputable)
|
||||
[progress setCompletedUnitCount:MIN(anEvent.loaded, [progress totalUnitCount] - 1)];
|
||||
};
|
||||
|
||||
reader.onload = function()
|
||||
{
|
||||
aCompletionHandler([CPData dataWithBytes:Array.from(new Uint8Array(reader.result))], nil);
|
||||
var data = [CPData dataWithRawString:CPItemProviderBinaryStringFromArrayBuffer(reader.result)];
|
||||
|
||||
[progress setCompletedUnitCount:[progress totalUnitCount]];
|
||||
aCompletionHandler(data, nil);
|
||||
};
|
||||
|
||||
reader.onerror = function()
|
||||
@@ -179,7 +219,20 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
aCompletionHandler(nil, [self _errorWithCode:CPItemProviderUnknownError description:description]);
|
||||
};
|
||||
|
||||
reader.onabort = function()
|
||||
{
|
||||
aCompletionHandler(nil, [self _cancelledError]);
|
||||
};
|
||||
|
||||
[progress setCancellationHandler:function()
|
||||
{
|
||||
if (reader.readyState === 1)
|
||||
reader.abort();
|
||||
}];
|
||||
|
||||
reader.readAsArrayBuffer(aFile);
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -192,6 +245,16 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
userInfo:@{ CPLocalizedDescriptionKey: aDescription }];
|
||||
}
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
*/
|
||||
+ (CPError)_cancelledError
|
||||
{
|
||||
return [CPError errorWithDomain:CPCocoaErrorDomain
|
||||
code:CPUserCancelledError
|
||||
userInfo:@{ CPLocalizedDescriptionKey: @"The operation was cancelled." }];
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes an item provider with no representations.
|
||||
*/
|
||||
@@ -215,10 +278,12 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
|
||||
The load handler is called with one argument, a completion handler. It must call that
|
||||
completion handler once, with the data, or with nil and an error. It may do so at once, or
|
||||
later.
|
||||
later. It may return a CPProgress for its work; the progress object returned by
|
||||
-loadDataRepresentationForTypeIdentifier:completionHandler: then follows it, and cancelling
|
||||
that progress object cancels it.
|
||||
|
||||
@param aTypeIdentifier a MIME type, such as "text/csv"
|
||||
@param aLoadHandler a function that takes a completion handler
|
||||
@param aLoadHandler a function that takes a completion handler, and may return a CPProgress
|
||||
*/
|
||||
- (void)registerDataRepresentationForTypeIdentifier:(CPString)aTypeIdentifier loadHandler:(Function /*(Function completionHandler)*/)aLoadHandler
|
||||
{
|
||||
@@ -276,14 +341,17 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
/*!
|
||||
Loads the first registered representation that matches \c aTypeIdentifier, which may be a
|
||||
wildcard of the form "image/*". The completion handler is called once, inside a run loop
|
||||
pass, with the data and nil, or with nil and an error.
|
||||
pass, with the data and nil, or with nil and an error. When the returned progress object is
|
||||
cancelled first, the error is a CPUserCancelledError.
|
||||
|
||||
@param aTypeIdentifier the type identifier to load
|
||||
@param aCompletionHandler a function that takes the data and an error
|
||||
@return a progress object for the load
|
||||
*/
|
||||
- (void)loadDataRepresentationForTypeIdentifier:(CPString)aTypeIdentifier completionHandler:(Function /*(CPData data, CPError error)*/)aCompletionHandler
|
||||
- (CPProgress)loadDataRepresentationForTypeIdentifier:(CPString)aTypeIdentifier completionHandler:(Function /*(CPData data, CPError error)*/)aCompletionHandler
|
||||
{
|
||||
var registeredTypeIdentifier = [self _registeredTypeIdentifierConformingToTypeIdentifier:aTypeIdentifier],
|
||||
progress = [CPProgress discreteProgressWithTotalUnitCount:1],
|
||||
didComplete = NO,
|
||||
complete = function(data, error)
|
||||
{
|
||||
@@ -292,8 +360,19 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
|
||||
didComplete = YES;
|
||||
|
||||
if ([progress isCancelled])
|
||||
{
|
||||
data = nil;
|
||||
error = [CPItemProvider _cancelledError];
|
||||
}
|
||||
else if (!data && !error)
|
||||
error = [CPItemProvider _errorWithCode:CPItemProviderUnknownError description:@"The item could not be loaded."];
|
||||
|
||||
if (data && ![progress isFinished])
|
||||
[progress setCompletedUnitCount:1];
|
||||
|
||||
if (aCompletionHandler)
|
||||
aCompletionHandler(data || nil, data ? nil : error || [CPItemProvider _errorWithCode:CPItemProviderUnknownError description:@"The item could not be loaded."]);
|
||||
aCompletionHandler(data || nil, data ? nil : error);
|
||||
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
|
||||
};
|
||||
@@ -301,21 +380,94 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
if (!registeredTypeIdentifier)
|
||||
{
|
||||
complete(nil, [CPItemProvider _errorWithCode:CPItemProviderItemUnavailableError description:@"The item has no representation of type " + aTypeIdentifier + @"."]);
|
||||
return;
|
||||
return progress;
|
||||
}
|
||||
|
||||
var loadHandler = [_loadHandlers objectForKey:registeredTypeIdentifier];
|
||||
|
||||
loadHandler(function(data, error)
|
||||
{
|
||||
if (data && ![data isKindOfClass:[CPData class]])
|
||||
var loadHandler = [_loadHandlers objectForKey:registeredTypeIdentifier],
|
||||
loadProgress = loadHandler(function(data, error)
|
||||
{
|
||||
complete(nil, [CPItemProvider _errorWithCode:CPItemProviderUnexpectedValueClassError description:@"The item's load handler returned an object that is not data."]);
|
||||
if (data && ![data isKindOfClass:[CPData class]])
|
||||
{
|
||||
complete(nil, [CPItemProvider _errorWithCode:CPItemProviderUnexpectedValueClassError description:@"The item's load handler returned an object that is not data."]);
|
||||
return;
|
||||
}
|
||||
|
||||
complete(data, error);
|
||||
});
|
||||
|
||||
if (loadProgress && [loadProgress isKindOfClass:[CPProgress class]] && !didComplete)
|
||||
[progress addChild:loadProgress withPendingUnitCount:1];
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
// MARK: Loading Objects
|
||||
|
||||
/*!
|
||||
Returns YES if an object of \c aClass can be loaded from the item. \c aClass must implement
|
||||
+readableTypeIdentifiersForItemProvider, and the item must have a representation of one of
|
||||
those types.
|
||||
*/
|
||||
- (BOOL)canLoadObjectOfClass:(Class)aClass
|
||||
{
|
||||
return [self _typeIdentifierReadableByClass:aClass] !== nil;
|
||||
}
|
||||
|
||||
/*!
|
||||
Loads an object of \c aClass from the item. The first representation of a type that the
|
||||
class can read is loaded, and given to +objectWithItemProviderData:typeIdentifier:error:.
|
||||
The completion handler is called once, inside a run loop pass, with the object and nil, or
|
||||
with nil and an error.
|
||||
|
||||
@param aClass a class that implements the item provider reading methods, such as CPString
|
||||
@param aCompletionHandler a function that takes the object and an error
|
||||
@return a progress object for the load
|
||||
*/
|
||||
- (CPProgress)loadObjectOfClass:(Class)aClass completionHandler:(Function /*(id object, CPError error)*/)aCompletionHandler
|
||||
{
|
||||
var typeIdentifier = [self _typeIdentifierReadableByClass:aClass];
|
||||
|
||||
if (!typeIdentifier)
|
||||
return [self loadDataRepresentationForTypeIdentifier:@"" completionHandler:function(data, error)
|
||||
{
|
||||
aCompletionHandler(nil, [CPItemProvider _errorWithCode:CPItemProviderItemUnavailableError description:@"The item has no representation that " + [aClass className] + @" can read."]);
|
||||
}];
|
||||
|
||||
return [self loadDataRepresentationForTypeIdentifier:typeIdentifier completionHandler:function(data, error)
|
||||
{
|
||||
if (!data)
|
||||
{
|
||||
aCompletionHandler(nil, error);
|
||||
return;
|
||||
}
|
||||
|
||||
complete(data, error);
|
||||
});
|
||||
var readError = nil,
|
||||
object = [aClass objectWithItemProviderData:data typeIdentifier:typeIdentifier error:@ref(readError)];
|
||||
|
||||
if (object === nil || object === undefined)
|
||||
aCompletionHandler(nil, readError || [CPItemProvider _errorWithCode:CPItemProviderUnexpectedValueClassError description:@"The item could not be read as " + [aClass className] + @"."]);
|
||||
else
|
||||
aCompletionHandler(object, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the first registered type identifier that \c aClass can read, or nil.
|
||||
@ignore
|
||||
*/
|
||||
- (CPString)_typeIdentifierReadableByClass:(Class)aClass
|
||||
{
|
||||
if (![aClass respondsToSelector:@selector(readableTypeIdentifiersForItemProvider)])
|
||||
return nil;
|
||||
|
||||
var readableTypeIdentifiers = [aClass readableTypeIdentifiersForItemProvider];
|
||||
|
||||
for (var i = 0, count = [_registeredTypeIdentifiers count]; i < count; i++)
|
||||
for (var j = 0, readableCount = [readableTypeIdentifiers count]; j < readableCount; j++)
|
||||
if ([CPItemProvider _typeIdentifier:_registeredTypeIdentifiers[i] conformsToTypeIdentifier:readableTypeIdentifiers[j]])
|
||||
return _registeredTypeIdentifiers[i];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -327,3 +479,43 @@ CPItemProviderUnexpectedValueClassError = -1100;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*!
|
||||
CPString reads text from an item provider. The encoding is detected, as by
|
||||
+stringEncodingForData:encodingOptions:convertedString:usedLossyConversion:.
|
||||
|
||||
Browsers report some text formats under other MIME types; for example, a CSV file may be
|
||||
reported as "application/vnd.ms-excel" on Windows. Such an item is not readable as a string
|
||||
by this method, but its data can be loaded and decoded directly.
|
||||
*/
|
||||
@implementation CPString (CPItemProvider)
|
||||
|
||||
/*!
|
||||
Returns the MIME types a string can be read from: every text type, JSON, XML and JavaScript.
|
||||
*/
|
||||
+ (CPArray)readableTypeIdentifiersForItemProvider
|
||||
{
|
||||
return [@"text/*", @"application/json", @"application/xml", @"application/javascript"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns a string decoded from an item provider's data, or nil with an error when no
|
||||
encoding fits the data.
|
||||
*/
|
||||
+ (id)objectWithItemProviderData:(CPData)someData typeIdentifier:(CPString)aTypeIdentifier error:(CPErrorRef)anErrorRef
|
||||
{
|
||||
var string = nil,
|
||||
encoding = [CPString stringEncodingForData:someData encodingOptions:nil convertedString:@ref(string) usedLossyConversion:nil];
|
||||
|
||||
if (encoding)
|
||||
return string;
|
||||
|
||||
if (anErrorRef)
|
||||
@deref(anErrorRef) = [CPError errorWithDomain:CPItemProviderErrorDomain
|
||||
code:CPItemProviderUnexpectedValueClassError
|
||||
userInfo:@{ CPLocalizedDescriptionKey: @"The item's data is not text in a known encoding." }];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,683 @@
|
||||
/*
|
||||
* CPProgress.j
|
||||
* Foundation
|
||||
*
|
||||
* Created by David Richardson on 2026-11-02.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* 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 "CPArray.j"
|
||||
@import "CPDictionary.j"
|
||||
@import "CPException.j"
|
||||
@import "CPKeyValueObserving.j"
|
||||
@import "CPObject.j"
|
||||
@import "CPSet.j"
|
||||
@import "CPString.j"
|
||||
|
||||
CPProgressKindFile = @"CPProgressKindFile";
|
||||
|
||||
CPProgressEstimatedTimeRemainingKey = @"CPProgressEstimatedTimeRemainingKey";
|
||||
CPProgressThroughputKey = @"CPProgressThroughputKey";
|
||||
CPProgressFileOperationKindKey = @"CPProgressFileOperationKindKey";
|
||||
CPProgressFileURLKey = @"CPProgressFileURLKey";
|
||||
CPProgressFileTotalCountKey = @"CPProgressFileTotalCountKey";
|
||||
CPProgressFileCompletedCountKey = @"CPProgressFileCompletedCountKey";
|
||||
|
||||
CPProgressFileOperationKindDownloading = @"CPProgressFileOperationKindDownloading";
|
||||
CPProgressFileOperationKindDecompressingAfterDownloading = @"CPProgressFileOperationKindDecompressingAfterDownloading";
|
||||
CPProgressFileOperationKindReceiving = @"CPProgressFileOperationKindReceiving";
|
||||
CPProgressFileOperationKindCopying = @"CPProgressFileOperationKindCopying";
|
||||
CPProgressFileOperationKindUploading = @"CPProgressFileOperationKindUploading";
|
||||
CPProgressFileOperationKindDuplicating = @"CPProgressFileOperationKindDuplicating";
|
||||
|
||||
/*
|
||||
The keys whose values follow from the unit counts of a progress object and of its children.
|
||||
They change together, so they are announced together. See -_performCountChange:.
|
||||
*/
|
||||
var CPProgressCountKeys = [@"totalUnitCount", @"completedUnitCount", @"fractionCompleted", @"finished", @"indeterminate", @"localizedDescription", @"localizedAdditionalDescription"],
|
||||
CPProgressManuallyNotifiedKeys = [CPSet setWithArray:CPProgressCountKeys.concat([@"cancelled", @"paused", @"cancellable", @"pausable", @"kind", @"userInfo"])],
|
||||
CPProgressCurrentStack = [];
|
||||
|
||||
/*
|
||||
TODO: Gaps against NSProgress that can be filled:
|
||||
|
||||
- The fileURL, fileTotalCount and fileCompletedCount properties. Only their user
|
||||
information keys exist.
|
||||
- The NSProgressReporting protocol, with its progress property.
|
||||
- The default descriptions. Cocoa's depend on the kind, and its additional description
|
||||
includes the estimated time remaining and the throughput. These are the percentage and
|
||||
the unit counts only.
|
||||
- The file icon and file animation user information keys.
|
||||
- Setting a cancellation or pausing handler on a progress object that is already cancelled
|
||||
or paused. Check whether Cocoa calls the handler at once; this class does not.
|
||||
- Key-value observing. Cocoa announces only the keys that change; this class announces
|
||||
every unit-count key of each progress object in the chain.
|
||||
*/
|
||||
|
||||
/*!
|
||||
@class CPProgress
|
||||
@ingroup foundation
|
||||
@brief Reports the progress of a task, and lets the user cancel or pause it.
|
||||
|
||||
This class is modelled on NSProgress. A progress object counts units of work: a total, and
|
||||
the units completed. Its fraction completed can be observed with key-value observing, and
|
||||
shown in a progress indicator.
|
||||
|
||||
Progress objects form a tree. A child progress object stands for a number of its parent's
|
||||
units, its pending unit count. As the child advances, the parent's fraction completed
|
||||
advances in proportion. When the child finishes, its pending units are added to the
|
||||
parent's completed units. A child is attached explicitly, with
|
||||
-addChild:withPendingUnitCount:, or implicitly: a progress object made while its parent is
|
||||
the current progress object, between -becomeCurrentWithPendingUnitCount: and
|
||||
-resignCurrent, becomes its child.
|
||||
|
||||
Cancelling or pausing a progress object also cancels or pauses its children, and calls its
|
||||
handler. A task checks -isCancelled, or sets a cancellation handler, to stop its work.
|
||||
|
||||
In a browser there is one thread, so handlers are called at once, on that thread. The
|
||||
unit-count keys are announced together whenever a count changes in the tree, so an
|
||||
observer may receive a notification for a key whose value did not change.
|
||||
|
||||
Publishing and subscribing (-publish, -unpublish, the subscriber methods and -isOld) are
|
||||
not provided. They share file progress between processes, and a page is one process.
|
||||
*/
|
||||
@implementation CPProgress : CPObject
|
||||
{
|
||||
CPProgress _parent;
|
||||
CPArray _children;
|
||||
|
||||
int _totalUnitCount;
|
||||
int _completedUnitCount;
|
||||
|
||||
BOOL _cancellable;
|
||||
BOOL _pausable;
|
||||
BOOL _cancelled;
|
||||
BOOL _paused;
|
||||
|
||||
Function _cancellationHandler @accessors(property=cancellationHandler);
|
||||
Function _pausingHandler @accessors(property=pausingHandler);
|
||||
Function _resumingHandler @accessors(property=resumingHandler);
|
||||
|
||||
CPString _localizedDescription;
|
||||
CPString _localizedAdditionalDescription;
|
||||
CPString _kind;
|
||||
CPMutableDictionary _userInfo;
|
||||
}
|
||||
|
||||
+ (BOOL)automaticallyNotifiesObserversForKey:(CPString)aKey
|
||||
{
|
||||
if ([CPProgressManuallyNotifiedKeys containsObject:aKey])
|
||||
return NO;
|
||||
|
||||
return [super automaticallyNotifiesObserversForKey:aKey];
|
||||
}
|
||||
|
||||
// MARK: Creating Progress Objects
|
||||
|
||||
/*!
|
||||
Returns the progress object that is current, or nil. See -becomeCurrentWithPendingUnitCount:.
|
||||
*/
|
||||
+ (CPProgress)currentProgress
|
||||
{
|
||||
var entry = CPProgressCurrentStack[CPProgressCurrentStack.length - 1];
|
||||
|
||||
return entry ? entry.progress : nil;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns a progress object with a total unit count. If a progress object is current, the new
|
||||
one becomes its child.
|
||||
*/
|
||||
+ (CPProgress)progressWithTotalUnitCount:(int)aCount
|
||||
{
|
||||
var progress = [[self alloc] initWithParent:[self currentProgress] userInfo:nil];
|
||||
|
||||
[progress setTotalUnitCount:aCount];
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns a progress object with a total unit count and no parent, even if a progress object
|
||||
is current.
|
||||
*/
|
||||
+ (CPProgress)discreteProgressWithTotalUnitCount:(int)aCount
|
||||
{
|
||||
var progress = [[self alloc] initWithParent:nil userInfo:nil];
|
||||
|
||||
[progress setTotalUnitCount:aCount];
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns a progress object with a total unit count, attached to \c aParent as a child that
|
||||
stands for \c aPendingUnitCount of its units.
|
||||
*/
|
||||
+ (CPProgress)progressWithTotalUnitCount:(int)aCount parent:(CPProgress)aParent pendingUnitCount:(int)aPendingUnitCount
|
||||
{
|
||||
var progress = [self discreteProgressWithTotalUnitCount:aCount];
|
||||
|
||||
[aParent addChild:progress withPendingUnitCount:aPendingUnitCount];
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes a progress object. If a progress object is current, the new one becomes its
|
||||
child.
|
||||
*/
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithParent:[CPProgress currentProgress] userInfo:nil];
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes a progress object. \c aParent must be nil or the current progress object; the
|
||||
new progress object becomes its child, standing for the pending unit count given to
|
||||
-becomeCurrentWithPendingUnitCount:. Only the first progress object made while a parent is
|
||||
current becomes its child.
|
||||
|
||||
@throws CPInvalidArgumentException if \c aParent is not nil and not current
|
||||
*/
|
||||
- (id)initWithParent:(CPProgress)aParent userInfo:(CPDictionary)aUserInfo
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_children = [];
|
||||
_totalUnitCount = 0;
|
||||
_completedUnitCount = 0;
|
||||
_cancellable = YES;
|
||||
_pausable = NO;
|
||||
_cancelled = NO;
|
||||
_paused = NO;
|
||||
_userInfo = aUserInfo ? [aUserInfo mutableCopy] : [CPMutableDictionary dictionary];
|
||||
|
||||
if (aParent)
|
||||
{
|
||||
var entry = CPProgressCurrentStack[CPProgressCurrentStack.length - 1];
|
||||
|
||||
if (!entry || entry.progress !== aParent)
|
||||
[CPException raise:CPInvalidArgumentException reason:@"The parent of a progress object must be nil or the current progress object."];
|
||||
|
||||
if (!entry.hasAttachedChild)
|
||||
{
|
||||
entry.hasAttachedChild = YES;
|
||||
[aParent addChild:self withPendingUnitCount:entry.pendingUnitCount];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: The Current Progress Object
|
||||
|
||||
/*!
|
||||
Makes the receiver the current progress object. The first progress object made before
|
||||
-resignCurrent becomes its child, standing for \c aPendingUnitCount of its units.
|
||||
*/
|
||||
- (void)becomeCurrentWithPendingUnitCount:(int)aPendingUnitCount
|
||||
{
|
||||
CPProgressCurrentStack.push({ progress: self, pendingUnitCount: aPendingUnitCount, hasAttachedChild: NO });
|
||||
}
|
||||
|
||||
/*!
|
||||
Ends the receiver's time as the current progress object. If no child was made meanwhile,
|
||||
the pending units are completed.
|
||||
|
||||
@throws CPInternalInconsistencyException if the receiver is not current
|
||||
*/
|
||||
- (void)resignCurrent
|
||||
{
|
||||
var entry = CPProgressCurrentStack[CPProgressCurrentStack.length - 1];
|
||||
|
||||
if (!entry || entry.progress !== self)
|
||||
[CPException raise:CPInternalInconsistencyException reason:@"-resignCurrent was sent to a progress object that is not current."];
|
||||
|
||||
CPProgressCurrentStack.pop();
|
||||
|
||||
if (!entry.hasAttachedChild)
|
||||
[self setCompletedUnitCount:_completedUnitCount + entry.pendingUnitCount];
|
||||
}
|
||||
|
||||
/*!
|
||||
Makes the receiver current, calls \c aBlock, and resigns.
|
||||
*/
|
||||
- (void)performAsCurrentWithPendingUnitCount:(int)aPendingUnitCount usingBlock:(Function)aBlock
|
||||
{
|
||||
[self becomeCurrentWithPendingUnitCount:aPendingUnitCount];
|
||||
|
||||
try
|
||||
{
|
||||
aBlock();
|
||||
}
|
||||
finally
|
||||
{
|
||||
[self resignCurrent];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Children
|
||||
|
||||
/*!
|
||||
Attaches \c aChild, which stands for \c aPendingUnitCount of the receiver's units. A child
|
||||
attached to a cancelled or paused progress object is cancelled or paused at once.
|
||||
|
||||
@throws CPInvalidArgumentException if \c aChild already has a parent
|
||||
*/
|
||||
- (void)addChild:(CPProgress)aChild withPendingUnitCount:(int)aPendingUnitCount
|
||||
{
|
||||
if (aChild._parent)
|
||||
[CPException raise:CPInvalidArgumentException reason:@"A progress object can have only one parent."];
|
||||
|
||||
[self _performCountChange:function()
|
||||
{
|
||||
aChild._parent = self;
|
||||
_children.push({ progress: aChild, pendingUnitCount: aPendingUnitCount });
|
||||
[aChild _finishIfComplete];
|
||||
}];
|
||||
|
||||
if (_cancelled)
|
||||
[aChild cancel];
|
||||
|
||||
if (_paused)
|
||||
[aChild pause];
|
||||
}
|
||||
|
||||
// MARK: Unit Counts
|
||||
|
||||
/*!
|
||||
Returns the total number of units of work.
|
||||
*/
|
||||
- (int)totalUnitCount
|
||||
{
|
||||
return _totalUnitCount;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the total number of units of work. A negative total makes the progress indeterminate.
|
||||
*/
|
||||
- (void)setTotalUnitCount:(int)aCount
|
||||
{
|
||||
if (aCount === _totalUnitCount)
|
||||
return;
|
||||
|
||||
[self _performCountChange:function()
|
||||
{
|
||||
_totalUnitCount = aCount;
|
||||
[self _finishIfComplete];
|
||||
}];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the number of units of work completed.
|
||||
*/
|
||||
- (int)completedUnitCount
|
||||
{
|
||||
return _completedUnitCount;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the number of units of work completed. When it reaches the total, a parent completes
|
||||
the units this progress object stands for.
|
||||
*/
|
||||
- (void)setCompletedUnitCount:(int)aCount
|
||||
{
|
||||
if (aCount === _completedUnitCount)
|
||||
return;
|
||||
|
||||
[self _performCountChange:function()
|
||||
{
|
||||
_completedUnitCount = aCount;
|
||||
[self _finishIfComplete];
|
||||
}];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the fraction of the work completed, including the progress of children that have
|
||||
not finished, from 0.0. It is 0.0 while the progress is indeterminate.
|
||||
*/
|
||||
- (double)fractionCompleted
|
||||
{
|
||||
if (_totalUnitCount <= 0)
|
||||
return 0.0;
|
||||
|
||||
var units = _completedUnitCount;
|
||||
|
||||
for (var i = 0; i < _children.length; i++)
|
||||
units += [_children[i].progress fractionCompleted] * _children[i].pendingUnitCount;
|
||||
|
||||
return units / _totalUnitCount;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns YES when the work is complete: the total is positive, and the completed units have
|
||||
reached it.
|
||||
*/
|
||||
- (BOOL)isFinished
|
||||
{
|
||||
return _totalUnitCount > 0 && _completedUnitCount >= _totalUnitCount;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns YES when the amount of work is unknown: either count is negative, or both are 0.
|
||||
*/
|
||||
- (BOOL)isIndeterminate
|
||||
{
|
||||
return _totalUnitCount < 0 || _completedUnitCount < 0 || (_totalUnitCount === 0 && _completedUnitCount === 0);
|
||||
}
|
||||
|
||||
/*
|
||||
Changes unit counts in the tree. Every count key of the receiver and of each of its
|
||||
ancestors is announced before and after the change, so that an observer of any of them sees
|
||||
a consistent value. A parent's counts and fraction depend on its children, so a change
|
||||
anywhere below reaches every ancestor.
|
||||
*/
|
||||
- (void)_performCountChange:(Function)aChange
|
||||
{
|
||||
var chain = [];
|
||||
|
||||
for (var progress = self; progress; progress = progress._parent)
|
||||
chain.push(progress);
|
||||
|
||||
for (var i = 0; i < chain.length; i++)
|
||||
for (var j = 0; j < CPProgressCountKeys.length; j++)
|
||||
[chain[i] willChangeValueForKey:CPProgressCountKeys[j]];
|
||||
|
||||
aChange();
|
||||
|
||||
for (var i = chain.length - 1; i >= 0; i--)
|
||||
for (var j = CPProgressCountKeys.length - 1; j >= 0; j--)
|
||||
[chain[i] didChangeValueForKey:CPProgressCountKeys[j]];
|
||||
}
|
||||
|
||||
/*
|
||||
When the receiver has finished, its parent removes it and completes the units it stood for.
|
||||
That may finish the parent in turn. Called inside -_performCountChange:, which has already
|
||||
announced the change for every ancestor.
|
||||
*/
|
||||
- (void)_finishIfComplete
|
||||
{
|
||||
var parent = _parent;
|
||||
|
||||
if (![self isFinished] || !parent)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < parent._children.length; i++)
|
||||
{
|
||||
var entry = parent._children[i];
|
||||
|
||||
if (entry.progress !== self)
|
||||
continue;
|
||||
|
||||
parent._children.splice(i, 1);
|
||||
parent._completedUnitCount += entry.pendingUnitCount;
|
||||
_parent = nil;
|
||||
|
||||
[parent _finishIfComplete];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Cancelling and Pausing
|
||||
|
||||
/*!
|
||||
Returns YES if the user may cancel the work. This is a hint for a user interface: -cancel
|
||||
works either way. The default is YES.
|
||||
*/
|
||||
- (BOOL)isCancellable
|
||||
{
|
||||
return _cancellable;
|
||||
}
|
||||
|
||||
- (void)setCancellable:(BOOL)isCancellable
|
||||
{
|
||||
[self _setValue:!!isCancellable forIvar:@"_cancellable" key:@"cancellable"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns YES if the user may pause the work. This is a hint for a user interface. The
|
||||
default is NO.
|
||||
*/
|
||||
- (BOOL)isPausable
|
||||
{
|
||||
return _pausable;
|
||||
}
|
||||
|
||||
- (void)setPausable:(BOOL)isPausable
|
||||
{
|
||||
[self _setValue:!!isPausable forIvar:@"_pausable" key:@"pausable"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns YES once the work has been cancelled.
|
||||
*/
|
||||
- (BOOL)isCancelled
|
||||
{
|
||||
return _cancelled;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns YES while the work is paused.
|
||||
*/
|
||||
- (BOOL)isPaused
|
||||
{
|
||||
return _paused;
|
||||
}
|
||||
|
||||
/*!
|
||||
Cancels the work: marks the receiver cancelled, calls its cancellation handler, and cancels
|
||||
its children. Cancelling again does nothing.
|
||||
*/
|
||||
- (void)cancel
|
||||
{
|
||||
if (_cancelled)
|
||||
return;
|
||||
|
||||
[self _setValue:YES forIvar:@"_cancelled" key:@"cancelled"];
|
||||
|
||||
if (_cancellationHandler)
|
||||
_cancellationHandler();
|
||||
|
||||
var children = _children.slice();
|
||||
|
||||
for (var i = 0; i < children.length; i++)
|
||||
[children[i].progress cancel];
|
||||
}
|
||||
|
||||
/*!
|
||||
Pauses the work: marks the receiver paused, calls its pausing handler, and pauses its
|
||||
children.
|
||||
*/
|
||||
- (void)pause
|
||||
{
|
||||
if (_paused)
|
||||
return;
|
||||
|
||||
[self _setValue:YES forIvar:@"_paused" key:@"paused"];
|
||||
|
||||
if (_pausingHandler)
|
||||
_pausingHandler();
|
||||
|
||||
var children = _children.slice();
|
||||
|
||||
for (var i = 0; i < children.length; i++)
|
||||
[children[i].progress pause];
|
||||
}
|
||||
|
||||
/*!
|
||||
Resumes paused work: marks the receiver not paused, calls its resuming handler, and resumes
|
||||
its children.
|
||||
*/
|
||||
- (void)resume
|
||||
{
|
||||
if (!_paused)
|
||||
return;
|
||||
|
||||
[self _setValue:NO forIvar:@"_paused" key:@"paused"];
|
||||
|
||||
if (_resumingHandler)
|
||||
_resumingHandler();
|
||||
|
||||
var children = _children.slice();
|
||||
|
||||
for (var i = 0; i < children.length; i++)
|
||||
[children[i].progress resume];
|
||||
}
|
||||
|
||||
// MARK: Descriptions
|
||||
|
||||
/*!
|
||||
Returns a description of the progress. Unless one has been set, it is the percentage
|
||||
completed, such as "42% completed".
|
||||
*/
|
||||
- (CPString)localizedDescription
|
||||
{
|
||||
if (_localizedDescription !== nil && _localizedDescription !== undefined)
|
||||
return _localizedDescription;
|
||||
|
||||
return ROUND([self fractionCompleted] * 100) + @"% completed";
|
||||
}
|
||||
|
||||
- (void)setLocalizedDescription:(CPString)aDescription
|
||||
{
|
||||
[self _setValue:aDescription forIvar:@"_localizedDescription" key:@"localizedDescription"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns more detail about the progress. Unless one has been set, it is the units completed
|
||||
and the total, such as "42 of 100".
|
||||
*/
|
||||
- (CPString)localizedAdditionalDescription
|
||||
{
|
||||
if (_localizedAdditionalDescription !== nil && _localizedAdditionalDescription !== undefined)
|
||||
return _localizedAdditionalDescription;
|
||||
|
||||
if ([self isIndeterminate])
|
||||
return @"";
|
||||
|
||||
return _completedUnitCount + @" of " + _totalUnitCount;
|
||||
}
|
||||
|
||||
- (void)setLocalizedAdditionalDescription:(CPString)aDescription
|
||||
{
|
||||
[self _setValue:aDescription forIvar:@"_localizedAdditionalDescription" key:@"localizedAdditionalDescription"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the kind of progress, such as CPProgressKindFile, or nil.
|
||||
*/
|
||||
- (CPString)kind
|
||||
{
|
||||
return _kind;
|
||||
}
|
||||
|
||||
- (void)setKind:(CPString)aKind
|
||||
{
|
||||
[self _setValue:aKind forIvar:@"_kind" key:@"kind"];
|
||||
}
|
||||
|
||||
// MARK: User Information
|
||||
|
||||
/*!
|
||||
Returns a copy of the receiver's user information.
|
||||
*/
|
||||
- (CPDictionary)userInfo
|
||||
{
|
||||
return [_userInfo copy];
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets a value in the receiver's user information, or removes it when \c anObject is nil.
|
||||
*/
|
||||
- (void)setUserInfoObject:(id)anObject forKey:(CPString)aKey
|
||||
{
|
||||
[self willChangeValueForKey:@"userInfo"];
|
||||
|
||||
if (anObject === nil || anObject === undefined)
|
||||
[_userInfo removeObjectForKey:aKey];
|
||||
else
|
||||
[_userInfo setObject:anObject forKey:aKey];
|
||||
|
||||
[self didChangeValueForKey:@"userInfo"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the estimated time remaining, in seconds, or nil. It is stored in the user
|
||||
information under CPProgressEstimatedTimeRemainingKey.
|
||||
*/
|
||||
- (CPNumber)estimatedTimeRemaining
|
||||
{
|
||||
return [_userInfo objectForKey:CPProgressEstimatedTimeRemainingKey];
|
||||
}
|
||||
|
||||
- (void)setEstimatedTimeRemaining:(CPNumber)aTime
|
||||
{
|
||||
[self setUserInfoObject:aTime forKey:CPProgressEstimatedTimeRemainingKey];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the throughput, in units per second, or nil. It is stored in the user information
|
||||
under CPProgressThroughputKey.
|
||||
*/
|
||||
- (CPNumber)throughput
|
||||
{
|
||||
return [_userInfo objectForKey:CPProgressThroughputKey];
|
||||
}
|
||||
|
||||
- (void)setThroughput:(CPNumber)aThroughput
|
||||
{
|
||||
[self setUserInfoObject:aThroughput forKey:CPProgressThroughputKey];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the kind of file operation, such as CPProgressFileOperationKindDownloading, or nil.
|
||||
It is stored in the user information under CPProgressFileOperationKindKey.
|
||||
*/
|
||||
- (CPString)fileOperationKind
|
||||
{
|
||||
return [_userInfo objectForKey:CPProgressFileOperationKindKey];
|
||||
}
|
||||
|
||||
- (void)setFileOperationKind:(CPString)aKind
|
||||
{
|
||||
[self setUserInfoObject:aKind forKey:CPProgressFileOperationKindKey];
|
||||
}
|
||||
|
||||
/*
|
||||
Sets an instance variable and announces the change of its key, when the value changes.
|
||||
*/
|
||||
- (void)_setValue:(id)aValue forIvar:(CPString)anIvarName key:(CPString)aKey
|
||||
{
|
||||
if (self[anIvarName] === aValue)
|
||||
return;
|
||||
|
||||
[self willChangeValueForKey:aKey];
|
||||
self[anIvarName] = aValue;
|
||||
[self didChangeValueForKey:aKey];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns a description of the progress object: its counts and fraction completed.
|
||||
*/
|
||||
- (CPString)description
|
||||
{
|
||||
return [super description] + @" " + _completedUnitCount + @"/" + _totalUnitCount + @" (" + [self fractionCompleted] + @")" + (_cancelled ? @" cancelled" : @"") + (_paused ? @" paused" : @"");
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -117,7 +117,7 @@ var CPStringEncodingDecodeWithTextDecoder = function(aLabel, bytes, isLossy)
|
||||
|
||||
try
|
||||
{
|
||||
return new TextDecoder(aLabel, { fatal: !isLossy }).decode(new Uint8Array(bytes));
|
||||
return new TextDecoder(aLabel, { fatal: !isLossy }).decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
@@ -142,7 +142,7 @@ var CPStringEncodingDecodeASCII = function(bytes, isLossy)
|
||||
if (!isLossy)
|
||||
return nil;
|
||||
|
||||
bytes = bytes.map(function(aByte) { return aByte > 0x7F ? CPStringEncodingReplacementCharacter : aByte; });
|
||||
bytes = Array.prototype.map.call(bytes, function(aByte) { return aByte > 0x7F ? CPStringEncodingReplacementCharacter : aByte; });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -705,6 +705,38 @@ var CPStringEncodingIsAvailable = function(aDescriptor)
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Returns the bytes of data as an array of bytes. Data that holds a binary string, such as
|
||||
file contents from CPItemProvider, is converted to a Uint8Array, which takes one byte for
|
||||
each byte. Asking CPData for its bytes would build a plain array of JavaScript numbers, many
|
||||
times the size, because CFData stores bytes that way, and keep it in the data.
|
||||
*/
|
||||
var CPStringEncodingBytesOfData = function(someData)
|
||||
{
|
||||
if (!someData)
|
||||
return [];
|
||||
|
||||
if (someData._bytes)
|
||||
return someData._bytes;
|
||||
|
||||
var string = someData.rawString(),
|
||||
count = string.length,
|
||||
bytes = new Uint8Array(count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
bytes[i] = string.charCodeAt(i) & 0xFF;
|
||||
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/*
|
||||
Returns the bytes from anIndex to the end, without copying when the bytes are a Uint8Array.
|
||||
*/
|
||||
var CPStringEncodingBytesFromIndex = function(bytes, anIndex)
|
||||
{
|
||||
return bytes.subarray ? bytes.subarray(anIndex) : bytes.slice(anIndex);
|
||||
};
|
||||
|
||||
var CPStringEncodingDecode = function(bytes, anEncoding, isLossy)
|
||||
{
|
||||
var descriptor = CPStringEncodingDescriptors[anEncoding];
|
||||
@@ -807,7 +839,12 @@ var CPStringEncodingDecode = function(bytes, anEncoding, isLossy)
|
||||
*/
|
||||
- (id)initWithData:(CPData)someData encoding:(CPStringEncoding)anEncoding
|
||||
{
|
||||
return [self initWithBytes:someData ? [someData bytes] : [] length:someData ? [someData length] : 0 encoding:anEncoding];
|
||||
var decoded = CPStringEncodingDecode(CPStringEncodingBytesOfData(someData), anEncoding, NO);
|
||||
|
||||
if (decoded === nil)
|
||||
return nil;
|
||||
|
||||
return [self initWithString:decoded];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -856,7 +893,7 @@ var CPStringEncodingDecode = function(bytes, anEncoding, isLossy)
|
||||
*/
|
||||
+ (CPStringEncoding)stringEncodingForData:(CPData)someData encodingOptions:(CPDictionary)someOptions convertedString:(CPStringRef)aStringRef usedLossyConversion:(BOOLRef)aLossyRef
|
||||
{
|
||||
var bytes = someData ? [someData bytes] : [],
|
||||
var bytes = CPStringEncodingBytesOfData(someData),
|
||||
suggested = [someOptions objectForKey:CPStringEncodingDetectionSuggestedEncodingsKey] || [],
|
||||
usesOnlySuggested = !![someOptions objectForKey:CPStringEncodingDetectionUseOnlySuggestedEncodingsKey],
|
||||
allowLossy = [someOptions objectForKey:CPStringEncodingDetectionAllowLossyKey],
|
||||
@@ -887,7 +924,7 @@ var CPStringEncodingDecode = function(bytes, anEncoding, isLossy)
|
||||
|
||||
if (bytes.length >= mark.length && mark.every(function(aByte, anIndex) { return bytes[anIndex] === aByte; }))
|
||||
{
|
||||
var decoded = CPStringEncodingDecode(bytes.slice(mark.length), byteOrderMarks[i][1], NO);
|
||||
var decoded = CPStringEncodingDecode(CPStringEncodingBytesFromIndex(bytes, mark.length), byteOrderMarks[i][1], NO);
|
||||
|
||||
if (decoded !== nil)
|
||||
return result(byteOrderMarks[i][1], decoded, NO);
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
@import "CPOperation.j"
|
||||
@import "CPOperationQueue.j"
|
||||
@import "CPPredicate.j"
|
||||
@import "CPProgress.j"
|
||||
@import "CPPropertyListSerialization.j"
|
||||
@import "CPRange.j"
|
||||
@import "CPRunLoop.j"
|
||||
|
||||
@@ -216,4 +216,95 @@
|
||||
[self assert:@"a.png" equals:[providers[1] suggestedName]];
|
||||
}
|
||||
|
||||
/*
|
||||
Installs a FileReader stand-in that reads nothing until the test drives it, and returns an
|
||||
object through which the test reaches the reader.
|
||||
*/
|
||||
- (JSObject)installPendingFileReader
|
||||
{
|
||||
var state = { reader: nil, aborted: NO };
|
||||
|
||||
globalThis.FileReader = function() { state.reader = this; this.readyState = 0; };
|
||||
globalThis.FileReader.prototype.readAsArrayBuffer = function(aFile) { this.readyState = 1; };
|
||||
globalThis.FileReader.prototype.abort = function()
|
||||
{
|
||||
state.aborted = YES;
|
||||
this.readyState = 2;
|
||||
this.onabort();
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
- (void)testFileProgressAndCancellation
|
||||
{
|
||||
var state = [self installPendingFileReader],
|
||||
provider = [CPItemProvider _itemProviderWithFile:{ name: "big.csv", type: "text/csv", size: 1000 }],
|
||||
resultError = nil,
|
||||
calls = 0,
|
||||
progress = [provider loadDataRepresentationForTypeIdentifier:@"text/csv" completionHandler:function(someData, anError)
|
||||
{
|
||||
calls++;
|
||||
resultError = anError;
|
||||
}];
|
||||
|
||||
state.reader.onprogress({ lengthComputable: YES, loaded: 250 });
|
||||
|
||||
[self assert:0.25 equals:[progress fractionCompleted]];
|
||||
|
||||
[progress cancel];
|
||||
|
||||
[self assertTrue:state.aborted];
|
||||
[self assert:1 equals:calls];
|
||||
[self assert:CPCocoaErrorDomain equals:[resultError domain]];
|
||||
[self assert:CPUserCancelledError equals:[resultError code]];
|
||||
}
|
||||
|
||||
- (void)testFileProgressFinishes
|
||||
{
|
||||
var state = [self installPendingFileReader],
|
||||
provider = [CPItemProvider _itemProviderWithFile:{ name: "small.csv", type: "text/csv", size: 2 }],
|
||||
result = nil,
|
||||
progress = [provider loadDataRepresentationForTypeIdentifier:@"text/csv" completionHandler:function(someData, anError)
|
||||
{
|
||||
result = someData;
|
||||
}];
|
||||
|
||||
state.reader.result = new Uint8Array([104, 105]).buffer;
|
||||
state.reader.onload();
|
||||
|
||||
[self assertTrue:[progress isFinished]];
|
||||
[self assert:@"hi" equals:[result rawString]];
|
||||
}
|
||||
|
||||
- (void)testLoadObjectOfClassString
|
||||
{
|
||||
var provider = [self providerWithType:@"text/csv" data:[CPData dataWithRawString:@"a,b"]],
|
||||
result = nil;
|
||||
|
||||
[self assertTrue:[provider canLoadObjectOfClass:[CPString class]]];
|
||||
|
||||
[provider loadObjectOfClass:[CPString class] completionHandler:function(anObject, anError)
|
||||
{
|
||||
result = anObject;
|
||||
}];
|
||||
|
||||
[self assert:@"a,b" equals:result];
|
||||
}
|
||||
|
||||
- (void)testCannotLoadStringFromImage
|
||||
{
|
||||
var provider = [self providerWithType:@"image/png" data:[CPData dataWithRawString:@"x"]],
|
||||
resultError = nil;
|
||||
|
||||
[self assertFalse:[provider canLoadObjectOfClass:[CPString class]]];
|
||||
|
||||
[provider loadObjectOfClass:[CPString class] completionHandler:function(anObject, anError)
|
||||
{
|
||||
resultError = anError;
|
||||
}];
|
||||
|
||||
[self assert:CPItemProviderItemUnavailableError equals:[resultError code]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* CPProgressTest.j
|
||||
* Foundation
|
||||
*
|
||||
* Created by David Richardson on 2026-11-02.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* 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 <Foundation/CPKeyValueObserving.j>
|
||||
@import <Foundation/CPProgress.j>
|
||||
|
||||
@implementation CPProgressTestObserver : CPObject
|
||||
{
|
||||
CPArray values;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
values = [];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CPArray)values
|
||||
{
|
||||
return values;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)aChange context:(id)aContext
|
||||
{
|
||||
values.push([aChange objectForKey:CPKeyValueChangeNewKey]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPProgressTest : OJTestCase
|
||||
{
|
||||
}
|
||||
|
||||
- (void)testCounts
|
||||
{
|
||||
var progress = [CPProgress discreteProgressWithTotalUnitCount:10];
|
||||
|
||||
[self assertTrue:[progress isCancellable]];
|
||||
[self assertFalse:[progress isPausable]];
|
||||
[self assert:0.0 equals:[progress fractionCompleted]];
|
||||
|
||||
[progress setCompletedUnitCount:4];
|
||||
|
||||
[self assert:0.4 equals:[progress fractionCompleted]];
|
||||
[self assertFalse:[progress isFinished]];
|
||||
[self assert:@"40% completed" equals:[progress localizedDescription]];
|
||||
[self assert:@"4 of 10" equals:[progress localizedAdditionalDescription]];
|
||||
|
||||
[progress setCompletedUnitCount:10];
|
||||
|
||||
[self assertTrue:[progress isFinished]];
|
||||
}
|
||||
|
||||
- (void)testIndeterminate
|
||||
{
|
||||
var progress = [CPProgress discreteProgressWithTotalUnitCount:0];
|
||||
|
||||
[self assertTrue:[progress isIndeterminate]];
|
||||
|
||||
[progress setTotalUnitCount:-1];
|
||||
[progress setCompletedUnitCount:3];
|
||||
|
||||
[self assertTrue:[progress isIndeterminate]];
|
||||
[self assert:0.0 equals:[progress fractionCompleted]];
|
||||
[self assertFalse:[progress isFinished]];
|
||||
|
||||
[progress setTotalUnitCount:6];
|
||||
|
||||
[self assertFalse:[progress isIndeterminate]];
|
||||
[self assert:0.5 equals:[progress fractionCompleted]];
|
||||
}
|
||||
|
||||
- (void)testExplicitChild
|
||||
{
|
||||
var parent = [CPProgress discreteProgressWithTotalUnitCount:100],
|
||||
child = [CPProgress progressWithTotalUnitCount:4 parent:parent pendingUnitCount:50];
|
||||
|
||||
[child setCompletedUnitCount:2];
|
||||
|
||||
[self assert:0.25 equals:[parent fractionCompleted]];
|
||||
[self assert:0 equals:[parent completedUnitCount]];
|
||||
|
||||
[child setCompletedUnitCount:4];
|
||||
|
||||
[self assert:50 equals:[parent completedUnitCount]];
|
||||
[self assert:0.5 equals:[parent fractionCompleted]];
|
||||
}
|
||||
|
||||
- (void)testFinishedChildFinishesAncestors
|
||||
{
|
||||
var root = [CPProgress discreteProgressWithTotalUnitCount:1],
|
||||
middle = [CPProgress progressWithTotalUnitCount:1 parent:root pendingUnitCount:1],
|
||||
leaf = [CPProgress progressWithTotalUnitCount:2 parent:middle pendingUnitCount:1];
|
||||
|
||||
[leaf setCompletedUnitCount:2];
|
||||
|
||||
[self assertTrue:[middle isFinished]];
|
||||
[self assertTrue:[root isFinished]];
|
||||
[self assert:1.0 equals:[root fractionCompleted]];
|
||||
}
|
||||
|
||||
- (void)testImplicitChild
|
||||
{
|
||||
var parent = [CPProgress discreteProgressWithTotalUnitCount:10];
|
||||
|
||||
[parent becomeCurrentWithPendingUnitCount:6];
|
||||
|
||||
var child = [CPProgress progressWithTotalUnitCount:2],
|
||||
second = [CPProgress progressWithTotalUnitCount:2];
|
||||
|
||||
[parent resignCurrent];
|
||||
|
||||
[self assertNull:[CPProgress currentProgress]];
|
||||
|
||||
[child setCompletedUnitCount:1];
|
||||
[second setCompletedUnitCount:2];
|
||||
|
||||
[self assert:0.3 equals:[parent fractionCompleted]];
|
||||
}
|
||||
|
||||
- (void)testResignWithoutChildCompletesPendingUnits
|
||||
{
|
||||
var parent = [CPProgress discreteProgressWithTotalUnitCount:10];
|
||||
|
||||
[parent performAsCurrentWithPendingUnitCount:3 usingBlock:function() {}];
|
||||
|
||||
[self assert:3 equals:[parent completedUnitCount]];
|
||||
}
|
||||
|
||||
- (void)testParentMustBeCurrent
|
||||
{
|
||||
var parent = [CPProgress discreteProgressWithTotalUnitCount:10];
|
||||
|
||||
[self assertThrows:function()
|
||||
{
|
||||
[[CPProgress alloc] initWithParent:parent userInfo:nil];
|
||||
}];
|
||||
|
||||
[self assertThrows:function()
|
||||
{
|
||||
[parent resignCurrent];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testCancellationReachesChildren
|
||||
{
|
||||
var parent = [CPProgress discreteProgressWithTotalUnitCount:10],
|
||||
child = [CPProgress progressWithTotalUnitCount:10 parent:parent pendingUnitCount:5],
|
||||
calls = 0;
|
||||
|
||||
[child setCancellationHandler:function() { calls++; }];
|
||||
[parent cancel];
|
||||
[parent cancel];
|
||||
|
||||
[self assertTrue:[parent isCancelled]];
|
||||
[self assertTrue:[child isCancelled]];
|
||||
[self assert:1 equals:calls];
|
||||
|
||||
var late = [CPProgress progressWithTotalUnitCount:1 parent:parent pendingUnitCount:1];
|
||||
|
||||
[self assertTrue:[late isCancelled]];
|
||||
}
|
||||
|
||||
- (void)testPausingAndResuming
|
||||
{
|
||||
var parent = [CPProgress discreteProgressWithTotalUnitCount:10],
|
||||
child = [CPProgress progressWithTotalUnitCount:10 parent:parent pendingUnitCount:5],
|
||||
events = [];
|
||||
|
||||
[child setPausingHandler:function() { events.push(@"pause"); }];
|
||||
[child setResumingHandler:function() { events.push(@"resume"); }];
|
||||
|
||||
[parent pause];
|
||||
|
||||
[self assertTrue:[child isPaused]];
|
||||
|
||||
[parent resume];
|
||||
|
||||
[self assertFalse:[child isPaused]];
|
||||
[self assert:[@"pause", @"resume"] equals:events];
|
||||
}
|
||||
|
||||
- (void)testObservingFractionCompletedThroughChild
|
||||
{
|
||||
var parent = [CPProgress discreteProgressWithTotalUnitCount:2],
|
||||
child = [CPProgress progressWithTotalUnitCount:4 parent:parent pendingUnitCount:2],
|
||||
observer = [[CPProgressTestObserver alloc] init];
|
||||
|
||||
[parent addObserver:observer forKeyPath:@"fractionCompleted" options:CPKeyValueObservingOptionNew context:nil];
|
||||
|
||||
[child setCompletedUnitCount:1];
|
||||
[child setCompletedUnitCount:4];
|
||||
|
||||
[parent removeObserver:observer forKeyPath:@"fractionCompleted"];
|
||||
|
||||
[self assert:[0.25, 1.0] equals:[observer values]];
|
||||
}
|
||||
|
||||
- (void)testUserInfo
|
||||
{
|
||||
var progress = [CPProgress discreteProgressWithTotalUnitCount:1];
|
||||
|
||||
[progress setThroughput:12];
|
||||
[progress setFileOperationKind:CPProgressFileOperationKindReceiving];
|
||||
|
||||
[self assert:12 equals:[progress throughput]];
|
||||
[self assert:CPProgressFileOperationKindReceiving equals:[[progress userInfo] objectForKey:CPProgressFileOperationKindKey]];
|
||||
|
||||
[progress setThroughput:nil];
|
||||
|
||||
[self assertNull:[progress throughput]];
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in new issue
Block a user