From: Joe Wreschnig Date: Sun, 17 Mar 2013 21:18:46 +0000 (+0100) Subject: Change two-pass behavior for loading mappings. Allow lazy binding of mappings by... X-Git-Tag: version-1.1~18 X-Git-Url: https://git.yukkurigames.com/?a=commitdiff_plain;h=24bdb92798b9abe86c7954042a47523791736b7c;p=enjoyable.git Change two-pass behavior for loading mappings. Allow lazy binding of mappings by name. Don't sleep the HID when debugging, as it's just a pain. --- diff --git a/Categories/NSProcessInfo+Debugging.h b/Categories/NSProcessInfo+Debugging.h new file mode 100644 index 0000000..75533ed --- /dev/null +++ b/Categories/NSProcessInfo+Debugging.h @@ -0,0 +1,15 @@ +// +// NSProcessInfo+Debugging.h +// Enjoyable +// +// Created by Joe Wreschnig on 3/17/13. +// +// + +#import + +@interface NSProcessInfo (Debugging) + +- (BOOL)isBeingDebugged; + +@end diff --git a/Categories/NSProcessInfo+Debugging.m b/Categories/NSProcessInfo+Debugging.m new file mode 100644 index 0000000..649de3b --- /dev/null +++ b/Categories/NSProcessInfo+Debugging.m @@ -0,0 +1,35 @@ +// +// NSProcessInfo+Debugging.m +// Enjoyable +// +// Created by Joe Wreschnig on 3/17/13. +// +// + +#import "NSProcessInfo+Debugging.h" + +#include +#include +#include +#include +#include + +@implementation NSProcessInfo (Debugging) + +- (BOOL)isBeingDebugged { + int mib[4]; + struct kinfo_proc info; + size_t size = sizeof(info); + + info.kp_proc.p_flag = 0; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = self.processIdentifier; + + return sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0) == 0 + && (info.kp_proc.p_flag & P_TRACED) != 0; +} + +@end diff --git a/Classes/EnjoyableApplicationDelegate.m b/Classes/EnjoyableApplicationDelegate.m index fe781ef..26ea09c 100644 --- a/Classes/EnjoyableApplicationDelegate.m +++ b/Classes/EnjoyableApplicationDelegate.m @@ -138,7 +138,6 @@ NSError *error; NSURL *URL = [NSURL fileURLWithPath:filename]; NJMapping *mapping = [NJMapping mappingWithContentsOfURL:URL - mappings:self.mappingsController error:&error]; if (mapping) { [self.mappingsController addOrMergeMapping:mapping]; @@ -213,7 +212,6 @@ [panel close]; NSError *error; NJMapping *mapping = [NJMapping mappingWithContentsOfURL:panel.URL - mappings:self.mappingsController error:&error]; if (mapping) { [self.mappingsController addOrMergeMapping:mapping]; diff --git a/Classes/NJDeviceController.m b/Classes/NJDeviceController.m index 329b353..000df5f 100644 --- a/Classes/NJDeviceController.m +++ b/Classes/NJDeviceController.m @@ -221,7 +221,7 @@ } - (void)stopHidIfDisabled:(NSNotification *)application { - if (!self.simulatingEvents) + if (!self.simulatingEvents && !NSProcessInfo.processInfo.isBeingDebugged) [self stopHid]; } diff --git a/Classes/NJMapping.h b/Classes/NJMapping.h index 0d5db89..be3aa76 100644 --- a/Classes/NJMapping.h +++ b/Classes/NJMapping.h @@ -15,12 +15,10 @@ @property (nonatomic, readonly) NSUInteger count; + (id)mappingWithContentsOfURL:(NSURL *)url - mappings:(id )mappings error:(NSError **)error; - (id)initWithName:(NSString *)name; -- (id)initWithSerialization:(NSDictionary *)serialization - mappings:(id )mappings; +- (id)initWithSerialization:(NSDictionary *)serialization; - (NJOutput *)objectForKeyedSubscript:(NJInput *)input; - (void)setObject:(NJOutput *)output forKeyedSubscript:(NJInput *)input; @@ -29,4 +27,6 @@ - (BOOL)hasConflictWith:(NJMapping *)other; - (void)mergeEntriesFrom:(NJMapping *)other; +- (void)postLoadProcess:(id )allMappings; + @end diff --git a/Classes/NJMapping.m b/Classes/NJMapping.m index 04f6e68..3d5cd67 100644 --- a/Classes/NJMapping.m +++ b/Classes/NJMapping.m @@ -33,15 +33,13 @@ return self; } -- (id)initWithSerialization:(NSDictionary *)serialization - mappings:(id )mappings { +- (id)initWithSerialization:(NSDictionary *)serialization { if ((self = [self initWithName:serialization[@"name"]])) { NSDictionary *entries = serialization[@"entries"]; if ([entries isKindOfClass:NSDictionary.class]) { for (id key in entries) { if ([key isKindOfClass:NSString.class]) { - NJOutput *output = [NJOutput outputDeserialize:entries[key] - withMappings:mappings]; + NJOutput *output = [NJOutput outputDeserialize:entries[key]]; if (output) _entries[key] = output; } @@ -100,7 +98,7 @@ return NO; } -+ (id)mappingWithContentsOfURL:(NSURL *)url mappings:(id )mappings error:(NSError **)error { ++ (id)mappingWithContentsOfURL:(NSURL *)url error:(NSError **)error { NSInputStream *stream = [NSInputStream inputStreamWithURL:url]; [stream open]; NSDictionary *serialization = stream && !*error @@ -121,8 +119,7 @@ return nil; } - return [[NJMapping alloc] initWithSerialization:serialization - mappings:mappings]; + return [[NJMapping alloc] initWithSerialization:serialization]; } - (void)mergeEntriesFrom:(NJMapping *)other { @@ -130,4 +127,10 @@ [_entries addEntriesFromDictionary:other->_entries]; } +- (void)postLoadProcess:(id )allMappings { + for (NJOutput *o in _entries.allValues) + [o postLoadProcess:allMappings]; +} + + @end diff --git a/Classes/NJMappingsController.m b/Classes/NJMappingsController.m index 4859970..9c7b641 100644 --- a/Classes/NJMappingsController.m +++ b/Classes/NJMappingsController.m @@ -42,6 +42,7 @@ } - (void)mappingsSet { + [self postLoadProcess]; [NSNotificationCenter.defaultCenter postNotificationName:NJEventMappingListChanged object:self @@ -111,26 +112,22 @@ [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"]; } +- (void)postLoadProcess { + for (NJMapping *mapping in self) + [mapping postLoadProcess:self]; +} + - (void)load { NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"]; NSArray *storedMappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"]; NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count]; - // Requires two passes to deal with inter-mapping references. First make - // an empty mapping for each serialized mapping. Then, deserialize the - // data pointing to the empty mappings. Then merge that data back into - // its equivalent empty one, which is the one we finally use. - for (NSDictionary *storedMapping in storedMappings) { - NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]]; - [newMappings addObject:mapping]; - } - for (unsigned i = 0; i < storedMappings.count; ++i) { - NJMapping *realMapping = [[NJMapping alloc] initWithSerialization:storedMappings[i] - mappings:newMappings]; - [newMappings[i] mergeEntriesFrom:realMapping]; + NJMapping *mapping = [[NJMapping alloc] initWithSerialization:storedMappings[i]]; + [newMappings addObject:mapping]; } + if (newMappings.count) { _mappings = newMappings; if (selected >= newMappings.count) @@ -256,7 +253,6 @@ atIndex:(NSInteger)index error:(NSError **)error { NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url - mappings:_mappings error:error]; [self addOrMergeMapping:mapping atIndex:index]; return !!mapping; diff --git a/Classes/NJOutput.h b/Classes/NJOutput.h index 5d4d470..aeb8e46 100644 --- a/Classes/NJOutput.h +++ b/Classes/NJOutput.h @@ -19,8 +19,9 @@ - (BOOL)update:(NJDeviceController *)jc; - (NSDictionary *)serialize; -+ (NJOutput *)outputDeserialize:(NSDictionary *)serialization - withMappings:(id )mappings; ++ (NJOutput *)outputDeserialize:(NSDictionary *)serialization; + (NSString *)serializationCode; +- (void)postLoadProcess:(id )allMappings; + @end diff --git a/Classes/NJOutput.m b/Classes/NJOutput.m index 35a56d9..54809ce 100644 --- a/Classes/NJOutput.m +++ b/Classes/NJOutput.m @@ -36,8 +36,7 @@ return [[self serialize] hash]; } -+ (NJOutput *)outputDeserialize:(NSDictionary *)serialization - withMappings:(id )mappings { ++ (NJOutput *)outputDeserialize:(NSDictionary *)serialization { // Don't crash loading old/bad mappings (but don't load them either). if (![serialization isKindOfClass:NSDictionary.class]) return nil; @@ -49,7 +48,7 @@ NJOutputMouseScroll.class ]) { if ([type isEqualToString:cls.serializationCode]) - return [cls outputDeserialize:serialization withMappings:mappings]; + return [cls outputDeserialize:serialization]; } return nil; @@ -83,5 +82,7 @@ } } +- (void)postLoadProcess:(id )allMappings { +} @end diff --git a/Classes/NJOutputController.h b/Classes/NJOutputController.h index 6f4c866..3974e2e 100644 --- a/Classes/NJOutputController.h +++ b/Classes/NJOutputController.h @@ -26,6 +26,7 @@ IBOutlet NJMappingsController *mappingsController; IBOutlet NJDeviceController *inputController; IBOutlet NSButton *smoothCheck; + IBOutlet NSButton *unknownMapping; } @property (assign) BOOL enabled; diff --git a/Classes/NJOutputController.m b/Classes/NJOutputController.m index 07c077b..fd160e6 100644 --- a/Classes/NJOutputController.m +++ b/Classes/NJOutputController.m @@ -53,8 +53,8 @@ if (row != 2) { [mappingPopup selectItemAtIndex:-1]; [mappingPopup resignIfFirstResponder]; - } else if (!mappingPopup.selectedItem) - [mappingPopup selectItemAtIndex:0]; + unknownMapping.hidden = YES; + } if (row != 3) { mouseDirSelect.selectedSegment = -1; @@ -108,6 +108,7 @@ - (void)mappingChosen:(id)sender { [radioButtons selectCellAtRow:2 column:0]; [mappingPopup.window makeFirstResponder:mappingPopup]; + unknownMapping.hidden = YES; [self commit]; } @@ -220,6 +221,8 @@ scrollDirSelect.enabled = enabled; smoothCheck.enabled = enabled; scrollSpeedSlider.enabled = enabled && smoothCheck.state; + if (!enabled) + unknownMapping.hidden = YES; } - (void)loadOutput:(NJOutput *)output forInput:(NJInput *)input { @@ -242,8 +245,8 @@ [radioButtons selectCellAtRow:2 column:0]; NSMenuItem *item = [mappingPopup itemWithRepresentedObject:[(NJOutputMapping *)output mapping]]; [mappingPopup selectItem:item]; - if (!item) - [radioButtons selectCellAtRow:self.enabled ? 0 : -1 column:0]; + unknownMapping.hidden = !!item; + unknownMapping.title = [(NJOutputMapping *)output mappingName]; } else if ([output isKindOfClass:NJOutputMouseMove.class]) { [radioButtons selectCellAtRow:3 column:0]; diff --git a/Classes/NJOutputKeyPress.m b/Classes/NJOutputKeyPress.m index e80aba5..7b8c631 100644 --- a/Classes/NJOutputKeyPress.m +++ b/Classes/NJOutputKeyPress.m @@ -21,8 +21,7 @@ : nil; } -+ (NJOutput *)outputDeserialize:(NSDictionary *)serialization - withMappings:(id )mappings { ++ (NJOutput *)outputDeserialize:(NSDictionary *)serialization { NJOutputKeyPress *output = [[NJOutputKeyPress alloc] init]; output.keyCode = [serialization[@"key"] intValue]; return output; diff --git a/Classes/NJOutputMapping.h b/Classes/NJOutputMapping.h index e8c3029..cc5b3ed 100644 --- a/Classes/NJOutputMapping.h +++ b/Classes/NJOutputMapping.h @@ -13,5 +13,6 @@ @interface NJOutputMapping : NJOutput @property (nonatomic, weak) NJMapping *mapping; +@property (nonatomic, copy) NSString *mappingName; @end diff --git a/Classes/NJOutputMapping.m b/Classes/NJOutputMapping.m index 28a84ab..efb7c00 100644 --- a/Classes/NJOutputMapping.m +++ b/Classes/NJOutputMapping.m @@ -18,27 +18,39 @@ } - (NSDictionary *)serialize { - return _mapping - ? @{ @"type": self.class.serializationCode, @"name": _mapping.name } + NSString *name = _mapping ? _mapping.name : self.mappingName; + return name + ? @{ @"type": self.class.serializationCode, @"name": name } : nil; } -+ (NJOutputMapping *)outputDeserialize:(NSDictionary *)serialization - withMappings:(id )mappings { ++ (NJOutputMapping *)outputDeserialize:(NSDictionary *)serialization { NSString *name = serialization[@"name"]; NJOutputMapping *output = [[NJOutputMapping alloc] init]; - for (NJMapping *mapping in mappings) { - if ([mapping.name isEqualToString:name]) { - output.mapping = mapping; - return output; - } - } - return nil; + output.mappingName = name; + return name ? output : nil; } - (void)trigger { EnjoyableApplicationDelegate *ctrl = (EnjoyableApplicationDelegate *)NSApplication.sharedApplication.delegate; - [ctrl.mappingsController activateMapping:_mapping]; + if (_mapping) { + [ctrl.mappingsController activateMapping:_mapping]; + self.mappingName = _mapping.name; + } else { + // TODO: Show an error message? Unobtrusively since something + // is probably running. + } +} + +- (void)postLoadProcess:(id )allMappings { + if (!self.mapping) { + for (NJMapping *mapping in allMappings) { + if ([mapping.name isEqualToString:self.mappingName]) { + self.mapping = mapping; + break; + } + } + } } @end diff --git a/Classes/NJOutputMouseButton.m b/Classes/NJOutputMouseButton.m index e01e763..0b039d1 100644 --- a/Classes/NJOutputMouseButton.m +++ b/Classes/NJOutputMouseButton.m @@ -36,8 +36,7 @@ return @{ @"type": self.class.serializationCode, @"button": @(_button) }; } -+ (NJOutput *)outputDeserialize:(NSDictionary *)serialization - withMappings:(id )mappings { ++ (NJOutput *)outputDeserialize:(NSDictionary *)serialization { NJOutputMouseButton *output = [[NJOutputMouseButton alloc] init]; output.button = [serialization[@"button"] intValue]; return output; diff --git a/Classes/NJOutputMouseMove.m b/Classes/NJOutputMouseMove.m index 8591923..84c0ea3 100644 --- a/Classes/NJOutputMouseMove.m +++ b/Classes/NJOutputMouseMove.m @@ -22,8 +22,7 @@ }; } -+ (NJOutput *)outputDeserialize:(NSDictionary *)serialization - withMappings:(id )mappings { ++ (NJOutput *)outputDeserialize:(NSDictionary *)serialization { NJOutputMouseMove *output = [[NJOutputMouseMove alloc] init]; output.axis = [serialization[@"axis"] intValue]; output.speed = [serialization[@"speed"] floatValue]; diff --git a/Classes/NJOutputMouseScroll.m b/Classes/NJOutputMouseScroll.m index 6fdc918..06913d6 100644 --- a/Classes/NJOutputMouseScroll.m +++ b/Classes/NJOutputMouseScroll.m @@ -21,8 +21,7 @@ }; } -+ (NJOutput *)outputDeserialize:(NSDictionary *)serialization - withMappings:(id )mappings { ++ (NJOutput *)outputDeserialize:(NSDictionary *)serialization { NJOutputMouseScroll *output = [[NJOutputMouseScroll alloc] init]; output.direction = [serialization[@"direction"] intValue]; output.speed = [serialization[@"speed"] floatValue]; diff --git a/Enjoyable.xcodeproj/project.pbxproj b/Enjoyable.xcodeproj/project.pbxproj index 27ca44a..890b562 100644 --- a/Enjoyable.xcodeproj/project.pbxproj +++ b/Enjoyable.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ EE3D897C16EA806E00596D1F /* Status Menu Icon Disabled@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = EE3D897B16EA806E00596D1F /* Status Menu Icon Disabled@2x.png */; }; EE3D897F16EA817E00596D1F /* Status Menu Icon Disabled.png in Resources */ = {isa = PBXBuildFile; fileRef = EE3D897D16EA817E00596D1F /* Status Menu Icon Disabled.png */; }; EE3D898016EA817E00596D1F /* Status Menu Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = EE3D897E16EA817E00596D1F /* Status Menu Icon.png */; }; + EE48263016F6680D001B0C64 /* NSProcessInfo+Debugging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE48262F16F6680D001B0C64 /* NSProcessInfo+Debugging.m */; }; EE52145C16F3E8BD00E3C574 /* NSOutlineView+ItemAccessors.m in Sources */ = {isa = PBXBuildFile; fileRef = EE52145B16F3E8BD00E3C574 /* NSOutlineView+ItemAccessors.m */; }; EE52145F16F404D500E3C574 /* NJDeviceViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE52145E16F404D500E3C574 /* NJDeviceViewController.m */; }; EE6A122E16E8F46300EDBD32 /* Icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = EE6A122D16E8F46300EDBD32 /* Icon.icns */; }; @@ -81,6 +82,8 @@ EE3D897B16EA806E00596D1F /* Status Menu Icon Disabled@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Status Menu Icon Disabled@2x.png"; path = "Resources/Status Menu Icon Disabled@2x.png"; sourceTree = ""; }; EE3D897D16EA817E00596D1F /* Status Menu Icon Disabled.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Status Menu Icon Disabled.png"; path = "Resources/Status Menu Icon Disabled.png"; sourceTree = ""; }; EE3D897E16EA817E00596D1F /* Status Menu Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Status Menu Icon.png"; path = "Resources/Status Menu Icon.png"; sourceTree = ""; }; + EE48262E16F6680D001B0C64 /* NSProcessInfo+Debugging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSProcessInfo+Debugging.h"; path = "Categories/NSProcessInfo+Debugging.h"; sourceTree = ""; }; + EE48262F16F6680D001B0C64 /* NSProcessInfo+Debugging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSProcessInfo+Debugging.m"; path = "Categories/NSProcessInfo+Debugging.m"; sourceTree = ""; }; EE52145A16F3E8BD00E3C574 /* NSOutlineView+ItemAccessors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSOutlineView+ItemAccessors.h"; path = "Categories/NSOutlineView+ItemAccessors.h"; sourceTree = ""; }; EE52145B16F3E8BD00E3C574 /* NSOutlineView+ItemAccessors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSOutlineView+ItemAccessors.m"; path = "Categories/NSOutlineView+ItemAccessors.m"; sourceTree = ""; }; EE52145D16F404D500E3C574 /* NJDeviceViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NJDeviceViewController.h; path = Classes/NJDeviceViewController.h; sourceTree = ""; }; @@ -317,6 +320,8 @@ EEE73B1516EA42E5009D9D99 /* NSRunningApplication+NJPossibleNames.m */, EE52145A16F3E8BD00E3C574 /* NSOutlineView+ItemAccessors.h */, EE52145B16F3E8BD00E3C574 /* NSOutlineView+ItemAccessors.m */, + EE48262E16F6680D001B0C64 /* NSProcessInfo+Debugging.h */, + EE48262F16F6680D001B0C64 /* NSProcessInfo+Debugging.m */, ); name = Categories; sourceTree = ""; @@ -460,6 +465,7 @@ EE52145C16F3E8BD00E3C574 /* NSOutlineView+ItemAccessors.m in Sources */, EE52145F16F404D500E3C574 /* NJDeviceViewController.m in Sources */, EE83ACEC16F545EA00083E94 /* NJMappingsViewController.m in Sources */, + EE48263016F6680D001B0C64 /* NSProcessInfo+Debugging.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Info.plist b/Info.plist index 94c5825..15d916e 100644 --- a/Info.plist +++ b/Info.plist @@ -46,7 +46,7 @@ CFBundleSignature ???? CFBundleVersion - 448 + 479 LSApplicationCategoryType public.app-category.utilities NSHumanReadableCopyright diff --git a/Other Sources/Enjoyable_Prefix.pch b/Other Sources/Enjoyable_Prefix.pch index e074f78..a873aec 100644 --- a/Other Sources/Enjoyable_Prefix.pch +++ b/Other Sources/Enjoyable_Prefix.pch @@ -17,3 +17,4 @@ #import "NSRunningApplication+NJPossibleNames.h" #import "NSRunningApplication+LoginItem.h" #import "NSOutlineView+ItemAccessors.h" +#import "NSProcessInfo+Debugging.h" diff --git a/Resources/English.lproj/MainMenu.xib b/Resources/English.lproj/MainMenu.xib index 6afbc50..e045c4d 100644 --- a/Resources/English.lproj/MainMenu.xib +++ b/Resources/English.lproj/MainMenu.xib @@ -566,7 +566,7 @@ aW5nLg {232, 321} - + YES NO YES @@ -696,7 +696,7 @@ aW5nLg {234, 323} - + 150034 @@ -718,6 +718,31 @@ aW5nLg 285 + + + 268 + {{197, 157}, {193, 21}} + + + + _NS:9 + YES + + 603979776 + 33554432 + Unknown Mapping + + _NS:9 + + -2046672896 + 129 + + + 200 + 25 + + NO + 265 @@ -766,6 +791,7 @@ aW5nLg {{343, 31}, {70, 18}} + _NS:9 YES @@ -958,7 +984,7 @@ aW5nLg {{188, 153}, {226, 26}} - + YES -2076180416 @@ -1207,7 +1233,7 @@ aW5nLg {198, 198} - + YES NO YES @@ -1299,7 +1325,7 @@ aW5nLg {{0, 20}, {200, 200}} - + 150034 @@ -1371,6 +1397,7 @@ aW5nLg {{166, -1}, {34, 23}} + YES 67108864 @@ -1973,6 +2000,14 @@ aW5nLg 949 + + + unknownMapping + + + + 1022 + delegate @@ -2730,6 +2765,7 @@ aW5nLg + @@ -3079,12 +3115,27 @@ aW5nLg + + 1020 + + + + + + + + 1021 + + + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3336,7 +3387,7 @@ aW5nLg - 1016 + 1022 @@ -3662,6 +3713,7 @@ aW5nLg NSSlider NSButton NSTextField + NSBUtton @@ -3712,6 +3764,10 @@ aW5nLg title NSTextField + + unknownMapping + NSBUtton + IBProjectSource