diff --git a/.gitignore b/.gitignore index 8b13789..e8667ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,64 @@ +.DS_Store +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..73c9169 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +## Unreleased + +### Added +- **Start at Login** — toggle in the menu bar menu; installs/removes a LaunchAgent automatically +- **Swift rewrite** — codebase migrated from Objective-C to Swift 5 + +### Changed +- Updated README to reflect the new Start at Login menu item + +--- + +## 1.0.6 + +### Added +- App icon + +--- + +## 1.0.5 + +### Added +- Hide Menu Bar Icon — menu item to hide the status bar icon; relaunch to restore it + +--- + +## 1.0.4 + +### Added +- Swap Buttons — option to swap the back/forward button assignments + +--- + +## 1.0.3 + +### Changed +- Text appearance and color tweaks in the about section + +--- + +## 1.0.0 + +### Added +- Initial release +- Simulates 3-finger swipes from mouse side buttons (buttons 3 & 4) +- Trigger on Mouse Down or Mouse Up option +- Accessibility permission check with guidance +- Donation prompt diff --git a/README.md b/README.md index 0ebacbd..f5b1f48 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,6 @@ macOS mostly ignores the M4/M5 mouse buttons, commonly used for navigation. Thir Extensive information on this tweak can be found here: http://sensible-side-buttons.archagon.net -To ensure SensibleSideButtons opens whenever you start your computer: - -1. Go to System Preferences -1. Click Users & Groups -1. Click your username in the left panel -1. Click Login Items at the top -1. Click the plus button at the bottom -1. Go to wherever you put the app (probably your Applications folder) and double-click it +To ensure SensibleSideButtons opens whenever you start your computer, click **Start at Login** in the app's menu bar menu. + +**Requires macOS 10.10 or later.** diff --git a/SideButtonFixer/AppDelegate.m b/SideButtonFixer/AppDelegate.m deleted file mode 100644 index abb2504..0000000 --- a/SideButtonFixer/AppDelegate.m +++ /dev/null @@ -1,534 +0,0 @@ -// -// AppDelegate.m -// -// SensibleSideButtons, a utility that fixes the navigation buttons on third-party mice in macOS -// Copyright (C) 2018 Alexei Baboulevitch (ssb@archagon.net) -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// - -#import "AppDelegate.h" -#import "TouchEvents.h" - -static NSMutableDictionary*>* swipeInfo = nil; -static NSArray* nullArray = nil; - -static void SBFFakeSwipe(TLInfoSwipeDirection dir) { - CGEventRef event1 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(swipeInfo[@(dir)][0]), (__bridge CFArrayRef)nullArray); - CGEventRef event2 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(swipeInfo[@(dir)][1]), (__bridge CFArrayRef)nullArray); - - CGEventPost(kCGHIDEventTap, event1); - CGEventPost(kCGHIDEventTap, event2); - - CFRelease(event1); - CFRelease(event2); -} - -static CGEventRef SBFMouseCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { - int64_t number = CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber); - BOOL down = (CGEventGetType(event) == kCGEventOtherMouseDown); - - BOOL mouseDown = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFMouseDown"]; - BOOL swapButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFSwapButtons"]; - - if (number == (swapButtons ? 4 : 3)) { - if ((mouseDown && down) || (!mouseDown && !down)) { - SBFFakeSwipe(kTLInfoSwipeLeft); - } - - return NULL; - } - else if (number == (swapButtons ? 3 : 4)) { - if ((mouseDown && down) || (!mouseDown && !down)) { - SBFFakeSwipe(kTLInfoSwipeRight); - } - - return NULL; - } - else { - return event; - } -} - -typedef NS_ENUM(NSInteger, MenuMode) { - MenuModeAccessibility, - MenuModeDonation, - MenuModeNormal -}; - -typedef NS_ENUM(NSInteger, MenuItem) { - MenuItemEnabled = 0, - MenuItemEnabledSeparator, - MenuItemTriggerOnMouseDown, - MenuItemSwapButtons, - MenuItemOptionsSeparator, - MenuItemStartupHide, - MenuItemStartupHideInfo, - MenuItemStartupSeparator, - MenuItemAboutText, - MenuItemAboutSeparator, - MenuItemDonate, - MenuItemWebsite, - MenuItemAccessibility, - MenuItemLinkSeparator, - MenuItemQuit -}; - -@interface AppDelegate () -@property (nonatomic, retain) NSStatusItem* statusItem; -@property (nonatomic, assign) CFMachPortRef tap; -@property (nonatomic, assign) MenuMode menuMode; -@end - -@interface AboutView: NSView -@property (nonatomic, retain) NSTextView* text; -@property (nonatomic, assign) MenuMode menuMode; --(CGFloat) margin; -@end - -@implementation AppDelegate - --(void) dealloc { - [self startTap:NO]; - - swipeInfo = nil; - nullArray = nil; -} - --(void) setMenuMode:(MenuMode)menuMode { - _menuMode = menuMode; - AboutView* view = (AboutView*)self.statusItem.menu.itemArray[MenuItemAboutText].view; - view.menuMode = menuMode; - [self refreshSettings]; -} - -// if the application is launched when it's already running, show the icon in the menu bar again --(BOOL) applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag { - if (@available(macOS 10.12, *)) { - [self.statusItem setVisible:YES]; - } - return NO; -} - --(void) applicationDidFinishLaunching:(NSNotification *)aNotification { - [[NSUserDefaults standardUserDefaults] registerDefaults:@{ - @"SBFWasEnabled": @YES, - @"SBFMouseDown": @YES, - @"SBFDonated": @NO, - @"SBFSwapButtons": @NO - }]; - - // setup globals - { - swipeInfo = [NSMutableDictionary dictionary]; - - for (NSNumber* direction in @[ @(kTLInfoSwipeUp), @(kTLInfoSwipeDown), @(kTLInfoSwipeLeft), @(kTLInfoSwipeRight) ]) { - NSDictionary* swipeInfo1 = [NSDictionary dictionaryWithObjectsAndKeys: - @(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype, - @(1), kTLInfoKeyGesturePhase, - nil]; - - NSDictionary* swipeInfo2 = [NSDictionary dictionaryWithObjectsAndKeys: - @(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype, - direction, kTLInfoKeySwipeDirection, - @(4), kTLInfoKeyGesturePhase, - nil]; - - swipeInfo[direction] = @[ swipeInfo1, swipeInfo2 ]; - } - - nullArray = @[]; - } - - // create status bar item - { - self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - } - - // create menu - { - NSMenu* menu = [NSMenu new]; - - menu.autoenablesItems = NO; - menu.delegate = self; - - NSMenuItem* enabledItem = [[NSMenuItem alloc] initWithTitle:@"Enabled" action:@selector(enabledToggle:) keyEquivalent:@"e"]; - [menu addItem:enabledItem]; - assert(menu.itemArray.count - 1 == MenuItemEnabled); - - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemEnabledSeparator); - - NSMenuItem* modeItem = [[NSMenuItem alloc] initWithTitle:@"Trigger on Mouse Down" action:@selector(mouseDownToggle:) keyEquivalent:@""]; - modeItem.state = NSControlStateValueOn; - [menu addItem:modeItem]; - assert(menu.itemArray.count - 1 == MenuItemTriggerOnMouseDown); - - NSMenuItem* swapItem = [[NSMenuItem alloc] initWithTitle:@"Swap Buttons" action:@selector(swapToggle:) keyEquivalent:@""]; - swapItem.state = NSControlStateValueOff; - [menu addItem:swapItem]; - assert(menu.itemArray.count - 1 == MenuItemSwapButtons); - - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemOptionsSeparator); - - - NSMenuItem* hideItem = [[NSMenuItem alloc] initWithTitle:@"Hide Menu Bar Icon" action:@selector(hideMenubarItem:) keyEquivalent:@""]; - [menu addItem:hideItem]; - assert(menu.itemArray.count - 1 == MenuItemStartupHide); - - NSMenuItem* hideInfoItem = [[NSMenuItem alloc] initWithTitle:@"Relaunch application to show again" action:NULL keyEquivalent:@""]; - [hideInfoItem setEnabled:NO]; - [menu addItem:hideInfoItem]; - assert(menu.itemArray.count - 1 == MenuItemStartupHideInfo); - - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemStartupSeparator); - - AboutView* text = [[AboutView alloc] initWithFrame:NSMakeRect(0, 0, 320, 100)]; //arbitrary height - NSMenuItem* aboutText = [[NSMenuItem alloc] initWithTitle:@"Text" action:NULL keyEquivalent:@""]; - aboutText.view = text; - [menu addItem:aboutText]; - assert(menu.itemArray.count - 1 == MenuItemAboutText); - - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemAboutSeparator); - - NSString* appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleNameKey]; - [menu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"%@ Website", appName] action:@selector(donate:) keyEquivalent:@""]]; - assert(menu.itemArray.count - 1 == MenuItemDonate); - - [menu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"%@ Website", appName] action:@selector(website:) keyEquivalent:@""]]; - assert(menu.itemArray.count - 1 == MenuItemWebsite); - - [menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Accessibility Whitelist" action:@selector(accessibility:) keyEquivalent:@""]]; - assert(menu.itemArray.count - 1 == MenuItemAccessibility); - - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemLinkSeparator); - - NSMenuItem* quit = [[NSMenuItem alloc] initWithTitle:@"Quit" action:@selector(quit:) keyEquivalent:@"q"]; - quit.keyEquivalentModifierMask = NSEventModifierFlagCommand; - [menu addItem:quit]; - assert(menu.itemArray.count - 1 == MenuItemQuit); - - self.statusItem.menu = menu; - } - - [self startTap:[[NSUserDefaults standardUserDefaults] boolForKey:@"SBFWasEnabled"]]; - - [self updateMenuMode]; - [self refreshSettings]; -} - --(void) updateMenuMode { - [self updateMenuMode:YES]; -} - --(void) updateMenuMode:(BOOL)active { - // TODO: this actually returns YES if SSB is deleted (not disabled) from Accessibility - NSDictionary* options = @{ (__bridge id)kAXTrustedCheckOptionPrompt: @(active ? YES : NO) }; - BOOL accessibilityEnabled = AXIsProcessTrustedWithOptions((CFDictionaryRef)options); - //BOOL accessibilityEnabled = YES; //is accessibility even required? seems to work fine without it - - if (accessibilityEnabled) { - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"SBFDonated"]) { - self.menuMode = MenuModeNormal; - } - else { - self.menuMode = MenuModeDonation; - } - } - else { - self.menuMode = MenuModeAccessibility; - } - - // QQQ: for testing - //self.menuMode = arc4random_uniform(3); -} - --(void) refreshSettings { - self.statusItem.menu.itemArray[MenuItemEnabled].state = self.tap != NULL && CGEventTapIsEnabled(self.tap); - self.statusItem.menu.itemArray[MenuItemTriggerOnMouseDown].state = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFMouseDown"]; - self.statusItem.menu.itemArray[MenuItemSwapButtons].state = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFSwapButtons"]; - - switch (self.menuMode) { - case MenuModeAccessibility: - self.statusItem.menu.itemArray[MenuItemEnabled].enabled = NO; - self.statusItem.menu.itemArray[MenuItemTriggerOnMouseDown].enabled = NO; - self.statusItem.menu.itemArray[MenuItemSwapButtons].enabled = NO; - self.statusItem.menu.itemArray[MenuItemDonate].hidden = YES; - self.statusItem.menu.itemArray[MenuItemWebsite].hidden = NO; - self.statusItem.menu.itemArray[MenuItemAccessibility].hidden = NO; - break; - case MenuModeDonation: - self.statusItem.menu.itemArray[MenuItemEnabled].enabled = YES; - self.statusItem.menu.itemArray[MenuItemTriggerOnMouseDown].enabled = YES; - self.statusItem.menu.itemArray[MenuItemSwapButtons].enabled = YES; - self.statusItem.menu.itemArray[MenuItemDonate].hidden = NO; - self.statusItem.menu.itemArray[MenuItemWebsite].hidden = YES; - self.statusItem.menu.itemArray[MenuItemAccessibility].hidden = YES; - break; - case MenuModeNormal: - self.statusItem.menu.itemArray[MenuItemEnabled].enabled = YES; - self.statusItem.menu.itemArray[MenuItemTriggerOnMouseDown].enabled = YES; - self.statusItem.menu.itemArray[MenuItemSwapButtons].enabled = YES; - self.statusItem.menu.itemArray[MenuItemDonate].hidden = YES; - self.statusItem.menu.itemArray[MenuItemWebsite].hidden = NO; - self.statusItem.menu.itemArray[MenuItemAccessibility].hidden = YES; - break; - } - - AboutView* view = (AboutView*)self.statusItem.menu.itemArray[MenuItemAboutText].view; - [view layoutSubtreeIfNeeded]; //used to auto-calculate the text view size - view.frame = NSMakeRect(0, 0, view.bounds.size.width, view.text.frame.size.height); - - // only show the menu item to hide the icon if the API is available - if (@available(macOS 10.12, *)) { - self.statusItem.menu.itemArray[MenuItemStartupHide].hidden = NO; - self.statusItem.menu.itemArray[MenuItemStartupHideInfo].hidden = NO; - } - else { - self.statusItem.menu.itemArray[MenuItemStartupHide].hidden = YES; - self.statusItem.menu.itemArray[MenuItemStartupHideInfo].hidden = YES; - } - - if (self.statusItem.button != nil) { - if (self.tap != NULL && CGEventTapIsEnabled(self.tap)) { - self.statusItem.button.image = [NSImage imageNamed:@"MenuIcon"]; - } - else { - self.statusItem.button.image = [NSImage imageNamed:@"MenuIconDisabled"]; - } - } -} - --(void) startTap:(BOOL)start { - if (start) { - if (self.tap == NULL) { - self.tap = CGEventTapCreate(kCGHIDEventTap, - kCGHeadInsertEventTap, - kCGEventTapOptionDefault, - CGEventMaskBit(kCGEventOtherMouseUp)|CGEventMaskBit(kCGEventOtherMouseDown), - &SBFMouseCallback, - NULL); - - if (self.tap != NULL) { - CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(NULL, self.tap, 0); - CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); - CFRelease(runLoopSource); - - CGEventTapEnable(self.tap, true); - } - } - } - else { - if (self.tap != NULL) { - CGEventTapEnable(self.tap, NO); - CFRelease(self.tap); - - self.tap = NULL; - } - } - - [[NSUserDefaults standardUserDefaults] setBool:self.tap != NULL && CGEventTapIsEnabled(self.tap) forKey:@"SBFWasEnabled"]; -} - --(void) enabledToggle:(id)sender { - [self startTap:self.tap == NULL]; - [self refreshSettings]; -} - --(void) mouseDownToggle:(id)sender { - [[NSUserDefaults standardUserDefaults] setBool:![[NSUserDefaults standardUserDefaults] boolForKey:@"SBFMouseDown"] forKey:@"SBFMouseDown"]; - [self refreshSettings]; -} - --(void) swapToggle:(id)sender { - [[NSUserDefaults standardUserDefaults] setBool:![[NSUserDefaults standardUserDefaults] boolForKey:@"SBFSwapButtons"] forKey:@"SBFSwapButtons"]; - [self refreshSettings]; -} - --(void) donate:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: @"http://sensible-side-buttons.archagon.net#donations"]]; - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"SBFDonated"]; - - [self updateMenuMode]; - [self refreshSettings]; -} - --(void) website:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: @"http://sensible-side-buttons.archagon.net"]]; -} - --(void) accessibility:(id)sender { - [self updateMenuMode]; - [self refreshSettings]; -} - --(void) hideMenubarItem:(id)sender { - if (@available(macOS 10.12, *)) { - [self.statusItem setVisible:NO]; - } -} - --(void) quit:(id)sender { - [NSApp terminate:self]; -} - -- (void) menuWillOpen:(NSMenu*)menu { - // TODO: theoretically, accessibility can be disabled while the menu is opened, but this is unlikely - [self updateMenuMode:NO]; - [self refreshSettings]; -} - -@end - -@implementation AboutView - --(CGFloat) margin { - return 17; -} - --(void) setMenuMode:(MenuMode)menuMode { - _menuMode = menuMode; - - NSFont* font = [NSFont menuFontOfSize:13]; - - NSFontDescriptor* boldFontDesc = [NSFontDescriptor fontDescriptorWithFontAttributes:@{ - NSFontFamilyAttribute: font.familyName, - NSFontFaceAttribute: @"Bold" - }]; - NSFont* boldFont = [NSFont fontWithDescriptor:boldFontDesc size:font.pointSize]; - if (!boldFont) { boldFont = font; } - - // AB: dynamic color component extraction experiments - //CGFloat h1, s1, b1, a1, h2, s2, b2, a2; - //NSColorSpace* space; - //if (@available(macOS 10.13, *)) { - // space = [[NSColor redColor] colorUsingType:NSColorTypeComponentBased].colorSpace; - //} else { - // space = [NSColorSpace deviceRGBColorSpace]; - //} - //NSColor* color = [[NSColor systemBlueColor] colorUsingColorSpace:space]; - //[color getHue:&h1 saturation:&s1 brightness:&b1 alpha:&a1]; - //color = [[NSColor systemRedColor] colorUsingColorSpace:space]; - //[color getHue:&h2 saturation:&s2 brightness:&b2 alpha:&a2]; - - //NSColor* regularColor = [NSColor colorWithRed:120/255.0 green:120/255.0 blue:160/255.0 alpha:1]; - //NSColor* regularColor = [NSColor colorWithHue:h1 saturation:s1 brightness:b1 alpha:a1]; - NSColor* regularColor = [NSColor secondaryLabelColor]; - NSColor* alertColor = [NSColor systemRedColor]; - - NSDictionary* regularAttributes = @{ - NSFontAttributeName: font, - NSForegroundColorAttributeName: regularColor - }; - NSDictionary* alertAttributes = @{ - NSFontAttributeName: font, - NSForegroundColorAttributeName: alertColor - }; - NSDictionary* smallReturnAttributes = @{ - NSFontAttributeName: [NSFont menuFontOfSize:3], - }; - - NSString* appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleNameKey]; - NSString* appDescription = [NSString stringWithFormat:@"%@ %@", appName, [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]; - NSString* copyright = @"Copyright © 2018 Alexei Baboulevitch."; - - switch (menuMode) { - case MenuModeAccessibility: { - NSString* text = [NSString stringWithFormat:@"Uh-oh! It looks like %@ is not whitelisted in the Accessibility panel of your Security & Privacy System Preferences. This app needs to be on the Accessibility whitelist in order to process global mouse events. Please open the Accessibility panel below and add the app to the whitelist.", appDescription]; - - NSMutableAttributedString* string = [[NSMutableAttributedString alloc] initWithString:text attributes:alertAttributes]; - [string addAttribute:NSFontAttributeName value:boldFont range:[text rangeOfString:appDescription]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:regularAttributes]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:smallReturnAttributes]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:copyright attributes:regularAttributes]]; - - [self.text.textStorage setAttributedString:string]; - } break; - case MenuModeDonation: { - NSString* text = [NSString stringWithFormat:@"Thanks for using %@!\nIf you find this utility useful, please consider making a purchase through the Amazon affiliate link on the website below. It won't cost you an extra cent! 😊", appDescription]; - - NSMutableAttributedString* string = [[NSMutableAttributedString alloc] initWithString:text attributes:regularAttributes]; - [string addAttribute:NSFontAttributeName value:boldFont range:[text rangeOfString:appDescription]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:regularAttributes]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:smallReturnAttributes]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:copyright attributes:regularAttributes]]; - - [self.text.textStorage setAttributedString:string]; - } break; - case MenuModeNormal: { - NSString* text = [NSString stringWithFormat:@"Thanks for using %@!", appDescription]; - - NSMutableAttributedString* string = [[NSMutableAttributedString alloc] initWithString:text attributes:regularAttributes]; - [string addAttribute:NSFontAttributeName value:boldFont range:[text rangeOfString:appDescription]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:regularAttributes]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:smallReturnAttributes]]; - [string appendAttributedString:[[NSAttributedString alloc] initWithString:copyright attributes:regularAttributes]]; - - [self.text.textStorage setAttributedString:string]; - } break; - } - - [self setNeedsLayout:YES]; -} - --(instancetype) initWithFrame:(NSRect)frameRect { - self = [super initWithFrame:frameRect]; - - if (self) { - //NSTextView* testColor = [NSTextView new]; - //testColor.backgroundColor = NSColor.greenColor; - //[self addSubview:testColor]; - - self.text = [NSTextView new]; - self.text.backgroundColor = NSColor.clearColor; - [self.text setEditable:NO]; - [self.text setSelectable:NO]; - [self addSubview:self.text]; - - self.menuMode = MenuModeNormal; - } - - return self; -} - --(void) layout { - [super layout]; - - CGFloat margin = [self margin]; - - // text view sizing - { - // first, set the correct width - CGFloat arbitraryHeight = 100; - self.text.frame = NSMakeRect(margin, 0, self.bounds.size.width - margin, arbitraryHeight); - - // next, autosize to get the height - [self.text sizeToFit]; - - // finally, position the view correctly - self.text.frame = NSMakeRect(self.text.frame.origin.x, self.bounds.size.height - self.text.frame.size.height, self.text.frame.size.width, self.text.frame.size.height); - } - - //NSView* testView = [self subviews][0]; - //testView.frame = self.bounds; - - //NSLog(@"Text size: %@, self size: %@", NSStringFromSize(self.text.frame.size), NSStringFromSize(self.bounds.size)); -} - -@end diff --git a/SideButtonFixer/AppDelegate.swift b/SideButtonFixer/AppDelegate.swift new file mode 100644 index 0000000..33949ce --- /dev/null +++ b/SideButtonFixer/AppDelegate.swift @@ -0,0 +1,500 @@ +// +// AppDelegate.swift +// +// SensibleSideButtons, a utility that fixes the navigation buttons on third-party mice in macOS +// Copyright (C) 2018 Alexei Baboulevitch (ssb@archagon.net) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// + +import Cocoa + +// CGEventTap C constants are not exported to Swift; define using raw values. +// See CGEventTypes.h: cgHIDEventTap=0, cgHeadInsertEventTap=0, cgEventTapOptionDefault=0 +// Force-unwrap is safe: these raw values are defined in CGEventTypes.h. +private let cgHIDEventTap = CGEventTapLocation(rawValue: 0)! +private let cgHeadInsertEventTap = CGEventTapPlacement(rawValue: 0)! +private let cgEventTapOptionDefault = CGEventTapOptions(rawValue: 0)! + +// MARK: - Module-level globals for C callback +// +// Plain C enums (without NS_ENUM) import as UInt32 typealiases in Swift, +// and their constants import as plain Int. We cast to UInt32 at call sites. + +private var swipeInfo: [UInt32: [NSDictionary]] = [:] +private var nullArray: NSArray = [] + +// MARK: - C-compatible free functions + +private func fakeSwipe(_ direction: UInt32) { + guard let events = swipeInfo[direction] else { return } + // TouchEvents.h is not in a CF-audited region, so Swift imports the return + // as Unmanaged?. takeRetainedValue() transfers ownership to ARC. + let e1 = tl_CGEventCreateFromGesture(events[0] as CFDictionary, nullArray as CFArray)?.takeRetainedValue() + let e2 = tl_CGEventCreateFromGesture(events[1] as CFDictionary, nullArray as CFArray)?.takeRetainedValue() + e1?.post(tap: cgHIDEventTap) + e2?.post(tap: cgHIDEventTap) +} + +// Must be a free function (not a closure) to serve as a C function pointer. +private func mouseCallback( + _ proxy: CGEventTapProxy, + _ type: CGEventType, + _ event: CGEvent?, + _ userInfo: UnsafeMutableRawPointer?) -> Unmanaged? { + guard let event = event else { return nil } + + let number = event.getIntegerValueField(.mouseEventButtonNumber) + let isDown = (type == .otherMouseDown) + + let defaults = UserDefaults.standard + let triggerOnDown = defaults.bool(forKey: "SBFMouseDown") + let swapButtons = defaults.bool(forKey: "SBFSwapButtons") + + let backButton: Int64 = swapButtons ? 4 : 3 + let forwardButton: Int64 = swapButtons ? 3 : 4 + let shouldTrigger = (triggerOnDown && isDown) || (!triggerOnDown && !isDown) + + if number == backButton { + if shouldTrigger { fakeSwipe(UInt32(kTLInfoSwipeLeft)) } + return nil + } else if number == forwardButton { + if shouldTrigger { fakeSwipe(UInt32(kTLInfoSwipeRight)) } + return nil + } + return Unmanaged.passUnretained(event) +} + +// MARK: - Enums + +private enum MenuMode { + case accessibility, donation, normal +} + +private enum MenuItem: Int { + case enabled = 0 + case enabledSeparator + case triggerOnMouseDown + case swapButtons + case optionsSeparator + case startAtLogin + case startupHide + case startupHideInfo + case startupSeparator + case aboutText + case aboutSeparator + case donate + case website + case accessibility + case linkSeparator + case quit +} + +// MARK: - AppDelegate + +class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { + + private var statusItem: NSStatusItem! + private var tap: CFMachPort? + + private var menuMode: MenuMode = .normal { + didSet { + (statusItem.menu?.items[MenuItem.aboutText.rawValue].view as? AboutView)?.menuMode = menuMode + refreshSettings() + } + } + + // MARK: - NSApplicationDelegate + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + if #available(macOS 10.12, *) { + statusItem.isVisible = true + } + return false + } + + func applicationDidFinishLaunching(_ notification: Notification) { + UserDefaults.standard.register(defaults: [ + "SBFWasEnabled": true, + "SBFMouseDown": true, + "SBFDonated": false, + "SBFSwapButtons": false, + ]) + + setupGlobals() + setupStatusItem() + buildMenu() + + startTap(UserDefaults.standard.bool(forKey: "SBFWasEnabled")) + updateMenuMode() + refreshSettings() + } + + // MARK: - Setup + + private func setupGlobals() { + nullArray = [] + // kTLInfoSwipeUp/Down/Left/Right are plain C enum constants (Int in Swift); cast to UInt32. + let directions = [kTLInfoSwipeUp, kTLInfoSwipeDown, kTLInfoSwipeLeft, kTLInfoSwipeRight].map { UInt32($0) } + for dir in directions { + let phase1: NSDictionary = [ + kTLInfoKeyGestureSubtype: NSNumber(value: UInt32(kTLInfoSubtypeSwipe)), + kTLInfoKeyGesturePhase: NSNumber(value: UInt32(1)), + ] + let phase2: NSDictionary = [ + kTLInfoKeyGestureSubtype: NSNumber(value: UInt32(kTLInfoSubtypeSwipe)), + kTLInfoKeySwipeDirection: NSNumber(value: dir), + kTLInfoKeyGesturePhase: NSNumber(value: UInt32(4)), + ] + swipeInfo[dir] = [phase1, phase2] + } + } + + private func setupStatusItem() { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) + } + + private func buildMenu() { + let menu = NSMenu() + menu.autoenablesItems = false + menu.delegate = self + + let enabledItem = NSMenuItem(title: "Enabled", action: #selector(enabledToggle(_:)), keyEquivalent: "e") + menu.addItem(enabledItem) + assert(menu.items.count - 1 == MenuItem.enabled.rawValue) + + menu.addItem(.separator()) + assert(menu.items.count - 1 == MenuItem.enabledSeparator.rawValue) + + let modeItem = NSMenuItem(title: "Trigger on Mouse Down", action: #selector(mouseDownToggle(_:)), keyEquivalent: "") + modeItem.state = .on + menu.addItem(modeItem) + assert(menu.items.count - 1 == MenuItem.triggerOnMouseDown.rawValue) + + let swapItem = NSMenuItem(title: "Swap Buttons", action: #selector(swapToggle(_:)), keyEquivalent: "") + swapItem.state = .off + menu.addItem(swapItem) + assert(menu.items.count - 1 == MenuItem.swapButtons.rawValue) + + menu.addItem(.separator()) + assert(menu.items.count - 1 == MenuItem.optionsSeparator.rawValue) + + let startAtLoginItem = NSMenuItem(title: "Start at Login", action: #selector(startAtLoginToggle(_:)), keyEquivalent: "") + menu.addItem(startAtLoginItem) + assert(menu.items.count - 1 == MenuItem.startAtLogin.rawValue) + + let hideItem = NSMenuItem(title: "Hide Menu Bar Icon", action: #selector(hideMenubarItem(_:)), keyEquivalent: "") + menu.addItem(hideItem) + assert(menu.items.count - 1 == MenuItem.startupHide.rawValue) + + let hideInfoItem = NSMenuItem(title: "Relaunch application to show again", action: nil, keyEquivalent: "") + hideInfoItem.isEnabled = false + menu.addItem(hideInfoItem) + assert(menu.items.count - 1 == MenuItem.startupHideInfo.rawValue) + + menu.addItem(.separator()) + assert(menu.items.count - 1 == MenuItem.startupSeparator.rawValue) + + let aboutView = AboutView(frame: NSRect(x: 0, y: 0, width: 320, height: 100)) + let aboutItem = NSMenuItem(title: "Text", action: nil, keyEquivalent: "") + aboutItem.view = aboutView + menu.addItem(aboutItem) + assert(menu.items.count - 1 == MenuItem.aboutText.rawValue) + + menu.addItem(.separator()) + assert(menu.items.count - 1 == MenuItem.aboutSeparator.rawValue) + + let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "" + menu.addItem(NSMenuItem(title: "\(appName) Website", action: #selector(donate(_:)), keyEquivalent: "")) + assert(menu.items.count - 1 == MenuItem.donate.rawValue) + + menu.addItem(NSMenuItem(title: "\(appName) Website", action: #selector(website(_:)), keyEquivalent: "")) + assert(menu.items.count - 1 == MenuItem.website.rawValue) + + menu.addItem(NSMenuItem(title: "Open Accessibility Whitelist", action: #selector(accessibility(_:)), keyEquivalent: "")) + assert(menu.items.count - 1 == MenuItem.accessibility.rawValue) + + menu.addItem(.separator()) + assert(menu.items.count - 1 == MenuItem.linkSeparator.rawValue) + + let quitItem = NSMenuItem(title: "Quit", action: #selector(quit(_:)), keyEquivalent: "q") + quitItem.keyEquivalentModifierMask = .command + menu.addItem(quitItem) + assert(menu.items.count - 1 == MenuItem.quit.rawValue) + + statusItem.menu = menu + } + + // MARK: - Event tap + + private func startTap(_ start: Bool) { + if start { + guard tap == nil else { return } + let mask = CGEventMask(1 << CGEventType.otherMouseUp.rawValue) + | CGEventMask(1 << CGEventType.otherMouseDown.rawValue) + tap = CGEvent.tapCreate(tap: cgHIDEventTap, place: cgHeadInsertEventTap, options: cgEventTapOptionDefault, + eventsOfInterest: mask, callback: mouseCallback, userInfo: nil) + if let tap { + if let source = CFMachPortCreateRunLoopSource(nil, tap, 0) { + CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes) + } + CGEvent.tapEnable(tap: tap, enable: true) + } + } else { + if let tap { + CGEvent.tapEnable(tap: tap, enable: false) + // CFMachPort is ARC-managed; no CFRelease needed. + } + tap = nil + } + let enabled = tap.map { CGEvent.tapIsEnabled(tap: $0) } ?? false + UserDefaults.standard.set(enabled, forKey: "SBFWasEnabled") + } + + // MARK: - Menu mode + + private func updateMenuMode(active: Bool = true) { + // TODO: this actually returns YES if SSB is deleted (not disabled) from Accessibility + // kAXTrustedCheckOptionPrompt imports as Unmanaged; unwrap before use as dict key. + let key = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String + let options = [key: active] as CFDictionary + let accessibilityEnabled = AXIsProcessTrustedWithOptions(options) + + if accessibilityEnabled { + menuMode = UserDefaults.standard.bool(forKey: "SBFDonated") ? .normal : .donation + } else { + menuMode = .accessibility + } + } + + private func refreshSettings() { + guard let menu = statusItem.menu else { return } + + let tapEnabled = tap.map { CGEvent.tapIsEnabled(tap: $0) } ?? false + menu.items[MenuItem.enabled.rawValue].state = tapEnabled ? .on : .off + menu.items[MenuItem.triggerOnMouseDown.rawValue].state = UserDefaults.standard.bool(forKey: "SBFMouseDown") ? .on : .off + menu.items[MenuItem.swapButtons.rawValue].state = UserDefaults.standard.bool(forKey: "SBFSwapButtons") ? .on : .off + menu.items[MenuItem.startAtLogin.rawValue].state = isStartAtLoginEnabled ? .on : .off + + switch menuMode { + case .accessibility: + menu.items[MenuItem.enabled.rawValue].isEnabled = false + menu.items[MenuItem.triggerOnMouseDown.rawValue].isEnabled = false + menu.items[MenuItem.swapButtons.rawValue].isEnabled = false + menu.items[MenuItem.donate.rawValue].isHidden = true + menu.items[MenuItem.website.rawValue].isHidden = false + menu.items[MenuItem.accessibility.rawValue].isHidden = false + case .donation: + menu.items[MenuItem.enabled.rawValue].isEnabled = true + menu.items[MenuItem.triggerOnMouseDown.rawValue].isEnabled = true + menu.items[MenuItem.swapButtons.rawValue].isEnabled = true + menu.items[MenuItem.donate.rawValue].isHidden = false + menu.items[MenuItem.website.rawValue].isHidden = true + menu.items[MenuItem.accessibility.rawValue].isHidden = true + case .normal: + menu.items[MenuItem.enabled.rawValue].isEnabled = true + menu.items[MenuItem.triggerOnMouseDown.rawValue].isEnabled = true + menu.items[MenuItem.swapButtons.rawValue].isEnabled = true + menu.items[MenuItem.donate.rawValue].isHidden = true + menu.items[MenuItem.website.rawValue].isHidden = false + menu.items[MenuItem.accessibility.rawValue].isHidden = true + } + + if let aboutView = menu.items[MenuItem.aboutText.rawValue].view as? AboutView { + aboutView.layoutSubtreeIfNeeded() + aboutView.frame = NSRect(x: 0, y: 0, width: aboutView.bounds.width, height: aboutView.text.frame.height) + } + + if #available(macOS 10.12, *) { + menu.items[MenuItem.startupHide.rawValue].isHidden = false + menu.items[MenuItem.startupHideInfo.rawValue].isHidden = false + } else { + menu.items[MenuItem.startupHide.rawValue].isHidden = true + menu.items[MenuItem.startupHideInfo.rawValue].isHidden = true + } + + if let button = statusItem.button { + button.image = tapEnabled + ? NSImage(named: "MenuIcon") + : NSImage(named: "MenuIconDisabled") + } + } + + // MARK: - Actions + + @objc private func enabledToggle(_ sender: Any) { + startTap(tap == nil) + refreshSettings() + } + + @objc private func mouseDownToggle(_ sender: Any) { + let key = "SBFMouseDown" + UserDefaults.standard.set(!UserDefaults.standard.bool(forKey: key), forKey: key) + refreshSettings() + } + + @objc private func swapToggle(_ sender: Any) { + let key = "SBFSwapButtons" + UserDefaults.standard.set(!UserDefaults.standard.bool(forKey: key), forKey: key) + refreshSettings() + } + + @objc private func donate(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "http://sensible-side-buttons.archagon.net#donations")!) + UserDefaults.standard.set(true, forKey: "SBFDonated") + updateMenuMode() + refreshSettings() + } + + @objc private func website(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "http://sensible-side-buttons.archagon.net")!) + } + + @objc private func accessibility(_ sender: Any) { + updateMenuMode() + refreshSettings() + } + + @objc private func hideMenubarItem(_ sender: Any) { + if #available(macOS 10.12, *) { + statusItem.isVisible = false + } + } + + @objc private func quit(_ sender: Any) { + NSApp.terminate(self) + } + + // MARK: - Start at Login + + private var launchAgentPlistPath: String { + let bundleId = Bundle.main.bundleIdentifier ?? "" + let dir = (NSHomeDirectory() as NSString).appendingPathComponent("Library/LaunchAgents") + return (dir as NSString).appendingPathComponent("\(bundleId).plist") + } + + private var isStartAtLoginEnabled: Bool { + return FileManager.default.fileExists(atPath: launchAgentPlistPath) + } + + private func setStartAtLogin(_ enabled: Bool) { + let path = launchAgentPlistPath + if enabled { + let plist: NSDictionary = [ + "Label": Bundle.main.bundleIdentifier ?? "", + "Program": Bundle.main.executablePath ?? "", + "RunAtLoad": true, + ] + let dir = (path as NSString).deletingLastPathComponent + try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil) + plist.write(toFile: path, atomically: true) + } else { + try? FileManager.default.removeItem(atPath: path) + } + } + + @objc private func startAtLoginToggle(_ sender: Any) { + setStartAtLogin(!isStartAtLoginEnabled) + refreshSettings() + } + + // MARK: - NSMenuDelegate + + func menuWillOpen(_ menu: NSMenu) { + // TODO: theoretically, accessibility can be disabled while the menu is opened, but this is unlikely + updateMenuMode(active: false) + refreshSettings() + } +} + +// MARK: - AboutView + +private class AboutView: NSView { + + let text = NSTextView() + var menuMode: MenuMode = .normal { didSet { updateText() } } + + private var margin: CGFloat { 17 } + + override init(frame: NSRect) { + super.init(frame: frame) + text.backgroundColor = .clear + text.isEditable = false + text.isSelectable = false + addSubview(text) + updateText() + } + + required init?(coder: NSCoder) { fatalError("not supported") } + + private func updateText() { + let font = NSFont.menuFont(ofSize: 13) + + let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.bold) + let boldFont = NSFont(descriptor: boldDescriptor, size: font.pointSize) ?? font + + let regularColor = NSColor.secondaryLabelColor + let alertColor = NSColor.systemRed + + let regularAttrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: regularColor] + let alertAttrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: alertColor] + let smallReturnAttrs: [NSAttributedString.Key: Any] = [.font: NSFont.menuFont(ofSize: 3)] + + let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "" + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + let appDesc = "\(appName) \(version)" + let copyright = "Copyright © 2018 Alexei Baboulevitch." + + let result = NSMutableAttributedString() + + switch menuMode { + case .accessibility: + let body = "Uh-oh! It looks like \(appDesc) is not whitelisted in the Accessibility panel of your Security & Privacy System Preferences. This app needs to be on the Accessibility whitelist in order to process global mouse events. Please open the Accessibility panel below and add the app to the whitelist." + let s = NSMutableAttributedString(string: body, attributes: alertAttrs) + if let r = body.range(of: appDesc) { s.addAttribute(.font, value: boldFont, range: NSRange(r, in: body)) } + result.append(s) + + case .donation: + let body = "Thanks for using \(appDesc)!\nIf you find this utility useful, please consider making a purchase through the Amazon affiliate link on the website below. It won't cost you an extra cent! 😊" + let s = NSMutableAttributedString(string: body, attributes: regularAttrs) + if let r = body.range(of: appDesc) { s.addAttribute(.font, value: boldFont, range: NSRange(r, in: body)) } + result.append(s) + + case .normal: + let body = "Thanks for using \(appDesc)!" + let s = NSMutableAttributedString(string: body, attributes: regularAttrs) + if let r = body.range(of: appDesc) { s.addAttribute(.font, value: boldFont, range: NSRange(r, in: body)) } + result.append(s) + } + + result.append(NSAttributedString(string: "\n", attributes: regularAttrs)) + result.append(NSAttributedString(string: "\n", attributes: smallReturnAttrs)) + result.append(NSAttributedString(string: copyright, attributes: regularAttrs)) + + text.textStorage?.setAttributedString(result) + if #available(macOS 10.12, *) { needsLayout = true } + } + + override func layout() { + super.layout() + let arbitraryHeight: CGFloat = 100 + text.frame = NSRect(x: margin, y: 0, width: bounds.width - margin, height: arbitraryHeight) + text.sizeToFit() + text.frame = NSRect(x: text.frame.origin.x, + y: bounds.height - text.frame.height, + width: text.frame.width, + height: text.frame.height) + } +} diff --git a/SideButtonFixer/Base.lproj/Main.storyboard b/SideButtonFixer/Base.lproj/Main.storyboard index 8129567..872f91f 100644 --- a/SideButtonFixer/Base.lproj/Main.storyboard +++ b/SideButtonFixer/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -620,7 +620,7 @@ - + @@ -675,7 +675,7 @@ - + @@ -685,11 +685,14 @@ - + + + + diff --git a/SideButtonFixer/AppDelegate.h b/SideButtonFixer/SensibleSideButtons-Bridging-Header.h similarity index 89% rename from SideButtonFixer/AppDelegate.h rename to SideButtonFixer/SensibleSideButtons-Bridging-Header.h index 56cc66c..cbd3dab 100644 --- a/SideButtonFixer/AppDelegate.h +++ b/SideButtonFixer/SensibleSideButtons-Bridging-Header.h @@ -1,5 +1,5 @@ // -// AppDelegate.h +// SensibleSideButtons-Bridging-Header.h // // SensibleSideButtons, a utility that fixes the navigation buttons on third-party mice in macOS // Copyright (C) 2018 Alexei Baboulevitch (ssb@archagon.net) @@ -19,7 +19,4 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // -#import - -@interface AppDelegate : NSObject -@end +#import "TouchEvents.h" diff --git a/SideButtonFixer/main.m b/SideButtonFixer/main.swift similarity index 74% rename from SideButtonFixer/main.m rename to SideButtonFixer/main.swift index c2c386d..a835556 100644 --- a/SideButtonFixer/main.m +++ b/SideButtonFixer/main.swift @@ -1,5 +1,5 @@ // -// main.m +// main.swift // // SensibleSideButtons, a utility that fixes the navigation buttons on third-party mice in macOS // Copyright (C) 2018 Alexei Baboulevitch (ssb@archagon.net) @@ -19,8 +19,11 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // -#import +import Cocoa -int main(int argc, const char * argv[]) { - return NSApplicationMain(argc, argv); -} +// Explicitly create the delegate so it's guaranteed to be set even if the +// storyboard can't resolve the Swift class by name at runtime. +let _delegate = AppDelegate() +NSApplication.shared.delegate = _delegate + +NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/SwipeSimulator.xcodeproj/project.pbxproj b/SwipeSimulator.xcodeproj/project.pbxproj index 54b5e01..bc54a2e 100644 --- a/SwipeSimulator.xcodeproj/project.pbxproj +++ b/SwipeSimulator.xcodeproj/project.pbxproj @@ -8,9 +8,9 @@ /* Begin PBXBuildFile section */ 4E9995151EE7172700A1CFA6 /* TouchEvents.c in Sources */ = {isa = PBXBuildFile; fileRef = 4ECC16A21EE54D870062AC72 /* TouchEvents.c */; }; - 4EC7C3B91EE711B400C9F725 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC7C3B81EE711B400C9F725 /* AppDelegate.m */; }; 4EC7C3BE1EE711B400C9F725 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4EC7C3BD1EE711B400C9F725 /* Assets.xcassets */; }; - 4EC7C3C41EE711B400C9F725 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC7C3C31EE711B400C9F725 /* main.m */; }; + AABB004400000000AABB0044 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABB001100000000AABB0011 /* AppDelegate.swift */; }; + AABB005500000000AABB0055 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABB002200000000AABB0022 /* main.swift */; }; 4ECC169A1EE54D3D0062AC72 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ECC16991EE54D3D0062AC72 /* main.m */; }; 4ECC16AB1EE54D870062AC72 /* TouchEvents.c in Sources */ = {isa = PBXBuildFile; fileRef = 4ECC16A21EE54D870062AC72 /* TouchEvents.c */; }; 4EF1B3A31EE71DC40075438A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4EF1B39F1EE71D690075438A /* Main.storyboard */; }; @@ -40,13 +40,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 4EC7C3B51EE711B400C9F725 /* SideButtonFixer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = SideButtonFixer.app; path = SensibleSideButtons.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 4EC7C3B71EE711B400C9F725 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 4EC7C3B81EE711B400C9F725 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 4EC7C3B51EE711B400C9F725 /* SensibleSideButtons.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SensibleSideButtons.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4EC7C3BD1EE711B400C9F725 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 4EC7C3C21EE711B400C9F725 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4EC7C3C31EE711B400C9F725 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4EC7C3C51EE711B400C9F725 /* SideButtonFixer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SideButtonFixer.entitlements; sourceTree = ""; }; + AABB001100000000AABB0011 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AABB002200000000AABB0022 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + AABB003300000000AABB0033 /* SensibleSideButtons-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SensibleSideButtons-Bridging-Header.h"; sourceTree = ""; }; 4ECC16961EE54D3D0062AC72 /* swipesiml */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = swipesiml; sourceTree = BUILT_PRODUCTS_DIR; }; 4ECC16991EE54D3D0062AC72 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4ECC16A21EE54D870062AC72 /* TouchEvents.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = TouchEvents.c; sourceTree = ""; }; @@ -85,11 +85,11 @@ 4EC7C3B61EE711B400C9F725 /* SideButtonFixer */ = { isa = PBXGroup; children = ( - 4EC7C3B71EE711B400C9F725 /* AppDelegate.h */, - 4EC7C3B81EE711B400C9F725 /* AppDelegate.m */, + AABB001100000000AABB0011 /* AppDelegate.swift */, + AABB002200000000AABB0022 /* main.swift */, + AABB003300000000AABB0033 /* SensibleSideButtons-Bridging-Header.h */, 4EC7C3BD1EE711B400C9F725 /* Assets.xcassets */, 4EC7C3C21EE711B400C9F725 /* Info.plist */, - 4EC7C3C31EE711B400C9F725 /* main.m */, 4EF1B39F1EE71D690075438A /* Main.storyboard */, 4EC7C3C51EE711B400C9F725 /* SideButtonFixer.entitlements */, ); @@ -111,7 +111,7 @@ children = ( 4ECC16961EE54D3D0062AC72 /* swipesiml */, 4EF68AE91EE55A9500F6B0E8 /* swipesimr */, - 4EC7C3B51EE711B400C9F725 /* SideButtonFixer.app */, + 4EC7C3B51EE711B400C9F725 /* SensibleSideButtons.app */, ); name = Products; sourceTree = ""; @@ -152,7 +152,7 @@ ); name = SensibleSideButtons; productName = SideButtonFixer; - productReference = 4EC7C3B51EE711B400C9F725 /* SideButtonFixer.app */; + productReference = 4EC7C3B51EE711B400C9F725 /* SensibleSideButtons.app */; productType = "com.apple.product-type.application"; }; 4ECC16951EE54D3D0062AC72 /* swipesiml */ = { @@ -200,7 +200,7 @@ TargetAttributes = { 4EC7C3B41EE711B400C9F725 = { CreatedOnToolsVersion = 9.0; - DevelopmentTeam = R4MX2B96J2; + DevelopmentTeam = 93P9B83QGL; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { @@ -223,6 +223,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -255,9 +256,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4EC7C3C41EE711B400C9F725 /* main.m in Sources */, + AABB004400000000AABB0044 /* AppDelegate.swift in Sources */, + AABB005500000000AABB0055 /* main.swift in Sources */, 4E9995151EE7172700A1CFA6 /* TouchEvents.c in Sources */, - 4EC7C3B91EE711B400C9F725 /* AppDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -306,18 +307,21 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = R4MX2B96J2; + DEVELOPMENT_TEAM = 93P9B83QGL; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); + HEADER_SEARCH_PATHS = "$(SRCROOT)/External"; INFOPLIST_FILE = SideButtonFixer/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_BUNDLE_IDENTIFIER = "net.archagon.sensible-side-buttons"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "SideButtonFixer/SensibleSideButtons-Bridging-Header.h"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -334,14 +338,17 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = R4MX2B96J2; + DEVELOPMENT_TEAM = 93P9B83QGL; GCC_C_LANGUAGE_STANDARD = gnu11; + HEADER_SEARCH_PATHS = "$(SRCROOT)/External"; INFOPLIST_FILE = SideButtonFixer/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_BUNDLE_IDENTIFIER = "net.archagon.sensible-side-buttons"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "SideButtonFixer/SensibleSideButtons-Bridging-Header.h"; + SWIFT_VERSION = 5.0; }; name = Release; };