diff --git a/Source/MIKMIDIMapping.h b/Source/MIKMIDIMapping.h index fc637f4c..43d63282 100644 --- a/Source/MIKMIDIMapping.h +++ b/Source/MIKMIDIMapping.h @@ -149,7 +149,6 @@ typedef NS_OPTIONS(NSUInteger, MIKMIDIResponderType){ */ @interface MIKMIDIMapping : NSObject -#if !TARGET_OS_IPHONE /** * Initializes and returns an MIKMIDIMapping object created from the XML file at url. * @@ -175,6 +174,7 @@ typedef NS_OPTIONS(NSUInteger, MIKMIDIResponderType){ */ - (instancetype)initWithFileAtURL:(NSURL *)url; +#if !TARGET_OS_IPHONE /** * Returns an NSXMLDocument representation of the receiver. * The XML document returned by this method can be written to disk. @@ -186,6 +186,20 @@ typedef NS_OPTIONS(NSUInteger, MIKMIDIResponderType){ * @see -writeToFileAtURL:error: */ - (NSXMLDocument *)XMLRepresentation; +#endif + +/** + * Returns a data containing an XML string representation of the receiver. + * The XML string data returned by this method can be written to disk. + * + * @note On OS X, this method returns the same string as [[mapping XMLRepresentation] XMLData]. See https://github.com/mixedinkey-opensource/MIKMIDI/issues/2 + * + * @return An NSData instance containing an XML string representation of the receiver. + * + * @see -XMLRepresentation + * @see -writeToFileAtURL:error: + */ +- (NSData *)XMLData; /** * Writes the receiver as an XML file to the specified URL. @@ -198,7 +212,6 @@ typedef NS_OPTIONS(NSUInteger, MIKMIDIResponderType){ * @return YES if writing the mapping to a file succeeded, NO if an error occurred. */ - (BOOL)writeToFileAtURL:(NSURL *)fileURL error:(NSError **)error; -#endif /** * The mapping items that map controls to responder. @@ -400,4 +413,4 @@ typedef NS_OPTIONS(NSUInteger, MIKMIDIResponderType){ */ - (MIKMIDIResponderType)MIDIResponderTypeForCommandIdentifier:(NSString *)commandID; // Optional. If not implemented, MIKMIDIResponderTypeAll will be assumed. -@end \ No newline at end of file +@end diff --git a/Source/MIKMIDIMapping.m b/Source/MIKMIDIMapping.m index 1cc03ced..fd83e233 100644 --- a/Source/MIKMIDIMapping.m +++ b/Source/MIKMIDIMapping.m @@ -13,6 +13,8 @@ #import "MIKMIDINoteOnCommand.h" #import "MIKMIDINoteOffCommand.h" #import "MIKMIDIPrivateUtilities.h" +#import "MIKMIDIUtilities.h" +#import "MIKMIDIMappingXMLParser.h" #if !__has_feature(objc_arc) #error MIKMIDIMapping.m must be compiled with ARC. Either turn on ARC for the project or set the -fobjc-arc flag for MIKMIDIMapping.m in the Build Phases for this target @@ -36,8 +38,6 @@ @interface MIKMIDIMapping () @implementation MIKMIDIMapping -#if !TARGET_OS_IPHONE - - (instancetype)initWithFileAtURL:(NSURL *)url { return [self initWithFileAtURL:url error:NULL]; @@ -46,6 +46,15 @@ - (instancetype)initWithFileAtURL:(NSURL *)url - (instancetype)initWithFileAtURL:(NSURL *)url error:(NSError **)error; { error = error ? error : &(NSError *__autoreleasing){ nil }; +#if TARGET_OS_IPHONE + // iOS + NSData *data = [NSData dataWithContentsOfURL:url options:0 error:error]; + if (!data) return nil; + MIKMIDIMappingXMLParser *parser = [MIKMIDIMappingXMLParser parserWithXMLData:data]; + self = [parser.mappings firstObject]; + return self; +#else + // OS X NSXMLDocument *xmlDocument = [[NSXMLDocument alloc] initWithContentsOfURL:url options:0 error:error]; if (!xmlDocument) { NSLog(@"Unable to read MIDI map XML file at %@: %@", url, *error); @@ -57,8 +66,10 @@ - (instancetype)initWithFileAtURL:(NSURL *)url error:(NSError **)error; if (![_name length]) _name = [[url lastPathComponent] stringByDeletingPathExtension]; } return self; +#endif // TARGET_OS_IPHONE } +#if !TARGET_OS_IPHONE - (instancetype)initWithXMLDocument:(NSXMLDocument *)xmlDocument { self = [self init]; @@ -70,7 +81,6 @@ - (instancetype)initWithXMLDocument:(NSXMLDocument *)xmlDocument } return self; } - #endif - (id)init @@ -135,20 +145,33 @@ - (NSXMLDocument *)XMLRepresentation; [result setCharacterEncoding:@"UTF-8"]; return result; } +#endif + +- (NSData *)XMLData; +{ +#if !TARGET_OS_IPHONE + return [[self XMLRepresentation] XMLDataWithOptions:NSXMLNodePrettyPrint]; +#endif + + NSMutableString *result = [NSMutableString string]; + + + return [result dataUsingEncoding:NSUTF8StringEncoding]; +} - (BOOL)writeToFileAtURL:(NSURL *)fileURL error:(NSError **)error; { +#if !TARGET_OS_IPHONE error = error ? error : &(NSError *__autoreleasing){ nil }; - NSData *mappingXMLData = [[self XMLRepresentation] XMLDataWithOptions:NSXMLNodePrettyPrint]; - if (![mappingXMLData writeToURL:fileURL options:NSDataWritingAtomic error:error]) { + if (![[self XMLData] writeToURL:fileURL options:NSDataWritingAtomic error:error]) { NSLog(@"Error saving MIDI mapping %@ to %@: %@", self.name, fileURL, *error); return NO; } return YES; + #endif + return NO; } -#endif - - (BOOL)isEqual:(MIKMIDIMapping *)otherMapping { if (self == otherMapping) return YES; @@ -473,7 +496,7 @@ - (NSUInteger)hash - (NSString *)description { - NSMutableString *result = [NSMutableString stringWithFormat:@"%@ %@ %@ CommandID: %@ Channel %li MIDI Command %li Control Number %lu flipped %i", [super description], [self stringForInteractionType:self.interactionType], self.MIDIResponderIdentifier, self.commandIdentifier, (long)self.channel, (long)self.commandType, (unsigned long)self.controlNumber, (int)self.flipped]; + NSMutableString *result = [NSMutableString stringWithFormat:@"%@ %@ %@ CommandID: %@ Channel %li MIDI Command %li Control Number %lu flipped %i", [super description], MIKMIDIMappingAttributeStringForInteractionType(self.interactionType), self.MIDIResponderIdentifier, self.commandIdentifier, (long)self.channel, (long)self.commandType, (unsigned long)self.controlNumber, (int)self.flipped]; if ([self.additionalAttributes count]) { for (NSString *key in self.additionalAttributes) { NSString *value = self.additionalAttributes[key]; @@ -487,28 +510,6 @@ - (NSString *)description #pragma mark - Private -- (NSString *)stringForInteractionType:(MIKMIDIResponderType)type -{ - NSDictionary *map = @{@(MIKMIDIResponderTypePressReleaseButton) : @"Key", - @(MIKMIDIResponderTypePressButton) : @"Tap", - @(MIKMIDIResponderTypeAbsoluteSliderOrKnob) : @"KnobSlider", - @(MIKMIDIResponderTypeRelativeKnob) : @"JogWheel", - @(MIKMIDIResponderTypeTurntableKnob) : @"TurnTable", - @(MIKMIDIResponderTypeRelativeAbsoluteKnob) : @"RelativeAbsoluteKnob"}; - return [map objectForKey:@(type)]; -} - -- (MIKMIDIResponderType)interactionTypeForString:(NSString *)string -{ - NSDictionary *map = @{@"Key" : @(MIKMIDIResponderTypePressReleaseButton), - @"Tap" : @(MIKMIDIResponderTypePressButton), - @"KnobSlider" : @(MIKMIDIResponderTypeAbsoluteSliderOrKnob), - @"JogWheel" : @(MIKMIDIResponderTypeRelativeKnob), - @"TurnTable" : @(MIKMIDIResponderTypeTurntableKnob), - @"RelativeAbsoluteKnob" : @(MIKMIDIResponderTypeRelativeAbsoluteKnob)}; - return [[map objectForKey:string] integerValue]; -} - #pragma mark - Properties @end diff --git a/Source/MIKMIDIMappingManager.m b/Source/MIKMIDIMappingManager.m index 2ca69ea8..96003a3f 100644 --- a/Source/MIKMIDIMappingManager.m +++ b/Source/MIKMIDIMappingManager.m @@ -154,8 +154,6 @@ - (NSURL *)userMappingsFolder - (void)loadAvailableUserMappings { NSMutableSet *mappings = [NSMutableSet set]; - -#if !TARGET_OS_IPHONE NSURL *mappingsFolder = [self userMappingsFolder]; NSFileManager *fm = [NSFileManager defaultManager]; @@ -172,9 +170,7 @@ - (void)loadAvailableUserMappings } else { NSLog(@"Unable to get contents of directory at %@: %@", mappingsFolder, error); } - -#endif - + self.internalUserMappings = mappings; } @@ -182,7 +178,6 @@ - (void)loadBundledMappings { NSMutableSet *mappings = [NSMutableSet set]; -#if !TARGET_OS_IPHONE NSBundle *bundle = [NSBundle mainBundle]; NSArray *bundledMappingFileURLs = [bundle URLsForResourcesWithExtension:kMIKMIDIMappingFileExtension subdirectory:nil]; for (NSURL *file in bundledMappingFileURLs) { @@ -190,7 +185,6 @@ - (void)loadBundledMappings mapping.bundledMapping = YES; if (mapping) [mappings addObject:mapping]; } -#endif self.bundledMappings = mappings; } diff --git a/Source/MIKMIDIMappingXMLParser.h b/Source/MIKMIDIMappingXMLParser.h new file mode 100644 index 00000000..5bdf5a8e --- /dev/null +++ b/Source/MIKMIDIMappingXMLParser.h @@ -0,0 +1,24 @@ +// +// MIKMIDIMappingXMLParser.h +// MIDI Soundboard +// +// Created by Andrew Madsen on 4/15/14. +// Copyright (c) 2014 Mixed In Key. All rights reserved. +// + +#import + +@class MIKMIDIMapping; + +/** + * A parser for XML MIDI mapping files. Only used on iOS. On OS X, NSXMLDocument is used + * directly instead. Should be considered "private" for use by MIKMIDIMapping. + */ +@interface MIKMIDIMappingXMLParser : NSObject + ++ (instancetype)parserWithXMLData:(NSData *)xmlData; +- (instancetype)initWithXMLData:(NSData *)xmlData; + +@property (nonatomic, strong, readonly) NSArray *mappings; + +@end diff --git a/Source/MIKMIDIMappingXMLParser.m b/Source/MIKMIDIMappingXMLParser.m new file mode 100644 index 00000000..a85fd4e8 --- /dev/null +++ b/Source/MIKMIDIMappingXMLParser.m @@ -0,0 +1,175 @@ +// +// MIKMIDIMappingXMLParser.m +// MIDI Soundboard +// +// Created by Andrew Madsen on 4/15/14. +// Copyright (c) 2014 Open Reel Software. All rights reserved. +// + +#import "MIKMIDIMappingXMLParser.h" +#import "MIKMIDIMapping.h" +#import "MIKMIDIUtilities.h" + +@interface NSString (MIKMIDIMappingXMLParserUtilities) + +- (NSString *)mik_uncapitalizedString; + +@end + +@interface MIKMIDIMappingXMLParser () + +@property (nonatomic, strong) NSXMLParser *parser; +@property (nonatomic, strong) NSData *xmlData; +@property (nonatomic, strong) NSMutableArray *internalMappings; +@property (nonatomic, strong) NSArray *mappings; +@property (nonatomic, strong) MIKMIDIMapping *currentMapping; + +@property (nonatomic, strong) NSMutableDictionary *currentItemInfo; +@property (nonatomic, strong) NSString *currentElementName; +@property (nonatomic, strong) NSMutableString *currentElementValueBuffer; + +@property (nonatomic) BOOL hasParsed; + +@end + +@implementation MIKMIDIMappingXMLParser + ++ (instancetype)parserWithXMLData:(NSData *)xmlData +{ + return [[self alloc] initWithXMLData:xmlData]; +} + +- (instancetype)initWithXMLData:(NSData *)xmlData +{ + if (![xmlData length]) return nil; + + self = [super init]; + if (self) { + _xmlData = xmlData; + _internalMappings = [NSMutableArray array]; + } + return self; +} + +#pragma mark - Private + +- (void)parse +{ + NSXMLParser *parser = [[NSXMLParser alloc] initWithData:self.xmlData]; + [parser setDelegate:self]; + + self.hasParsed = [parser parse]; +} + +#pragma mark - NSXMLParserDelegate + +- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict +{ + if ([elementName isEqualToString:@"Mapping"]) { + self.currentMapping = [[MIKMIDIMapping alloc] init]; + self.currentMapping.controllerName = attributeDict[@"ControllerName"]; + return; + } + + if ([elementName isEqualToString:@"MappingItem"]) { + self.currentItemInfo = [NSMutableDictionary dictionary]; + self.currentItemInfo[@"additionalAttributes"] = [NSMutableDictionary dictionary]; + for (NSString *key in attributeDict) { + id attributeValue = attributeDict[key]; + if ([key isEqualToString:@"InteractionType"]) { + self.currentItemInfo[@"interactionType"] = @(MIKMIDIMappingInteractionTypeForAttributeString(attributeValue)); + return; + } else if ([key isEqualToString:@"Flipped"]) { + self.currentItemInfo[@"flipped"] = @([attributeValue boolValue]); + return; + } + + self.currentItemInfo[@"additionalAttributes"][[key mik_uncapitalizedString]] = attributeValue; + } + return; + } + + if (self.currentItemInfo) { + // In the middle parsing a mapping item + self.currentElementName = elementName; + self.currentElementValueBuffer = [NSMutableString string]; + + return; + } +} + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string +{ + [self.currentElementValueBuffer appendString:string]; +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName +{ + if ([elementName isEqualToString:@"Mapping"]) { + if (!self.currentMapping) return; + [self.internalMappings addObject:self.currentMapping]; + self.currentMapping = nil; + + return; + } + + if ([elementName isEqualToString:@"MappingItem"]) { + NSString *responderID = self.currentItemInfo[@"responderIdentifier"]; + [self.currentItemInfo removeObjectForKey:@"responderIdentifier"]; + NSString *commandID = self.currentItemInfo[@"commandIdentifier"]; + [self.currentItemInfo removeObjectForKey:@"commandIdentifier"]; + MIKMIDIMappingItem *item = [[MIKMIDIMappingItem alloc] initWithMIDIResponderIdentifier:responderID andCommandIdentifier:commandID]; + + [item setValuesForKeysWithDictionary:self.currentItemInfo]; + + [self.currentMapping addMappingItemsObject:item]; + + self.currentItemInfo = nil; + + return; + } + + if ([elementName isEqualToString:self.currentElementName]) { + self.currentItemInfo[[elementName mik_uncapitalizedString]] = [self.currentElementValueBuffer copy]; + self.currentElementName = nil; + self.currentElementValueBuffer = nil; + + return; + } +} + +- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError +{ + NSLog(@"Parsing failed with error: %@", parseError); + self.currentMapping = nil; +} + +- (void)parserDidEndDocument:(NSXMLParser *)parser +{ + self.mappings = [self.internalMappings copy]; +} + +#pragma mark - Properties + +- (NSArray *)mappings +{ + if (!self.hasParsed) [self parse]; + return _mappings; +} + +@end + +@implementation NSString (MIKMIDIMappingXMLParserUtilities) + +- (NSString *)mik_uncapitalizedString +{ + // This is a bit quick and dirty, but it does the job + if (![self length]) return self; + + NSMutableString *scratch = [self mutableCopy]; + NSString *firstCharacter = [self substringToIndex:1]; + [scratch replaceCharactersInRange:NSMakeRange(0, 1) withString:[firstCharacter lowercaseString]]; + return [scratch copy]; +} + +@end \ No newline at end of file diff --git a/Source/MIKMIDIUtilities.h b/Source/MIKMIDIUtilities.h index 30da8cf6..453d675e 100644 --- a/Source/MIKMIDIUtilities.h +++ b/Source/MIKMIDIUtilities.h @@ -8,6 +8,7 @@ #import #import +#import "MIKMIDIMapping.h" NSString *MIKStringPropertyFromMIDIObject(MIDIObjectRef object, CFStringRef propertyID, NSError *__autoreleasing*error); BOOL MIKSetStringPropertyOnMIDIObject(MIDIObjectRef object, CFStringRef propertyID, NSString *string, NSError *__autoreleasing*error); @@ -15,4 +16,7 @@ BOOL MIKSetStringPropertyOnMIDIObject(MIDIObjectRef object, CFStringRef property SInt32 MIKIntegerPropertyFromMIDIObject(MIDIObjectRef object, CFStringRef propertyID, NSError *__autoreleasing*error); BOOL MIKSetIntegerPropertyFromMIDIObject(MIDIObjectRef object, CFStringRef propertyID, SInt32 integerValue, NSError *__autoreleasing*error); -MIDIObjectType MIKMIDIObjectTypeOfObject(MIDIObjectRef object, NSError *__autoreleasing*error); \ No newline at end of file +MIDIObjectType MIKMIDIObjectTypeOfObject(MIDIObjectRef object, NSError *__autoreleasing*error); + +NSString *MIKMIDIMappingAttributeStringForInteractionType(MIKMIDIResponderType type); +MIKMIDIResponderType MIKMIDIMappingInteractionTypeForAttributeString(NSString *string); \ No newline at end of file diff --git a/Source/MIKMIDIUtilities.m b/Source/MIKMIDIUtilities.m index 7c36e7cf..953c688e 100644 --- a/Source/MIKMIDIUtilities.m +++ b/Source/MIKMIDIUtilities.m @@ -82,4 +82,26 @@ MIDIObjectType MIKMIDIObjectTypeOfObject(MIDIObjectRef object, NSError *__autore } return objectType; -} \ No newline at end of file +} + +NSString *MIKMIDIMappingAttributeStringForInteractionType(MIKMIDIResponderType type) +{ + NSDictionary *map = @{@(MIKMIDIResponderTypePressReleaseButton) : @"Key", + @(MIKMIDIResponderTypePressButton) : @"Tap", + @(MIKMIDIResponderTypeAbsoluteSliderOrKnob) : @"KnobSlider", + @(MIKMIDIResponderTypeRelativeKnob) : @"JogWheel", + @(MIKMIDIResponderTypeTurntableKnob) : @"TurnTable", + @(MIKMIDIResponderTypeRelativeAbsoluteKnob) : @"RelativeAbsoluteKnob"}; + return [map objectForKey:@(type)]; +} + +MIKMIDIResponderType MIKMIDIMappingInteractionTypeForAttributeString(NSString *string) +{ + NSDictionary *map = @{@"Key" : @(MIKMIDIResponderTypePressReleaseButton), + @"Tap" : @(MIKMIDIResponderTypePressButton), + @"KnobSlider" : @(MIKMIDIResponderTypeAbsoluteSliderOrKnob), + @"JogWheel" : @(MIKMIDIResponderTypeRelativeKnob), + @"TurnTable" : @(MIKMIDIResponderTypeTurntableKnob), + @"RelativeAbsoluteKnob" : @(MIKMIDIResponderTypeRelativeAbsoluteKnob)}; + return [[map objectForKey:string] integerValue]; +}