From ff0b9a887340a7b2c90a5fc67d80f09753dcb0c0 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:02:29 +0000 Subject: [PATCH] fix(macos): convert `Main.storyboard` using programmatic AppKit APIs --- .../app/ios/ReactTestApp/AppDelegate.swift | 3 +- .../macos/Localizations/en.lproj/Main.strings | 17 - .../ReactTestApp.xcodeproj/project.pbxproj | 22 +- .../app/macos/ReactTestApp/AppDelegate.swift | 44 +- .../ReactTestApp/Base.lproj/Main.storyboard | 487 ------------------ packages/app/macos/ReactTestApp/Info.plist | 2 - .../macos/ReactTestApp/WindowController.swift | 324 ++++++++++++ packages/app/test/ios/infoPlist.test.mts | 8 - packages/app/test/ios/localizations.test.mts | 13 +- packages/app/test/pack.test.mts | 3 +- 10 files changed, 372 insertions(+), 551 deletions(-) delete mode 100644 packages/app/macos/Localizations/en.lproj/Main.strings delete mode 100644 packages/app/macos/ReactTestApp/Base.lproj/Main.storyboard create mode 100644 packages/app/macos/ReactTestApp/WindowController.swift diff --git a/packages/app/ios/ReactTestApp/AppDelegate.swift b/packages/app/ios/ReactTestApp/AppDelegate.swift index c64406e1f..40b66bc00 100644 --- a/packages/app/ios/ReactTestApp/AppDelegate.swift +++ b/packages/app/ios/ReactTestApp/AppDelegate.swift @@ -4,7 +4,8 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { private weak var application: UIApplication? - @objc var window: UIWindow? { + @objc + var window: UIWindow? { get { // Copy the implementation of RCTKeyWindow() as it changes a lot upstream if let connectedScenes = application?.connectedScenes { diff --git a/packages/app/macos/Localizations/en.lproj/Main.strings b/packages/app/macos/Localizations/en.lproj/Main.strings deleted file mode 100644 index 26d593d13..000000000 --- a/packages/app/macos/Localizations/en.lproj/Main.strings +++ /dev/null @@ -1,17 +0,0 @@ -/* Class = "NSMenuItem"; title = "ReactTestApp"; ObjectID = "1Xt-HY-uBw"; */ -"1Xt-HY-uBw.title" = "ReactTestApp"; - -/* Class = "NSMenuItem"; title = "Quit ReactTestApp"; ObjectID = "4sb-4s-VLi"; */ -"4sb-4s-VLi.title" = "Quit ReactTestApp"; - -/* Class = "NSMenuItem"; title = "About ReactTestApp"; ObjectID = "5kV-Vb-QxS"; */ -"5kV-Vb-QxS.title" = "About ReactTestApp"; - -/* Class = "NSMenuItem"; title = "ReactTestApp Help"; ObjectID = "FKE-Sm-Kum"; */ -"FKE-Sm-Kum.title" = "ReactTestApp Help"; - -/* Class = "NSMenuItem"; title = "Hide ReactTestApp"; ObjectID = "Olw-nP-bQN"; */ -"Olw-nP-bQN.title" = "Hide ReactTestApp"; - -/* Class = "NSMenu"; title = "ReactTestApp"; ObjectID = "uQy-DD-JDr"; */ -"uQy-DD-JDr.title" = "ReactTestApp"; diff --git a/packages/app/macos/ReactTestApp.xcodeproj/project.pbxproj b/packages/app/macos/ReactTestApp.xcodeproj/project.pbxproj index b7eb3b8a4..0ec2a5a99 100644 --- a/packages/app/macos/ReactTestApp.xcodeproj/project.pbxproj +++ b/packages/app/macos/ReactTestApp.xcodeproj/project.pbxproj @@ -11,12 +11,12 @@ 193EF063247A736200BE8C79 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF062247A736200BE8C79 /* AppDelegate.swift */; }; 193EF065247A736200BE8C79 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF064247A736200BE8C79 /* ViewController.swift */; }; 193EF067247A736300BE8C79 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 193EF066247A736300BE8C79 /* Assets.xcassets */; }; - 193EF06A247A736300BE8C79 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 193EF068247A736300BE8C79 /* Main.storyboard */; }; 193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF08E247A799D00BE8C79 /* Manifest.swift */; }; 193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */; }; 193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF097247B130700BE8C79 /* ReactInstance.swift */; }; 1960F339258C97C400AEC7A2 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1960F338258C97C400AEC7A2 /* Session.swift */; }; 196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22632490CBAB00449D3C /* React+Compatibility.m */; }; + 199F3E152FF7AA0000270357 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199F3E142FF7A9FB00270357 /* WindowController.swift */; }; 19C4C89327710D8500157870 /* Manifest+Embedded.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C4C89227710D8500157870 /* Manifest+Embedded.g.swift */; }; 19E791C024B08E1400FA6468 /* ReactTestAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E791BF24B08E1400FA6468 /* ReactTestAppTests.swift */; }; 19E791C324B08E4D00FA6468 /* ReactTestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E791C224B08E4D00FA6468 /* ReactTestAppUITests.swift */; }; @@ -45,7 +45,6 @@ 193EF062247A736200BE8C79 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 193EF064247A736200BE8C79 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 193EF066247A736300BE8C79 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = SOURCE_ROOT; }; - 193EF069247A736300BE8C79 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 193EF06B247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 193EF06C247A736300BE8C79 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = SOURCE_ROOT; }; 193EF071247A736300BE8C79 /* ReactTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -60,11 +59,11 @@ 1960F338258C97C400AEC7A2 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Session.swift; path = ../Shared/Session.swift; sourceTree = ""; }; 196C22632490CBAB00449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "React+Compatibility.m"; path = "../Shared/React+Compatibility.m"; sourceTree = ""; }; 196C22642490CBAB00449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "React+Compatibility.h"; path = "../Shared/React+Compatibility.h"; sourceTree = ""; }; + 199F3E142FF7A9FB00270357 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; 19B368BC24B12C24002CCEFF /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = ""; }; 19B368BD24B12C24002CCEFF /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = ""; }; 19B368BE24B12C24002CCEFF /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = ""; }; 19C4C89227710D8500157870 /* Manifest+Embedded.g.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Manifest+Embedded.g.swift"; sourceTree = SOURCE_ROOT; }; - 19E0B90C27E9F8FD006FD558 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = ../Localizations/en.lproj/Main.strings; sourceTree = ""; }; 19E791BF24B08E1400FA6468 /* ReactTestAppTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppTests.swift; sourceTree = ""; }; 19E791C224B08E4D00FA6468 /* ReactTestAppUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppUITests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -123,6 +122,7 @@ 193EF097247B130700BE8C79 /* ReactInstance.swift */, 1960F338258C97C400AEC7A2 /* Session.swift */, 193EF064247A736200BE8C79 /* ViewController.swift */, + 199F3E142FF7A9FB00270357 /* WindowController.swift */, 193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */, 196C22642490CBAB00449D3C /* React+Compatibility.h */, 196C22632490CBAB00449D3C /* React+Compatibility.m */, @@ -130,7 +130,6 @@ 193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */, 193EF06C247A736300BE8C79 /* App.entitlements */, 193EF066247A736300BE8C79 /* Assets.xcassets */, - 193EF068247A736300BE8C79 /* Main.storyboard */, 193EF06B247A736300BE8C79 /* Info.plist */, 191A67872BDFD9E90094F246 /* PrivacyInfo.xcprivacy */, 19B368BC24B12C24002CCEFF /* ReactTestApp.common.xcconfig */, @@ -248,7 +247,6 @@ hasScannedForEncodings = 0; knownRegions = ( en, - Base, ); mainGroup = 193EF056247A736100BE8C79; productRefGroup = 193EF060247A736200BE8C79 /* Products */; @@ -269,7 +267,6 @@ files = ( 191A67882BDFD9E90094F246 /* PrivacyInfo.xcprivacy in Resources */, 193EF067247A736300BE8C79 /* Assets.xcassets in Resources */, - 193EF06A247A736300BE8C79 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -321,6 +318,7 @@ 193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */, 19C4C89327710D8500157870 /* Manifest+Embedded.g.swift in Sources */, 196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */, + 199F3E152FF7AA0000270357 /* WindowController.swift in Sources */, 193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */, 1960F339258C97C400AEC7A2 /* Session.swift in Sources */, 193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */, @@ -359,18 +357,6 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - 193EF068247A736300BE8C79 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 193EF069247A736300BE8C79 /* Base */, - 19E0B90C27E9F8FD006FD558 /* en */, - ); - name = Main.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ 193EF083247A736300BE8C79 /* Debug */ = { isa = XCBuildConfiguration; diff --git a/packages/app/macos/ReactTestApp/AppDelegate.swift b/packages/app/macos/ReactTestApp/AppDelegate.swift index 3242fb608..8830425a5 100644 --- a/packages/app/macos/ReactTestApp/AppDelegate.swift +++ b/packages/app/macos/ReactTestApp/AppDelegate.swift @@ -3,17 +3,20 @@ import ReactTestApp_DevSupport @main final class AppDelegate: NSObject, NSApplicationDelegate { - @IBOutlet var reactMenu: NSMenu! - @IBOutlet var rememberLastComponentMenuItem: NSMenuItem! + static func main() { + let delegate = AppDelegate() + let app = NSApplication.shared + app.delegate = delegate + app.run() + } + + var reactMenu: NSMenu! + var rememberLastComponentMenuItem: NSMenuItem! private(set) lazy var reactInstance = ReactInstance() - private lazy var mainWindow: NSWindow? = { - // `keyWindow` might be `nil` while loading or when the window is not - // active. Use `identifier` to find our main window. - let windows = NSApplication.shared.windows - return windows.first { $0.identifier?.rawValue == "MainWindow" } - }() + private lazy var windowController = WindowController() + private lazy var mainWindow = windowController.window private var contentDidAppearToken: NSObjectProtocol? @@ -21,7 +24,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate { true } + /// Builds the main menu and main window programmatically. This replaces + /// what used to be provided by `Main.storyboard`, and must run before the + /// rest of `applicationWillFinishLaunching(_:)` since it relies on + /// `reactMenu`/`rememberLastComponentMenuItem` and `mainWindow` being set. + private func initWindow() { + guard windowController.window != nil else { + fatalError("Failed to create window") + } + + NSApp.mainMenu = makeMainMenu(title: Manifest.load().displayName) + } + func applicationDidFinishLaunching(_: Notification) { + windowController.showWindow(nil) + NSApp.activate() + NotificationCenter.default.post( name: .ReactAppDidFinishLaunching, object: nil @@ -58,17 +76,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // MARK: User interaction - @IBAction + @objc func onLoadEmbeddedBundleSelected(_: NSMenuItem) { reactInstance.remoteBundleURL = nil } - @IBAction + @objc func onLoadFromDevServerSelected(_: NSMenuItem) { reactInstance.remoteBundleURL = ReactInstance.jsBundleURL() } - @IBAction + @objc func onRememberLastComponentSelected(_ menuItem: NSMenuItem) { onRememberLastComponentSelectedInternal(menuItem) } @@ -149,6 +167,8 @@ extension AppDelegate { } func applicationWillFinishLaunching(_: Notification) { + initWindow() + // applicationWillFinishLaunching(_:) [ENABLE_SINGLE_APP_MODE=0] if Session.shouldRememberLastComponent { @@ -280,6 +300,8 @@ extension AppDelegate { func initialize() {} func applicationWillFinishLaunching(_: Notification) { + initWindow() + guard let window = mainWindow else { assertionFailure("Main window should have been instantiated by now") return diff --git a/packages/app/macos/ReactTestApp/Base.lproj/Main.storyboard b/packages/app/macos/ReactTestApp/Base.lproj/Main.storyboard deleted file mode 100644 index fbf60425c..000000000 --- a/packages/app/macos/ReactTestApp/Base.lproj/Main.storyboard +++ /dev/null @@ -1,487 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/app/macos/ReactTestApp/Info.plist b/packages/app/macos/ReactTestApp/Info.plist index 54609f963..47e3851be 100644 --- a/packages/app/macos/ReactTestApp/Info.plist +++ b/packages/app/macos/ReactTestApp/Info.plist @@ -31,8 +31,6 @@ NSHumanReadableCopyright Copyright © 2020 Microsoft. All rights reserved. - NSMainStoryboardFile - Main NSPrincipalClass NSApplication NSSupportsAutomaticTermination diff --git a/packages/app/macos/ReactTestApp/WindowController.swift b/packages/app/macos/ReactTestApp/WindowController.swift new file mode 100644 index 000000000..1944d6c09 --- /dev/null +++ b/packages/app/macos/ReactTestApp/WindowController.swift @@ -0,0 +1,324 @@ +import AppKit + +final class WindowController: NSWindowController { + private static let identifier = "MainWindow" + + init() { + let viewController = ViewController() + let window = NSWindow(contentViewController: viewController) + window.identifier = NSUserInterfaceItemIdentifier(WindowController.identifier) + window.setFrameAutosaveName(WindowController.identifier) + + super.init(window: window) + + windowFrameAutosaveName = WindowController.identifier + contentViewController = viewController + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension NSMenu { + convenience init(title: String, items: [NSMenuItem]) { + self.init(title: title) + self.items = items + } +} + +private extension NSMenuItem { + convenience init(title: String, + action: Selector? = nil, + target: AnyObject? = nil, + keyEquivalent: String = "", + keyEquivalentModifierMask: NSEvent.ModifierFlags = [.command], + tag: Int? = nil) + { + self.init(title: title, action: action, keyEquivalent: keyEquivalent) + self.target = target + if !keyEquivalent.isEmpty { + self.keyEquivalentModifierMask = keyEquivalentModifierMask + } + if let tag { + self.tag = tag + } + } + + convenience init(submenu: String, items: [NSMenuItem]) { + self.init(title: submenu) + self.submenu = NSMenu(title: submenu, items: items) + } + + convenience init(submenu: NSMenu) { + self.init(title: submenu.title) + self.submenu = submenu + } +} + +extension AppDelegate { + func makeMainMenu(title: String) -> NSMenu { + NSMenu(title: "Main Menu", items: [ + makeAppMenuItem(title: title), + makeFileMenuItem(), + makeEditMenuItem(), + makeViewMenuItem(), + makeReactMenuItem(), + makeWindowMenuItem(), + makeHelpMenuItem(title: title), + ]) + } + + private func makeAppMenuItem(title: String) -> NSMenuItem { + let servicesMenu = NSMenu(title: "Services") + NSApp.servicesMenu = servicesMenu + + return NSMenuItem(submenu: title, items: [ + NSMenuItem(title: "About \(title)", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:))), + .separator(), + NSMenuItem(title: "Preferences…", keyEquivalent: ","), + .separator(), + NSMenuItem(submenu: servicesMenu), + .separator(), + NSMenuItem(title: "Hide \(title)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h"), + NSMenuItem( + title: "Hide Others", + action: #selector(NSApplication.hideOtherApplications(_:)), + keyEquivalent: "h", + keyEquivalentModifierMask: [.option, .command] + ), + NSMenuItem(title: "Show All", action: #selector(NSApplication.unhideAllApplications(_:))), + .separator(), + NSMenuItem(title: "Quit \(title)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"), + ]) + } + + private func makeFileMenuItem() -> NSMenuItem { + NSMenuItem(submenu: "File", items: [ + NSMenuItem(title: "New", action: #selector(NSDocumentController.newDocument(_:)), keyEquivalent: "n"), + NSMenuItem(title: "Open…", action: #selector(NSDocumentController.openDocument(_:)), keyEquivalent: "o"), + NSMenuItem(submenu: "Open Recent", items: [ + NSMenuItem(title: "Clear Menu", action: #selector(NSDocumentController.clearRecentDocuments(_:))), + ]), + .separator(), + NSMenuItem(title: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w"), + NSMenuItem(title: "Save…", action: #selector(NSDocument.save(_:)), keyEquivalent: "s"), + NSMenuItem(title: "Save As…", action: #selector(NSDocument.saveAs(_:)), keyEquivalent: "S"), + NSMenuItem(title: "Revert to Saved", action: #selector(NSDocument.revertToSaved(_:)), keyEquivalent: "r"), + .separator(), + NSMenuItem( + title: "Page Setup…", + action: #selector(NSDocument.runPageLayout(_:)), + keyEquivalent: "P", + keyEquivalentModifierMask: [.shift, .command] + ), + NSMenuItem(title: "Print…", action: #selector(NSView.printView(_:)), keyEquivalent: "p"), + ]) + } + + private func makeEditMenuItem() -> NSMenuItem { + NSMenuItem(submenu: "Edit", items: [ + NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z"), + NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "Z"), + .separator(), + NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"), + NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"), + NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"), + NSMenuItem( + title: "Paste and Match Style", + action: #selector(NSTextView.pasteAsPlainText(_:)), + keyEquivalent: "V", + keyEquivalentModifierMask: [.option, .command] + ), + NSMenuItem(title: "Delete", action: #selector(NSText.delete(_:))), + NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"), + .separator(), + NSMenuItem(submenu: "Find", items: [ + NSMenuItem( + title: "Find…", + action: #selector(NSTextView.performFindPanelAction(_:)), + keyEquivalent: "f", + tag: 1 + ), + NSMenuItem( + title: "Find and Replace…", + action: #selector(NSTextView.performFindPanelAction(_:)), + keyEquivalent: "f", + keyEquivalentModifierMask: [.option, .command], + tag: 12 + ), + NSMenuItem( + title: "Find Next", + action: #selector(NSTextView.performFindPanelAction(_:)), + keyEquivalent: "g", + tag: 2 + ), + NSMenuItem( + title: "Find Previous", + action: #selector(NSTextView.performFindPanelAction(_:)), + keyEquivalent: "G", + tag: 3 + ), + NSMenuItem( + title: "Use Selection for Find", + action: #selector(NSTextView.performFindPanelAction(_:)), + keyEquivalent: "e", + tag: 7 + ), + NSMenuItem( + title: "Jump to Selection", + action: #selector(NSTextView.centerSelectionInVisibleArea(_:)), + keyEquivalent: "j" + ), + ]), + NSMenuItem(submenu: "Spelling and Grammar", items: [ + NSMenuItem( + title: "Show Spelling and Grammar", + action: #selector(NSText.showGuessPanel(_:)), + keyEquivalent: ":" + ), + NSMenuItem( + title: "Check Document Now", + action: #selector(NSText.checkSpelling(_:)), + keyEquivalent: ";" + ), + .separator(), + NSMenuItem( + title: "Check Spelling While Typing", + action: #selector(NSTextView.toggleContinuousSpellChecking(_:)) + ), + NSMenuItem( + title: "Check Grammar With Spelling", + action: #selector(NSTextView.toggleGrammarChecking(_:)) + ), + NSMenuItem( + title: "Correct Spelling Automatically", + action: #selector(NSTextView.toggleAutomaticSpellingCorrection(_:)) + ), + ]), + NSMenuItem(submenu: "Substitutions", items: [ + NSMenuItem( + title: "Show Substitutions", + action: #selector(NSTextView.orderFrontSubstitutionsPanel(_:)) + ), + .separator(), + NSMenuItem( + title: "Smart Copy/Paste", + action: #selector(NSTextView.toggleSmartInsertDelete(_:)) + ), + NSMenuItem( + title: "Smart Quotes", + action: #selector(NSTextView.toggleAutomaticQuoteSubstitution(_:)) + ), + NSMenuItem( + title: "Smart Dashes", + action: #selector(NSTextView.toggleAutomaticDashSubstitution(_:)) + ), + NSMenuItem( + title: "Smart Links", + action: #selector(NSTextView.toggleAutomaticLinkDetection(_:)) + ), + NSMenuItem( + title: "Data Detectors", + action: #selector(NSTextView.toggleAutomaticDataDetection(_:)) + ), + NSMenuItem( + title: "Text Replacement", + action: #selector(NSTextView.toggleAutomaticTextReplacement(_:)) + ), + ]), + NSMenuItem(submenu: "Transformations", items: [ + NSMenuItem(title: "Make Upper Case", action: #selector(NSTextView.uppercaseWord(_:))), + NSMenuItem(title: "Make Lower Case", action: #selector(NSTextView.lowercaseWord(_:))), + NSMenuItem(title: "Capitalize", action: #selector(NSTextView.capitalizeWord(_:))), + ]), + NSMenuItem(submenu: "Speech", items: [ + NSMenuItem(title: "Start Speaking", action: #selector(NSTextView.startSpeaking(_:))), + NSMenuItem(title: "Stop Speaking", action: #selector(NSTextView.stopSpeaking(_:))), + ]), + ]) + } + + private func makeViewMenuItem() -> NSMenuItem { + NSMenuItem(submenu: "View", items: [ + NSMenuItem( + title: "Show Toolbar", + action: #selector(NSWindow.toggleToolbarShown(_:)), + keyEquivalent: "t", + keyEquivalentModifierMask: [.option, .command] + ), + NSMenuItem( + title: "Customize Toolbar…", + action: #selector(NSWindow.runToolbarCustomizationPalette(_:)) + ), + .separator(), + NSMenuItem( + title: "Show Sidebar", + action: #selector(NSSplitViewController.toggleSidebar(_:)), + keyEquivalent: "s", + keyEquivalentModifierMask: [.control, .command] + ), + NSMenuItem( + title: "Enter Full Screen", + action: #selector(NSWindow.toggleFullScreen(_:)), + keyEquivalent: "f", + keyEquivalentModifierMask: [.control, .command] + ), + ]) + } + + private func makeReactMenuItem() -> NSMenuItem { + let rememberLastComponentMenuItem = NSMenuItem( + title: "Remember Last Opened Component", + action: #selector(onRememberLastComponentSelected(_:)), + target: self + ) + rememberLastComponentMenuItem.isEnabled = false + self.rememberLastComponentMenuItem = rememberLastComponentMenuItem + + let reactMenu = NSMenu(title: "React", items: [ + NSMenuItem( + title: "Load Embedded JS Bundle", + action: #selector(onLoadEmbeddedBundleSelected(_:)), + target: self + ), + NSMenuItem( + title: "Load From Dev Server", + action: #selector(onLoadFromDevServerSelected(_:)), + target: self + ), + rememberLastComponentMenuItem, + .separator(), + ]) + reactMenu.autoenablesItems = false + self.reactMenu = reactMenu + + let reactMenuItem = NSMenuItem(submenu: reactMenu) + reactMenuItem.isHidden = true + return reactMenuItem + } + + private func makeWindowMenuItem() -> NSMenuItem { + let windowMenu = NSMenu(title: "Window", items: [ + NSMenuItem(title: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m"), + NSMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:))), + .separator(), + NSMenuItem(title: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:))), + ]) + + NSApp.windowsMenu = windowMenu + + return NSMenuItem(submenu: windowMenu) + } + + private func makeHelpMenuItem(title: String) -> NSMenuItem { + let helpMenu = NSMenu(title: "Help", items: [ + NSMenuItem(title: "\(title) Help", action: #selector(NSApplication.showHelp(_:)), keyEquivalent: "?"), + ]) + + NSApp.helpMenu = helpMenu + + return NSMenuItem(submenu: helpMenu) + } +} diff --git a/packages/app/test/ios/infoPlist.test.mts b/packages/app/test/ios/infoPlist.test.mts index 277f67644..9e0d9456e 100644 --- a/packages/app/test/ios/infoPlist.test.mts +++ b/packages/app/test/ios/infoPlist.test.mts @@ -200,8 +200,6 @@ const DEFAULT_INFO_PLIST = { " ", " NSHumanReadableCopyright", " Copyright © 2020 Microsoft. All rights reserved.", - " NSMainStoryboardFile", - " Main", " NSPrincipalClass", " NSApplication", " NSSupportsAutomaticTermination", @@ -428,8 +426,6 @@ const INFO_PLIST_WITH_FONTS = { " ", " NSHumanReadableCopyright", " Copyright © 2020 Microsoft. All rights reserved.", - " NSMainStoryboardFile", - " Main", " NSPrincipalClass", " NSApplication", " NSSupportsAutomaticTermination", @@ -567,8 +563,6 @@ const INFO_PLIST_MACOS = { " ", " NSHumanReadableCopyright", " Copyright © 2020 Microsoft. All rights reserved.", - " NSMainStoryboardFile", - " Main", " NSPrincipalClass", " NSApplication", " NSSupportsAutomaticTermination", @@ -613,8 +607,6 @@ const INFO_PLIST_MACOS = { " ", " NSHumanReadableCopyright", " Copyright © 2025 Contoso. All rights reserved.", - " NSMainStoryboardFile", - " Main", " NSPrincipalClass", " NSApplication", " NSSupportsAutomaticTermination", diff --git a/packages/app/test/ios/localizations.test.mts b/packages/app/test/ios/localizations.test.mts index 96d32fb5b..5276c57be 100644 --- a/packages/app/test/ios/localizations.test.mts +++ b/packages/app/test/ios/localizations.test.mts @@ -1,4 +1,5 @@ import { deepEqual, equal } from "node:assert/strict"; +import * as nodefs from "node:fs"; import * as path from "node:path"; import { afterEach, describe, it } from "node:test"; import { @@ -13,6 +14,10 @@ import { fs, setMockFiles, toJSON } from "../fs.mock.mts"; const macosOnly = { skip: process.platform === "win32" }; describe("generateLocalizations()", macosOnly, () => { + const localizationsDir = projectPath("Localizations", "macos"); + const enStrings = path.join(localizationsDir, "en.lproj", "Main.strings"); + const stringsExist = nodefs.existsSync(enStrings); + function generateLocalizations( appConfig: JSONObject, targetPlatform: ApplePlatform, @@ -33,17 +38,15 @@ describe("generateLocalizations()", macosOnly, () => { }); } - it("[macos] generates localization files", () => { + it("[macos] generates localization files", { skip: !stringsExist }, () => { const name = "ContosoApp"; // Read strings from disk - const localizationsDir = projectPath("Localizations", "macos"); - const english = path.join(localizationsDir, "en.lproj", "Main.strings"); - const strings = readTextFile(english); + const strings = readTextFile(enStrings); setMockFiles({ [`${localizationsDir}/.ignoreme`]: "", - [english]: strings, + [enStrings]: strings, }); generateLocalizations({ name }, "macos", "."); diff --git a/packages/app/test/pack.test.mts b/packages/app/test/pack.test.mts index ef965a60b..55d01c142 100644 --- a/packages/app/test/pack.test.mts +++ b/packages/app/test/pack.test.mts @@ -157,18 +157,17 @@ describe("npm pack", () => { "ios/use_react_native-0.71.rb", "ios/utils.mjs", "ios/xcode.mjs", - "macos/Localizations/en.lproj/Main.strings", "macos/ReactTestApp.xcodeproj/project.pbxproj", "macos/ReactTestApp.xcodeproj/xcshareddata/xcschemes/ReactTestApp.xcscheme", "macos/ReactTestApp/AppDelegate.swift", "macos/ReactTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json", "macos/ReactTestApp/Assets.xcassets/Contents.json", - "macos/ReactTestApp/Base.lproj/Main.storyboard", "macos/ReactTestApp/Info.plist", "macos/ReactTestApp/ReactTestApp.common.xcconfig", "macos/ReactTestApp/ReactTestApp.debug.xcconfig", "macos/ReactTestApp/ReactTestApp.release.xcconfig", "macos/ReactTestApp/ViewController.swift", + "macos/ReactTestApp/WindowController.swift", "macos/ReactTestAppTests/Info.plist", "macos/ReactTestAppTests/ReactTestAppTests.swift", "macos/ReactTestAppUITests/Info.plist",