hammerspoon/Pods/MIKMIDI/Source/MIKMIDIMappingManager.m

382 lines
12 KiB
Objective-C

//
// MIKMIDIMappingManager.m
// Danceability
//
// Created by Andrew Madsen on 7/18/13.
// Copyright (c) 2013 Mixed In Key. All rights reserved.
//
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#else
#import <AppKit/AppKit.h>
#endif
#import "MIKMIDIMappingManager.h"
#import "MIKMIDIMapping.h"
#import "MIKMIDIErrors.h"
#if !__has_feature(objc_arc)
#error MIKMIDIMappingManager.m must be compiled with ARC. Either turn on ARC for the project or set the -fobjc-arc flag for MIKMIDIMappingManager.m in the Build Phases for this target
#endif
@interface MIKMIDIMapping (SemiPrivate)
@property (nonatomic, readwrite, getter = isBundledMapping) BOOL bundledMapping;
@end
@interface MIKMIDIMappingManager ()
@property (nonatomic, strong, readwrite) NSSet *bundledMappings;
@property (nonatomic, strong) NSMutableSet *internalUserMappings;
@property (nonatomic, strong) NSMutableArray *blockBasedObservers;
@end
static MIKMIDIMappingManager *sharedManager = nil;
@implementation MIKMIDIMappingManager
+ (instancetype)sharedManager;
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[self alloc] init];
});
return sharedManager;
}
- (id)init
{
self = [super init];
if (self) {
[self loadBundledMappings];
[self loadAvailableUserMappings];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
#if !TARGET_OS_IPHONE
NSString *appTerminateNotification = NSApplicationWillTerminateNotification;
#else
NSString *appTerminateNotification = UIApplicationWillTerminateNotification;
#endif
self.blockBasedObservers = [NSMutableArray array];
id observer = [nc addObserverForName:appTerminateNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
[self saveMappingsToDisk];
}];
[self.blockBasedObservers addObject:observer];
}
return self;
}
- (void)dealloc
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self];
for (id observer in self.blockBasedObservers) { [nc removeObserver:observer]; }
self.blockBasedObservers = nil;
}
#pragma mark - Public
- (NSSet *)mappingsForControllerName:(NSString *)name;
{
if (![name length]) return [NSSet set];
NSSet *bundledMappings = [self bundledMappingsForControllerName:name];
NSSet *userMappings = [self userMappingsForControllerName:name];
return [bundledMappings setByAddingObjectsFromSet:userMappings];
}
- (NSSet *)bundledMappingsForControllerName:(NSString *)name
{
if (![name length]) return [NSSet set];
NSMutableSet *result = [NSMutableSet set];
for (MIKMIDIMapping *mapping in self.bundledMappings) {
if ([mapping.controllerName isEqualToString:name]) {
[result addObject:mapping];
}
}
return result;
}
- (NSSet *)userMappingsForControllerName:(NSString *)name
{
if (![name length]) return [NSSet set];
NSMutableSet *result = [NSMutableSet set];
for (MIKMIDIMapping *mapping in self.userMappings) {
if ([mapping.controllerName isEqualToString:name]) {
[result addObject:mapping];
}
}
return result;
}
- (NSArray *)userMappingsWithName:(NSString *)mappingName;
{
NSMutableArray *result = [NSMutableArray array];
for (MIKMIDIMapping *mapping in self.userMappings) {
if ([mapping.name isEqualToString:mappingName]) {
[result addObject:mapping];
}
}
return result;
}
- (NSArray *)bundledMappingsWithName:(NSString *)mappingName;
{
NSMutableArray *result = [NSMutableArray array];
for (MIKMIDIMapping *mapping in self.bundledMappings) {
if ([mapping.name isEqualToString:mappingName]) {
[result addObject:mapping];
}
}
return result;
}
- (NSArray *)mappingsWithName:(NSString *)mappingName;
{
NSMutableArray *result = [NSMutableArray arrayWithArray:[self userMappingsWithName:mappingName]];
[result addObjectsFromArray:[self bundledMappingsWithName:mappingName]];
return result;
}
- (MIKMIDIMapping *)importMappingFromFileAtURL:(NSURL *)URL overwritingExistingMapping:(BOOL)shouldOverwrite error:(NSError **)error;
{
error = error ? error : &(NSError *__autoreleasing){ nil };
if (![[URL pathExtension] isEqualToString:kMIKMIDIMappingFileExtension]) {
NSString *recoverySuggestion = [NSString stringWithFormat:NSLocalizedString(@"%1$@ can't be imported, because it does not have the file extension %2$@.", @"MIDI mapping import failed because of incorrect file extension message. Placeholder 1 is the filename, 2 is the required extension (e.g. 'midimap')"), [URL path], kMIKMIDIMappingFileExtension];
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey : NSLocalizedString(@"Incorrect File Extension", @"Incorrect File Extension"),
NSLocalizedRecoverySuggestionErrorKey : recoverySuggestion};
*error = [NSError MIKMIDIErrorWithCode:MIKMIDIMappingIncorrectFileExtensionErrorCode userInfo:userInfo];
return nil;
}
MIKMIDIMapping *mapping = [[MIKMIDIMapping alloc] initWithFileAtURL:URL error:error];;
if (!mapping) return nil;
if ([self.userMappings containsObject:mapping]) return mapping; // Already have it, so don't copy the file.
NSFileManager *fm = [NSFileManager defaultManager];
// FIXME: This should write the newly imported mapping file immediately.
NSURL *destinationURL = [self fileURLForMapping:mapping shouldBeUnique:!shouldOverwrite];
if (shouldOverwrite && [fm fileExistsAtPath:[destinationURL path]]) {
if (![fm removeItemAtURL:destinationURL error:error]) return nil;
}
[self addUserMappingsObject:mapping];
return mapping;
}
- (void)saveMappingsToDisk
{
for (MIKMIDIMapping *mapping in self.userMappings) {
NSURL *fileURL = [self fileURLForMapping:mapping shouldBeUnique:NO];
if (!fileURL) {
NSLog(@"Unable to saving mapping %@ to disk. No file path could be generated", mapping);
continue;
}
[mapping writeToFileAtURL:fileURL error:NULL];
}
}
#pragma mark - Private
- (NSURL *)userMappingsFolder
{
NSFileManager *fm = [NSFileManager defaultManager];
NSArray *appSupportFolders = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
if (![appSupportFolders count]) return nil;
NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
if (![bundleID length]) bundleID = @"com.mixedinkey.MIKMIDI"; // Shouldn't happen, except perhaps in command line app.
NSString *mappingsFolder = [[[appSupportFolders lastObject] stringByAppendingPathComponent:bundleID] stringByAppendingPathComponent:@"MIDI Mappings"];
BOOL isDirectory;
BOOL folderExists = [fm fileExistsAtPath:mappingsFolder isDirectory:&isDirectory];
if (!folderExists) {
NSError *error = nil;
if (![fm createDirectoryAtPath:mappingsFolder withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(@"Unable to create MIDI mappings folder: %@", error);
return nil;
}
}
return [NSURL fileURLWithPath:mappingsFolder isDirectory:YES];
}
- (void)loadAvailableUserMappings
{
NSMutableSet *mappings = [NSMutableSet set];
NSURL *mappingsFolder = [self userMappingsFolder];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *userMappingFileURLs = [fm contentsOfDirectoryAtURL:mappingsFolder includingPropertiesForKeys:nil options:0 error:&error];
if (userMappingFileURLs) {
for (NSURL *file in userMappingFileURLs) {
if (![[file pathExtension] isEqualToString:kMIKMIDIMappingFileExtension]) continue;
// process the mapping file
NSError *error = nil;
MIKMIDIMapping *mapping = [[MIKMIDIMapping alloc] initWithFileAtURL:file error:&error];
if (!mapping) {
NSLog(@"Error loading MIDI mapping from %@: %@", file, error);
continue;
}
if (mapping) [mappings addObject:mapping];
}
} else {
NSLog(@"Unable to get contents of directory at %@: %@", mappingsFolder, error);
}
self.internalUserMappings = mappings;
}
- (void)loadBundledMappings
{
NSMutableSet *mappings = [NSMutableSet set];
NSBundle *bundle = [NSBundle mainBundle];
NSArray *bundledMappingFileURLs = [bundle URLsForResourcesWithExtension:kMIKMIDIMappingFileExtension subdirectory:nil];
for (NSURL *file in bundledMappingFileURLs) {
NSError *error = nil;
MIKMIDIMapping *mapping = [[MIKMIDIMapping alloc] initWithFileAtURL:file error:&error];
if (!mapping) {
NSLog(@"Error loading MIDI mapping from %@: %@", file, error);
continue;
}
mapping.bundledMapping = YES;
if (mapping) [mappings addObject:mapping];
}
self.bundledMappings = mappings;
}
- (NSURL *)fileURLForMapping:(MIKMIDIMapping *)mapping shouldBeUnique:(BOOL)unique
{
NSURL *fileURL = [self fileURLWithBaseFilename:[self fileNameForMapping:mapping]];
if (unique) {
NSURL *mappingsFolder = [self userMappingsFolder];
NSFileManager *fm = [NSFileManager defaultManager];
unsigned long numberSuffix = 0;
while ([fm fileExistsAtPath:[fileURL path]]) {
MIKMIDIMapping *existingMapping = [[MIKMIDIMapping alloc] initWithFileAtURL:fileURL error:NULL];
if ([existingMapping isEqual:mapping]) break;
if (numberSuffix > 1000) return nil; // Don't go crazy
NSString *name = [mapping.name stringByAppendingFormat:@" %lu", ++numberSuffix];
NSString *filename = [name stringByAppendingPathExtension:kMIKMIDIMappingFileExtension];
fileURL = [mappingsFolder URLByAppendingPathComponent:filename];
}
}
return fileURL;
}
- (NSURL *)fileURLWithBaseFilename:(NSString *)baseFileName
{
NSString *filename = [baseFileName stringByAppendingPathExtension:kMIKMIDIMappingFileExtension];
return [[self userMappingsFolder] URLByAppendingPathComponent:filename];
}
- (NSString *)fileNameForMapping:(MIKMIDIMapping *)mapping
{
id<MIKMIDIMappingManagerDelegate> delegate = self.delegate;
NSString *result = nil;
if ([delegate respondsToSelector:@selector(mappingManager:fileNameForMapping:)]) {
result = [delegate mappingManager:self fileNameForMapping:mapping];
}
return [result length] ? result : mapping.name;
}
- (NSArray *)legacyFileNamesForUserMappingsObject:(MIKMIDIMapping *)mapping
{
id<MIKMIDIMappingManagerDelegate> delegate = self.delegate;
if (![delegate respondsToSelector:@selector(mappingManager:legacyFileNamesForUserMapping:)]) return nil;
return [delegate mappingManager:self legacyFileNamesForUserMapping:mapping];
}
#pragma mark - Properties
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"userMappings"]) {
keyPaths = [keyPaths setByAddingObject:@"internalUserMappings"];
}
if ([key isEqualToString:@"mappings"]) {
keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"userMappings", @"bundledMappings"]];
}
return keyPaths;
}
- (NSSet *)userMappings { return [self.internalUserMappings copy]; }
- (NSSet *)mappings { return [self.bundledMappings setByAddingObjectsFromSet:self.userMappings]; }
- (void)addUserMappingsObject:(MIKMIDIMapping *)mapping
{
if (mapping.isBundledMapping) mapping = [MIKMIDIMapping userMappingFromBundledMapping:mapping];
[self.internalUserMappings addObject:mapping];
[self saveMappingsToDisk];
}
- (void)removeUserMappingsObject:(MIKMIDIMapping *)mapping
{
[self.internalUserMappings removeObject:mapping];
if ([self.bundledMappings containsObject:mapping]) return;
// Remove XML file for mapping from disk
NSURL *mappingURL = [self fileURLForMapping:mapping shouldBeUnique:NO];
NSArray *legacyFilenames = [self legacyFileNamesForUserMappingsObject:mapping];
if (!mappingURL && !legacyFilenames.count) return;
NSMutableArray *possibleURLs = [NSMutableArray array];
if (mappingURL) [possibleURLs addObject:mappingURL];
for (NSString *filename in legacyFilenames) {
[possibleURLs addObject:[self fileURLWithBaseFilename:filename]];
}
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
BOOL removedAtLeastOneFile = NO;
for (NSURL *url in possibleURLs) {
if (![fm fileExistsAtPath:url.path]) continue;
if ([fm removeItemAtURL:url error:&error]) {
removedAtLeastOneFile = YES;
} else {
NSLog(@"Error removing mapping file for MIDI mapping %@: %@", mapping, error);
}
}
if (!removedAtLeastOneFile) {
NSLog(@"No mapping files were found to delete for the mapping named \"%@\"", mapping.name);
}
}
@end
#pragma mark - Deprecated
@implementation MIKMIDIMappingManager (Deprecated)
- (MIKMIDIMapping *)mappingWithName:(NSString *)mappingName;
{
MIKMIDIMapping *result = [[self userMappingsWithName:mappingName] firstObject];
return result ?: [[self bundledMappingsWithName:mappingName] firstObject];
}
@end