From 57de64527f09ac655e0bedb300c4d6ec1763539f Mon Sep 17 00:00:00 2001 From: lidawen Date: Wed, 4 Jun 2025 15:39:11 +0800 Subject: [PATCH] feat: add ignore apps lists --- SideButtonFixer/AppDelegate.m | 1232 +++++++++++++------- SideButtonFixer/Base.lproj/Main.storyboard | 5 +- SwipeSimulator.xcodeproj/project.pbxproj | 35 +- 3 files changed, 823 insertions(+), 449 deletions(-) diff --git a/SideButtonFixer/AppDelegate.m b/SideButtonFixer/AppDelegate.m index b44357e..1b075b3 100644 --- a/SideButtonFixer/AppDelegate.m +++ b/SideButtonFixer/AppDelegate.m @@ -21,538 +21,904 @@ #import "AppDelegate.h" #import "TouchEvents.h" +#import +#import + // MARK: Constants -static NSArray* ignored_application_bundle_ids; -static NSMutableDictionary*>* swipeInfo = nil; -static NSArray* nullArray = nil; +static NSArray *kDefaultIgnoredBundleIds; +static NSMutableArray *kUserIgnoredBundleIds = nil; +static NSMutableDictionary *> *kSwipeInfo = nil; +static NSArray *kNullArray = nil; +typedef NS_ENUM(NSInteger, MenuItem) { + MenuItemEnabled = 0, + MenuItemEnabledSeparator, + MenuItemTriggerOnMouseDown, + MenuItemSwapButtons, + MenuItemOptionsSeparator, + MenuItemStartupHide, + MenuItemStartupHideInfo, + MenuItemStartupSeparator, + MenuItemIgnoredApps, + MenuItemIgnoredAppsSeparator, + MenuItemAboutText, + MenuItemAboutSeparator, + MenuItemDonate, + MenuItemWebsite, + MenuItemAccessibility, + MenuItemLinkSeparator, + MenuItemQuit +}; +typedef NS_ENUM(NSInteger, MenuMode) { + MenuModeAccessibility, + MenuModeDonation, + MenuModeNormal +}; -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); +static void SSBFakeSwipe(TLInfoSwipeDirection direction) +{ + CGEventRef event1 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(kSwipeInfo[@(direction)][0]), (__bridge CFArrayRef)kNullArray); + CGEventRef event2 = tl_CGEventCreateFromGesture((__bridge CFDictionaryRef)(kSwipeInfo[@(direction)][1]), (__bridge CFArrayRef)kNullArray); - CGEventPost(kCGHIDEventTap, event1); - CGEventPost(kCGHIDEventTap, event2); + CGEventPost(kCGHIDEventTap, event1); + CGEventPost(kCGHIDEventTap, event2); - CFRelease(event1); - CFRelease(event2); + CFRelease(event1); + CFRelease(event2); } -static BOOL is_valid_application(void) { - NSString* front_app_bundleID = [[[NSWorkspace sharedWorkspace] frontmostApplication] bundleIdentifier]; +@interface AppDelegate () +@property (nonatomic, strong) NSStatusItem *statusItem; +@property (nonatomic, strong) NSAlert *currentAlert; +@property (nonatomic, assign) MenuMode menuMode; +@property (nonatomic, assign) CFMachPortRef eventTap; +@end - if (!front_app_bundleID) { - return YES; - } +@interface AboutView : NSView +@property (nonatomic, retain) NSTextView *text; +@property (nonatomic, assign) MenuMode menuMode; +- (CGFloat)margin; +@end - for (NSString* bundleID in ignored_application_bundle_ids) { - if ([bundleID isEqualToString:front_app_bundleID]) { - return NO; - } - } +// Create a custom view class to handle drag and drop +@interface DropView : NSView +@property (nonatomic, weak) NSTextField *inputField; +@end - return YES; +@implementation AppDelegate + +- (void)dealloc { + [self startEventTap:NO]; + + kSwipeInfo = nil; + kNullArray = nil; } -static CGEventRef SBFMouseCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { - // NOTE(bojanin): VSCode doesnt support sensibleSideButtons, but without sensible side buttons, pressing the side buttons works. - // This basically checks if the mouse side button press happened inside VSCode, if so it returns the original CGEventRef. - if (!is_valid_application()) { - return event; - } +- (void)setMenuMode:(MenuMode)menuMode { + _menuMode = menuMode; + AboutView *view = (AboutView *)self.statusItem.menu.itemArray[MenuItemAboutText].view; + view.menuMode = menuMode; + [self refreshSettings]; +} - int64_t number = CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber); +// 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; +} + +// Get running applications list +- (NSArray *)runningApplications { + NSMutableArray *apps = [NSMutableArray array]; + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + NSArray *runningApps = [workspace runningApplications]; + + for (NSRunningApplication *app in runningApps) { + NSString *bundleId = app.bundleIdentifier; + NSString *name = app.localizedName; + if (bundleId && name) { + [apps addObject:@{ + @"name": name, + @"bundleId": bundleId + }]; + } + } - BOOL down = (CGEventGetType(event) == kCGEventOtherMouseDown); + // Sort by application name + [apps sortUsingComparator:^NSComparisonResult (NSDictionary *app1, NSDictionary *app2) { + return [app1[@"name"] compare:app2[@"name"]]; + }]; - BOOL mouseDown = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFMouseDown"]; - BOOL swapButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFSwapButtons"]; + return apps; +} - if (number == (swapButtons ? 4 : 3)) { - if ((mouseDown && down) || (!mouseDown && !down)) { - SBFFakeSwipe(kTLInfoSwipeLeft); +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"SBFWasEnabled": @YES, + @"SBFMouseDown": @YES, + @"SBFDonated": @NO, + @"SBFSwapButtons": @NO, + @"SBFIgnoredBundleIds": @[] + }]; + + // Initialize global variables + kDefaultIgnoredBundleIds = @[ + @"com.microsoft.VSCode", + @"com.todesktop.230313mzl4w4u92", + @"com.trae.app", + @"cn.trae.app", + @"com.exafunction.windsurf", + @"com.blizzard.worldofwarcraft", + ]; + + // Initialize user ignored bundle IDs from user defaults + kUserIgnoredBundleIds = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] arrayForKey:@"SBFIgnoredBundleIds"]]; + + kSwipeInfo = [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]; + + kSwipeInfo[direction] = @[ swipeInfo1, swipeInfo2 ]; } - return NULL; - } - else if (number == (swapButtons ? 3 : 4)) { - if ((mouseDown && down) || (!mouseDown && !down)) { - SBFFakeSwipe(kTLInfoSwipeRight); + kNullArray = @[]; + + // create status bar item + self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + + // MARK: - 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); + + // Add menu items for ignored apps + NSMenuItem *ignoredAppsMenu = [[NSMenuItem alloc] initWithTitle:@"Ignored Apps" action:@selector(addNewBundleId:) keyEquivalent:@""]; + NSMenu *submenu = [[NSMenu alloc] initWithTitle:@"Ignored Apps"]; + + // Add separator before showing ignored apps + [submenu addItem:[NSMenuItem separatorItem]]; + + // Add existing ignored apps to the list + for (NSString *bundleId in kUserIgnoredBundleIds) { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:bundleId action:@selector(removeBundleId:) keyEquivalent:@""]; + [item setRepresentedObject:bundleId]; + [submenu addItem:item]; } - return NULL; - } - else { - return event; - } -} + ignoredAppsMenu.submenu = submenu; + [menu addItem:ignoredAppsMenu]; + assert(menu.itemArray.count - 1 == MenuItemIgnoredApps); -typedef NS_ENUM(NSInteger, MenuMode) { - MenuModeAccessibility, - MenuModeDonation, - MenuModeNormal -}; + [menu addItem:[NSMenuItem separatorItem]]; + assert(menu.itemArray.count - 1 == MenuItemIgnoredAppsSeparator); -typedef NS_ENUM(NSInteger, MenuItem) { - MenuItemEnabled = 0, - MenuItemEnabledSeparator, - MenuItemTriggerOnMouseDown, - MenuItemSwapButtons, - MenuItemOptionsSeparator, - MenuItemStartupHide, - MenuItemStartupHideInfo, - MenuItemStartupSeparator, - MenuItemAboutText, - MenuItemAboutSeparator, - MenuItemDonate, - MenuItemWebsite, - MenuItemAccessibility, - MenuItemLinkSeparator, - MenuItemQuit -}; + 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); -@interface AppDelegate () -@property (nonatomic, retain) NSStatusItem* statusItem; -@property (nonatomic, assign) CFMachPortRef tap; -@property (nonatomic, assign) MenuMode menuMode; -@end + [menu addItem:[NSMenuItem separatorItem]]; + assert(menu.itemArray.count - 1 == MenuItemAboutSeparator); -@interface AboutView: NSView -@property (nonatomic, retain) NSTextView* text; -@property (nonatomic, assign) MenuMode menuMode; --(CGFloat) margin; -@end + 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); -@implementation AppDelegate + [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); --(void) dealloc { - [self startTap:NO]; + [menu addItem:[NSMenuItem separatorItem]]; + assert(menu.itemArray.count - 1 == MenuItemLinkSeparator); - swipeInfo = nil; - nullArray = nil; + 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 startEventTap:[[NSUserDefaults standardUserDefaults] boolForKey:@"SBFWasEnabled"]]; + + [self updateMenuMode]; + [self refreshSettings]; } --(void) setMenuMode:(MenuMode)menuMode { - _menuMode = menuMode; - AboutView* view = (AboutView*)self.statusItem.menu.itemArray[MenuItemAboutText].view; - view.menuMode = menuMode; - [self refreshSettings]; +- (void)updateMenuMode { + [self updateMenuMode:YES]; } -// 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)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) applicationDidFinishLaunching:(NSNotification *)aNotification { - [[NSUserDefaults standardUserDefaults] registerDefaults:@{ - @"SBFWasEnabled": @YES, - @"SBFMouseDown": @YES, - @"SBFDonated": @NO, - @"SBFSwapButtons": @NO - }]; +- (void)refreshSettings { + self.statusItem.menu.itemArray[MenuItemEnabled].state = self.eventTap != NULL && CGEventTapIsEnabled(self.eventTap); + 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; + } - // MARK: - setup globals + 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; + } - ignored_application_bundle_ids = [NSArray arrayWithObject: @"com.microsoft.VSCode"]; + if (self.statusItem.button != nil) { + if (self.eventTap != NULL && CGEventTapIsEnabled(self.eventTap)) { + self.statusItem.button.image = [NSImage imageNamed:@"MenuIcon"]; + } else { + self.statusItem.button.image = [NSImage imageNamed:@"MenuIconDisabled"]; + } + } +} +- (void)startEventTap:(BOOL)shouldStart { + if (shouldStart) { + if (self.eventTap == NULL) { + self.eventTap = CGEventTapCreate(kCGHIDEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + CGEventMaskBit(kCGEventOtherMouseUp) | CGEventMaskBit(kCGEventOtherMouseDown), + &SSBMouseCallback, + NULL); + + if (self.eventTap != NULL) { + CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(NULL, self.eventTap, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); + CFRelease(runLoopSource); + + CGEventTapEnable(self.eventTap, true); + } + } + } else { + if (self.eventTap != NULL) { + CGEventTapEnable(self.eventTap, NO); + CFRelease(self.eventTap); + + self.eventTap = NULL; + } + } - swipeInfo = [NSMutableDictionary dictionary]; + [[NSUserDefaults standardUserDefaults] setBool:self.eventTap != NULL && CGEventTapIsEnabled(self.eventTap) forKey:@"SBFWasEnabled"]; +} - for (NSNumber* direction in @[ @(kTLInfoSwipeUp), @(kTLInfoSwipeDown), @(kTLInfoSwipeLeft), @(kTLInfoSwipeRight) ]) { - NSDictionary* swipeInfo1 = [NSDictionary dictionaryWithObjectsAndKeys: - @(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype, - @(1), kTLInfoKeyGesturePhase, - nil]; +- (void)enabledToggle:(id)sender { + [self startEventTap:self.eventTap == NULL]; + [self refreshSettings]; +} + +- (void)mouseDownToggle:(id)sender { + [[NSUserDefaults standardUserDefaults] setBool:![[NSUserDefaults standardUserDefaults] boolForKey:@"SBFMouseDown"] forKey:@"SBFMouseDown"]; + [self refreshSettings]; +} - NSDictionary* swipeInfo2 = [NSDictionary dictionaryWithObjectsAndKeys: - @(kTLInfoSubtypeSwipe), kTLInfoKeyGestureSubtype, - direction, kTLInfoKeySwipeDirection, - @(4), kTLInfoKeyGesturePhase, - nil]; +- (void)swapToggle:(id)sender { + [[NSUserDefaults standardUserDefaults] setBool:![[NSUserDefaults standardUserDefaults] boolForKey:@"SBFSwapButtons"] forKey:@"SBFSwapButtons"]; + [self refreshSettings]; +} - swipeInfo[direction] = @[ swipeInfo1, swipeInfo2 ]; - } +- (void)donate:(id)sender { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://sensible-side-buttons.archagon.net#donations"]]; + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"SBFDonated"]; - nullArray = @[]; + [self updateMenuMode]; + [self refreshSettings]; +} - // create status bar item - self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; +- (void)website:(id)sender { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://sensible-side-buttons.archagon.net"]]; +} - // MARK: - create menu - NSMenu* menu = [NSMenu new]; +- (void)accessibility:(id)sender { + [self updateMenuMode]; + [self refreshSettings]; +} - menu.autoenablesItems = NO; - menu.delegate = self; +- (void)hideMenubarItem:(id)sender { + if (@available(macOS 10.12, *)) { + [self.statusItem setVisible:NO]; + } +} - NSMenuItem* enabledItem = [[NSMenuItem alloc] initWithTitle:@"Enabled" action:@selector(enabledToggle:) keyEquivalent:@"e"]; - [menu addItem:enabledItem]; - assert(menu.itemArray.count - 1 == MenuItemEnabled); +- (void)quit:(id)sender { + [NSApp terminate:self]; +} - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemEnabledSeparator); +// Update removeBundleId: method to handle menu item removal +- (void)removeBundleId:(id)sender { + NSMenuItem *item = (NSMenuItem *)sender; + NSString *bundleId = item.representedObject; - 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); + // Remove from ignored list + [kUserIgnoredBundleIds removeObject:bundleId]; - NSMenuItem* swapItem = [[NSMenuItem alloc] initWithTitle:@"Swap Buttons" action:@selector(swapToggle:) keyEquivalent:@""]; - swapItem.state = NSControlStateValueOff; - [menu addItem:swapItem]; - assert(menu.itemArray.count - 1 == MenuItemSwapButtons); + // Remove from menu + [item.menu removeItem:item]; - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemOptionsSeparator); + // Save to user defaults + [[NSUserDefaults standardUserDefaults] setObject:kUserIgnoredBundleIds forKey:@"SBFIgnoredBundleIds"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} +// Helper method to get application name +- (NSString *)applicationNameForBundleId:(NSString *)bundleId { + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + // If not running, try to get from installed applications + NSURL *appURL = [workspace URLForApplicationWithBundleIdentifier:bundleId]; + if (appURL) { + NSBundle *appBundle = [NSBundle bundleWithURL:appURL]; + if (appBundle) { + NSString *name = [appBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + if (!name) { + name = [appBundle objectForInfoDictionaryKey:@"CFBundleName"]; + } + if (name) { + return name; + } + } + } - NSMenuItem* hideItem = [[NSMenuItem alloc] initWithTitle:@"Hide Menu Bar Icon" action:@selector(hideMenubarItem:) keyEquivalent:@""]; - [menu addItem:hideItem]; - assert(menu.itemArray.count - 1 == MenuItemStartupHide); + // If still not found, return bundle ID as fallback + return bundleId; +} - 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); +// Update the places where we use the application name +- (void)addNewBundleId:(id)sender { + // Create custom view with drag & drop support + DropView *customView = [[DropView alloc] initWithFrame:NSMakeRect(0, 0, 400, 300)]; + + // Create input field + NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 270, 300, 24)]; + [customView addSubview:input]; + customView.inputField = input; + + // Create hint label + NSTextField *dropLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 235, 390, 30)]; + [dropLabel setStringValue:@"Tip: You can also drag and drop applications here"]; + [dropLabel setTextColor:[NSColor secondaryLabelColor]]; + [dropLabel setFont:[NSFont systemFontOfSize:11]]; + [dropLabel setBezeled:NO]; + [dropLabel setDrawsBackground:NO]; + [dropLabel setEditable:NO]; + [dropLabel setSelectable:NO]; + [customView addSubview:dropLabel]; + + // Create add button + NSButton *addButton = [[NSButton alloc] initWithFrame:NSMakeRect(310, 270, 80, 24)]; + [addButton setTitle:@"Add"]; + [addButton setBezelStyle:NSBezelStyleRounded]; + [addButton setTarget:self]; + [addButton setAction:@selector(addButtonClicked:)]; + [customView addSubview:addButton]; + + // Create table scroll view + NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 390, 230)]; + [scrollView setHasVerticalScroller:YES]; + [scrollView setHasHorizontalScroller:NO]; + [scrollView setBorderType:NSBezelBorder]; + + // Create table view + NSTableView *tableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; + + // Add app name column + NSTableColumn *nameColumn = [[NSTableColumn alloc] initWithIdentifier:@"name"]; + [nameColumn setTitle:@"App Name"]; + [nameColumn setWidth:190]; + [tableView addTableColumn:nameColumn]; + + // Add bundle ID column + NSTableColumn *bundleIdColumn = [[NSTableColumn alloc] initWithIdentifier:@"bundleId"]; + [bundleIdColumn setTitle:@"Bundle ID"]; + [bundleIdColumn setWidth:200]; + [tableView addTableColumn:bundleIdColumn]; + + [scrollView setDocumentView:tableView]; + [customView addSubview:scrollView]; + + // Create data source + NSMutableArray *allIgnoredApps = [NSMutableArray array]; + + // Add built-in ignored apps + for (NSString *bundleId in kDefaultIgnoredBundleIds) { + NSString *name = [self applicationNameForBundleId:bundleId]; + [allIgnoredApps addObject:@{ + @"name": name, + @"bundleId": bundleId, + @"type": @"Internal" + }]; + } - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemStartupSeparator); + // Add user-defined apps + for (NSString *bundleId in kUserIgnoredBundleIds) { + NSString *name = [self applicationNameForBundleId:bundleId]; + [allIgnoredApps addObject:@{ + @"name": name, + @"bundleId": bundleId, + @"type": @"Custom" + }]; + } - 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); + // Set table data source and delegate + tableView.dataSource = self; + tableView.delegate = self; + objc_setAssociatedObject(tableView, "tableData", allIgnoredApps, OBJC_ASSOCIATION_RETAIN); + [tableView reloadData]; + + // Add double-click handler + [tableView setTarget:self]; + [tableView setDoubleAction:@selector(tableViewDoubleClick:)]; + + // Configure alert + NSAlert *alert = [[NSAlert alloc] init]; + self.currentAlert = alert; + self.currentAlert.messageText = @"Manage Ignored Apps"; + self.currentAlert.informativeText = @"Enter the Bundle ID of the app you want to ignore:"; + self.currentAlert.accessoryView = customView; + + [self.currentAlert addButtonWithTitle:@"Done"]; + + // Get current active window or create a temporary one + NSWindow *window = [NSApp keyWindow]; + if (!window) { + window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100) + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:NO]; + [window center]; + [window makeKeyAndOrderFront:nil]; + } - [menu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Accessibility Whitelist" action:@selector(accessibility:) keyEquivalent:@""]]; - assert(menu.itemArray.count - 1 == MenuItemAccessibility); + // Store input field and table view references + objc_setAssociatedObject(self.currentAlert, "inputField", input, OBJC_ASSOCIATION_RETAIN); + objc_setAssociatedObject(self.currentAlert, "tableView", tableView, OBJC_ASSOCIATION_RETAIN); - [menu addItem:[NSMenuItem separatorItem]]; - assert(menu.itemArray.count - 1 == MenuItemLinkSeparator); + // Make input field first responder + [input becomeFirstResponder]; - 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]; + [self.currentAlert beginSheetModalForWindow:window completionHandler:^(NSModalResponse response) { + if (![NSApp keyWindow]) { + [window close]; + } + self.currentAlert = nil; // Clean up reference + }]; } --(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; +// Update addButtonClicked method +- (void)addButtonClicked:(NSButton *)sender { + if (!self.currentAlert) { + return; } - else { - self.menuMode = MenuModeDonation; + + NSTextField *input = objc_getAssociatedObject(self.currentAlert, "inputField"); + NSTableView *tableView = objc_getAssociatedObject(self.currentAlert, "tableView"); + + NSString *bundleId = [input stringValue]; + if (bundleId.length > 0) { + // Check if already exists + if (![kUserIgnoredBundleIds containsObject:bundleId]) { + [kUserIgnoredBundleIds addObject:bundleId]; + + // Update menu + NSMenuItem *ignoredAppsItem = self.statusItem.menu.itemArray[MenuItemIgnoredApps]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:bundleId action:@selector(removeBundleId:) keyEquivalent:@""]; + [item setRepresentedObject:bundleId]; + [ignoredAppsItem.submenu addItem:item]; + + // Save to user defaults + [[NSUserDefaults standardUserDefaults] setObject:kUserIgnoredBundleIds forKey:@"SBFIgnoredBundleIds"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + // Update table + NSMutableArray *tableData = objc_getAssociatedObject(tableView, "tableData"); + NSString *name = [self applicationNameForBundleId:bundleId]; + [tableData addObject:@{ + @"name": name, + @"bundleId": bundleId, + @"type": @"Custom" + }]; + [tableView reloadData]; + + // Clear input field + [input setStringValue:@""]; + } } - } - else { - self.menuMode = MenuModeAccessibility; - } +} + +#pragma mark - NSTableViewDataSource - // QQQ: for testing - //self.menuMode = arc4random_uniform(3); +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { + NSArray *tableData = objc_getAssociatedObject(tableView, "tableData"); + return tableData.count; } --(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"]; +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { + NSArray *tableData = objc_getAssociatedObject(tableView, "tableData"); + NSDictionary *rowData = tableData[row]; + return rowData[tableColumn.identifier]; +} - 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; - } +#pragma mark - NSTableViewDelegate - 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); +- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { + NSArray *tableData = objc_getAssociatedObject(tableView, "tableData"); + NSDictionary *rowData = tableData[row]; - // 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; - } + NSString *type = rowData[@"type"]; + if ([type isEqualToString:@"Internal"]) { + [(NSTextFieldCell *)cell setTextColor:[NSColor grayColor]]; + } else { + [(NSTextFieldCell *)cell setTextColor:[NSColor textColor]]; + } +} - if (self.statusItem.button != nil) { - if (self.tap != NULL && CGEventTapIsEnabled(self.tap)) { - self.statusItem.button.image = [NSImage imageNamed:@"MenuIcon"]; +// Handle double-click +- (void)tableViewDoubleClick:(id)sender { + NSTableView *tableView = (NSTableView *)sender; + NSInteger row = [tableView clickedRow]; + if (row != -1) { + [self removeIgnoredAppAtRow:row inTableView:tableView]; } - else { - self.statusItem.button.image = [NSImage imageNamed:@"MenuIconDisabled"]; +} + +// Common method to remove ignored app +- (void)removeIgnoredAppAtRow:(NSInteger)row inTableView:(NSTableView *)tableView { + NSMutableArray *tableData = objc_getAssociatedObject(tableView, "tableData"); + NSDictionary *rowData = tableData[row]; + + // Only custom items can be deleted + if (![rowData[@"type"] isEqualToString:@"Custom"]) { + return; } - } + + NSString *bundleId = rowData[@"bundleId"]; + + // Remove from ignored list + [kUserIgnoredBundleIds removeObject:bundleId]; + + // Remove from menu + NSMenuItem *ignoredAppsItem = self.statusItem.menu.itemArray[MenuItemIgnoredApps]; + for (NSMenuItem *item in ignoredAppsItem.submenu.itemArray) { + if ([item.representedObject isEqualToString:bundleId]) { + [ignoredAppsItem.submenu removeItem:item]; + break; + } + } + + // Remove from table data + [tableData removeObjectAtIndex:row]; + [tableView reloadData]; + + // Save to user defaults + [[NSUserDefaults standardUserDefaults] setObject:kUserIgnoredBundleIds forKey:@"SBFIgnoredBundleIds"]; + [[NSUserDefaults standardUserDefaults] synchronize]; } --(void) startTap:(BOOL)start { - if (start) { - if (self.tap == NULL) { - self.tap = CGEventTapCreate(kCGHIDEventTap, - kCGHeadInsertEventTap, - kCGEventTapOptionDefault, - CGEventMaskBit(kCGEventOtherMouseUp)|CGEventMaskBit(kCGEventOtherMouseDown), - &SBFMouseCallback, - NULL); +// Update is_valid_application to use global variables +static BOOL isApplicationValid(void) +{ + NSString *frontAppBundleID = [[[NSWorkspace sharedWorkspace] frontmostApplication] bundleIdentifier]; - if (self.tap != NULL) { - CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(NULL, self.tap, 0); - CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); - CFRelease(runLoopSource); + if (!frontAppBundleID) { + return YES; + } - CGEventTapEnable(self.tap, true); - } + // Check user-defined ignore list + if ([kUserIgnoredBundleIds containsObject:frontAppBundleID]) { + return NO; } - } - else { - if (self.tap != NULL) { - CGEventTapEnable(self.tap, NO); - CFRelease(self.tap); - self.tap = NULL; + // Check built-in ignore list + for (NSString *bundleID in kDefaultIgnoredBundleIds) { + if ([bundleID isEqualToString:frontAppBundleID]) { + return NO; + } } - } - [[NSUserDefaults standardUserDefaults] setBool:self.tap != NULL && CGEventTapIsEnabled(self.tap) forKey:@"SBFWasEnabled"]; + return YES; } --(void) enabledToggle:(id)sender { - [self startTap:self.tap == NULL]; - [self refreshSettings]; -} +static CGEventRef SSBMouseCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) +{ + if (!isApplicationValid()) { + return event; + } --(void) mouseDownToggle:(id)sender { - [[NSUserDefaults standardUserDefaults] setBool:![[NSUserDefaults standardUserDefaults] boolForKey:@"SBFMouseDown"] forKey:@"SBFMouseDown"]; - [self refreshSettings]; + int64_t number = CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber); + BOOL isDown = (CGEventGetType(event) == kCGEventOtherMouseDown); + BOOL shouldTriggerOnMouseDown = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFMouseDown"]; + BOOL shouldSwapButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"SBFSwapButtons"]; + + if (number == (shouldSwapButtons ? 4 : 3)) { + if ((shouldTriggerOnMouseDown && isDown) || (!shouldTriggerOnMouseDown && !isDown)) { + SSBFakeSwipe(kTLInfoSwipeLeft); + } + return NULL; + } else if (number == (shouldSwapButtons ? 3 : 4)) { + if ((shouldTriggerOnMouseDown && isDown) || (!shouldTriggerOnMouseDown && !isDown)) { + SSBFakeSwipe(kTLInfoSwipeRight); + } + return NULL; + } else { + return event; + } } --(void) swapToggle:(id)sender { - [[NSUserDefaults standardUserDefaults] setBool:![[NSUserDefaults standardUserDefaults] boolForKey:@"SBFSwapButtons"] forKey:@"SBFSwapButtons"]; - [self refreshSettings]; -} +@end --(void) donate:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: @"http://sensible-side-buttons.archagon.net#donations"]]; - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"SBFDonated"]; +@implementation AboutView - [self updateMenuMode]; - [self refreshSettings]; +- (CGFloat)margin { + return 17; } --(void) website:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: @"http://sensible-side-buttons.archagon.net"]]; -} +- (void)setMenuMode:(MenuMode)menuMode { + _menuMode = menuMode; --(void) accessibility:(id)sender { - [self updateMenuMode]; - [self refreshSettings]; -} + 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; + } --(void) hideMenubarItem:(id)sender { - if (@available(macOS 10.12, *)) { - [self.statusItem setVisible:NO]; - } + [self setNeedsLayout:YES]; } --(void) quit:(id)sender { - [NSApp terminate:self]; +- (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) menuWillOpen:(NSMenu*)menu { - // TODO: theoretically, accessibility can be disabled while the menu is opened, but this is unlikely - [self updateMenuMode:NO]; - [self refreshSettings]; +- (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 -@implementation AboutView +@implementation DropView --(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)); +- (instancetype)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + if (self) { + [self registerForDraggedTypes:@[NSPasteboardTypeFileURL]]; + } + return self; +} + +- (NSDragOperation)draggingEntered:(id)sender { + NSPasteboard *pboard = [sender draggingPasteboard]; + if ([[pboard types] containsObject:NSPasteboardTypeFileURL]) { + NSURL *fileURL = [NSURL URLFromPasteboard:pboard]; + if ([[fileURL pathExtension] isEqualToString:@"app"]) { + return NSDragOperationCopy; + } + } + return NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id)sender { + NSPasteboard *pboard = [sender draggingPasteboard]; + NSURL *fileURL = [NSURL URLFromPasteboard:pboard]; + + if ([[fileURL pathExtension] isEqualToString:@"app"]) { + NSBundle *appBundle = [NSBundle bundleWithURL:fileURL]; + if (appBundle) { + NSString *bundleId = [appBundle bundleIdentifier]; + if (bundleId) { + [self.inputField setStringValue:bundleId]; + return YES; + } + } + } + return NO; } @end diff --git a/SideButtonFixer/Base.lproj/Main.storyboard b/SideButtonFixer/Base.lproj/Main.storyboard index 7a50162..b1f2a5e 100644 --- a/SideButtonFixer/Base.lproj/Main.storyboard +++ b/SideButtonFixer/Base.lproj/Main.storyboard @@ -1,8 +1,7 @@ - + - - + diff --git a/SwipeSimulator.xcodeproj/project.pbxproj b/SwipeSimulator.xcodeproj/project.pbxproj index a02b25b..def2bb5 100644 --- a/SwipeSimulator.xcodeproj/project.pbxproj +++ b/SwipeSimulator.xcodeproj/project.pbxproj @@ -200,7 +200,7 @@ TargetAttributes = { 4EC7C3B41EE711B400C9F725 = { CreatedOnToolsVersion = 9.0; - DevelopmentTeam = E3G5SAS68Z; + DevelopmentTeam = 2A5Y5ZXK84; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { @@ -210,11 +210,12 @@ }; 4ECC16951EE54D3D0062AC72 = { CreatedOnToolsVersion = 8.3.1; - DevelopmentTeam = R4MX2B96J2; + DevelopmentTeam = 2A5Y5ZXK84; ProvisioningStyle = Automatic; }; 4EF68AE01EE55A9500F6B0E8 = { - DevelopmentTeam = R4MX2B96J2; + DevelopmentTeam = 2A5Y5ZXK84; + ProvisioningStyle = Automatic; }; }; }; @@ -306,7 +307,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = E3G5SAS68Z; + DEVELOPMENT_TEAM = 2A5Y5ZXK84; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -315,7 +316,7 @@ ); INFOPLIST_FILE = SideButtonFixer/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "net.archagon.sensible-side-buttons"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -335,12 +336,12 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = E3G5SAS68Z; + DEVELOPMENT_TEAM = 2A5Y5ZXK84; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = SideButtonFixer/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "net.archagon.sensible-side-buttons"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -456,7 +457,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_IDENTITY = "-"; - DEVELOPMENT_TEAM = R4MX2B96J2; + DEVELOPMENT_TEAM = 2A5Y5ZXK84; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", @@ -465,6 +466,7 @@ "LEFT=1", "DEBUG=1", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -473,12 +475,13 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_IDENTITY = "-"; - DEVELOPMENT_TEAM = R4MX2B96J2; + DEVELOPMENT_TEAM = 2A5Y5ZXK84; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); GCC_PREPROCESSOR_DEFINITIONS = "LEFT=1"; + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -486,8 +489,9 @@ 4EF68AE71EE55A9500F6B0E8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = "-"; - DEVELOPMENT_TEAM = R4MX2B96J2; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 2A5Y5ZXK84; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", @@ -496,21 +500,26 @@ "RIGHT=1", "DEBUG=1", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Debug; }; 4EF68AE81EE55A9500F6B0E8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = "-"; - DEVELOPMENT_TEAM = R4MX2B96J2; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 2A5Y5ZXK84; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); GCC_PREPROCESSOR_DEFINITIONS = "RIGHT=1"; + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Release; };