From ab99eeed4e43ae306493c7609bbe035cacf5bcb4 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 4 Nov 2020 12:04:11 -0500 Subject: [PATCH] Add monitoring for new themes --- Frameworks/Cocoa Extensions | 2 +- .../Private/TPCThemeControllerPrivate.h | 7 +- .../Preferences/Themes/TPCThemeController.m | 168 +++++++++++++++++- 3 files changed, 172 insertions(+), 5 deletions(-) diff --git a/Frameworks/Cocoa Extensions b/Frameworks/Cocoa Extensions index e479602d6..2a7671186 160000 --- a/Frameworks/Cocoa Extensions +++ b/Frameworks/Cocoa Extensions @@ -1 +1 @@ -Subproject commit e479602d6e412c4b97bf6b4fd4f8d02114d435af +Subproject commit 2a767118676a22495de923effe49b4058e7b7c4c diff --git a/Sources/App/Classes/Headers/Private/TPCThemeControllerPrivate.h b/Sources/App/Classes/Headers/Private/TPCThemeControllerPrivate.h index f9d47948f..9063e912c 100644 --- a/Sources/App/Classes/Headers/Private/TPCThemeControllerPrivate.h +++ b/Sources/App/Classes/Headers/Private/TPCThemeControllerPrivate.h @@ -46,7 +46,12 @@ NS_ASSUME_NONNULL_BEGIN - (void)load; - (void)reload; -- (void)reloadMonitoringActiveThemePath; +#if TEXTUAL_BUILT_WITH_ICLOUD_SUPPORT == 1 +/* We should probably do this internally by monitoring for + a notification that access to the cloud container changed, + but I don't want to touch the iCloud code. It's gross. */ +- (void)reloadThemeMonitoring; +#endif - (void)recreateTemporaryCopyOfThemeIfNecessary; diff --git a/Sources/App/Classes/Preferences/Themes/TPCThemeController.m b/Sources/App/Classes/Preferences/Themes/TPCThemeController.m index dfe13e9a4..d91de8cab 100644 --- a/Sources/App/Classes/Preferences/Themes/TPCThemeController.m +++ b/Sources/App/Classes/Preferences/Themes/TPCThemeController.m @@ -36,8 +36,6 @@ * *********************************************************************** */ -#warning TODO: Add monitoring for changes to themes including being deleted. - #import "TXAppearance.h" #import "TXGlobalModels.h" #import "TXMasterController.h" @@ -92,6 +90,7 @@ typedef NSMutableDictionary *TPCThemeControllerThemeLi @property (nonatomic, strong) TPCThemeControllerThemeListMutable bundledThemes; @property (nonatomic, strong) TPCThemeControllerThemeListMutable customThemes; @property (nonatomic, strong) TPCThemeControllerThemeListMutable cloudThemes; +@property (nonatomic, strong) XRFileSystemMonitor *themeMonitor; @end #pragma mark - @@ -118,6 +117,8 @@ typedef NSMutableDictionary *TPCThemeControllerThemeLi [self populateThemes]; + [self startMonitoringThemes]; + [RZNotificationCenter() addObserver:self selector:@selector(applicationAppearanceChanged:) name:TXApplicationAppearanceChangedNotification @@ -159,7 +160,7 @@ typedef NSMutableDictionary *TPCThemeControllerThemeLi LogToConsoleTerminationProgress("Removing theme change observers."); -// [self stopMonitoringActiveThemePath]; + [self stopMonitoringThemes]; LogToConsoleTerminationProgress("Empty theme cache."); @@ -295,6 +296,17 @@ typedef NSMutableDictionary *TPCThemeControllerThemeLi } - (nullable TPCTheme *)themeAtURL:(NSURL *)url withFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation inList:(TPCThemeControllerThemeListMutable)list createIfNecessary:(BOOL)createIfNecessary skipFileExists:(BOOL)skipFileExists +{ + return [self themeAtURL:url + withFilename:name + storageLocation:storageLocation + inList:list + createIfNecessary:createIfNecessary + wasCreated:NULL + skipFileExists:skipFileExists]; +} + +- (nullable TPCTheme *)themeAtURL:(NSURL *)url withFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation inList:(TPCThemeControllerThemeListMutable)list createIfNecessary:(BOOL)createIfNecessary wasCreated:(nullable BOOL *)wasCreated skipFileExists:(BOOL)skipFileExists { NSParameterAssert(url != nil); NSParameterAssert(url.isFileURL); @@ -326,6 +338,10 @@ typedef NSMutableDictionary *TPCThemeControllerThemeLi [self addTheme:theme withFilename:name storageLocation:storageLocation]; + if ( wasCreated) { + *wasCreated = YES; + } + return theme; } @@ -467,6 +483,150 @@ typedef NSMutableDictionary *TPCThemeControllerThemeLi return nil; } +- (void)startMonitoringThemes +{ + NSMutableArray *urls = [NSMutableArray arrayWithCapacity:2]; + + NSMutableDictionary *context = [NSMutableDictionary dictionaryWithCapacity:2]; + + void (^_addStorageLocation)(TPCThemeStorageLocation) = ^(TPCThemeStorageLocation storageLocation) + { + NSString *path = [self.class pathOfStorageLocation:storageLocation]; + + if (path == nil) { + return; + } + + NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES]; + + [urls addObject:url]; + + [context setObject:@(storageLocation) forKey:url]; + }; + + _addStorageLocation(TPCThemeStorageLocationCustom); + _addStorageLocation(TPCThemeStorageLocationCloud); + + __weak TPCThemeController *weakSelf = self; + + XRFileSystemMonitor *monitor = + [[XRFileSystemMonitor alloc] initWithFileURLs:urls context:context callbackBlock:^(NSArray *events) { + [weakSelf reactToMonitoringEvents:events]; + }]; + + [monitor startMonitoringWithLatency:1.0]; + + self.themeMonitor = monitor; +} + +- (void)stopMonitoringThemes +{ + XRFileSystemMonitor *monitor = self.themeMonitor; + + if (monitor == nil) { + return; + } + + [monitor stopMonitoring]; + + self.themeMonitor = nil; +} + +#if TEXTUAL_BUILT_WITH_ICLOUD_SUPPORT == 1 +- (void)reloadThemeMonitoring +{ + [self stopMonitoringThemes]; + + [self startMonitoringThemes]; +} +#endif + +- (void)reactToMonitoringEvents:(NSArray *)events +{ + NSParameterAssert(events != nil); + + for (XRFileSystemEvent *event in events) { + [self reactToMonitoringEvent:event]; + } +} + +- (void)reactToMonitoringEvent:(XRFileSystemEvent *)event +{ + NSParameterAssert(event != nil); + + /* The purpose of the theme monitor is to recognize when + new themes have appeared aso that we can make them an + option for the user immediately. To accomplish this we + monitor the directory of each storage location. */ + /* Monitor is recrusive which means we have to use flags + and context information to narrow scope of events. */ + FSEventStreamEventFlags flags = event.flags; + + if ((flags & kFSEventStreamEventFlagItemIsDir) != kFSEventStreamEventFlagItemIsDir) { +// LogToConsoleDebug("Ignoring monitoring event for non-directory."); + + return; + } + + /* Each URL that is monitored is assigned the storage location + enum value as its context object. We can use this to understand + with certainty which storage location an event is related to. */ + NSURL *url = event.url; + + NSURL *parentURL = url.URLByDeletingLastPathComponent; + + /* If the parent URL of this event does not contain an object, + then the event is not related to a subfolder of the storage location. */ + NSNumber *parentContext = [self.themeMonitor contextObjectForURL:parentURL]; + + if (parentContext == nil) { +// LogToConsoleDebug("Ignoring monitoring event for unrelated directory."); + + return; + } + + TPCThemeStorageLocation storageLocation = parentContext.unsignedIntegerValue; + + [self reactToMonitoringEventAtURL:url storageLocation:storageLocation flags:flags]; +} + +- (void)reactToMonitoringEventAtURL:(NSURL *)url storageLocation:(TPCThemeStorageLocation)storageLocation flags:(FSEventStreamEventFlags)flags +{ + NSParameterAssert(url != nil); + NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); + + /* TPCTheme objects will announce when they are deleted. + We are not interested in events related to those. */ + if ([RZFileManager() fileExistsAtURL:url] == NO) { + return; + } + + /* Create theme */ + NSString *themeName = [url resourceValueForKey:NSURLNameKey]; + + TPCThemeControllerThemeListMutable themeList = [self mutableListForStorageLocation:storageLocation]; + + BOOL themeCreated = NO; + + TPCTheme *theme = + [self themeAtURL:url + withFilename:themeName + storageLocation:storageLocation + inList:themeList + createIfNecessary:YES + wasCreated:&themeCreated + skipFileExists:YES]; + + /* Only post notification if the theme was created. */ + if (themeCreated == NO) { + return; + } + + LogToConsoleDebug("Theme '%@' named '%@' at '%@' created.", theme, themeName, url); + + [RZNotificationCenter() postNotificationName:TPCThemeControllerThemeListDidChangeNotification object:self]; +} + - (void)populateThemes { [self populateThemesFromStorageLocation:TPCThemeStorageLocationBundle]; @@ -637,6 +797,8 @@ typedef NSMutableDictionary *TPCThemeControllerThemeLi [self removeThemeWithFilename:theme.name storageLocation:theme.storageLocation]; if (self.theme != theme) { + [RZNotificationCenter() postNotificationName:TPCThemeControllerThemeListDidChangeNotification object:self]; + return; }