diff --git a/Apps/Keyty/Sources/Keyty/App/Lifecycle/AppController.swift b/Apps/Keyty/Sources/Keyty/App/Lifecycle/AppController.swift index 4440537..0e9a3be 100644 --- a/Apps/Keyty/Sources/Keyty/App/Lifecycle/AppController.swift +++ b/Apps/Keyty/Sources/Keyty/App/Lifecycle/AppController.swift @@ -35,6 +35,7 @@ final class AppController: NSObject { ) super.init() self.menuController.setAppController(self) + NSApp.mainMenu = self.menuController.makeMainMenu() self.dependencies.captureController.onCapturingChanged = { [weak self] isCapturing in self?.statusItemController.isCapturing = isCapturing } diff --git a/Apps/Keyty/Sources/Keyty/App/Shell/MainMenu.swift b/Apps/Keyty/Sources/Keyty/App/Shell/MainMenu.swift index 4fd2839..60bb341 100644 --- a/Apps/Keyty/Sources/Keyty/App/Shell/MainMenu.swift +++ b/Apps/Keyty/Sources/Keyty/App/Shell/MainMenu.swift @@ -30,15 +30,41 @@ final class MenuController { let menu = NSMenu(title: AppConstants.appName) menu.addItem(shortcutItem) menu.addItem(self.makeSettingsMenuItem()) - let quit = NSMenuItem(title: L10n.MainMenu.quit(AppConstants.appName), action: #selector(AppController.quitApplication(_:)), keyEquivalent: "q") - quit.keyEquivalentModifierMask = [.command] - menu.addItem(self.register(quit)) + menu.addItem(self.makeQuitMenuItem()) return menu } + + func makeMainMenu() -> NSMenu { + let mainMenu = NSMenu(title: AppConstants.appName) + + let appMenuItem = NSMenuItem() + appMenuItem.submenu = self.makeApplicationMenu() + mainMenu.addItem(appMenuItem) + + let fileMenuItem = NSMenuItem() + fileMenuItem.submenu = self.makeFileMenu() + mainMenu.addItem(fileMenuItem) + + return mainMenu + } } // MARK: - Private API private extension MenuController { + private func makeApplicationMenu() -> NSMenu { + let menu = NSMenu(title: AppConstants.appName) + menu.addItem(self.makeQuitMenuItem()) + return menu + } + + private func makeFileMenu() -> NSMenu { + let menu = NSMenu(title: "File") + let close = NSMenuItem(title: "Close Window", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") + close.keyEquivalentModifierMask = [.command] + menu.addItem(close) + return menu + } + private func makeSettingsMenuItem() -> NSMenuItem { let item = NSMenuItem( title: L10n.MainMenu.settings, @@ -54,6 +80,12 @@ private extension MenuController { return self.register(item) } + private func makeQuitMenuItem() -> NSMenuItem { + let item = NSMenuItem(title: L10n.MainMenu.quit(AppConstants.appName), action: #selector(AppController.quitApplication(_:)), keyEquivalent: "q") + item.keyEquivalentModifierMask = [.command] + return self.register(item) + } + private func register(_ item: NSMenuItem) -> NSMenuItem { item.target = self.appController self.controlledMenuItems.append(item) diff --git a/Apps/Keyty/Tests/KeytyTests/App/Shell/MenuControllerTests.swift b/Apps/Keyty/Tests/KeytyTests/App/Shell/MenuControllerTests.swift new file mode 100644 index 0000000..37df76d --- /dev/null +++ b/Apps/Keyty/Tests/KeytyTests/App/Shell/MenuControllerTests.swift @@ -0,0 +1,35 @@ +// +// MenuControllerTests.swift +// KeytyTests +// +// SPDX-FileCopyrightText: 2026 Serhii Bykov +// SPDX-License-Identifier: BSD-3-Clause +// + +import AppKit +import XCTest +@testable import Keyty + +final class MenuControllerTests: XCTestCase { + func testMainMenuIncludesOnlyRequiredCommandQAndCommandWShortcuts() throws { + let controller = MenuController() + + let menu = controller.makeMainMenu() + XCTAssertEqual(menu.items.count, 2) + + let appMenu = try XCTUnwrap(menu.items.first?.submenu) + XCTAssertEqual(appMenu.items.count, 1) + let quitItem = try XCTUnwrap(appMenu.items.first) + XCTAssertEqual(quitItem.keyEquivalent, "q") + XCTAssertEqual(quitItem.keyEquivalentModifierMask, [.command]) + XCTAssertEqual(quitItem.action, #selector(AppController.quitApplication(_:))) + + let fileMenu = try XCTUnwrap(menu.items.dropFirst().first?.submenu) + XCTAssertEqual(fileMenu.items.count, 1) + let closeItem = try XCTUnwrap(fileMenu.items.first) + XCTAssertEqual(closeItem.keyEquivalent, "w") + XCTAssertEqual(closeItem.keyEquivalentModifierMask, [.command]) + XCTAssertEqual(closeItem.action, #selector(NSWindow.performClose(_:))) + XCTAssertNil(closeItem.target) + } +}