From c501619e4b01e85826e503c51f9ceb5337360c37 Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Date: Mon, 19 May 2025 16:10:22 -0300 Subject: [PATCH 1/2] [menu-bar][macos] Extract Popover logic to a new class --- .../macos/ExpoMenuBar-macOS/AppDelegate.h | 9 +- .../macos/ExpoMenuBar-macOS/AppDelegate.m | 110 +------------ .../ExpoMenuBar-macOS/AutoResizerRootView.m | 6 +- .../ExpoMenuBar-macOS-Bridging-Header.h | 6 +- .../ExpoMenuBar-macOS/PopoverManager.swift | 145 ++++++++++++++++++ .../ExpoMenuBar.xcodeproj/project.pbxproj | 22 ++- 6 files changed, 169 insertions(+), 129 deletions(-) create mode 100644 apps/menu-bar/macos/ExpoMenuBar-macOS/PopoverManager.swift diff --git a/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.h b/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.h index bfa449ec..fb21f5a0 100644 --- a/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.h +++ b/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.h @@ -3,22 +3,15 @@ #import "Expo_Orbit-Swift.h" -@class RCTBridge; - @interface AppDelegate : RCTAppDelegate { - NSStatusItem *statusItem; - NSPopover *popover; SwifterWrapper *httpServer; + PopoverManager *popoverManager; } -@property(nonatomic, strong, readonly) NSPopover *popover; #if RCT_DEV @property (nonatomic, strong) NSWindowController *devWindowController; #endif -- (void)openPopover; -- (void)closePopover; -- (void)setPopoverContentSize:(NSSize)size; - (IBAction)showHelp:(id)sender; @end diff --git a/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.m b/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.m index 28ffc11a..b770a714 100644 --- a/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.m +++ b/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.m @@ -2,13 +2,10 @@ #import #import -#import #import #import "DevViewController.h" -#import "WindowNavigator.h" #import "Expo_Orbit-Swift.h" -#import "DragDropStatusItemView.h" @implementation AppDelegate @@ -25,27 +22,14 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification { - (void)loadReactNativeWindow:(NSDictionary *)launchOptions { - statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - DragDropStatusItemView *dragDropView = [[DragDropStatusItemView alloc] initWithFrame:NSMakeRect(0, 0, 22, 22)]; - dragDropView.openPopoverAction = ^{ - [self openPopover]; - }; - [statusItem.button addSubview:dragDropView]; - [statusItem.button setTarget:self]; - [statusItem.button sendActionOn:NSEventMaskRightMouseUp | NSEventMaskLeftMouseUp]; - [statusItem.button setAction:@selector(onPressStatusItem:)]; - RCTPlatformView *rootView = [self.rootViewFactory viewWithModuleName:self.moduleName initialProperties:self.initialProps launchOptions:launchOptions]; NSViewController *rootViewController = [[NSViewController alloc] init]; rootViewController.view = rootView; - popover = [[NSPopover alloc] init]; - popover.contentSize = NSMakeSize(380, 450); - popover.contentViewController = rootViewController; - popover.behavior = NSPopoverBehaviorTransient; - [self addPopoverObservers]; + popoverManager = [PopoverManager initializeSharedWithDelegate:self]; + [popoverManager setContentViewController:rootViewController]; #ifdef SHOW_DEV_WINDOW #if RCT_DEV @@ -72,92 +56,11 @@ - (void)customizeRootView:(RCTUIView *)rootView - (BOOL)application:(NSApplication *)_ openFile:(NSString *)filename { - [self openPopover]; - + [popoverManager openPopover]; [NSNotificationCenter.defaultCenter postNotificationName:@"ExpoOrbit_OnOpenFile" object:filename]; return YES; } -- (NSMenu *)createContextMenu { - NSMenu *menu = [[NSMenu alloc] initWithTitle:@"My Menu"]; - - [menu addItemWithTitle:@"Settings..." action:@selector(settingsAction:) keyEquivalent:@""]; - [menu addItemWithTitle:@"Quit" action:@selector(quitAction:) keyEquivalent:@"q"]; - - return menu; -} - - -- (void)quitAction:(id)sender { - exit(0); -} - - -- (void)settingsAction:(id)sender { - WindowNavigator *windowNavigator = [WindowNavigator shared]; - [windowNavigator openWindow:@"Settings" options:@{ - @"windowStyle": @{ - @"titlebarAppearsTransparent": @YES, - @"height": @(580.0), - @"width": @(500.0) - } - }]; -} - -- (void)openPopover { - [popover showRelativeToRect:statusItem.button.bounds - ofView:statusItem.button - preferredEdge:NSMinYEdge]; - [popover.contentViewController.view.window makeKeyWindow]; - [self.bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" - args:@[@"popoverFocused", @{ - @"screenSize": @{ - @"height": @([[NSScreen mainScreen] frame].size.height), - @"width": @([[NSScreen mainScreen] frame].size.width) - } - }]]; -} - -- (void)closePopover { - [popover close]; -} - -- (void)setPopoverContentSize:(NSSize)size { - [popover setContentSize:size]; - [popover.contentViewController.view setFrameSize:size]; -} - -- (void)addPopoverObservers { - NSNotificationCenter *notificationCenter = NSNotificationCenter.defaultCenter; - __weak typeof(self) weakSelf = self; - - [notificationCenter addObserverForName:@"ExpoOrbit_OpenPopover" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) { - [weakSelf openPopover]; - }]; - [notificationCenter addObserverForName:@"ExpoOrbit_ClosePopover" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) { - [weakSelf closePopover]; - }]; -} - -- (void)onPressStatusItem:(id)sender { - NSEvent *event = [NSApp currentEvent]; - if (event.type == NSEventTypeRightMouseUp) { - NSMenu *contextMenu = [self createContextMenu]; - [statusItem popUpStatusItemMenu:contextMenu]; - return; - } - - if (popover.isShown) { - [self closePopover]; - } else { - [self openPopover]; - } -} - -- (void)applicationWillTerminate:(NSNotification *)aNotification { - // Insert code here to tear down your application -} - - (void)applicationWillFinishLaunching:(NSNotification *)__unused aNotification { [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; @@ -171,7 +74,7 @@ - (void)applicationWillFinishLaunching:(NSNotification *)__unused aNotification // Called when the user tries to reopen the app from the Dock or Spotlight - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)visibleWindows { if (!visibleWindows) { - [self openPopover]; + [popoverManager openPopover]; } return YES; @@ -179,13 +82,10 @@ - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows: - (void)getUrlEventHandler:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { - [self openPopover]; + [popoverManager openPopover]; [RCTLinkingManager getUrlEventHandler:event withReplyEvent:replyEvent]; } -- (NSPopover *)popover { - return popover; -} #pragma mark - RCTBridgeDelegate Methods diff --git a/apps/menu-bar/macos/ExpoMenuBar-macOS/AutoResizerRootView.m b/apps/menu-bar/macos/ExpoMenuBar-macOS/AutoResizerRootView.m index b38b04d5..958f82a7 100644 --- a/apps/menu-bar/macos/ExpoMenuBar-macOS/AutoResizerRootView.m +++ b/apps/menu-bar/macos/ExpoMenuBar-macOS/AutoResizerRootView.m @@ -1,6 +1,5 @@ #import "AutoResizerRootView.h" -#import "AppDelegate.h" - +#import "Expo_Orbit-Swift.h" const CGFloat minimumViewSize = 40.0; @@ -30,8 +29,7 @@ - (void)layout CGFloat newHeight = frameHeight <= maxHeight ? frameHeight : maxHeight; dispatch_async(dispatch_get_main_queue(), ^{ - AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; - [appDelegate setPopoverContentSize:CGSizeMake(self.frame.size.width, newHeight)]; + [PopoverManager.shared setPopoverContentSize:CGSizeMake(self.frame.size.width, newHeight)]; }); } diff --git a/apps/menu-bar/macos/ExpoMenuBar-macOS/ExpoMenuBar-macOS-Bridging-Header.h b/apps/menu-bar/macos/ExpoMenuBar-macOS/ExpoMenuBar-macOS-Bridging-Header.h index 1b2cb5d6..55e20085 100644 --- a/apps/menu-bar/macos/ExpoMenuBar-macOS/ExpoMenuBar-macOS-Bridging-Header.h +++ b/apps/menu-bar/macos/ExpoMenuBar-macOS/ExpoMenuBar-macOS-Bridging-Header.h @@ -1,4 +1,2 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// - +#import "DragDropStatusItemView.h" +#import "WindowNavigator.h" diff --git a/apps/menu-bar/macos/ExpoMenuBar-macOS/PopoverManager.swift b/apps/menu-bar/macos/ExpoMenuBar-macOS/PopoverManager.swift new file mode 100644 index 00000000..6e7835ac --- /dev/null +++ b/apps/menu-bar/macos/ExpoMenuBar-macOS/PopoverManager.swift @@ -0,0 +1,145 @@ +import Cocoa +import React_RCTAppDelegate + +class PopoverManager: NSObject { + @objc public static private(set) var shared: PopoverManager! + + @objc public var delegate: RCTAppDelegate + private var statusItem: NSStatusItem! + @objc var popover: NSPopover! + + @objc public static func initializeShared(delegate: RCTAppDelegate) -> PopoverManager { + if shared == nil { + shared = PopoverManager(delegate: delegate) + } + return shared + } + + init(delegate: RCTAppDelegate) { + self.delegate = delegate + super.init() + + self.setupPopover() + self.setupStatusItem() + } + + private func setupStatusItem() { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) + + let dragDropView = DragDropStatusItemView(frame: NSRect(x: 0, y: 0, width: 22, height: 22))! + dragDropView.openPopoverAction = { [weak self] in + self?.openPopover() + } + + let button = statusItem.button! + button.addSubview(dragDropView) + button.target = self + button.sendAction(on: .init([.leftMouseUp, .rightMouseUp])) + button.action = #selector(handleStatusItemClick(_:)) + } + + private func setupPopover() { + popover = NSPopover() + popover.contentSize = NSSize(width: 380, height: 450) + popover.behavior = .transient + setupObservers() + } + + private func setupObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(openPopover), + name: Notification.Name("ExpoOrbit_OpenPopover"), + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(closePopover), + name: Notification.Name("ExpoOrbit_ClosePopover"), + object: nil + ) + } + + // MARK: - Event Handling + + @objc private func handleStatusItemClick(_ sender: Any?) { + guard let event = NSApp.currentEvent else { return } + + if event.type == .rightMouseUp { + showContextMenu() + } else { + popover.isShown ? closePopover() : openPopover() + } + } + + private func showContextMenu() { + let menu = NSMenu() + + let settingsItem = NSMenuItem( + title: "Settings...", + action: #selector(settingsAction), + keyEquivalent: "" + ) + settingsItem.target = self + + let quitItem = NSMenuItem( + title: "Quit", + action: #selector(quitAction(_:)), + keyEquivalent: "q" + ) + quitItem.target = self + + menu.addItem(settingsItem) + menu.addItem(quitItem) + statusItem.popUpMenu(menu) + } + + // MARK: - Actions + + @objc private func settingsAction() { + WindowNavigator.shared().openWindow( + "Settings", + options: [ + "windowStyle": ["titlebarAppearsTransparent": true, "height": 580.0, "width": 500.0] + ]) + } + + @objc private func quitAction(_ sender: Any?) { + NSApp.terminate(nil) + } + + // MARK: - Public Interface + + @objc func setContentViewController(_ viewController: NSViewController) { + popover.contentViewController = viewController + } + + // MARK: - Popover Management + @objc func openPopover() { + guard let button = statusItem.button else { return } + + popover.show( + relativeTo: button.bounds, + of: button, + preferredEdge: .minY) + popover.contentViewController?.view.window?.makeKey() + + let screenSize: [String: Any] = [ + "height": NSScreen.main?.frame.height ?? 0, + "width": NSScreen.main?.frame.width ?? 0 + ] + + delegate.bridge?.enqueueJSCall( + "RCTDeviceEventEmitter.emit", args: ["popoverFocused", ["screenSize": screenSize]]) + } + + @objc func closePopover() { + popover.close() + } + + @objc func setPopoverContentSize(_ size: NSSize) { + popover.contentSize = size + popover.contentViewController?.view.frame.size = size + } +} diff --git a/apps/menu-bar/macos/ExpoMenuBar.xcodeproj/project.pbxproj b/apps/menu-bar/macos/ExpoMenuBar.xcodeproj/project.pbxproj index deabfd5e..1c0ca1b4 100644 --- a/apps/menu-bar/macos/ExpoMenuBar.xcodeproj/project.pbxproj +++ b/apps/menu-bar/macos/ExpoMenuBar.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ C08E65342A5D04910079E3A9 /* WindowNavigator.m in Sources */ = {isa = PBXBuildFile; fileRef = C08E65332A5D04910079E3A9 /* WindowNavigator.m */; }; C0B36EA22A65E25A004F2D8C /* Checkbox.m in Sources */ = {isa = PBXBuildFile; fileRef = C0B36E9F2A65E25A004F2D8C /* Checkbox.m */; }; C0B36EA32A65E25A004F2D8C /* CheckboxManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C0B36EA12A65E25A004F2D8C /* CheckboxManager.m */; }; + C0D4407A2DDB9B9E006B7C16 /* PopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D440792DDB9B9E006B7C16 /* PopoverManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -100,6 +101,7 @@ C0B36E9F2A65E25A004F2D8C /* Checkbox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Checkbox.m; sourceTree = ""; }; C0B36EA02A65E25A004F2D8C /* CheckboxManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CheckboxManager.h; sourceTree = ""; }; C0B36EA12A65E25A004F2D8C /* CheckboxManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CheckboxManager.m; sourceTree = ""; }; + C0D440792DDB9B9E006B7C16 /* PopoverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverManager.swift; sourceTree = ""; }; CE0E74F571E1F48FF00A8324 /* Pods-Shared-ExpoMenuBar-macOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Shared-ExpoMenuBar-macOS.release.xcconfig"; path = "Target Support Files/Pods-Shared-ExpoMenuBar-macOS/Pods-Shared-ExpoMenuBar-macOS.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; @@ -196,6 +198,7 @@ C06B8F8F2AAB77D0009F2BB5 /* DragDropStatusItemView.m */, C06B8F912AAB77EC009F2BB5 /* DragDropStatusItemView.h */, 6DF0AD98E1DDDB5E7C991DB8 /* PrivacyInfo.xcprivacy */, + C0D440792DDB9B9E006B7C16 /* PopoverManager.swift */, ); path = "ExpoMenuBar-macOS"; sourceTree = ""; @@ -434,10 +437,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ExpoMenuBar-macOS/Pods-ExpoMenuBar-macOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ExpoMenuBar-macOS/Pods-ExpoMenuBar-macOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoMenuBar-macOS/Pods-ExpoMenuBar-macOS-frameworks.sh\"\n"; @@ -489,10 +496,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ExpoMenuBar-macOS/Pods-ExpoMenuBar-macOS-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ExpoMenuBar-macOS/Pods-ExpoMenuBar-macOS-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoMenuBar-macOS/Pods-ExpoMenuBar-macOS-resources.sh\"\n"; @@ -506,6 +517,7 @@ buildActionMask = 2147483647; files = ( C0B36EA22A65E25A004F2D8C /* Checkbox.m in Sources */, + C0D4407A2DDB9B9E006B7C16 /* PopoverManager.swift in Sources */, C061C7A82A26DD9A00A53D8D /* SystemIconViewManager.m in Sources */, C0523ED02A55980D003371AF /* WindowWithDeallocCallback.m in Sources */, C06B8F902AAB77D0009F2BB5 /* DragDropStatusItemView.m in Sources */, @@ -564,10 +576,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - OTHER_CFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_CFLAGS = "$(inherited) "; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -605,10 +614,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - OTHER_CFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_CFLAGS = "$(inherited) "; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", From 838107ea9bc7cdc9fe996389528d41fe118ce177 Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Date: Tue, 20 May 2025 09:04:02 -0300 Subject: [PATCH 2/2] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7712ae22..fad64e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Upgrade to `expo` SDK 53 and react-native `0.79`. ([#254](https://github.com/expo/orbit/pull/254) by [@gabrieldonadel](https://github.com/gabrieldonadel)) - Update ESLint, `eslint-config-universe` and setups, bump TypeScript versions. ([#261](https://github.com/expo/orbit/pull/261) by [@Simek](https://github.com/Simek)) - Add macOS build script. ([#262](https://github.com/expo/orbit/pull/262) by [@gabrieldonadel](https://github.com/gabrieldonadel)) +- Simplify `AppDelegate.m` logic in to align with Expo's template. ([#263](https://github.com/expo/orbit/pull/263) by [@gabrieldonadel](https://github.com/gabrieldonadel)) ## 2.0.3 — 2025-05-16