Index: src/Makefile ================================================================== --- src/Makefile +++ src/Makefile @@ -52,10 +52,11 @@ OFMutableTarArchiveEntry.m \ OFMutableTriple.m \ OFMutableURL.m \ OFMutableZIPArchiveEntry.m \ OFNotification.m \ + OFNotificationCenter.m \ OFNull.m \ OFNumber.m \ OFObject.m \ OFObject+KeyValueCoding.m \ OFObject+Serialization.m \ Index: src/OFNotification.h ================================================================== --- src/OFNotification.h +++ src/OFNotification.h @@ -28,11 +28,11 @@ typedef OFConstantString *OFNotificationName; /** * @class OFNotification OFNotification.h ObjFW/OFNotification.h * - * @brief An class to represent a notification for or from + * @brief A class to represent a notification for or from * @ref OFNotificationCenter. */ OF_SUBCLASSING_RESTRICTED @interface OFNotification: OFObject { ADDED src/OFNotificationCenter.h Index: src/OFNotificationCenter.h ================================================================== --- src/OFNotificationCenter.h +++ src/OFNotificationCenter.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2008-2021 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFObject.h" +#import "OFNotification.h" + +OF_ASSUME_NONNULL_BEGIN + +@class OFMutableDictionary OF_GENERIC(KeyType, ObjectType); +#ifdef OF_HAVE_THREADS +@class OFMutex; +#endif + +/** + * @class OFNotificationCenter OFNotificationCenter.h \ + * ObjFW/OFNotificationCenter.h + * + * @brief A class to send and register for notifications. + */ +@interface OFNotificationCenter: OFObject +{ +#ifdef OF_HAVE_THREADS + OFMutex *_mutex; +#endif + OFMutableDictionary *_notifications; + OF_RESERVE_IVARS(OFNotificationCenter, 4) +} + +#ifdef OF_HAVE_CLASS_PROPERTIES +@property (class, readonly, nonatomic) OFNotificationCenter *defaultCenter; +#endif + +/** + * @brief Returns the default notification center. + */ ++ (OFNotificationCenter *)defaultCenter; + +/** + * @brief Adds an observer for the specified notification and object. + * + * @param observer The object that should receive notifications + * @param selector The selector to call on the observer on notifications. The + * method must take exactly one object of type @ref + * OFNotification. + * @param name The name of the notification to observe + * @param object The object that should be sending the notification, or `nil` + * if the object should be ignored to determine what + * notifications to deliver + */ +- (void)addObserver: (id)observer + selector: (SEL)selector + name: (OFNotificationName)name + object: (nullable id)object; + +/** + * @brief Removes an observer. All parameters must match those used with + * @ref addObserver:selector:name:object:. + * + * @param observer The observer that was specified when adding the observer + * @param selector The selector that was specified when adding the observer + * @param name The name that was specified when adding the observer + * @param object The object that was specified when adding the observer + */ +- (void)removeObserver: (id)observer + selector: (SEL)selector + name: (OFNotificationName)name + object: (nullable id)object; + +/** + * @brief Posts the specified notification. + * + * @param notification The notification to post + */ +- (void)postNotification: (OFNotification *)notification; +@end + +OF_ASSUME_NONNULL_END ADDED src/OFNotificationCenter.m Index: src/OFNotificationCenter.m ================================================================== --- src/OFNotificationCenter.m +++ src/OFNotificationCenter.m @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2008-2021 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#import "OFNotificationCenter.h" +#import "OFDictionary.h" +#ifdef OF_HAVE_THREADS +# import "OFMutex.h" +#endif +#import "OFSet.h" +#import "OFString.h" + +@interface OFDefaultNotificationCenter: OFNotificationCenter +@end + +@interface OFNotificationRegistration: OFObject +{ +@public + id _observer; + SEL _selector; + unsigned long _selectorHash; + id _object; +} + +- (instancetype)initWithObserver: (id)observer + selector: (SEL)selector + object: (id)object; +@end + +static OFNotificationCenter *defaultCenter; + +@implementation OFNotificationRegistration +- (instancetype)initWithObserver: (id)observer + selector: (SEL)selector + object: (id)object +{ + self = [super init]; + + @try { + void *pool = objc_autoreleasePoolPush(); + + _observer = [observer retain]; + _selector = selector; + _object = [object retain]; + + _selectorHash = [[OFString stringWithUTF8String: + sel_getName(_selector)] hash]; + + objc_autoreleasePoolPop(pool); + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [_observer release]; + [_object release]; + + [super dealloc]; +} + +- (bool)isEqual: (OFNotificationRegistration *)registration +{ + if (![registration isKindOfClass: [OFNotificationRegistration class]]) + return false; + + if (![registration->_observer isEqual: _observer]) + return false; + + if (!sel_isEqual(registration->_selector, _selector)) + return false; + + if (registration->_object != _object && + ![registration->_object isEqual: _object]) + return false; + + return true; +} + +- (unsigned long)hash +{ + unsigned long hash; + + OFHashInit(&hash); + + OFHashAddHash(&hash, [_observer hash]); + OFHashAddHash(&hash, _selectorHash); + OFHashAddHash(&hash, [_object hash]); + + OFHashFinalize(&hash); + + return hash; +} +@end + +@implementation OFNotificationCenter ++ (void)initialize +{ + if (self != [OFNotificationCenter class]) + return; + + defaultCenter = [[OFDefaultNotificationCenter alloc] init]; +} + ++ (OFNotificationCenter *)defaultCenter +{ + return defaultCenter; +} + +- (instancetype)init +{ + self = [super init]; + + @try { +#ifdef OF_HAVE_THREADS + _mutex = [[OFMutex alloc] init]; +#endif + _notifications = [[OFMutableDictionary alloc] init]; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ +#ifdef OF_HAVE_THREADS + [_mutex release]; +#endif + [_notifications release]; + + [super dealloc]; +} + +- (void)addObserver: (id)observer + selector: (SEL)selector + name: (OFNotificationName)name + object: (id)object +{ + void *pool = objc_autoreleasePoolPush(); + OFNotificationRegistration *registration = + [[[OFNotificationRegistration alloc] + initWithObserver: observer + selector: selector + object: object] autorelease]; + +#ifdef OF_HAVE_THREADS + [_mutex lock]; + @try { + OFMutableSet *notificationsForName = + [_notifications objectForKey: name]; + + if (notificationsForName == nil) { + notificationsForName = [OFMutableSet set]; + [_notifications setObject: notificationsForName + forKey: name]; + } + + [notificationsForName addObject: registration]; +#endif +#ifdef OF_HAVE_THREADS + } @finally { + [_mutex unlock]; + } +#endif + + objc_autoreleasePoolPop(pool); +} + +- (void)removeObserver: (id)observer + selector: (SEL)selector + name: (OFNotificationName)name + object: (id)object +{ + void *pool = objc_autoreleasePoolPush(); + OFNotificationRegistration *registration = + [[[OFNotificationRegistration alloc] + initWithObserver: observer + selector: selector + object: object] autorelease]; + +#ifdef OF_HAVE_THREADS + [_mutex lock]; + @try { + [[_notifications objectForKey: name] + removeObject: registration]; +#endif +#ifdef OF_HAVE_THREADS + } @finally { + [_mutex unlock]; + } +#endif + + objc_autoreleasePoolPop(pool); +} + +- (void)postNotification: (OFNotification *)notification +{ +#ifdef OF_HAVE_THREADS + [_mutex lock]; + @try { + for (OFNotificationRegistration *registration in + [_notifications objectForKey: notification.name]) { + void (*callback)(id, SEL, OFNotification *); + + if (registration->_object != nil && + registration->_object != notification.object) + continue; + + callback = (void (*)(id, SEL, OFNotification *)) + [registration->_observer methodForSelector: + registration->_selector]; + callback(registration->_observer, + registration->_selector, notification); + } +#endif +#ifdef OF_HAVE_THREADS + } @finally { + [_mutex unlock]; + } +#endif +} +@end + +@implementation OFDefaultNotificationCenter +- (instancetype)autorelease +{ + return self; +} + +- (instancetype)retain +{ + return self; +} + +- (void)release +{ +} + +- (unsigned int)retainCount +{ + return OFMaxRetainCount; +} +@end Index: src/ObjFW.h ================================================================== --- src/ObjFW.h +++ src/ObjFW.h @@ -49,10 +49,11 @@ #import "OFURL.h" #import "OFURLHandler.h" #import "OFColor.h" #import "OFNotification.h" +#import "OFNotificationCenter.h" #import "OFStream.h" #import "OFStdIOStream.h" #import "OFInflateStream.h" #import "OFInflate64Stream.h" Index: tests/Makefile ================================================================== --- tests/Makefile +++ tests/Makefile @@ -22,10 +22,11 @@ OFInvocationTests.m \ OFJSONTests.m \ OFListTests.m \ OFLocaleTests.m \ OFMethodSignatureTests.m \ + OFNotificationCenterTests.m \ OFNumberTests.m \ OFObjectTests.m \ OFPBKDF2Tests.m \ OFPropertyListTests.m \ OFScryptTests.m \ ADDED tests/OFNotificationCenterTests.m Index: tests/OFNotificationCenterTests.m ================================================================== --- tests/OFNotificationCenterTests.m +++ tests/OFNotificationCenterTests.m @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2008-2021 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#import "TestsAppDelegate.h" + +static OFString *const module = @"OFNotificationCenter"; +static const OFNotificationName notificationName = + @"OFNotificationCenterTestName"; +static const OFNotificationName otherNotificationName = + @"OFNotificationCenterTestOtherName"; + +@interface OFNotificationCenterTest: OFObject +{ +@public + id _expectedObject; + int _received; +} + +- (void)handleNotification: (OFNotification *)notification; +@end + +@implementation OFNotificationCenterTest +- (void)handleNotification: (OFNotification *)notification +{ + OFEnsure([notification.name isEqual: notificationName]); + OFEnsure(_expectedObject == nil || + notification.object == _expectedObject); + + _received++; +} +@end + +@implementation TestsAppDelegate (OFNotificationCenterTests) +- (void)notificationCenterTests +{ + void *pool = objc_autoreleasePoolPush(); + OFNotificationCenter *center = [OFNotificationCenter defaultCenter]; + OFNotificationCenterTest *test1, *test2, *test3, *test4; + OFNotification *notification; + + test1 = + [[[OFNotificationCenterTest alloc] init] autorelease]; + test1->_expectedObject = self; + test2 = + [[[OFNotificationCenterTest alloc] init] autorelease]; + test3 = + [[[OFNotificationCenterTest alloc] init] autorelease]; + test3->_expectedObject = self; + test4 = + [[[OFNotificationCenterTest alloc] init] autorelease]; + + /* First one intentionally added twice to test deduplication. */ + TEST(@"-[addObserver:selector:name:object:]", + R([center addObserver: test1 + selector: @selector(handleNotification:) + name: notificationName + object: self]) && + R([center addObserver: test1 + selector: @selector(handleNotification:) + name: notificationName + object: self]) && + R([center addObserver: test2 + selector: @selector(handleNotification:) + name: notificationName + object: nil]) && + R([center addObserver: test3 + selector: @selector(handleNotification:) + name: otherNotificationName + object: self]) && + R([center addObserver: test4 + selector: @selector(handleNotification:) + name: otherNotificationName + object: nil])) + + notification = [OFNotification notificationWithName: notificationName + object: nil]; + TEST(@"-[postNotification:] #1", + R([center postNotification: notification]) && + test1->_received == 0 && test2->_received == 1 && + test3->_received == 0 && test4->_received == 0) + + notification = [OFNotification notificationWithName: notificationName + object: self]; + TEST(@"-[postNotification:] #2", + R([center postNotification: notification]) && + test1->_received == 1 && test2->_received == 2 && + test3->_received == 0 && test4->_received == 0) + + notification = [OFNotification notificationWithName: notificationName + object: @"foo"]; + TEST(@"-[postNotification:] #3", + R([center postNotification: notification]) && + test1->_received == 1 && test2->_received == 3 && + test3->_received == 0 && test4->_received == 0) + + TEST(@"-[removeObserver:selector:name:object:]", + R([center removeObserver: test1 + selector: @selector(handleNotification:) + name: notificationName + object: self]) && + R([center removeObserver: test2 + selector: @selector(handleNotification:) + name: notificationName + object: nil]) && + R([center removeObserver: test3 + selector: @selector(handleNotification:) + name: otherNotificationName + object: self]) && + R([center removeObserver: test4 + selector: @selector(handleNotification:) + name: otherNotificationName + object: nil])) + + notification = [OFNotification notificationWithName: notificationName + object: self]; + TEST(@"-[postNotification:] with no observers", + R([center postNotification: notification]) && + test1->_received == 1 && test2->_received == 3 && + test3->_received == 0 && test4->_received == 0) + + objc_autoreleasePoolPop(pool); +} +@end Index: tests/TestsAppDelegate.h ================================================================== --- tests/TestsAppDelegate.h +++ tests/TestsAppDelegate.h @@ -136,10 +136,14 @@ @end @interface TestsAppDelegate (OFMethodSignatureTests) - (void)methodSignatureTests; @end + +@interface TestsAppDelegate (OFNotificationCenterTests) +- (void)notificationCenterTests; +@end @interface TestsAppDelegate (OFNumberTests) - (void)numberTests; @end Index: tests/TestsAppDelegate.m ================================================================== --- tests/TestsAppDelegate.m +++ tests/TestsAppDelegate.m @@ -329,10 +329,11 @@ [self setTests]; [self dateTests]; [self valueTests]; [self numberTests]; [self streamTests]; + [self notificationCenterTests]; #ifdef OF_HAVE_FILES [self MD5HashTests]; [self RIPEMD160HashTests]; [self SHA1HashTests]; [self SHA224HashTests];