mirror of
https://github.com/gnustep/libs-base.git
synced 2025-04-23 17:10:48 +00:00
Initial port of WinObjC's KVO implementation to GNUstep (#420)
* GSAtomic: Add prefix to macro definitions * NSKVOSupport: Import * NSKVOSupport: Add test cases * NSKVOSwizzling: Ugly C Rewrite * NSKeyValueObserving: Use old implementation as fallback * NSKeyValueObserving: Rename TypeEncodingCases header * NSKVOSupport: Fix new objects not being added to NSKeyValueChangeNew set on set mutation * NSKeyValueMutableSet: Fix will and didChange notifications for set operations * NSKeyValueMutableSet: Document Accessor Search Patterns * NSKVOSupport: Add toMany test * NSKeyValueCoding: Change notifications when changing value via setValue:forKey: * NSKVOSupport: Add more tests * NSKVOSupport: Do not wrap block in try/finally to avoid crash in windows * NSKVOSwizzling: use _alloca on Windows * NSKVOSupport: Do not autorelease newWithObservee: * NSKVOSupport: Do not leak Observee and TestFacade objects * Improve runtime detection in makefile * Add file extension of source file in GNUMakefile * NSKVOSupport: Remove @status comments * NSKVOSupport: Implement private notify method * NSUserDefaults: KVO Support and fix macOS incompatibilities * NSKeyValueObserving: Set old to null if nil * NSKeyValueObserving: Remove cached new value * NSMethodSignature: Add signature cache * NSKVOSupport: Remove ObjC2 features and mark tests failing on GCC as hopeful * Call class method instead of private _keyPathsForValuesAffectingValueForKey * Move _keyPathsForValuesAffectingValueForKey body into class method and statically construct empty NSSet * NSUserDefaults: Change notification should contain old value from other domains aswell * NSUserDefaults: Fetch new value from all domains * NSKVOInternal: Fixup filename in header * NSUserDefaults: Go through search list instead of only one domain in KVO change * Making indentation a bit less worse * Add NSUserDefaults KVO tests * NSKVOSupport: NSUserDefaults test small fixes * Add autoreleasepool * NSUserDefaults: Only emit change notifications if value changed * Avoid compiler warnings and tidy some of the whitespace/formatting --------- Co-authored-by: Frederik Seiffert <frederik@algoriddim.com> Co-authored-by: rfm <richardfrithmacdonald@gmail.com> Co-authored-by: rfm <rfm@gnu.org>
This commit is contained in:
parent
544dcce482
commit
6681a3da47
20 changed files with 6710 additions and 116 deletions
|
@ -254,7 +254,6 @@ NSJSONSerialization.m \
|
|||
NSKeyedArchiver.m \
|
||||
NSKeyedUnarchiver.m \
|
||||
NSKeyValueCoding.m \
|
||||
NSKeyValueObserving.m \
|
||||
NSLengthFormatter.m \
|
||||
NSLinguisticTagger.m \
|
||||
NSLocale.m \
|
||||
|
@ -358,6 +357,18 @@ NSZone.m \
|
|||
externs.m \
|
||||
objc-load.m
|
||||
|
||||
# We have two implementations for Key Value Observing.
|
||||
# One highly-optimised one that depends on libobjc2
|
||||
# and the original implementation.
|
||||
ifeq ($(OBJC_RUNTIME_LIB), ng)
|
||||
BASE_MFILES += \
|
||||
NSKVOSupport.m \
|
||||
NSKVOSwizzling.m
|
||||
else
|
||||
BASE_MFILES += \
|
||||
NSKeyValueObserving.m
|
||||
endif
|
||||
|
||||
ifeq ($(OBJC_RUNTIME_LIB), ng)
|
||||
BASE_MFILES += \
|
||||
NSKeyValueCoding+Caching.m
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
* Use native C11 atomic operations. _Atomic() should be defined by the
|
||||
* compiler.
|
||||
*/
|
||||
#define atomic_load_explicit(object, order) \
|
||||
#define gs_atomic_load_explicit(object, order) \
|
||||
__c11_atomic_load(object, order)
|
||||
#define atomic_store_explicit(object, desired, order) \
|
||||
#define gs_atomic_store_explicit(object, desired, order) \
|
||||
__c11_atomic_store(object, desired, order)
|
||||
|
||||
#else
|
||||
|
@ -33,7 +33,7 @@
|
|||
#define _Atomic(T) struct { T volatile __val; }
|
||||
#if __has_builtin(__sync_swap)
|
||||
/* Clang provides a full-barrier atomic exchange - use it if available. */
|
||||
#define atomic_exchange_explicit(object, desired, order) \
|
||||
#define gs_atomic_exchange_explicit(object, desired, order) \
|
||||
((void)(order), __sync_swap(&(object)->__val, desired))
|
||||
#else
|
||||
/*
|
||||
|
@ -41,7 +41,7 @@
|
|||
* practice it is usually a full barrier) so we need an explicit barrier before
|
||||
* it.
|
||||
*/
|
||||
#define atomic_exchange_explicit(object, desired, order) \
|
||||
#define gs_atomic_exchange_explicit(object, desired, order) \
|
||||
__extension__ ({ \
|
||||
__typeof__(object) __o = (object); \
|
||||
__typeof__(desired) __d = (desired); \
|
||||
|
@ -50,10 +50,10 @@ __extension__ ({ \
|
|||
__sync_lock_test_and_set(&(__o)->__val, __d); \
|
||||
})
|
||||
#endif
|
||||
#define atomic_load_explicit(object, order) \
|
||||
#define gs_atomic_load_explicit(object, order) \
|
||||
((void)(order), __sync_fetch_and_add(&(object)->__val, 0))
|
||||
#define atomic_store_explicit(object, desired, order) \
|
||||
((void)atomic_exchange_explicit(object, desired, order))
|
||||
#define gs_atomic_store_explicit(object, desired, order) \
|
||||
((void)gs_atomic_exchange_explicit(object, desired, order))
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -64,9 +64,9 @@ __extension__ ({ \
|
|||
/*
|
||||
* Convenience functions.
|
||||
*/
|
||||
#define atomic_load(object) \
|
||||
atomic_load_explicit(object, __ATOMIC_SEQ_CST)
|
||||
#define atomic_store(object, desired) \
|
||||
atomic_store_explicit(object, desired, __ATOMIC_SEQ_CST)
|
||||
#define gs_atomic_load(object) \
|
||||
gs_atomic_load_explicit(object, __ATOMIC_SEQ_CST)
|
||||
#define gs_atomic_store(object, desired) \
|
||||
gs_atomic_store_explicit(object, desired, __ATOMIC_SEQ_CST)
|
||||
|
||||
#endif // _GSAtomic_h_
|
||||
|
|
|
@ -61,12 +61,18 @@ typedef CONDITION_VARIABLE gs_cond_t;
|
|||
#define GS_COND_BROADCAST(cond) WakeAllConditionVariable(&(cond))
|
||||
|
||||
/* Pthread-like locking primitives defined in NSLock.m */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void gs_mutex_init(gs_mutex_t *l, gs_mutex_attr_t attr);
|
||||
int gs_mutex_lock(gs_mutex_t *l);
|
||||
int gs_mutex_trylock(gs_mutex_t *l);
|
||||
int gs_mutex_unlock(gs_mutex_t *l);
|
||||
int gs_cond_wait(gs_cond_t *cond, gs_mutex_t *mutex);
|
||||
int gs_cond_timedwait(gs_cond_t *cond, gs_mutex_t *mutex, DWORD millisecs);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Threading primitives.
|
||||
|
|
129
Source/NSKVOInternal.h
Normal file
129
Source/NSKVOInternal.h
Normal file
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
NSKVOInternal.h
|
||||
|
||||
Copyright (C) 2024 Free Software Foundation, Inc.
|
||||
|
||||
Written by: Hugo Melder <hugo@algoriddim.com>
|
||||
Date: June 2024
|
||||
|
||||
Based on WinObjC KVO tests by Microsoft Corporation.
|
||||
|
||||
This file is part of GNUStep-base
|
||||
|
||||
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 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.
|
||||
|
||||
If you are interested in a warranty or support for this source code,
|
||||
contact Scott Christley <scottc@net-community.com> for more information.
|
||||
|
||||
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 USA.
|
||||
*/
|
||||
/**
|
||||
Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
This code is licensed under the MIT License (MIT).
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* This Key Value Observing Implementation is tied to libobjc2 */
|
||||
|
||||
#import <Foundation/NSObject.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSArray.h>
|
||||
#import <Foundation/NSSet.h>
|
||||
#import <Foundation/NSKeyValueObserving.h>
|
||||
#import <Foundation/NSException.h>
|
||||
|
||||
#if defined(__OBJC2__)
|
||||
|
||||
#import "GSPThread.h"
|
||||
|
||||
#define NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath) \
|
||||
do \
|
||||
{ \
|
||||
[NSException \
|
||||
raise: NSInvalidArgumentException \
|
||||
format: @"-[%s %s] is not supported. Key path: %@", \
|
||||
object_getClassName(self), sel_getName(_cmd), keyPath]; \
|
||||
} while (false)
|
||||
|
||||
@class _NSKVOKeypathObserver;
|
||||
|
||||
@interface _NSKVOKeyObserver : NSObject
|
||||
- (instancetype)initWithObject:(id)object
|
||||
keypathObserver:(_NSKVOKeypathObserver *)keypathObserver
|
||||
key:(NSString *)key
|
||||
restOfKeypath:(NSString *)restOfKeypath
|
||||
affectedObservers:(NSArray *)affectedObservers;
|
||||
@property (nonatomic, retain) _NSKVOKeypathObserver *keypathObserver;
|
||||
@property (nonatomic, retain) _NSKVOKeyObserver *restOfKeypathObserver;
|
||||
@property (nonatomic, retain) NSArray *dependentObservers;
|
||||
@property (nonatomic, assign) id object;
|
||||
@property (nonatomic, copy) NSString *key;
|
||||
@property (nonatomic, copy) NSString *restOfKeypath;
|
||||
@property (nonatomic, retain) NSArray *affectedObservers;
|
||||
@property (nonatomic, assign) BOOL root;
|
||||
@property (nonatomic, readonly) BOOL isRemoved;
|
||||
@end
|
||||
|
||||
@interface _NSKVOKeypathObserver : NSObject
|
||||
- (instancetype)initWithObject:(id)object
|
||||
observer:(id)observer
|
||||
keyPath:(NSString *)keypath
|
||||
options:(NSKeyValueObservingOptions)options
|
||||
context:(void *)context;
|
||||
@property (nonatomic, assign) id object;
|
||||
@property (nonatomic, assign) id observer;
|
||||
@property (nonatomic, copy) NSString *keypath;
|
||||
@property (nonatomic, assign) NSKeyValueObservingOptions options;
|
||||
@property (nonatomic, assign) void *context;
|
||||
|
||||
@property (atomic, retain) NSMutableDictionary *pendingChange;
|
||||
@end
|
||||
|
||||
@interface _NSKVOObservationInfo : NSObject
|
||||
{
|
||||
NSMutableDictionary<NSString *, NSMutableArray<_NSKVOKeyObserver *> *>
|
||||
*_keyObserverMap;
|
||||
NSInteger _dependencyDepth;
|
||||
NSMutableSet<NSString *> *_existingDependentKeys;
|
||||
gs_mutex_t _lock;
|
||||
}
|
||||
- (instancetype)init;
|
||||
- (NSArray *)observersForKey:(NSString *)key;
|
||||
- (void)addObserver:(_NSKVOKeyObserver *)observer;
|
||||
@end
|
||||
|
||||
// From NSKVOSwizzling
|
||||
void
|
||||
_NSKVOEnsureKeyWillNotify(id object, NSString *key);
|
||||
|
||||
#endif
|
||||
|
||||
/* Implementation in NSKVOSupport.m for ObjC2 and NSKeyValueObserving
|
||||
* respectively
|
||||
*/
|
||||
@interface
|
||||
NSObject (NSKeyValueObservingPrivate)
|
||||
- (void)_notifyObserversOfChangeForKey:(NSString *)key
|
||||
oldValue:(id)oldValue
|
||||
newValue:(id)newValue;
|
||||
@end
|
1256
Source/NSKVOSupport.m
Normal file
1256
Source/NSKVOSupport.m
Normal file
File diff suppressed because it is too large
Load diff
695
Source/NSKVOSwizzling.m
Normal file
695
Source/NSKVOSwizzling.m
Normal file
|
@ -0,0 +1,695 @@
|
|||
/**
|
||||
NSKVOSwizzling.m
|
||||
|
||||
Copyright (C) 2024 Free Software Foundation, Inc.
|
||||
|
||||
Written by: Hugo Melder <hugo@algoriddim.com>
|
||||
Date: June 2024
|
||||
|
||||
Based on WinObjC KVO tests by Microsoft Corporation.
|
||||
|
||||
This file is part of GNUStep-base
|
||||
|
||||
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 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.
|
||||
|
||||
If you are interested in a warranty or support for this source code,
|
||||
contact Scott Christley <scottc@net-community.com> for more information.
|
||||
|
||||
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 USA.
|
||||
*/
|
||||
/**
|
||||
Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
This code is licensed under the MIT License (MIT).
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* This Key Value Observing Implementation is tied to libobjc2 */
|
||||
|
||||
#import "common.h"
|
||||
#import "NSKVOInternal.h"
|
||||
|
||||
#import <objc/encoding.h>
|
||||
#import <objc/runtime.h>
|
||||
#import "NSKVOTypeEncodingCases.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifdef WIN32
|
||||
#define alloca(x) _alloca(x)
|
||||
#endif
|
||||
|
||||
/* These are defined by the ABI and the runtime. */
|
||||
#define ABI_SUPER(obj) (((Class **) obj)[0][1])
|
||||
#define ABI_ISA(obj) (((Class *) obj)[0])
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-objc-pointer-introspection"
|
||||
// Small objects, as defined by libobjc2, are tagged pointers. Instead of being
|
||||
// heap-allocated, the object is stored in the sizeof(id) -
|
||||
// OBJC_SMALL_OBJECT_BITS bits of the "pointer" itself.
|
||||
static inline bool
|
||||
isSmallObject_np(id object)
|
||||
{
|
||||
return ((((uintptr_t) object) & OBJC_SMALL_OBJECT_MASK) != 0);
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
static void
|
||||
NSKVO$setObject$forKey$(id self, SEL _cmd, id object, NSString *key);
|
||||
static void
|
||||
NSKVO$removeObjectForKey$(id self, SEL _cmd, NSString *key);
|
||||
static void
|
||||
NSKVO$nilIMP(id self, SEL _cmd)
|
||||
{}
|
||||
|
||||
static void
|
||||
_NSKVOEnsureObjectIsKVOAware(id object)
|
||||
{
|
||||
// We have to use ABI_ISA here, because object_getClass will skip the secret
|
||||
// hidden subclass.
|
||||
if (class_respondsToSelector(ABI_ISA(object), @selector(_isKVOAware)))
|
||||
{
|
||||
// The presence of _isKVOAware signals that we have already mangled this
|
||||
// object.
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
A crucial design decision was made here: because object_getClass() skips
|
||||
autogenerated subclasses, the likes of which are used for associated objects,
|
||||
the KVO machinery (if implemented using manual subclassing) would delete all
|
||||
associated objects on an observed instance. That was deemed unacceptable.
|
||||
|
||||
Likewise, this implementation is not free of issues:
|
||||
- The _np methods are nonportable.
|
||||
- Anyone using class_getMethodImplementation(object_getClass(...), ...) will
|
||||
receive the original IMP for any overridden method.
|
||||
- We have to manually load isa to get at the secret subclass (thus the use of
|
||||
ABI_ISA/ABI_SUPER.)
|
||||
- It is dependent upon a libobjc2 implementation detail:
|
||||
object_addMethod_np creates a hidden subclass for the object's class (one
|
||||
per object!)
|
||||
*/
|
||||
|
||||
object_addMethod_np(object, @selector(setObject:forKey:),
|
||||
(IMP)(NSKVO$setObject$forKey$), "v@:@@");
|
||||
object_addMethod_np(object, @selector(removeObjectForKey:),
|
||||
(IMP)(NSKVO$removeObjectForKey$), "v@:@");
|
||||
object_addMethod_np(object, @selector(_isKVOAware),
|
||||
(IMP)(NSKVO$nilIMP), "v@:");
|
||||
}
|
||||
|
||||
#pragma region Method Implementations
|
||||
// Selector mappings: the class-level mapping from a selector (setX:) to the KVC
|
||||
// key ("x") to which it corresponds. This is necessary because both "X" and "x"
|
||||
// map to setX:, but we need to be cognizant of precisely which it was for any
|
||||
// given observee. The sole reason we can get away with keeping selector<->key
|
||||
// mappings on the class is that, through the lifetime of said class, it can
|
||||
// never lose selectors. This mapping will be pertinent to every instance of the
|
||||
// class.
|
||||
static inline NSMapTable *
|
||||
_selectorMappingsForObject(id object)
|
||||
{
|
||||
static char s_selMapKey;
|
||||
Class cls;
|
||||
|
||||
// here we explicitly want the public
|
||||
// (non-hidden) class associated with the object.
|
||||
cls = object_getClass(object);
|
||||
|
||||
@synchronized(cls)
|
||||
{
|
||||
NSMapTable *selMappings
|
||||
= (NSMapTable *) objc_getAssociatedObject(cls, &s_selMapKey);
|
||||
if (!selMappings)
|
||||
{
|
||||
selMappings = [NSMapTable
|
||||
mapTableWithKeyOptions: NSPointerFunctionsOpaqueMemory
|
||||
| NSPointerFunctionsOpaquePersonality
|
||||
valueOptions: NSPointerFunctionsStrongMemory
|
||||
| NSPointerFunctionsObjectPersonality];
|
||||
objc_setAssociatedObject(cls, &s_selMapKey, (id) selMappings,
|
||||
OBJC_ASSOCIATION_RETAIN);
|
||||
}
|
||||
return selMappings;
|
||||
}
|
||||
}
|
||||
|
||||
static inline NSString *
|
||||
_keyForSelector(id object, SEL selector)
|
||||
{
|
||||
return (NSString *) NSMapGet(_selectorMappingsForObject(object),
|
||||
sel_getName(selector));
|
||||
}
|
||||
|
||||
static inline void
|
||||
_associateSelectorWithKey(id object, SEL selector, NSString *key)
|
||||
{
|
||||
/* this must be insertIfAbsent. otherwise, when calling a setter that itself
|
||||
* causes observers to be added/removed on this key, this would call this
|
||||
* method and overwrite the association selector->key with an identical key
|
||||
* but another object. unfortunately, this would mean that the
|
||||
* notifyingSetImpl below would then have a dead ```key``` pointer once it
|
||||
* came time to call didChangeValueForKey (because apparently ARC doesn't take
|
||||
* care of this properly)
|
||||
*/
|
||||
NSMapInsertIfAbsent(_selectorMappingsForObject(object),
|
||||
sel_getName(selector), key);
|
||||
}
|
||||
|
||||
static void
|
||||
notifyingVariadicSetImpl(id self, SEL _cmd, ...)
|
||||
{
|
||||
NSString *key = _keyForSelector(self, _cmd);
|
||||
|
||||
/* [Source: NSInvocation.mm]
|
||||
* This attempts to flatten the method's arguments (as determined by its type
|
||||
* encoding) from the stack into a buffer. That buffer is then emitted back
|
||||
* onto the stack for the imp callthrough. This only works if we assume that
|
||||
* our calling convention passes variadics and non-variadics in the same way:
|
||||
* on the stack. For our two supported platforms, this seems to hold true.
|
||||
*/
|
||||
NSMethodSignature *sig = [self methodSignatureForSelector: _cmd];
|
||||
size_t argSz = objc_sizeof_type([sig getArgumentTypeAtIndex: 2]);
|
||||
size_t nStackArgs = argSz / sizeof(uintptr_t);
|
||||
uintptr_t *raw = (uintptr_t *) (calloc(sizeof(uintptr_t), nStackArgs));
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, _cmd);
|
||||
for (uintptr_t i = 0; i < nStackArgs; ++i)
|
||||
{
|
||||
raw[i] = va_arg(ap, uintptr_t);
|
||||
}
|
||||
va_end(ap);
|
||||
|
||||
[self willChangeValueForKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
IMP imp = (id(*)(id, SEL, ...)) objc_msg_lookup_super(&super, _cmd);
|
||||
|
||||
// VSO 5955259; NSInvocation informs this implementation and this will need
|
||||
// to be cleaned up when NSInvocation is.
|
||||
switch (nStackArgs)
|
||||
{
|
||||
case 1:
|
||||
imp(self, _cmd, raw[0]);
|
||||
break;
|
||||
case 2:
|
||||
imp(self, _cmd, raw[0], raw[1]);
|
||||
break;
|
||||
case 3:
|
||||
imp(self, _cmd, raw[0], raw[1], raw[2]);
|
||||
break;
|
||||
case 4:
|
||||
imp(self, _cmd, raw[0], raw[1], raw[2], raw[3]);
|
||||
break;
|
||||
case 5:
|
||||
imp(self, _cmd, raw[0], raw[1], raw[2], raw[3], raw[4]);
|
||||
break;
|
||||
case 6:
|
||||
imp(self, _cmd, raw[0], raw[1], raw[2], raw[3], raw[4], raw[5]);
|
||||
break;
|
||||
default:
|
||||
NSLog(@"Can't override setter with more than 6"
|
||||
@" sizeof(long int) stack arguments.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
[self didChangeValueForKey: key];
|
||||
|
||||
free(raw);
|
||||
}
|
||||
|
||||
typedef void (*insertObjectAtIndexIMP)(id, SEL, id, NSUInteger);
|
||||
typedef void (*removeObjectAtIndexIMP)(id, SEL, NSUInteger);
|
||||
typedef void (*insertAtIndexesIMP)(id, SEL, id, NSIndexSet *);
|
||||
typedef void (*removeAtIndexesIMP)(id, SEL, NSIndexSet *);
|
||||
typedef void (*replaceAtIndexesIMP)(id, SEL, NSIndexSet *, NSArray *);
|
||||
typedef void (*setObjectForKeyIMP)(id, SEL, id, NSString *);
|
||||
typedef void (*removeObjectForKeyIMP)(id, SEL, NSString *);
|
||||
typedef void (*replaceObjectAtIndexWithObjectIMP)(id, SEL, NSUInteger, id);
|
||||
|
||||
static void
|
||||
NSKVONotifying$insertObject$inXxxAtIndex$(id self, SEL _cmd, id object,
|
||||
NSUInteger index)
|
||||
{
|
||||
NSString *key = _keyForSelector(self, _cmd);
|
||||
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index];
|
||||
|
||||
[self willChange: NSKeyValueChangeInsertion
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
insertObjectAtIndexIMP imp = (void (*)(id, SEL, id, NSUInteger))
|
||||
objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, object, index);
|
||||
}
|
||||
[self didChange: NSKeyValueChangeInsertion
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
}
|
||||
|
||||
static void
|
||||
NSKVONotifying$insertXxx$atIndexes$(id self, SEL _cmd, id object,
|
||||
NSIndexSet *indexes)
|
||||
{
|
||||
NSString *key = _keyForSelector(self, _cmd);
|
||||
|
||||
[self willChange: NSKeyValueChangeInsertion
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
insertAtIndexesIMP imp = (void (*)(id, SEL, id, NSIndexSet *))
|
||||
objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, object, indexes);
|
||||
}
|
||||
[self didChange: NSKeyValueChangeInsertion
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
}
|
||||
|
||||
static void
|
||||
NSKVONotifying$removeObjectFromXxxAtIndex$(id self, SEL _cmd, NSUInteger index)
|
||||
{
|
||||
NSString *key = _keyForSelector(self, _cmd);
|
||||
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index];
|
||||
|
||||
[self willChange: NSKeyValueChangeRemoval
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
removeObjectAtIndexIMP imp = (void (*)(id, SEL, NSUInteger))
|
||||
objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, index);
|
||||
}
|
||||
[self didChange: NSKeyValueChangeRemoval
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
}
|
||||
|
||||
static void
|
||||
NSKVONotifying$removeXxxAtIndexes$(id self, SEL _cmd, NSIndexSet *indexes)
|
||||
{
|
||||
NSString *key = _keyForSelector(self, _cmd);
|
||||
|
||||
[self willChange: NSKeyValueChangeRemoval
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
removeAtIndexesIMP imp = (void (*)(id, SEL, NSIndexSet *))
|
||||
objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, indexes);
|
||||
}
|
||||
[self didChange: NSKeyValueChangeRemoval
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
}
|
||||
|
||||
static void
|
||||
NSKVONotifying$replaceObjectInXxxAtIndex$withObject$(id self, SEL _cmd,
|
||||
NSUInteger index, id object)
|
||||
{
|
||||
NSString *key = _keyForSelector(self, _cmd);
|
||||
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index];
|
||||
|
||||
[self willChange: NSKeyValueChangeReplacement
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
replaceObjectAtIndexWithObjectIMP imp = (void (*)(id, SEL, NSUInteger, id))
|
||||
objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, index, object);
|
||||
}
|
||||
[self didChange: NSKeyValueChangeReplacement
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
}
|
||||
|
||||
static void
|
||||
NSKVONotifying$replaceXxxAtIndexes$withXxx$(id self, SEL _cmd,
|
||||
NSIndexSet *indexes, NSArray *objects)
|
||||
{
|
||||
NSString *key = _keyForSelector(self, _cmd);
|
||||
|
||||
[self willChange: NSKeyValueChangeReplacement
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
replaceAtIndexesIMP imp = (void (*)(id, SEL, NSIndexSet *, NSArray *))
|
||||
objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, indexes, objects);
|
||||
}
|
||||
[self didChange: NSKeyValueChangeReplacement
|
||||
valuesAtIndexes: indexes
|
||||
forKey: key];
|
||||
}
|
||||
|
||||
#define GENERATE_NSKVOSetDispatch_IMPL(Kind) \
|
||||
static inline void _NSKVOSetDispatch_##Kind(id self, SEL _cmd, NSSet *set) \
|
||||
{ \
|
||||
NSString *key = _keyForSelector(self, _cmd); \
|
||||
[self willChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \
|
||||
{ \
|
||||
struct objc_super super = {self, ABI_SUPER(self)}; \
|
||||
void (*imp)(id, SEL, NSSet *) \
|
||||
= (void (*)(id, SEL, NSSet *)) objc_msg_lookup_super(&super, _cmd); \
|
||||
imp(self, _cmd, set); \
|
||||
} \
|
||||
[self didChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \
|
||||
}
|
||||
|
||||
#define GENERATE_NSKVOSetDispatchIndividual_IMPL(Kind) \
|
||||
static inline void _NSKVOSetDispatchIndividual_##Kind(id self, SEL _cmd, \
|
||||
id obj) \
|
||||
{ \
|
||||
NSSet *set = [NSSet setWithObject: obj]; \
|
||||
NSString *key = _keyForSelector(self, _cmd); \
|
||||
[self willChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \
|
||||
{ \
|
||||
struct objc_super super = {self, ABI_SUPER(self)}; \
|
||||
void (*imp)(id, SEL, id) \
|
||||
= (void (*)(id, SEL, id)) objc_msg_lookup_super(&super, _cmd); \
|
||||
imp(self, _cmd, obj); \
|
||||
} \
|
||||
[self didChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \
|
||||
}
|
||||
|
||||
GENERATE_NSKVOSetDispatchIndividual_IMPL(NSKeyValueUnionSetMutation);
|
||||
GENERATE_NSKVOSetDispatchIndividual_IMPL(NSKeyValueMinusSetMutation);
|
||||
//GENERATE_NSKVOSetDispatchIndividual_IMPL(NSKeyValueIntersectSetMutation);
|
||||
|
||||
GENERATE_NSKVOSetDispatch_IMPL(NSKeyValueUnionSetMutation);
|
||||
GENERATE_NSKVOSetDispatch_IMPL(NSKeyValueMinusSetMutation);
|
||||
GENERATE_NSKVOSetDispatch_IMPL(NSKeyValueIntersectSetMutation);
|
||||
|
||||
// - (void)setObject: (id)object forKey: (NSString*)key
|
||||
static void
|
||||
NSKVO$setObject$forKey$(id self, SEL _cmd, id object, NSString *key)
|
||||
{
|
||||
[self willChangeValueForKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
setObjectForKeyIMP imp;
|
||||
|
||||
imp = (void (*)(id, SEL, id, NSString *)) objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, object, key);
|
||||
}
|
||||
[self didChangeValueForKey: key];
|
||||
}
|
||||
|
||||
// - (void)removeObjectForKey: (NSString*)key
|
||||
static void
|
||||
NSKVO$removeObjectForKey$(id self, SEL _cmd, NSString *key)
|
||||
{
|
||||
[self willChangeValueForKey: key];
|
||||
{
|
||||
struct objc_super super = {self, ABI_SUPER(self)};
|
||||
removeObjectForKeyIMP imp;
|
||||
|
||||
imp = (void (*)(id, SEL, NSString *)) objc_msg_lookup_super(&super, _cmd);
|
||||
imp(self, _cmd, key);
|
||||
}
|
||||
[self didChangeValueForKey: key];
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#define GENERATE_NOTIFYING_SET_IMPL(funcName, type) \
|
||||
static void funcName(id self, SEL _cmd, type val) \
|
||||
{ \
|
||||
struct objc_super super = {self, ABI_SUPER(self)}; \
|
||||
NSString *key = _keyForSelector(self, _cmd); \
|
||||
void (*imp)(id, SEL, type) \
|
||||
= (void (*)(id, SEL, type)) objc_msg_lookup_super(&super, _cmd); \
|
||||
[self willChangeValueForKey: key]; \
|
||||
imp(self, _cmd, val); \
|
||||
[self didChangeValueForKey: key]; \
|
||||
}
|
||||
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplDouble, double);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplFloat, float);
|
||||
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplChar, signed char);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplInt, int);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplShort, short);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplLong, long);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplLongLong, long long);
|
||||
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedChar, unsigned char);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedInt, unsigned int);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedShort, unsigned short);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedLong, unsigned long);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedLongLong,
|
||||
unsigned long long);
|
||||
|
||||
// GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplBool, bool);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplObject, id);
|
||||
GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplPointer, void *);
|
||||
|
||||
#define KVO_SET_IMPL_CASE(type, name, capitalizedName, encodingChar) \
|
||||
case encodingChar: { \
|
||||
newImpl = (IMP) (¬ifyingSetImpl##capitalizedName); \
|
||||
break; \
|
||||
}
|
||||
|
||||
SEL
|
||||
KVCSetterForPropertyName(NSObject *self, const char *key)
|
||||
{
|
||||
SEL sel = nil;
|
||||
size_t len = strlen(key);
|
||||
// For the key "example", we must construct the following buffer:
|
||||
// _ _ _ _ x a m p l e _ \0
|
||||
// and fill it with the following characters:
|
||||
// s e t E x a m p l e : \0
|
||||
char *buf = (char *) alloca(3 + len + 2);
|
||||
memcpy(buf + 4, key + 1, len);
|
||||
buf[0] = 's';
|
||||
buf[1] = 'e';
|
||||
buf[2] = 't';
|
||||
buf[3] = toupper(key[0]);
|
||||
buf[3 + len] = ':';
|
||||
buf[3 + len + 1] = '\0';
|
||||
sel = sel_getUid(buf);
|
||||
if ([self respondsToSelector: sel])
|
||||
{
|
||||
return sel;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
// invariant: rawKey has already been capitalized
|
||||
static inline void
|
||||
_NSKVOEnsureSimpleKeyWillNotify(id object, NSString *key, const char *rawKey)
|
||||
{
|
||||
Method originalMethod;
|
||||
const char *valueType;
|
||||
const char *types;
|
||||
NSMethodSignature *sig;
|
||||
SEL sel;
|
||||
IMP newImpl = NULL;
|
||||
|
||||
sel = KVCSetterForPropertyName(object, rawKey);
|
||||
originalMethod = class_getInstanceMethod(object_getClass(object), sel);
|
||||
types = method_getTypeEncoding(originalMethod);
|
||||
if (!types)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sig = [NSMethodSignature signatureWithObjCTypes: types];
|
||||
valueType = [sig getArgumentTypeAtIndex: 2];
|
||||
|
||||
switch (valueType[0])
|
||||
{
|
||||
OBJC_APPLY_ALL_TYPE_ENCODINGS(KVO_SET_IMPL_CASE);
|
||||
case '{':
|
||||
case '[': {
|
||||
size_t valueSize = objc_sizeof_type(valueType);
|
||||
if (valueSize > 6 * sizeof(uintptr_t))
|
||||
{
|
||||
[NSException raise: NSInvalidArgumentException
|
||||
format: @"Class %s key %@ has a value size of %u bytes"
|
||||
@", and cannot currently be KVO compliant.",
|
||||
class_getName(object_getClass(object)), key,
|
||||
(unsigned int) (valueSize)];
|
||||
}
|
||||
newImpl = (IMP) (¬ifyingVariadicSetImpl);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
[NSException raise: NSInvalidArgumentException
|
||||
format: @"Class %s is not KVO compliant for key %@.",
|
||||
class_getName(object_getClass(object)), key];
|
||||
return;
|
||||
}
|
||||
|
||||
_associateSelectorWithKey(object, sel, key);
|
||||
object_addMethod_np(object, sel, newImpl, types);
|
||||
}
|
||||
|
||||
static void
|
||||
replaceAndAssociateWithKey(id object, SEL sel, NSString *key, IMP imp)
|
||||
{
|
||||
if ([object respondsToSelector: sel])
|
||||
{
|
||||
const char *selName = sel_getName(sel);
|
||||
Method method = class_getInstanceMethod(object_getClass(object), sel);
|
||||
if (!method)
|
||||
{
|
||||
NSWarnLog(@"NSObject (NSKeyValueObservation): Unable to find method "
|
||||
@"for %s on class %s; perhaps it is a forward?",
|
||||
selName, object_getClassName(object));
|
||||
return;
|
||||
}
|
||||
|
||||
_associateSelectorWithKey(object, sel, key);
|
||||
object_replaceMethod_np(object, sel, imp, method_getTypeEncoding(method));
|
||||
}
|
||||
}
|
||||
|
||||
static SEL
|
||||
formatSelector(NSString *format, ...)
|
||||
{
|
||||
NSString *s;
|
||||
SEL sel;
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, format);
|
||||
s = [[NSString alloc] initWithFormat: format arguments: ap];
|
||||
sel = NSSelectorFromString([s autorelease]);
|
||||
va_end(ap);
|
||||
return sel;
|
||||
}
|
||||
|
||||
// invariant: rawKey has already been capitalized
|
||||
static inline void
|
||||
_NSKVOEnsureOrderedCollectionWillNotify(id object, NSString *key,
|
||||
const char *rawKey)
|
||||
{
|
||||
SEL insertOne = formatSelector(@"insertObject:in%sAtIndex:", rawKey);
|
||||
SEL insertMany = formatSelector(@"insert%s:atIndexes:", rawKey);
|
||||
|
||||
if ([object respondsToSelector: insertOne]
|
||||
|| [object respondsToSelector: insertMany])
|
||||
{
|
||||
replaceAndAssociateWithKey(object, insertOne, key,
|
||||
(IMP)NSKVONotifying$insertObject$inXxxAtIndex$);
|
||||
replaceAndAssociateWithKey(object, insertMany, key,
|
||||
(IMP)NSKVONotifying$insertXxx$atIndexes$);
|
||||
replaceAndAssociateWithKey(
|
||||
object, formatSelector(@"removeObjectFrom%sAtIndex:", rawKey), key,
|
||||
(IMP)NSKVONotifying$removeObjectFromXxxAtIndex$);
|
||||
replaceAndAssociateWithKey(object,
|
||||
formatSelector(@"remove%sAtIndexes:", rawKey),
|
||||
key, (IMP)NSKVONotifying$removeXxxAtIndexes$);
|
||||
replaceAndAssociateWithKey(
|
||||
object, formatSelector(@"replaceObjectIn%sAtIndex:withObject:", rawKey),
|
||||
key, (IMP)NSKVONotifying$replaceObjectInXxxAtIndex$withObject$);
|
||||
replaceAndAssociateWithKey(
|
||||
object, formatSelector(@"replace%sAtIndexes:with%s:", rawKey, rawKey),
|
||||
key, (IMP)NSKVONotifying$replaceXxxAtIndexes$withXxx$);
|
||||
}
|
||||
}
|
||||
|
||||
// invariant: rawKey has already been capitalized
|
||||
static inline void
|
||||
_NSKVOEnsureUnorderedCollectionWillNotify(id object, NSString *key,
|
||||
const char *rawKey)
|
||||
{
|
||||
SEL addOne = formatSelector(@"add%sObject:", rawKey);
|
||||
SEL addMany = formatSelector(@"add%s:", rawKey);
|
||||
SEL removeOne = formatSelector(@"remove%sObject:", rawKey);
|
||||
SEL removeMany = formatSelector(@"remove%s:", rawKey);
|
||||
|
||||
if (([object respondsToSelector: addOne]
|
||||
|| [object respondsToSelector: addMany])
|
||||
&& ([object respondsToSelector: removeOne]
|
||||
|| [object respondsToSelector: removeMany]))
|
||||
{
|
||||
replaceAndAssociateWithKey(
|
||||
object, addOne, key,
|
||||
(IMP)_NSKVOSetDispatchIndividual_NSKeyValueUnionSetMutation);
|
||||
replaceAndAssociateWithKey(
|
||||
object, addMany, key,
|
||||
(IMP)_NSKVOSetDispatch_NSKeyValueUnionSetMutation);
|
||||
replaceAndAssociateWithKey(
|
||||
object, removeOne, key,
|
||||
(IMP)_NSKVOSetDispatchIndividual_NSKeyValueMinusSetMutation);
|
||||
replaceAndAssociateWithKey(
|
||||
object, removeMany, key,
|
||||
(IMP)_NSKVOSetDispatch_NSKeyValueMinusSetMutation);
|
||||
replaceAndAssociateWithKey(
|
||||
object, formatSelector(@"intersect%s:", rawKey), key,
|
||||
(IMP)_NSKVOSetDispatch_NSKeyValueIntersectSetMutation);
|
||||
}
|
||||
}
|
||||
|
||||
char *
|
||||
mutableBufferFromString(NSString *string)
|
||||
{
|
||||
NSUInteger lengthInBytes = [string length] + 1;
|
||||
char *rawKey = (char *) malloc(lengthInBytes);
|
||||
|
||||
[string getCString: rawKey
|
||||
maxLength: lengthInBytes
|
||||
encoding: NSASCIIStringEncoding];
|
||||
return rawKey;
|
||||
}
|
||||
|
||||
// NSKVOEnsureKeyWillNotify is the main entrypoint into the swizzling code.
|
||||
void
|
||||
_NSKVOEnsureKeyWillNotify(id object, NSString *key)
|
||||
{
|
||||
char *rawKey;
|
||||
|
||||
// Since we cannot replace the isa of tagged pointer objects, we can't swizzle
|
||||
// them.
|
||||
if (isSmallObject_np(object))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A class is allowed to decline automatic swizzling for any/all of its keys.
|
||||
if (![[object class] automaticallyNotifiesObserversForKey: key])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rawKey = mutableBufferFromString(key);
|
||||
rawKey[0] = toupper(rawKey[0]);
|
||||
|
||||
@synchronized(object)
|
||||
{
|
||||
_NSKVOEnsureObjectIsKVOAware(object);
|
||||
_NSKVOEnsureSimpleKeyWillNotify(object, key, rawKey);
|
||||
_NSKVOEnsureOrderedCollectionWillNotify(object, key, rawKey);
|
||||
_NSKVOEnsureUnorderedCollectionWillNotify(object, key, rawKey);
|
||||
}
|
||||
|
||||
free(rawKey);
|
||||
}
|
90
Source/NSKVOTypeEncodingCases.h
Normal file
90
Source/NSKVOTypeEncodingCases.h
Normal file
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
NSKVOSwizzling.m
|
||||
|
||||
Copyright (C) 2024 Free Software Foundation, Inc.
|
||||
|
||||
Written by: Hugo Melder <hugo@algoriddim.com>
|
||||
Date: June 2024
|
||||
|
||||
Based on WinObjC KVO tests by Microsoft Corporation.
|
||||
|
||||
This file is part of GNUStep-base
|
||||
|
||||
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 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.
|
||||
|
||||
If you are interested in a warranty or support for this source code,
|
||||
contact Scott Christley <scottc@net-community.com> for more information.
|
||||
|
||||
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 USA.
|
||||
*/
|
||||
/**
|
||||
Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
This code is licensed under the MIT License (MIT).
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* This Key Value Observing Implementation is tied to libobjc2 */
|
||||
|
||||
#pragma once
|
||||
|
||||
// Each OBJC_APPLY_*_TYPE_ENCODINGS macro expects a single argument: the name
|
||||
// of a macro to apply for every type encoding. That macro should take the form
|
||||
// #define name(ctype, objectiveCName, CapitalizedObjectiveCName,
|
||||
// typeEncodingCharacter)
|
||||
|
||||
#if defined(_Bool)
|
||||
#define OBJC_APPLY_BOOL_TYPE_ENCODING(_APPLY_TYPE_MACRO) \
|
||||
_APPLY_TYPE_MACRO(_Bool, bool, Bool, 'B')
|
||||
#else
|
||||
#define OBJC_APPLY_BOOL_TYPE_ENCODING(_APPLY_TYPE_MACRO) // Nothing
|
||||
#endif
|
||||
|
||||
#define OBJC_APPLY_NUMERIC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \
|
||||
_APPLY_TYPE_MACRO(double, double, Double, 'd') \
|
||||
_APPLY_TYPE_MACRO(float, float, Float, 'f') \
|
||||
_APPLY_TYPE_MACRO(signed char, char, Char, 'c') \
|
||||
_APPLY_TYPE_MACRO(int, int, Int, 'i') \
|
||||
_APPLY_TYPE_MACRO(short, short, Short, 's') \
|
||||
_APPLY_TYPE_MACRO(long, long, Long, 'l') \
|
||||
_APPLY_TYPE_MACRO(long long, longLong, LongLong, 'q') \
|
||||
_APPLY_TYPE_MACRO(unsigned char, unsignedChar, UnsignedChar, 'C') \
|
||||
_APPLY_TYPE_MACRO(unsigned short, unsignedShort, UnsignedShort, 'S') \
|
||||
_APPLY_TYPE_MACRO(unsigned int, unsignedInt, UnsignedInt, 'I') \
|
||||
_APPLY_TYPE_MACRO(unsigned long, unsignedLong, UnsignedLong, 'L') \
|
||||
_APPLY_TYPE_MACRO(unsigned long long, unsignedLongLong, UnsignedLongLong, \
|
||||
'Q') \
|
||||
OBJC_APPLY_BOOL_TYPE_ENCODING(_APPLY_TYPE_MACRO)
|
||||
|
||||
//APPLY_TYPE(__int128, int128, Int128, 't') \
|
||||
//APPLY_TYPE(unsigned __int128, unsignedInt128, UnsignedInt128, 'T')
|
||||
|
||||
#define OBJC_APPLY_OBJECTIVEC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \
|
||||
_APPLY_TYPE_MACRO(id, object, Object, '@') \
|
||||
_APPLY_TYPE_MACRO(Class, class, Pointer, '#') \
|
||||
_APPLY_TYPE_MACRO(SEL, selector, Pointer, ':')
|
||||
#define OBJC_APPLY_POINTER_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \
|
||||
_APPLY_TYPE_MACRO(char *, cString, Pointer, '*')
|
||||
|
||||
#define OBJC_APPLY_ALL_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \
|
||||
OBJC_APPLY_NUMERIC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \
|
||||
OBJC_APPLY_OBJECTIVEC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \
|
||||
OBJC_APPLY_POINTER_TYPE_ENCODINGS(_APPLY_TYPE_MACRO)
|
|
@ -143,6 +143,7 @@ SetValueForKey(NSObject *self, id anObject, const char *key, unsigned size)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
GSObjCSetVal(self, key, anObject, sel, type, size, off);
|
||||
}
|
||||
|
||||
|
@ -350,6 +351,8 @@ static id ValueForKey(NSObject *self, const char *key, unsigned size)
|
|||
{
|
||||
unsigned size = [aKey length] * 8;
|
||||
char key[size + 1];
|
||||
BOOL shouldNotify = [[self class] automaticallyNotifiesObserversForKey:aKey];
|
||||
|
||||
#ifdef WANT_DEPRECATED_KVC_COMPAT
|
||||
IMP o = [self methodForSelector: @selector(takeValue:forKey:)];
|
||||
|
||||
|
@ -365,7 +368,18 @@ static id ValueForKey(NSObject *self, const char *key, unsigned size)
|
|||
maxLength: size + 1
|
||||
encoding: NSUTF8StringEncoding];
|
||||
size = strlen(key);
|
||||
|
||||
if (shouldNotify)
|
||||
{
|
||||
[self willChangeValueForKey: aKey];
|
||||
}
|
||||
|
||||
SetValueForKey(self, anObject, key, size);
|
||||
|
||||
if (shouldNotify)
|
||||
{
|
||||
[self didChangeValueForKey: aKey];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -103,6 +103,8 @@
|
|||
}
|
||||
|
||||
|
||||
/* Ordering as specified in "Accessor Search Patterns" from Key-Value Coding
|
||||
* Programming Guide */
|
||||
proxy = [NSKeyValueFastMutableSet setForKey: aKey
|
||||
ofObject: anObject
|
||||
withCapitalizedKey: keybuf];
|
||||
|
@ -717,14 +719,14 @@
|
|||
{
|
||||
[object willChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueUnionSetMutation
|
||||
usingObjects: [NSSet setWithObject: anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
[set unionSet: anObject];
|
||||
if (notifiesObservers && !changeInProgress)
|
||||
{
|
||||
[object didChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueUnionSetMutation
|
||||
usingObjects: [NSSet setWithObject:anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -734,14 +736,14 @@
|
|||
{
|
||||
[object willChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueMinusSetMutation
|
||||
usingObjects: [NSSet setWithObject: anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
[set minusSet: anObject];
|
||||
if (notifiesObservers && !changeInProgress)
|
||||
{
|
||||
[object didChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueMinusSetMutation
|
||||
usingObjects: [NSSet setWithObject: anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -751,14 +753,14 @@
|
|||
{
|
||||
[object willChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueIntersectSetMutation
|
||||
usingObjects: [NSSet setWithObject: anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
[set intersectSet: anObject];
|
||||
if (notifiesObservers && !changeInProgress)
|
||||
{
|
||||
[object didChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueIntersectSetMutation
|
||||
usingObjects: [NSSet setWithObject: anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -768,14 +770,14 @@
|
|||
{
|
||||
[object willChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueSetSetMutation
|
||||
usingObjects: [NSSet setWithObject: anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
[set setSet: anObject];
|
||||
if (notifiesObservers && !changeInProgress)
|
||||
{
|
||||
[object didChangeValueForKey: key
|
||||
withSetMutation: NSKeyValueSetSetMutation
|
||||
usingObjects: [NSSet setWithObject: anObject]];
|
||||
usingObjects: anObject];
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
|
|
@ -2071,6 +2071,60 @@ cifframe_callback(ffi_cif *cif, void *retp, void **args, void *user)
|
|||
|
||||
@end
|
||||
|
||||
@implementation NSObject (NSKeyValueObservingPrivate)
|
||||
|
||||
- (void)_notifyObserversOfChangeForKey:(NSString *)aKey
|
||||
oldValue:(id)old
|
||||
newValue:(id)new
|
||||
{
|
||||
GSKVOPathInfo *pathInfo;
|
||||
GSKVOInfo *info;
|
||||
|
||||
info = (GSKVOInfo *)[self observationInfo];
|
||||
if (info == nil)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (new == nil)
|
||||
{
|
||||
new = null;
|
||||
}
|
||||
if (old == nil)
|
||||
{
|
||||
old = null;
|
||||
}
|
||||
|
||||
pathInfo = [info lockReturningPathInfoForKey: aKey];
|
||||
if (pathInfo != nil)
|
||||
{
|
||||
if (pathInfo->recursion++ == 0)
|
||||
{
|
||||
[pathInfo->change setObject: old
|
||||
forKey: NSKeyValueChangeOldKey];
|
||||
[pathInfo->change removeObjectForKey: NSKeyValueChangeNewKey];
|
||||
[pathInfo->change setValue:
|
||||
[NSNumber numberWithInt: NSKeyValueChangeSetting]
|
||||
forKey: NSKeyValueChangeKindKey];
|
||||
[pathInfo notifyForKey: aKey ofInstance: [info instance] prior: YES];
|
||||
|
||||
[pathInfo->change setValue: new
|
||||
forKey: NSKeyValueChangeNewKey];
|
||||
[pathInfo notifyForKey: aKey ofInstance: [info instance] prior: NO];
|
||||
}
|
||||
if (pathInfo->recursion > 0)
|
||||
{
|
||||
pathInfo->recursion--;
|
||||
}
|
||||
[info unlock];
|
||||
}
|
||||
|
||||
[self willChangeValueForDependentsOfKey: aKey];
|
||||
[self didChangeValueForDependentsOfKey: aKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSObject (NSKeyValueObservingCustomization)
|
||||
|
||||
+ (BOOL) automaticallyNotifiesObserversForKey: (NSString*)aKey
|
||||
|
|
|
@ -951,12 +951,12 @@ gs_mutex_lock(gs_mutex_t *mutex)
|
|||
{
|
||||
assert(mutex->depth == 0);
|
||||
mutex->depth = 1;
|
||||
atomic_store(&mutex->owner, thisThread);
|
||||
gs_atomic_store(&mutex->owner, thisThread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// needs to be atomic because another thread can concurrently set it
|
||||
ownerThread = atomic_load(&mutex->owner);
|
||||
ownerThread = gs_atomic_load(&mutex->owner);
|
||||
if (ownerThread == thisThread)
|
||||
{
|
||||
// this thread already owns this lock
|
||||
|
@ -985,7 +985,7 @@ gs_mutex_lock(gs_mutex_t *mutex)
|
|||
AcquireSRWLockExclusive(&mutex->lock);
|
||||
assert(mutex->depth == 0);
|
||||
mutex->depth = 1;
|
||||
atomic_store(&mutex->owner, thisThread);
|
||||
gs_atomic_store(&mutex->owner, thisThread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -999,12 +999,12 @@ gs_mutex_trylock(gs_mutex_t *mutex)
|
|||
{
|
||||
assert(mutex->depth == 0);
|
||||
mutex->depth = 1;
|
||||
atomic_store(&mutex->owner, thisThread);
|
||||
gs_atomic_store(&mutex->owner, thisThread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// needs to be atomic because another thread can concurrently set it
|
||||
ownerThread = atomic_load(&mutex->owner);
|
||||
ownerThread = gs_atomic_load(&mutex->owner);
|
||||
if (ownerThread == thisThread && mutex->attr == gs_mutex_attr_recursive)
|
||||
{
|
||||
// this thread already owns this lock and it's recursive
|
||||
|
@ -1028,7 +1028,7 @@ gs_mutex_unlock(gs_mutex_t *mutex)
|
|||
case gs_mutex_attr_recursive: {
|
||||
// return error if lock is not held by this thread
|
||||
DWORD thisThread = GetCurrentThreadId();
|
||||
DWORD ownerThread = atomic_load(&mutex->owner);
|
||||
DWORD ownerThread = gs_atomic_load(&mutex->owner);
|
||||
if (ownerThread != thisThread) {
|
||||
return EPERM;
|
||||
}
|
||||
|
@ -1046,7 +1046,7 @@ gs_mutex_unlock(gs_mutex_t *mutex)
|
|||
{
|
||||
assert(mutex->depth == 1);
|
||||
mutex->depth = 0;
|
||||
atomic_store(&mutex->owner, 0);
|
||||
gs_atomic_store(&mutex->owner, 0);
|
||||
ReleaseSRWLockExclusive(&mutex->lock);
|
||||
return 0;
|
||||
}
|
||||
|
@ -1060,7 +1060,7 @@ gs_cond_timedwait(gs_cond_t *cond, gs_mutex_t *mutex, DWORD millisecs)
|
|||
|
||||
assert(mutex->depth == 1);
|
||||
mutex->depth = 0;
|
||||
atomic_store(&mutex->owner, 0);
|
||||
gs_atomic_store(&mutex->owner, 0);
|
||||
|
||||
if (!SleepConditionVariableSRW(cond, &mutex->lock, millisecs, 0))
|
||||
{
|
||||
|
@ -1074,7 +1074,7 @@ gs_cond_timedwait(gs_cond_t *cond, gs_mutex_t *mutex, DWORD millisecs)
|
|||
|
||||
assert(mutex->depth == 0);
|
||||
mutex->depth = 1;
|
||||
atomic_store(&mutex->owner, GetCurrentThreadId());
|
||||
gs_atomic_store(&mutex->owner, GetCurrentThreadId());
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,28 @@
|
|||
#import "Foundation/NSException.h"
|
||||
#import "Foundation/NSCoder.h"
|
||||
|
||||
|
||||
static inline unsigned int
|
||||
gs_string_hash(const char *s)
|
||||
{
|
||||
unsigned int val = 0;
|
||||
while (*s != 0)
|
||||
{
|
||||
val = (val << 5) + val + *s++;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
#define GSI_MAP_RETAIN_KEY(M, X)
|
||||
#define GSI_MAP_RELEASE_KEY(M, X)
|
||||
#define GSI_MAP_HASH(M, X) (gs_string_hash(X.ptr))
|
||||
#define GSI_MAP_EQUAL(M, X,Y) (strcmp(X.ptr, Y.ptr) == 0)
|
||||
#define GSI_MAP_KTYPES GSUNION_PTR
|
||||
#define GSI_MAP_VTYPES GSUNION_OBJ
|
||||
#import "GNUstepBase/GSIMap.h"
|
||||
|
||||
#import "GSInvocation.h"
|
||||
#import "GSPThread.h"
|
||||
|
||||
#ifdef HAVE_MALLOC_H
|
||||
#if !defined(__OpenBSD__)
|
||||
|
@ -555,7 +576,29 @@ next_arg(const char *typePtr, NSArgumentInfo *info, char *outTypes)
|
|||
|
||||
+ (NSMethodSignature*) signatureWithObjCTypes: (const char*)t
|
||||
{
|
||||
return AUTORELEASE([[[self class] alloc] _initWithObjCTypes: t]);
|
||||
GSIMapNode node;
|
||||
NSMethodSignature *sig;
|
||||
|
||||
static GSIMapTable_t cacheTable = {};
|
||||
static gs_mutex_t cacheTableLock = GS_MUTEX_INIT_STATIC;
|
||||
|
||||
GS_MUTEX_LOCK(cacheTableLock);
|
||||
if (cacheTable.zone == 0)
|
||||
{
|
||||
GSIMapInitWithZoneAndCapacity(&cacheTable, [self zone], 8);
|
||||
}
|
||||
|
||||
node = GSIMapNodeForKey(&cacheTable, (GSIMapKey)t);
|
||||
if (node == 0)
|
||||
{
|
||||
sig = [[self alloc] _initWithObjCTypes: t];
|
||||
GSIMapAddPair(&cacheTable, (GSIMapKey)t, (GSIMapVal)(id)sig);
|
||||
} else {
|
||||
sig = RETAIN(node->value.obj);
|
||||
}
|
||||
GS_MUTEX_UNLOCK(cacheTableLock);
|
||||
|
||||
return AUTORELEASE(sig);
|
||||
}
|
||||
|
||||
- (NSArgumentInfo) argumentInfoAtIndex: (NSUInteger)index
|
||||
|
|
|
@ -54,6 +54,8 @@
|
|||
#import "GNUstepBase/NSProcessInfo+GNUstepBase.h"
|
||||
#import "GNUstepBase/NSString+GNUstepBase.h"
|
||||
|
||||
#import "NSKVOInternal.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#import "win32/NSString+Win32Additions.h"
|
||||
|
||||
|
@ -359,15 +361,15 @@ systemLanguages()
|
|||
NSMutableArray *names = [NSMutableArray arrayWithCapacity: 10];
|
||||
|
||||
#if defined(_WIN32)
|
||||
NSEnumerator *enumerator;
|
||||
NSArray *languages;
|
||||
NSString *locale;
|
||||
BOOL ret;
|
||||
|
||||
NSEnumerator *enumerator;
|
||||
NSArray *languages;
|
||||
NSString *locale;
|
||||
BOOL ret;
|
||||
unsigned long numberOfLanguages = 0;
|
||||
unsigned long length = 7;
|
||||
unsigned long factor = sizeof(wchar_t);
|
||||
wchar_t *buffer = malloc(length * factor);
|
||||
wchar_t *buffer = malloc(length * factor);
|
||||
|
||||
if (!buffer)
|
||||
{
|
||||
return names;
|
||||
|
@ -377,56 +379,61 @@ systemLanguages()
|
|||
* two-letter language code, and CC is the two-letter country code.
|
||||
*/
|
||||
ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages,
|
||||
buffer, &length);
|
||||
buffer, &length);
|
||||
if (!ret)
|
||||
{
|
||||
length = 0;
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER &&
|
||||
GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages,
|
||||
NULL, &length)) {
|
||||
wchar_t *oldBuffer = buffer;
|
||||
buffer = realloc(buffer, length * factor);
|
||||
if (!buffer)
|
||||
{
|
||||
free(oldBuffer);
|
||||
return names;
|
||||
}
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER
|
||||
&& GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages,
|
||||
NULL, &length))
|
||||
{
|
||||
wchar_t *oldBuffer = buffer;
|
||||
|
||||
ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, buffer, &length);
|
||||
if (!ret)
|
||||
{
|
||||
free(buffer);
|
||||
return names;
|
||||
}
|
||||
}
|
||||
buffer = realloc(buffer, length * factor);
|
||||
if (!buffer)
|
||||
{
|
||||
free(oldBuffer);
|
||||
return names;
|
||||
}
|
||||
|
||||
ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME,
|
||||
&numberOfLanguages, buffer, &length);
|
||||
if (!ret)
|
||||
{
|
||||
free(buffer);
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
languages = [NSString arrayFromWCharList:buffer length:length];
|
||||
languages = [NSString arrayFromWCharList: buffer length: length];
|
||||
enumerator = [languages objectEnumerator];
|
||||
free(buffer);
|
||||
|
||||
while (nil != (locale = [enumerator nextObject]))
|
||||
{
|
||||
while (nil != (locale = [enumerator nextObject]))
|
||||
{
|
||||
/* Replace "-" Separator with "_" */
|
||||
locale = [locale stringByReplacingOccurrencesOfString:@"-" withString:@"_"];
|
||||
[names addObjectsFromArray: GSLanguagesFromLocale(locale)];
|
||||
}
|
||||
locale = [locale stringByReplacingOccurrencesOfString: @"-"
|
||||
withString: @"_"];
|
||||
[names addObjectsFromArray: GSLanguagesFromLocale(locale)];
|
||||
}
|
||||
#elif defined(__ANDROID__)
|
||||
// When running on Android, the process must be correctly initialized
|
||||
// with GSInitializeProcessAndroid (See NSProcessInfo).
|
||||
//
|
||||
// If the minimum API level is 24 or higher, the user-prefered locales
|
||||
// are retrieved from the Android system and passed as GSAndroidLocaleList
|
||||
// process argument
|
||||
NSArray *args = [[NSProcessInfo processInfo] arguments];
|
||||
NSEnumerator *enumerator = [args objectEnumerator];
|
||||
NSString *key = nil;
|
||||
NSString *localeList = nil;
|
||||
/* When running on Android, the process must be correctly initialized
|
||||
* with GSInitializeProcessAndroid (See NSProcessInfo).
|
||||
*
|
||||
* If the minimum API level is 24 or higher, the user-prefered locales
|
||||
* are retrieved from the Android system and passed as GSAndroidLocaleList
|
||||
* process argument
|
||||
*/
|
||||
NSArray *args = [[NSProcessInfo processInfo] arguments];
|
||||
NSEnumerator *enumerator = [args objectEnumerator];
|
||||
NSString *key = nil;
|
||||
NSString *localeList = nil;
|
||||
|
||||
[enumerator nextObject]; // Skip process name.
|
||||
while (nil != (key = [enumerator nextObject]))
|
||||
{
|
||||
if ([key isEqualToString:@"-GSAndroidLocaleList"])
|
||||
if ([key isEqualToString: @"-GSAndroidLocaleList"])
|
||||
{
|
||||
localeList = [enumerator nextObject];
|
||||
break;
|
||||
|
@ -436,8 +443,8 @@ systemLanguages()
|
|||
// The locale list is a comma-separated list of locales of form ll-CC
|
||||
if (localeList != nil)
|
||||
{
|
||||
NSString *locale;
|
||||
NSArray *locales = [localeList componentsSeparatedByString: @","];
|
||||
NSString *locale;
|
||||
NSArray *locales = [localeList componentsSeparatedByString: @","];
|
||||
|
||||
enumerator = [locales objectEnumerator];
|
||||
while (nil != (locale = [enumerator nextObject]))
|
||||
|
@ -446,16 +453,19 @@ systemLanguages()
|
|||
}
|
||||
}
|
||||
#else
|
||||
// Add the languages listed in the LANGUAGE environment variable
|
||||
// (a non-POSIX GNU extension)
|
||||
/* Add the languages listed in the LANGUAGE environment variable
|
||||
* (a non-POSIX GNU extension)
|
||||
*/
|
||||
{
|
||||
NSString *env = [[[NSProcessInfo processInfo] environment]
|
||||
objectForKey: @"LANGUAGE"];
|
||||
NSString *env;
|
||||
|
||||
env = [[[NSProcessInfo processInfo] environment] objectForKey: @"LANGUAGE"];
|
||||
if (env != nil && [env length] > 0)
|
||||
{
|
||||
NSArray *array = [env componentsSeparatedByString: @":"];
|
||||
NSEnumerator *enumerator = [array objectEnumerator];
|
||||
NSString *locale;
|
||||
NSArray *array = [env componentsSeparatedByString: @":"];
|
||||
NSEnumerator *enumerator = [array objectEnumerator];
|
||||
NSString *locale;
|
||||
|
||||
while (nil != (locale = [enumerator nextObject]))
|
||||
{
|
||||
[names addObjectsFromArray: GSLanguagesFromLocale(locale)];
|
||||
|
@ -656,6 +666,23 @@ newLanguages(NSArray *oldNames)
|
|||
DESTROY(syncLock);
|
||||
}
|
||||
|
||||
/* Opt-out off automatic willChange/didChange notifications
|
||||
* as the KVO behaviour for NSUserDefaults is slightly different.
|
||||
*
|
||||
* We do not notify observers of changes that do not actually
|
||||
* change the value of a key (the value is equal to the old value).
|
||||
*
|
||||
* https://developer.apple.com/documentation/foundation/nsuserdefaults#2926902
|
||||
* "You can use key-value observing to be notified of any updates to
|
||||
* a particular default value. You can also register as an observer for
|
||||
* NSUserDefaultsDidChangeNotification on the defaultCenter notification center
|
||||
* in order to be notified of all updates to a local defaults database."
|
||||
*/
|
||||
+ (BOOL) automaticallyNotifiesObserversForKey: (NSString*)key
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void) initialize
|
||||
{
|
||||
if (self == [NSUserDefaults class])
|
||||
|
@ -1494,12 +1521,35 @@ newLanguages(NSArray *oldNames)
|
|||
NS_DURING
|
||||
{
|
||||
GSPersistentDomain *pd = [_persDomains objectForKey: processName];
|
||||
id old = [self objectForKey: defaultName];
|
||||
|
||||
if (nil != pd)
|
||||
{
|
||||
if ([pd setObject: nil forKey: defaultName])
|
||||
if ([pd setObject: nil forKey: defaultName])
|
||||
{
|
||||
id new;
|
||||
[self _changePersistentDomain: processName];
|
||||
new = [self objectForKey: defaultName];
|
||||
/* Emit only a KVO notification when the value has actually
|
||||
* changed, meaning -objectForKey: would return a different
|
||||
* value than before.
|
||||
*/
|
||||
if ([new hash] != [old hash])
|
||||
{
|
||||
[self _notifyObserversOfChangeForKey: defaultName
|
||||
oldValue: old
|
||||
newValue: new];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We always notify observers of a change, even if the value
|
||||
* itself is unchanged.
|
||||
*/
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName: NSUserDefaultsDidChangeNotification
|
||||
object: self];
|
||||
|
||||
}
|
||||
}
|
||||
[_lock unlock];
|
||||
|
@ -1619,6 +1669,7 @@ static BOOL isPlistObject(id o)
|
|||
NS_DURING
|
||||
{
|
||||
GSPersistentDomain *pd;
|
||||
id old;
|
||||
|
||||
pd = [_persDomains objectForKey: processName];
|
||||
if (nil == pd)
|
||||
|
@ -1628,9 +1679,35 @@ static BOOL isPlistObject(id o)
|
|||
[_persDomains setObject: pd forKey: processName];
|
||||
RELEASE(pd);
|
||||
}
|
||||
// Make sure to search all domains and not only the process domain
|
||||
old = [self objectForKey: defaultName];
|
||||
if ([pd setObject: value forKey: defaultName])
|
||||
{
|
||||
id new;
|
||||
|
||||
/* New value must be fetched from all domains, as there might be
|
||||
* a registered default if value is nil, or the value is
|
||||
* superseded by GSPrimary or NSArgumentDomain
|
||||
*/
|
||||
new = [self objectForKey: defaultName];
|
||||
[self _changePersistentDomain: processName];
|
||||
|
||||
// Emit only a KVO notification when the value has actually changed
|
||||
if ([new hash] != [old hash])
|
||||
{
|
||||
[self _notifyObserversOfChangeForKey: defaultName
|
||||
oldValue: old
|
||||
newValue: new];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We always notify observers of a change, even if the value
|
||||
* itself is unchanged.
|
||||
*/
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName: NSUserDefaultsDidChangeNotification
|
||||
object: self];
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
|
0
Tests/base/NSKVOSupport/TestInfo
Normal file
0
Tests/base/NSKVOSupport/TestInfo
Normal file
120
Tests/base/NSKVOSupport/basic.m
Normal file
120
Tests/base/NSKVOSupport/basic.m
Normal file
|
@ -0,0 +1,120 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import "ObjectTesting.h"
|
||||
|
||||
@interface Foo : NSObject
|
||||
{
|
||||
@public
|
||||
BOOL a;
|
||||
NSInteger b;
|
||||
NSString *c;
|
||||
NSArray *d;
|
||||
}
|
||||
- (void) setA: (BOOL)v;
|
||||
- (void) setB: (NSInteger)v;
|
||||
- (void) setC: (NSString *)v;
|
||||
@end
|
||||
|
||||
@implementation Foo
|
||||
- (void) setA: (BOOL)v
|
||||
{
|
||||
a = v;
|
||||
}
|
||||
- (void) setB: (NSInteger)v
|
||||
{
|
||||
b = v;
|
||||
}
|
||||
- (void) setC: (NSString *)v
|
||||
{
|
||||
c = v;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface Observer : NSObject
|
||||
{
|
||||
Foo *object;
|
||||
NSString *expectedKeyPath;
|
||||
NSInteger receivedCalls;
|
||||
}
|
||||
- (NSString*) expectedKeyPath;
|
||||
- (void) setExpectedKeyPath: (NSString*)s;
|
||||
- (NSInteger) receivedCalls;
|
||||
- (void) setReceivedCalls: (NSInteger)i;
|
||||
@end
|
||||
|
||||
@implementation Observer
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
receivedCalls = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static char observerContext;
|
||||
|
||||
- (void)startObserving:(Foo *)target
|
||||
{
|
||||
object = target;
|
||||
[target addObserver:self forKeyPath:@"a" options:0 context:&observerContext];
|
||||
[target addObserver:self forKeyPath:@"b" options:0 context:&observerContext];
|
||||
[target addObserver:self forKeyPath:@"c" options:0 context:&observerContext];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)o
|
||||
change:(NSDictionary *)change
|
||||
context:(void *)context
|
||||
{
|
||||
PASS(context == &observerContext, "context");
|
||||
PASS(o == self->object, "object");
|
||||
PASS([keyPath isEqualToString: [self expectedKeyPath]], "key path");
|
||||
[self setReceivedCalls: [self receivedCalls] + 1];
|
||||
}
|
||||
|
||||
- (NSString*) expectedKeyPath
|
||||
{
|
||||
return expectedKeyPath;
|
||||
}
|
||||
- (void) setExpectedKeyPath: (NSString*)s
|
||||
{
|
||||
expectedKeyPath = s;
|
||||
}
|
||||
- (NSInteger) receivedCalls
|
||||
{
|
||||
return receivedCalls;
|
||||
}
|
||||
- (void) setReceivedCalls: (NSInteger)i
|
||||
{
|
||||
receivedCalls = i;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
[NSAutoreleasePool new];
|
||||
|
||||
Foo *foo = [Foo new];
|
||||
Observer *obs = [Observer new];
|
||||
|
||||
[obs startObserving: foo];
|
||||
|
||||
[obs setExpectedKeyPath: @"a"];
|
||||
[foo setA: YES];
|
||||
PASS([obs receivedCalls] == 1, "received calls")
|
||||
|
||||
[obs setExpectedKeyPath: @"b"];
|
||||
[foo setB: 1];
|
||||
PASS([obs receivedCalls] == 2, "received calls")
|
||||
|
||||
[obs setExpectedKeyPath: @"c"];
|
||||
[foo setC: @"henlo"];
|
||||
PASS([obs receivedCalls] == 3, "received calls")
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
2172
Tests/base/NSKVOSupport/general.m
Normal file
2172
Tests/base/NSKVOSupport/general.m
Normal file
File diff suppressed because it is too large
Load diff
1242
Tests/base/NSKVOSupport/kvoToMany.m
Normal file
1242
Tests/base/NSKVOSupport/kvoToMany.m
Normal file
File diff suppressed because it is too large
Load diff
239
Tests/base/NSKVOSupport/newoldvalues.m
Normal file
239
Tests/base/NSKVOSupport/newoldvalues.m
Normal file
|
@ -0,0 +1,239 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import "ObjectTesting.h"
|
||||
#import "Testing.h"
|
||||
|
||||
#if defined (__OBJC2__)
|
||||
#define FLAKY_ON_GCC_START
|
||||
#define FLAKY_ON_GCC_END
|
||||
#else
|
||||
#define FLAKY_ON_GCC_START \
|
||||
testHopeful = YES;
|
||||
#define FLAKY_ON_GCC_END \
|
||||
testHopeful = NO;
|
||||
#endif
|
||||
|
||||
@class Bar;
|
||||
|
||||
@interface Foo : NSObject
|
||||
{
|
||||
Bar *globalBar;
|
||||
NSInteger a;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface Bar : NSObject
|
||||
{
|
||||
NSInteger x;
|
||||
Foo *firstFoo;
|
||||
Foo *secondFoo;
|
||||
}
|
||||
- (NSInteger) x;
|
||||
@end
|
||||
|
||||
@implementation Foo
|
||||
|
||||
+ (NSSet *) keyPathsForValuesAffectingB
|
||||
{
|
||||
return [NSSet setWithArray: [NSArray arrayWithObjects:
|
||||
@"a", @"globalBar.x", nil]];
|
||||
}
|
||||
|
||||
- (NSInteger) a
|
||||
{
|
||||
return a;
|
||||
}
|
||||
- (void) setA: (NSInteger)v
|
||||
{
|
||||
a = v;
|
||||
}
|
||||
- (NSInteger) b
|
||||
{
|
||||
return [self a] + [globalBar x];
|
||||
}
|
||||
- (Bar*) globalBar
|
||||
{
|
||||
return globalBar;
|
||||
}
|
||||
- (void) setGlobalBar: (Bar*)v
|
||||
{
|
||||
globalBar = v;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation Bar
|
||||
|
||||
- (Foo*) firstFoo
|
||||
{
|
||||
return firstFoo;
|
||||
}
|
||||
- (void) setFirstFoo: (Foo*)v
|
||||
{
|
||||
firstFoo = v;
|
||||
}
|
||||
- (Foo*) secondFoo
|
||||
{
|
||||
return secondFoo;
|
||||
}
|
||||
- (void) setSecondFoo: (Foo*)v
|
||||
{
|
||||
secondFoo = v;
|
||||
}
|
||||
- (NSInteger) x
|
||||
{
|
||||
return x;
|
||||
}
|
||||
- (void) setX: (NSInteger)v
|
||||
{
|
||||
x = v;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
[self setFirstFoo: [Foo new]];
|
||||
[[self firstFoo] setGlobalBar: self];
|
||||
[self setSecondFoo: [Foo new]];
|
||||
[[self secondFoo] setGlobalBar: self];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface Observer : NSObject
|
||||
{
|
||||
Foo *object;
|
||||
NSInteger expectedOldValue;
|
||||
NSInteger expectedNewValue;
|
||||
NSInteger receivedCalls;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation Observer
|
||||
|
||||
- (NSInteger) expectedOldValue
|
||||
{
|
||||
return expectedOldValue;
|
||||
}
|
||||
- (void) setExpectedOldValue: (NSInteger)v
|
||||
{
|
||||
expectedOldValue = v;
|
||||
}
|
||||
- (NSInteger) expectedNewValue
|
||||
{
|
||||
return expectedNewValue;
|
||||
}
|
||||
- (void) setExpectedNewValue: (NSInteger)v
|
||||
{
|
||||
expectedNewValue = v;
|
||||
}
|
||||
- (Foo*) object
|
||||
{
|
||||
return object;
|
||||
}
|
||||
- (void) setObject: (Foo*)v
|
||||
{
|
||||
object = v;
|
||||
}
|
||||
- (NSInteger) receivedCalls
|
||||
{
|
||||
return receivedCalls;
|
||||
}
|
||||
- (void) setReceivedCalls: (NSInteger)v
|
||||
{
|
||||
receivedCalls = v;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
[self setReceivedCalls: 0];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static char observerContext;
|
||||
|
||||
- (void) startObserving:(Foo *)target
|
||||
{
|
||||
[self setObject: target];
|
||||
[target
|
||||
addObserver:self
|
||||
forKeyPath:@"b"
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
context:&observerContext];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)o
|
||||
change:(NSDictionary *)change
|
||||
context:(void *)context
|
||||
{
|
||||
PASS(context == &observerContext, "context is correct");
|
||||
PASS(o == [self object], "object is correct");
|
||||
|
||||
id newValue = [change objectForKey: NSKeyValueChangeNewKey];
|
||||
id oldValue = [change objectForKey: NSKeyValueChangeOldKey];
|
||||
|
||||
PASS([oldValue integerValue] == self.expectedOldValue,
|
||||
"new value in change dict");
|
||||
PASS([newValue integerValue] == self.expectedNewValue,
|
||||
"old value in change dict");
|
||||
[self setReceivedCalls: [self receivedCalls] + 1];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
NSAutoreleasePool *arp = [NSAutoreleasePool new];
|
||||
|
||||
START_SET("newoldvalues");
|
||||
FLAKY_ON_GCC_START
|
||||
|
||||
Bar *bar = [Bar new];
|
||||
[bar setX: 0];
|
||||
[[bar firstFoo] setA: 1];
|
||||
[[bar secondFoo] setA: 2];
|
||||
|
||||
Observer *obs1 = [Observer new];
|
||||
Observer *obs2 = [Observer new];
|
||||
[obs1 startObserving: [bar firstFoo]];
|
||||
[obs2 startObserving: [bar secondFoo]];
|
||||
|
||||
[obs1 setExpectedOldValue: 1];
|
||||
[obs1 setExpectedNewValue: 2];
|
||||
[obs2 setExpectedOldValue: 2];
|
||||
[obs2 setExpectedNewValue: 3];
|
||||
[bar setX: 1];
|
||||
PASS(obs1.receivedCalls == 1, "num observe calls");
|
||||
PASS(obs2.receivedCalls == 1, "num observe calls");
|
||||
|
||||
[obs1 setExpectedOldValue: 2];
|
||||
[obs1 setExpectedNewValue: 2];
|
||||
[obs2 setExpectedOldValue: 3];
|
||||
[obs2 setExpectedNewValue: 3];
|
||||
[bar setX: 1];
|
||||
PASS([obs1 receivedCalls] == 2, "num observe calls");
|
||||
PASS([obs2 receivedCalls] == 2, "num observe calls");
|
||||
|
||||
[obs1 setExpectedOldValue: 2];
|
||||
[obs1 setExpectedNewValue: 3];
|
||||
[[bar firstFoo] setA: 2];
|
||||
PASS([obs1 receivedCalls] == 3, "num observe calls");
|
||||
PASS([obs2 receivedCalls] == 2, "num observe calls");
|
||||
|
||||
FLAKY_ON_GCC_END
|
||||
END_SET("newoldvalues");
|
||||
|
||||
|
||||
DESTROY(arp);
|
||||
|
||||
return 0;
|
||||
}
|
173
Tests/base/NSKVOSupport/userdefaults.m
Normal file
173
Tests/base/NSKVOSupport/userdefaults.m
Normal file
|
@ -0,0 +1,173 @@
|
|||
#import <Foundation/NSObject.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSValue.h>
|
||||
#import <Foundation/NSNull.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSAutoreleasePool.h>
|
||||
#import <Foundation/NSUserDefaults.h>
|
||||
#import <Foundation/NSKeyValueObserving.h>
|
||||
|
||||
#import <Testing.h>
|
||||
|
||||
/* NSUserDefaults KeyValueObserving Tests
|
||||
*
|
||||
* Behaviour was validated on macOS 15.0.1 (24A348)
|
||||
*/
|
||||
|
||||
@interface Observer : NSObject
|
||||
{
|
||||
@public
|
||||
NSInteger called;
|
||||
NSString *lastKeyPath;
|
||||
id lastObject;
|
||||
NSDictionary *lastChange;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation Observer
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary *)change
|
||||
context:(void *)context
|
||||
{
|
||||
called++;
|
||||
ASSIGN(lastKeyPath, keyPath);
|
||||
ASSIGN(lastObject, object);
|
||||
ASSIGN(lastChange, change);
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
RELEASE(lastKeyPath);
|
||||
RELEASE(lastObject);
|
||||
RELEASE(lastChange);
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// NSUserDefaults Domain Search List:
|
||||
// NSArgumentDomain
|
||||
// Application Domain
|
||||
// NSGlobalDomain
|
||||
// NSRegistrationDomain
|
||||
//
|
||||
// Terminology:
|
||||
// - Entry: An entry is a key value pair.
|
||||
// - Object and Value: both used interchangeably in the NSUserDefaults API to
|
||||
// describe the value associated with a given key.
|
||||
//
|
||||
// Note that -removeObjectForKey: and -setObject:ForKey: emit only a
|
||||
// KVO notification when the value has actually changed, meaning
|
||||
// -objectForKey: would return a different value than before.
|
||||
//
|
||||
// Example:
|
||||
// Assume that a key with the same value is registered in both NSArgumentDomain
|
||||
// and the application domain. If we remove the value with -removeObjectForKey:,
|
||||
// we set the value for the key in the application domain to nil, but we stil
|
||||
// have an entry in the NSArgumentDomain. Thus -objectForKey will return the
|
||||
// same value as before and no change notification is emitted.
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
NSAutoreleasePool *pool = [NSAutoreleasePool new];
|
||||
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
|
||||
Observer *obs = [Observer new];
|
||||
NSString *key1 = @"key1";
|
||||
NSString *value1 = @"value1";
|
||||
NSString *key2 = @"key2";
|
||||
NSString *value2 = @"value2";
|
||||
NSString *value2Alt = @"value2Alt";
|
||||
|
||||
[defs addObserver:obs
|
||||
forKeyPath:key1
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
context:NULL];
|
||||
[defs addObserver:obs
|
||||
forKeyPath:key2
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
context:NULL];
|
||||
|
||||
// Check if we receive KVO notifications when setting default key in the
|
||||
// standard application domain
|
||||
[defs setObject:value1 forKey:key1];
|
||||
PASS(obs->called == 1, "KVO notification received");
|
||||
PASS(obs->lastObject != nil, "object is not nil");
|
||||
PASS(obs->lastChange != nil, "change is not nil");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
|
||||
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"old"], [NSNull null],
|
||||
"value for 'old' is [NSNull null]");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"new"], value1,
|
||||
"value for 'new' is 'value1'");
|
||||
|
||||
[defs removeObjectForKey:key1];
|
||||
PASS(obs->called == 2, "KVO notification received");
|
||||
PASS(obs->lastObject != nil, "object is not nil");
|
||||
PASS(obs->lastChange != nil, "change is not nil");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
|
||||
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"old"], value1,
|
||||
"value for 'old' is value1");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"new"], [NSNull null],
|
||||
"value for 'new' is [NSNull null]");
|
||||
|
||||
// Test setting two different values for the same key in application domain
|
||||
// and registration domain. When removing the value in the application domain,
|
||||
// the value for 'new' in the change dictionary is not nil, but rather the
|
||||
// value from the registration domain.
|
||||
[defs setObject:value2 forKey:key2];
|
||||
PASS(obs->called == 3, "KVO notification received");
|
||||
PASS(obs->lastObject != nil, "object is not nil");
|
||||
PASS(obs->lastChange != nil, "change is not nil");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
|
||||
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"old"], [NSNull null],
|
||||
"value for 'old' is [NSNull null]");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"new"], value2,
|
||||
"value for 'new' is 'value2'");
|
||||
|
||||
// Set default key in registration domain that is _different_ to the key
|
||||
// registered in the application domain. This will trigger a change
|
||||
// notification, when the entry is removed from the application domain.
|
||||
NSDictionary *registrationDict = [NSDictionary dictionaryWithObject:value2Alt
|
||||
forKey:key2];
|
||||
[defs registerDefaults:registrationDict];
|
||||
|
||||
[defs removeObjectForKey:key2];
|
||||
PASS(obs->called == 4, "KVO notification received");
|
||||
PASS(obs->lastObject != nil, "object is not nil");
|
||||
PASS(obs->lastChange != nil, "change is not nil");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
|
||||
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"old"], value2,
|
||||
"value for 'old' is value2");
|
||||
// this must not be null in this case
|
||||
PASS_EQUAL([obs->lastChange objectForKey:@"new"], value2Alt,
|
||||
"value for 'new' is 'value2Alt'");
|
||||
|
||||
// Set default key in registration domain that is _equal_ to the key
|
||||
// registered in the application domain. This will _not_ trigger a change
|
||||
// notification, when the entry is removed from the application domain.
|
||||
registrationDict = [NSDictionary dictionaryWithObject:value1 forKey:key1];
|
||||
[defs registerDefaults:registrationDict];
|
||||
|
||||
// Does not emit a KVO notification as value is not changed
|
||||
[defs setObject:value1 forKey:key1];
|
||||
PASS(obs->called == 4,
|
||||
"KVO notification was not emitted as other domain has the same entry");
|
||||
|
||||
// Remove the entry from the application domain.
|
||||
[defs removeObjectForKey:key1];
|
||||
PASS(obs->called == 4,
|
||||
"KVO notification was not emitted as other domain has the same entry");
|
||||
|
||||
[defs removeObserver:obs forKeyPath:key1];
|
||||
[defs removeObserver:obs forKeyPath:key2];
|
||||
|
||||
[pool drain];
|
||||
[obs release];
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -2,79 +2,350 @@
|
|||
#import <Foundation/NSNotification.h>
|
||||
#import <Foundation/NSUserDefaults.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSKeyValueObserving.h>
|
||||
#import <Foundation/NSValue.h>
|
||||
#import <Foundation/NSNull.h>
|
||||
#import "ObjectTesting.h"
|
||||
|
||||
@interface Observer : NSObject
|
||||
@interface Observer : NSObject
|
||||
{
|
||||
unsigned count;
|
||||
NSInteger count;
|
||||
NSInteger kvoCount;
|
||||
}
|
||||
- (NSString*) count;
|
||||
- (void) notified: (NSNotification*)n;
|
||||
- (NSInteger)count;
|
||||
- (NSInteger)kvoCount;
|
||||
- (void)notified:(NSNotification *)n;
|
||||
@end
|
||||
|
||||
@implementation Observer
|
||||
- (NSString*) count
|
||||
- (NSInteger)count
|
||||
{
|
||||
return [NSString stringWithFormat: @"%u", count];
|
||||
return count;
|
||||
}
|
||||
- (void) notified: (NSNotification*)n
|
||||
- (NSInteger)kvoCount
|
||||
{
|
||||
return kvoCount;
|
||||
}
|
||||
- (void)notified:(NSNotification *)n
|
||||
{
|
||||
count++;
|
||||
}
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary *)change
|
||||
context:(void *)context
|
||||
{
|
||||
id old = [change objectForKey:NSKeyValueChangeOldKey];
|
||||
id new = [ change objectForKey : NSKeyValueChangeNewKey ];
|
||||
NSKeyValueChange kind =
|
||||
[[change objectForKey:NSKeyValueChangeKindKey] intValue];
|
||||
id isPrior = [change objectForKey:NSKeyValueChangeNotificationIsPriorKey];
|
||||
|
||||
NSLog(@"KVO: %@: old = %@, new = %@, kind = %ld, isPrior = %@",
|
||||
keyPath, old, new, kind, isPrior);
|
||||
|
||||
if ([keyPath isEqualToString:@"Test Suite Bool"])
|
||||
{
|
||||
switch (kvoCount)
|
||||
{
|
||||
case 0: // Initial
|
||||
{
|
||||
PASS_EQUAL(
|
||||
new, [NSNull null],
|
||||
"KVO: Initial setting of 'Test Suite Bool' has new = null");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Bool' is of kind "
|
||||
"NSKeyValueChangeSetting (initial)");
|
||||
break;
|
||||
}
|
||||
case 3: // Prior to [defs setBool:YES forKey:@"Test Suite Bool"];
|
||||
{
|
||||
PASS_EQUAL(
|
||||
old, [NSNull null],
|
||||
"KVO: First setting of 'Test Suite Bool' has old = null (prior)");
|
||||
PASS(new == nil,
|
||||
"KVO: First setting of 'Test Suite Bool' has no new (prior)");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Bool' is of kind "
|
||||
"NSKeyValueChangeSetting (prior)");
|
||||
PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES],
|
||||
"KVO: notification for 'Test Suite Bool' is prior");
|
||||
break;
|
||||
}
|
||||
case 4: // [defs setBool:YES forKey:@"Test Suite Bool"];
|
||||
{
|
||||
PASS_EQUAL(
|
||||
old, [NSNull null],
|
||||
"KVO: First setting of 'Test Suite Bool' has old = null");
|
||||
PASS([new isKindOfClass:[ NSNumber class ]],
|
||||
"KVO: New value for 'Test Suite Bool' has NSNumber");
|
||||
PASS(YES == [new boolValue],
|
||||
"KVO: new value for 'Test Suite Bool' is YES");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Bool' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
break;
|
||||
}
|
||||
case 9: // Prior to [defs removeObjectForKey:@"Test Suite Bool"];
|
||||
{
|
||||
PASS([old isKindOfClass:[NSNumber class]],
|
||||
"KVO: First setting of 'Test Suite Bool' has old NSNumber");
|
||||
PASS(YES == [old boolValue],
|
||||
"KVO: old value for 'Test Suite Bool' is YES");
|
||||
PASS(new == nil,
|
||||
"KVO: First setting of 'Test Suite Bool' has no new");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Bool' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES],
|
||||
"KVO: notification for 'Test Suite Bool' is prior");
|
||||
break;
|
||||
}
|
||||
case 10: // [defs removeObjectForKey:@"Test Suite Bool"];
|
||||
{
|
||||
PASS([old isKindOfClass:[NSNumber class]],
|
||||
"KVO: First setting of 'Test Suite Bool' has old NSNumber");
|
||||
PASS(YES == [old boolValue],
|
||||
"KVO: old value for 'Test Suite Bool' is YES");
|
||||
PASS_EQUAL(
|
||||
new, [NSNull null],
|
||||
"KVO: First setting of 'Test Suite Bool' has new = null");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Bool' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
PASS(NO, "KVO: unexpected count for 'Test Suite Bool'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ([keyPath isEqualToString:@"Test Suite Int"])
|
||||
{
|
||||
switch (kvoCount)
|
||||
{
|
||||
case 1: // Initial
|
||||
{
|
||||
PASS_EQUAL(
|
||||
new, [NSNull null],
|
||||
"KVO: Initial setting of 'Test Suite Int' has new = null");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Int' is of kind "
|
||||
"NSKeyValueChangeSetting (initial)");
|
||||
break;
|
||||
}
|
||||
case 5: // Prior to [defs setInteger:34 forKey:@"Test
|
||||
// Suite Int"];
|
||||
{
|
||||
PASS_EQUAL(old, [NSNull null],
|
||||
"KVO: First setting of 'Test Suite Int' has old = null");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Int' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES],
|
||||
"KVO: notification for 'Test Suite Int' is prior");
|
||||
break;
|
||||
}
|
||||
case 6: // [defs setInteger:34 forKey:@"Test Suite Int"];
|
||||
{
|
||||
PASS_EQUAL(
|
||||
old, [NSNull null],
|
||||
"KVO: Second setting of 'Test Suite Int' has old = null");
|
||||
PASS([new isKindOfClass:[ NSNumber class ]],
|
||||
"KVO: New value for 'Test Suite Int' has NSNumber");
|
||||
PASS(34 == [new intValue],
|
||||
"KVO: new value for 'Test Suite Int' is 34");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Int' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
break;
|
||||
}
|
||||
case 11: // Prior to [defs setObject:nil
|
||||
// forKey:@"Test Suite Int"];
|
||||
{
|
||||
PASS([old isKindOfClass:[NSNumber class]],
|
||||
"KVO: First setting of 'Test Suite Int' has old NSNumber");
|
||||
PASS(34 == [old intValue],
|
||||
"KVO: old value for 'Test Suite Int' is 34");
|
||||
PASS(new == nil,
|
||||
"KVO: First setting of 'Test Suite Int' has no new");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Int' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES],
|
||||
"KVO: notification for 'Test Suite Int' is prior");
|
||||
break;
|
||||
}
|
||||
case 12: // [defs setObject:nil forKey:@"Test Suite Int"];
|
||||
{
|
||||
PASS([old isKindOfClass:[NSNumber class]],
|
||||
"KVO: First setting of 'Test Suite Int' has old NSNumber");
|
||||
PASS(34 == [old intValue],
|
||||
"KVO: old value for 'Test Suite Int' is 34");
|
||||
PASS_EQUAL(new, [NSNull null],
|
||||
"KVO: First setting of 'Test Suite Int' has new = null");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Int' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
PASS(NO, "KVO: unexpected count for 'Test Suite Int'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ([keyPath isEqualToString:@"Test Suite Str"])
|
||||
{
|
||||
switch (kvoCount)
|
||||
{
|
||||
case 2: // Initial
|
||||
{
|
||||
PASS_EQUAL(
|
||||
new, [NSNull null],
|
||||
"KVO: Initial setting of 'Test Suite Str' has new = null");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Str' is of kind "
|
||||
"NSKeyValueChangeSetting (initial)");
|
||||
break;
|
||||
}
|
||||
case 7: // Prior to [defs setObject:@"SetString"
|
||||
// forKey:@"Test Suite Str"];
|
||||
{
|
||||
PASS_EQUAL(old, [NSNull null],
|
||||
"KVO: First setting of 'Test Suite Str' has old = null");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Str' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES],
|
||||
"KVO: notification for 'Test Suite Str' is prior");
|
||||
break;
|
||||
}
|
||||
case 8: // [defs setObject:@"SetString"
|
||||
// forKey:@"Test Suite Str"];
|
||||
{
|
||||
PASS_EQUAL(
|
||||
old, [NSNull null],
|
||||
"KVO: Second setting of 'Test Suite Str' has old = null");
|
||||
PASS([new isKindOfClass:[ NSString class ]],
|
||||
"KVO: New value for 'Test Suite Str' has NSString");
|
||||
PASS([new isEqual:@"SetString"],
|
||||
"KVO: new value for 'Test Suite Str' is 'SetString'");
|
||||
PASS(kind == NSKeyValueChangeSetting,
|
||||
"KVO: notification for 'Test Suite Str' is of kind "
|
||||
"NSKeyValueChangeSetting");
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
PASS(NO, "KVO: unexpected count for 'Test Suite Str'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
kvoCount++;
|
||||
}
|
||||
@end
|
||||
|
||||
int main()
|
||||
int
|
||||
main()
|
||||
{
|
||||
NSAutoreleasePool *arp = [NSAutoreleasePool new];
|
||||
Observer *obs = [[Observer new] autorelease];
|
||||
NSUserDefaults *defs;
|
||||
NSAutoreleasePool *arp = [NSAutoreleasePool new];
|
||||
Observer *obs = [[Observer new] autorelease];
|
||||
NSUserDefaults *defs;
|
||||
|
||||
defs = [NSUserDefaults standardUserDefaults];
|
||||
PASS(defs != nil && [defs isKindOfClass: [NSUserDefaults class]],
|
||||
PASS(defs != nil && [defs isKindOfClass:[NSUserDefaults class]],
|
||||
"NSUserDefaults understands +standardUserDefaults");
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: obs
|
||||
selector: @selector(notified:)
|
||||
name: NSUserDefaultsDidChangeNotification
|
||||
object: nil];
|
||||
/* Reset the defaults */
|
||||
[defs removeObjectForKey:@"Test Suite Bool"];
|
||||
[defs removeObjectForKey:@"Test Suite Int"];
|
||||
[defs removeObjectForKey:@"Test Suite Str"];
|
||||
|
||||
[defs setBool: YES forKey: @"Test Suite Bool"];
|
||||
PASS([defs boolForKey: @"Test Suite Bool"],
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:obs
|
||||
selector:@selector(notified:)
|
||||
name:NSUserDefaultsDidChangeNotification
|
||||
object:nil];
|
||||
|
||||
[defs addObserver:obs
|
||||
forKeyPath:@"Test Suite Bool"
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
| NSKeyValueObservingOptionPrior
|
||||
| NSKeyValueObservingOptionInitial
|
||||
context:NULL];
|
||||
|
||||
[defs addObserver:obs
|
||||
forKeyPath:@"Test Suite Int"
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
| NSKeyValueObservingOptionPrior
|
||||
| NSKeyValueObservingOptionInitial
|
||||
context:NULL];
|
||||
|
||||
[defs addObserver:obs
|
||||
forKeyPath:@"Test Suite Str"
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
| NSKeyValueObservingOptionPrior
|
||||
| NSKeyValueObservingOptionInitial
|
||||
context:NULL];
|
||||
PASS([obs kvoCount] == 3, "KVO: initial count is 3");
|
||||
|
||||
[defs setBool:YES forKey:@"Test Suite Bool"];
|
||||
PASS([defs boolForKey:@"Test Suite Bool"],
|
||||
"NSUserDefaults can set/get a BOOL");
|
||||
PASS([[defs objectForKey: @"Test Suite Bool"] isKindOfClass:[NSNumber class]],
|
||||
PASS([[defs objectForKey:@"Test Suite Bool"] isKindOfClass:[NSNumber class]],
|
||||
"NSUserDefaults returns NSNumber for a BOOL");
|
||||
|
||||
PASS_EQUAL([obs count], @"1", "setting a boolean causes notification");
|
||||
PASS([obs count] == 1, "setting a boolean causes notification");
|
||||
PASS([obs kvoCount] == 5, "KVO: setting boolean caused 2 notifications");
|
||||
|
||||
[defs setInteger: 34 forKey: @"Test Suite Int"];
|
||||
PASS([defs integerForKey: @"Test Suite Int"] == 34,
|
||||
[defs setInteger:34 forKey:@"Test Suite Int"];
|
||||
PASS([defs integerForKey:@"Test Suite Int"] == 34,
|
||||
"NSUserDefaults can set/get an int");
|
||||
PASS([[defs objectForKey: @"Test Suite Int"] isKindOfClass:[NSNumber class]],
|
||||
PASS([[defs objectForKey:@"Test Suite Int"] isKindOfClass:[NSNumber class]],
|
||||
"NSUserDefaults returns NSNumber for an int");
|
||||
|
||||
PASS_EQUAL([obs count], @"2", "setting an integer causes notification");
|
||||
PASS([obs count] == 2, "setting an integer causes notification");
|
||||
PASS([obs kvoCount] == 7, "KVO: setting integer caused 2 notifications");
|
||||
|
||||
[defs setObject: @"SetString" forKey: @"Test Suite Str"];
|
||||
PASS([[defs stringForKey: @"Test Suite Str"] isEqual: @"SetString"],
|
||||
[defs setObject:@"SetString" forKey:@"Test Suite Str"];
|
||||
PASS([[defs stringForKey:@"Test Suite Str"] isEqual:@"SetString"],
|
||||
"NSUserDefaults can set/get a string");
|
||||
PASS([[defs objectForKey: @"Test Suite Str"] isKindOfClass:[NSString class]],
|
||||
PASS([[defs objectForKey:@"Test Suite Str"] isKindOfClass:[NSString class]],
|
||||
"NSUserDefaults returns NSString for a string");
|
||||
|
||||
PASS_EQUAL([obs count], @"3", "setting a string causes notification");
|
||||
PASS([obs count] == 3, "setting a string causes notification");
|
||||
PASS([obs kvoCount] == 9, "KVO: setting integer caused 2 notifications");
|
||||
|
||||
[defs removeObjectForKey: @"Test Suite Bool"];
|
||||
PASS(nil == [defs objectForKey: @"Test Suite Bool"],
|
||||
[defs removeObjectForKey:@"Test Suite Bool"];
|
||||
PASS(nil == [defs objectForKey:@"Test Suite Bool"],
|
||||
"NSUserDefaults can use -removeObjectForKey: to remove a bool");
|
||||
|
||||
PASS_EQUAL([obs count], @"4", "removing a key causes notification");
|
||||
PASS([obs count] == 4, "removing a key causes notification");
|
||||
PASS([obs kvoCount] == 11, "KVO: removing bool caused 2 notifications");
|
||||
|
||||
[defs setObject: nil forKey: @"Test Suite Int"];
|
||||
PASS(nil == [defs objectForKey: @"Test Suite Int"],
|
||||
[defs setObject:nil forKey:@"Test Suite Int"];
|
||||
PASS(nil == [defs objectForKey:@"Test Suite Int"],
|
||||
"NSUserDefaults can use -setObject:forKey: to remove an int");
|
||||
|
||||
PASS_EQUAL([obs count], @"5", "setting nil object causes notification");
|
||||
PASS([obs count] == 5, "setting nil object causes notification");
|
||||
PASS([obs kvoCount] == 13, "KVO: removing int caused 2 notifications");
|
||||
|
||||
[arp release]; arp = nil;
|
||||
[defs setObject:@"SetString" forKey:@"Test Suite Str"];
|
||||
PASS([[defs objectForKey:@"Test Suite Str"] isKindOfClass:[NSString class]],
|
||||
"NSUserDefaults returns NSString for an updated string");
|
||||
|
||||
PASS([obs count] == 6, "setting a string causes notification");
|
||||
|
||||
[defs setObject:nil forKey:@"Test Suite Int"];
|
||||
PASS([obs count] == 7, "setting nil object twice causes notification");
|
||||
|
||||
[defs removeObserver:obs forKeyPath:@"Test Suite Bool" context:NULL];
|
||||
[defs removeObserver:obs forKeyPath:@"Test Suite Int" context:NULL];
|
||||
[defs removeObserver:obs forKeyPath:@"Test Suite Str" context:NULL];
|
||||
|
||||
[arp release];
|
||||
arp = nil;
|
||||
return 0;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue