From dacfcc6b0bf3bfebc89b5e6e8266da9eb36177cd Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Mon, 11 Mar 2013 03:20:29 +0100 Subject: [PATCH] Generalize and share mapping menu (main, status, dock) behavior. --- Classes/EnjoyableApplicationDelegate.h | 7 +- Classes/EnjoyableApplicationDelegate.m | 85 ++--------- Classes/NJMappingMenuController.h | 50 +++++++ Classes/NJMappingMenuController.m | 111 ++++++++++++++ Classes/NJMappingsController.m | 3 +- Enjoyable.xcodeproj/project.pbxproj | 6 + Info.plist | 2 +- Resources/English.lproj/MainMenu.xib | 197 ++++++++++++++++++++++++- 8 files changed, 372 insertions(+), 89 deletions(-) create mode 100644 Classes/NJMappingMenuController.h create mode 100644 Classes/NJMappingMenuController.m diff --git a/Classes/EnjoyableApplicationDelegate.h b/Classes/EnjoyableApplicationDelegate.h index 36374d3..f61578b 100644 --- a/Classes/EnjoyableApplicationDelegate.h +++ b/Classes/EnjoyableApplicationDelegate.h @@ -6,13 +6,14 @@ // Copyright 2009 University of Otago. All rights reserved. // -@class NJDeviceController; @class NJMappingsController; -@interface EnjoyableApplicationDelegate : NSObject { +#import "NJMappingMenuController.h" + +@interface EnjoyableApplicationDelegate : NSObject { IBOutlet NSMenu *dockMenu; IBOutlet NSMenu *statusItemMenu; - IBOutlet NSMenu *mappingsMenu; IBOutlet NSWindow *window; } diff --git a/Classes/EnjoyableApplicationDelegate.m b/Classes/EnjoyableApplicationDelegate.m index c66cf6b..b45771b 100644 --- a/Classes/EnjoyableApplicationDelegate.m +++ b/Classes/EnjoyableApplicationDelegate.m @@ -29,11 +29,6 @@ selector:@selector(mappingDidChange:) name:NJEventMappingChanged object:nil]; - [NSNotificationCenter.defaultCenter - addObserver:self - selector:@selector(mappingListDidChange:) - name:NJEventMappingListChanged - object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(eventTranslationActivated:) @@ -99,8 +94,6 @@ } - (void)eventTranslationActivated:(NSNotification *)note { - [dockMenu itemAtIndex:0].state = NSOnState; - [statusItemMenu itemAtIndex:0].state = NSOnState; statusItem.image = [NSImage imageNamed:@"Status Menu Icon"]; [NSWorkspace.sharedWorkspace.notificationCenter addObserver:self @@ -110,8 +103,6 @@ } - (void)eventTranslationDeactivated:(NSNotification *)note { - [dockMenu itemAtIndex:0].state = NSOffState; - [statusItemMenu itemAtIndex:0].state = NSOffState; statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"]; [NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self @@ -119,63 +110,7 @@ object:nil]; } -- (void)restoreWindowAndShowMappings:(id)sender { - [self restoreToForeground:sender]; - [self.mappingsController mappingPressed:sender]; -} - -- (void)addMappings:(NSArray *)mappings - toMenu:(NSMenu *)menu - withKeys:(BOOL)withKeys - atIndex:(NSInteger)index { - static const NSUInteger MAXIMUM_ITEMS = 15; - int added = 0; - for (NJMapping *mapping in mappings) { - NSString *keyEquiv = (++added < 10 && withKeys) ? @(added).stringValue : @""; - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:mapping.name - action:@selector(chooseMapping:) - keyEquivalent:keyEquiv]; - item.representedObject = mapping; - item.state = mapping == self.mappingsController.currentMapping; - [menu insertItem:item atIndex:index++]; - if (added == MAXIMUM_ITEMS && self.mappingsController.mappings.count > MAXIMUM_ITEMS + 1) { - NSString *msg = [NSString stringWithFormat:@"(and %lu more…)", - self.mappingsController.mappings.count - MAXIMUM_ITEMS]; - NSMenuItem *end = [[NSMenuItem alloc] initWithTitle:msg - action:@selector(restoreWindowAndShowMappings:) - keyEquivalent:@""]; - // There must be a represented object here so the item gets - // removed correctly when the menus are regenerated. - end.representedObject = mappings; - end.target = self; - [menu insertItem:end atIndex:index++]; - break; - } - } -} - -- (void)mappingListDidChange:(NSNotification *)note { - NSArray *mappings = note.userInfo[@"mappings"]; - while (mappingsMenu.lastItem.representedObject) - [mappingsMenu removeLastItem]; - [self addMappings:mappings - toMenu:mappingsMenu - withKeys:YES - atIndex:mappingsMenu.numberOfItems]; - while ([statusItemMenu itemAtIndex:2].representedObject) - [statusItemMenu removeItemAtIndex:2]; - [self addMappings:mappings toMenu:statusItemMenu withKeys:NO atIndex:2]; -} - - (void)mappingDidChange:(NSNotification *)note { - NJMapping *current = note.userInfo[@"mapping"]; - for (NSMenuItem *item in mappingsMenu.itemArray) - if (item.representedObject) - item.state = item.representedObject == current; - for (NSMenuItem *item in statusItemMenu.itemArray) - if (item.representedObject) - item.state = item.representedObject == current; - if (!window.isVisible) for (int i = 0; i < 4; ++i) [self performSelector:@selector(flashStatusItem) @@ -183,18 +118,7 @@ afterDelay:0.2 * i]; } -- (void)chooseMapping:(NSMenuItem *)sender { - NJMapping *chosen = sender.representedObject; - [self.mappingsController activateMapping:chosen]; -} - - (NSMenu *)applicationDockMenu:(NSApplication *)sender { - while (dockMenu.lastItem.representedObject) - [dockMenu removeLastItem]; - [self addMappings:self.mappingsController.mappings - toMenu:dockMenu - withKeys:NO - atIndex:dockMenu.numberOfItems]; return dockMenu; } @@ -205,5 +129,14 @@ return YES; } +- (void)mappingWasChosen:(NJMapping *)mapping { + [self.mappingsController activateMapping:mapping]; +} + +- (void)mappingListShouldOpen { + [self restoreToForeground:self]; + [self.mappingsController mappingPressed:self]; +} + @end diff --git a/Classes/NJMappingMenuController.h b/Classes/NJMappingMenuController.h new file mode 100644 index 0000000..9c9432a --- /dev/null +++ b/Classes/NJMappingMenuController.h @@ -0,0 +1,50 @@ +// +// NJMappingMenuController.h +// Enjoyable +// +// Created by Joe Wreschnig on 3/11/13. +// +// + +#import + +@class NJMapping; + +@protocol NJMappingMenuDelegate + +- (void)mappingWasChosen:(NJMapping *)mapping; + // Called when a menu item created by the controller is chosen. + +- (void)mappingListShouldOpen; + // Called when the "overflow" menu item is chosen, this means the + // user should be presented with the list of available mappings. + +@end + +@interface NJMappingMenuController : NSObject + // Mapping menu controllers manage the contents of a menu that + // shows a list of all available mappings, as well as the current + // event translation state. The menu may have other items in it as + // well, but none at an adjacent index that also have NJMappings + // as represented objects. + +@property (nonatomic, strong) IBOutlet NSMenu *menu; + // The menu to put mapping items in. + +@property (nonatomic, weak) IBOutlet id delegate; + // The delegate to inform about requested mapping changes. + +@property (nonatomic, assign) NSInteger firstMappingIndex; + // The index in the menu to insert mappings into. The menu can + // have other dynamic items as long as you update this index as + // you add or remove them. + +@property (nonatomic, assign) BOOL hasKeyEquivalents; + // Whether or not to add key equivalents to the menu items. + +@property (nonatomic, strong) IBOutlet NSMenuItem *eventTranslationToggle; + // A menu item representing the current event translation state. + // This outlet is optional. + + +@end diff --git a/Classes/NJMappingMenuController.m b/Classes/NJMappingMenuController.m new file mode 100644 index 0000000..0fa6183 --- /dev/null +++ b/Classes/NJMappingMenuController.m @@ -0,0 +1,111 @@ +// +// NJMappingMenuController.m +// Enjoyable +// +// Created by Joe Wreschnig on 3/11/13. +// +// + +#import "NJMappingMenuController.h" + +#import "NJEvents.h" +#import "NJMapping.h" + +#define MAXIMUM_MAPPINGS_IN_MENU 15 + +@implementation NJMappingMenuController + +- (id)init { + if ((self = [super init])) { + NSNotificationCenter *center = NSNotificationCenter.defaultCenter; + [center addObserver:self + selector:@selector(mappingsListDidChange:) + name:NJEventMappingListChanged + object:nil]; + [center addObserver:self + selector:@selector(mappingDidChange:) + name:NJEventMappingChanged + object:nil]; + [center addObserver:self + selector:@selector(eventTranslationActivated:) + name:NJEventTranslationActivated + object:nil]; + [center addObserver:self + selector:@selector(eventTranslationDeactivated:) + name:NJEventTranslationDeactivated + object:nil]; + } + return self; +} + +- (void)dealloc { + [NSNotificationCenter.defaultCenter removeObserver:self]; +} + +- (void)_mappingWasChosen:(NSMenuItem *)sender { + NJMapping *mapping = sender.representedObject; + [self.delegate mappingWasChosen:mapping]; +} + +- (void)_mappingListWasChosen:(NSMenuItem *)sender { + [self.delegate mappingListShouldOpen]; +} + +- (void)mappingsListDidChange:(NSNotification *)note { + NSArray *mappings = note.userInfo[@"mappings"]; + NJMapping *currentMapping = note.userInfo[@"mapping"]; + NSMenuItem *toRemove; + while (self.menu.numberOfItems > self.firstMappingIndex + && (toRemove = [self.menu itemAtIndex:self.firstMappingIndex]) + && ([toRemove.representedObject isKindOfClass:NJMapping.class] + || toRemove.representedObject == self.class)) + [self.menu removeItemAtIndex:self.firstMappingIndex]; + + int added = 0; + NSUInteger index = self.firstMappingIndex; + for (NJMapping *mapping in mappings) { + NSString *keyEquiv = (++added < 10 && self.hasKeyEquivalents) + ? @(added).stringValue + : @""; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:mapping.name + action:@selector(_mappingWasChosen:) + keyEquivalent:keyEquiv]; + item.representedObject = mapping; + item.state = mapping == currentMapping; + item.target = self; + [self.menu insertItem:item atIndex:index++]; + if (added == MAXIMUM_MAPPINGS_IN_MENU + && mappings.count > MAXIMUM_MAPPINGS_IN_MENU + 1) { + NSString *msg = [NSString stringWithFormat:@"(and %lu more…)", + mappings.count - MAXIMUM_MAPPINGS_IN_MENU]; + NSMenuItem *end = [[NSMenuItem alloc] initWithTitle:msg + action:@selector(_mappingListWasChosen:) + keyEquivalent:@""]; + // There must be a represented object here so the item gets + // removed correctly when the menus are regenerated. + end.representedObject = self.class; + end.target = self; + [self.menu insertItem:end atIndex:index]; + break; + } + } +} + +- (void)mappingDidChange:(NSNotification *)note { + NJMapping *mapping = note.userInfo[@"mapping"]; + for (NSMenuItem *item in self.menu.itemArray) + if ([item.representedObject isKindOfClass:NJMapping.class]) + item.state = mapping == item.representedObject; +} + +- (void)eventTranslationActivated:(NSNotification *)note { + self.eventTranslationToggle.title = @"Disable"; +} + +- (void)eventTranslationDeactivated:(NSNotification *)note { + self.eventTranslationToggle.title = @"Enable"; +} + + + +@end diff --git a/Classes/NJMappingsController.m b/Classes/NJMappingsController.m index ad88d73..ad257cf 100644 --- a/Classes/NJMappingsController.m +++ b/Classes/NJMappingsController.m @@ -52,7 +52,8 @@ [NSNotificationCenter.defaultCenter postNotificationName:NJEventMappingListChanged object:self - userInfo:@{ @"mappings": _mappings }]; + userInfo:@{ @"mappings": _mappings, + @"mapping": _currentMapping }]; } - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state diff --git a/Enjoyable.xcodeproj/project.pbxproj b/Enjoyable.xcodeproj/project.pbxproj index 837baa9..3d6cb56 100644 --- a/Enjoyable.xcodeproj/project.pbxproj +++ b/Enjoyable.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 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 */; }; EE6A122E16E8F46300EDBD32 /* Icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = EE6A122D16E8F46300EDBD32 /* Icon.icns */; }; + EED4CE6E16ED692400C65AA8 /* NJMappingMenuController.m in Sources */ = {isa = PBXBuildFile; fileRef = EED4CE6D16ED692400C65AA8 /* NJMappingMenuController.m */; }; EEE73B1616EA42E5009D9D99 /* NSRunningApplication+NJPossibleNames.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE73B1516EA42E5009D9D99 /* NSRunningApplication+NJPossibleNames.m */; }; EEF17D1916E8E21A00D7DC4D /* com.yukkurigames.Enjoyable.mapping.icns in Resources */ = {isa = PBXBuildFile; fileRef = EEF17D1716E8E21A00D7DC4D /* com.yukkurigames.Enjoyable.mapping.icns */; }; EEF17D1F16E8E23A00D7DC4D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EEF17D1B16E8E23A00D7DC4D /* InfoPlist.strings */; }; @@ -57,6 +58,8 @@ 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 = ""; }; EE6A122D16E8F46300EDBD32 /* Icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = Icon.icns; sourceTree = ""; }; + EED4CE6C16ED692400C65AA8 /* NJMappingMenuController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NJMappingMenuController.h; path = Classes/NJMappingMenuController.h; sourceTree = ""; }; + EED4CE6D16ED692400C65AA8 /* NJMappingMenuController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NJMappingMenuController.m; path = Classes/NJMappingMenuController.m; sourceTree = ""; }; EEE73B1416EA42E5009D9D99 /* NSRunningApplication+NJPossibleNames.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSRunningApplication+NJPossibleNames.h"; path = "Categories/NSRunningApplication+NJPossibleNames.h"; sourceTree = ""; }; EEE73B1516EA42E5009D9D99 /* NSRunningApplication+NJPossibleNames.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSRunningApplication+NJPossibleNames.m"; path = "Categories/NSRunningApplication+NJPossibleNames.m"; sourceTree = ""; }; EEF17D1716E8E21A00D7DC4D /* com.yukkurigames.Enjoyable.mapping.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = com.yukkurigames.Enjoyable.mapping.icns; path = Resources/com.yukkurigames.Enjoyable.mapping.icns; sourceTree = ""; }; @@ -166,6 +169,8 @@ EEF17D5916E8E2EF00D7DC4D /* NJOutputMouseMove.m */, EEF17D5A16E8E2EF00D7DC4D /* NJOutputMouseScroll.h */, EEF17D5B16E8E2EF00D7DC4D /* NJOutputMouseScroll.m */, + EED4CE6C16ED692400C65AA8 /* NJMappingMenuController.h */, + EED4CE6D16ED692400C65AA8 /* NJMappingMenuController.m */, ); name = Classes; sourceTree = ""; @@ -396,6 +401,7 @@ EEF17D6B16E8E2EF00D7DC4D /* NJOutputMouseMove.m in Sources */, EEF17D6C16E8E2EF00D7DC4D /* NJOutputMouseScroll.m in Sources */, EEE73B1616EA42E5009D9D99 /* NSRunningApplication+NJPossibleNames.m in Sources */, + EED4CE6E16ED692400C65AA8 /* NJMappingMenuController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Info.plist b/Info.plist index ebc0a1d..bc0cc7d 100644 --- a/Info.plist +++ b/Info.plist @@ -46,7 +46,7 @@ CFBundleSignature ???? CFBundleVersion - 109 + 122 LSApplicationCategoryType public.app-category.utilities NSHumanReadableCopyright diff --git a/Resources/English.lproj/MainMenu.xib b/Resources/English.lproj/MainMenu.xib index 2cf1266..0a20186 100644 --- a/Resources/English.lproj/MainMenu.xib +++ b/Resources/English.lproj/MainMenu.xib @@ -524,7 +524,7 @@ aW5nLg {232, 321} - + YES NO YES @@ -654,7 +654,7 @@ aW5nLg {234, 323} - + 150034 @@ -1483,6 +1483,15 @@ aW5nLg + + NJMappingMenuController + + + NJMappingMenuController + + + NJMappingMenuController + @@ -2030,6 +2039,78 @@ aW5nLg 933 + + + eventTranslationToggle + + + + 951 + + + + menu + + + + 952 + + + + delegate + + + + 956 + + + + menu + + + + 957 + + + + delegate + + + + 958 + + + + eventTranslationToggle + + + + 959 + + + + delegate + + + + 961 + + + + menu + + + + 964 + + + + eventTranslationToggle + + + + 965 + @@ -2811,6 +2892,24 @@ aW5nLg + + 950 + + + Main Mappings Menu Controller + + + 953 + + + Dock Menu Controller + + + 960 + + + Status Bar Menu Controller + @@ -2997,12 +3096,72 @@ aW5nLg com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + + IBUserDefinedRuntimeAttributesPlaceholderName + + IBUserDefinedRuntimeAttributesPlaceholderName + + + + com.apple.InterfaceBuilder.userDefinedRuntimeAttributeType.number + firstMappingIndex + + + + com.apple.InterfaceBuilder.userDefinedRuntimeAttributeType.boolean + hasKeyEquivalents + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + IBUserDefinedRuntimeAttributesPlaceholderName + + IBUserDefinedRuntimeAttributesPlaceholderName + + + + com.apple.InterfaceBuilder.userDefinedRuntimeAttributeType.number + firstMappingIndex + + + + com.apple.InterfaceBuilder.userDefinedRuntimeAttributeType.boolean + hasKeyEquivalents + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + IBUserDefinedRuntimeAttributesPlaceholderName + + IBUserDefinedRuntimeAttributesPlaceholderName + + + + com.apple.InterfaceBuilder.userDefinedRuntimeAttributeType.number + firstMappingIndex + + + + com.apple.InterfaceBuilder.userDefinedRuntimeAttributeType.boolean + hasKeyEquivalents + + + + + + com.apple.InterfaceBuilder.CocoaPlugin - 949 + 965 @@ -3023,7 +3182,6 @@ aW5nLg NSMenu NJMappingsController - NSMenu NSMenu NSWindow @@ -3036,10 +3194,6 @@ aW5nLg mappingsController NJMappingsController - - mappingsMenu - NSMenu - statusItemMenu NSMenu @@ -3126,6 +3280,33 @@ aW5nLg ./Classes/NJKeyInputField.h + + NJMappingMenuController + NSObject + + id + NSMenuItem + NSMenu + + + + delegate + id + + + eventTranslationToggle + NSMenuItem + + + menu + NSMenu + + + + IBProjectSource + ./Classes/NJMappingMenuController.h + + NJMappingsController NSObject -- 2.30.2