Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions Apps/Keyty/Sources/Keyty/App/Lifecycle/AppController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ final class AppController: NSObject {
self.updateService = UpdateServiceFactory.make()
self.menuController = MenuController()
let statusShortcutItem = self.menuController.makeStatusShortcutMenuItem()
self.statusItemController = StatusItemController(
menu: self.menuController.makeStatusMenu(shortcutItem: statusShortcutItem),
shortcutItem: statusShortcutItem
)
self.dependencies = AppDependencies(
statusShortcutItem: statusShortcutItem,
updateService: self.updateService
)
self.statusItemController = StatusItemController(
menu: self.menuController.makeStatusMenu(
shortcutItem: statusShortcutItem,
settings: self.dependencies.settings
),
shortcutItem: statusShortcutItem
)
super.init()
self.menuController.setAppController(self)
NSApp.mainMenu = self.menuController.makeMainMenu()
Expand Down Expand Up @@ -135,6 +138,26 @@ extension AppController {
self.dependencies.captureController.toggleCapturing()
}

@objc
func toggleKeyboardVisualizer(_ sender: Any?) {
self.dependencies.settings.keyboardVisualizerSettings.isEnabled.toggle()
}

@objc
func togglePointerRingVisualizer(_ sender: Any?) {
self.dependencies.settings.pointerRingSettings.isEnabled.toggle()
}

@objc
func togglePointerRipplesVisualizer(_ sender: Any?) {
self.dependencies.settings.pointerRipplesSettings.isEnabled.toggle()
}

@objc
func togglePointerIconVisualizer(_ sender: Any?) {
self.dependencies.settings.pointerIconSettings.isEnabled.toggle()
}

@objc
func quitApplication(_ sender: Any?) {
NSApp.terminate(sender)
Expand Down
91 changes: 89 additions & 2 deletions Apps/Keyty/Sources/Keyty/App/Shell/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import AppKit

/// Builds and wires the status item menu used by the menu-bar-only app.
final class MenuController {
final class MenuController: NSObject {
private weak var appController: AppController?
private var controlledMenuItems: [NSMenuItem] = []
private var visualizerMenuItems: [Visualizer: NSMenuItem] = [:]
private var settings: AppSettingsContainer?

init(appController: AppController? = nil) {
self.appController = appController
Expand All @@ -26,11 +28,15 @@ final class MenuController {
self.makeShortcutMenuItem(action: #selector(AppController.toggleCapturing(_:)))
}

func makeStatusMenu(shortcutItem: NSMenuItem) -> NSMenu {
func makeStatusMenu(shortcutItem: NSMenuItem, settings: AppSettingsContainer) -> NSMenu {
self.settings = settings

let menu = NSMenu(title: AppConstants.appName)
menu.addItem(shortcutItem)
menu.addItem(self.makeVisualizersMenuItem())
menu.addItem(self.makeSettingsMenuItem())
menu.addItem(self.makeQuitMenuItem())
menu.delegate = self
return menu
}

Expand All @@ -49,6 +55,13 @@ final class MenuController {
}
}

// MARK: - NSMenuDelegate
extension MenuController: NSMenuDelegate {
func menuNeedsUpdate(_ menu: NSMenu) {
self.updateVisualizerMenuItemStates()
}
}

// MARK: - Private API
private extension MenuController {
private func makeApplicationMenu() -> NSMenu {
Expand All @@ -74,6 +87,32 @@ private extension MenuController {
return self.register(item)
}

private func makeVisualizersMenuItem() -> NSMenuItem {
let menu = NSMenu(title: L10n.MainMenu.visualizers)
menu.addItem(self.makeVisualizerMenuItem(.keyboard))
menu.addItem(self.makeVisualizerMenuItem(.pointerRing))
menu.addItem(self.makeVisualizerMenuItem(.pointerRipples))
menu.addItem(self.makeVisualizerMenuItem(.pointerIcon))

let item = NSMenuItem(title: L10n.MainMenu.visualizers, action: nil, keyEquivalent: "")
item.submenu = menu
return item
}

private func makeVisualizerMenuItem(_ visualizer: Visualizer) -> NSMenuItem {
let item = NSMenuItem(title: visualizer.title, action: visualizer.action, keyEquivalent: "")
self.visualizerMenuItems[visualizer] = self.register(item)
return item
}

private func updateVisualizerMenuItemStates() {
guard let settings = self.settings else { return }

self.visualizerMenuItems.forEach { visualizer, item in
item.state = visualizer.isEnabled(in: settings) ? .on : .off
}
}

private func makeShortcutMenuItem(action: Selector) -> NSMenuItem {
let item = NSMenuItem(title: L10n.General.startCapturing, action: action, keyEquivalent: "S")
item.keyEquivalentModifierMask = [.shift, .option]
Expand All @@ -92,3 +131,51 @@ private extension MenuController {
return item
}
}

private extension MenuController {
enum Visualizer: CaseIterable {
case keyboard
case pointerRing
case pointerRipples
case pointerIcon

var title: String {
switch self {
case .keyboard:
return L10n.Settings.Pane.keyboard
case .pointerRing:
return L10n.Mouse.tabRing
case .pointerRipples:
return L10n.Mouse.tabRipples
case .pointerIcon:
return L10n.Mouse.tabIcon
}
}

var action: Selector {
switch self {
case .keyboard:
return #selector(AppController.toggleKeyboardVisualizer(_:))
case .pointerRing:
return #selector(AppController.togglePointerRingVisualizer(_:))
case .pointerRipples:
return #selector(AppController.togglePointerRipplesVisualizer(_:))
case .pointerIcon:
return #selector(AppController.togglePointerIconVisualizer(_:))
}
}

func isEnabled(in settings: AppSettingsContainer) -> Bool {
switch self {
case .keyboard:
return settings.keyboardVisualizerSettings.isEnabled
case .pointerRing:
return settings.pointerRingSettings.isEnabled
case .pointerRipples:
return settings.pointerRipplesSettings.isEnabled
case .pointerIcon:
return settings.pointerIconSettings.isEnabled
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Fenster schließen";
"main_menu.file" = "Ablage";
"main_menu.settings" = "Einstellungen…";
"main_menu.visualizers" = "Visualisierungen";
"main_menu.quit" = "%@ beenden";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Close Window";
"main_menu.file" = "File";
"main_menu.settings" = "Settings…";
"main_menu.visualizers" = "Visualizers";
"main_menu.quit" = "Quit %@";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Cerrar ventana";
"main_menu.file" = "Archivo";
"main_menu.settings" = "Ajustes…";
"main_menu.visualizers" = "Visualizadores";
"main_menu.quit" = "Salir de %@";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Fermer la fenêtre";
"main_menu.file" = "Fichier";
"main_menu.settings" = "Réglages…";
"main_menu.visualizers" = "Visualiseurs";
"main_menu.quit" = "Quitter %@";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Chiudi finestra";
"main_menu.file" = "File";
"main_menu.settings" = "Impostazioni…";
"main_menu.visualizers" = "Visualizzatori";
"main_menu.quit" = "Esci da %@";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "ウインドウを閉じる";
"main_menu.file" = "ファイル";
"main_menu.settings" = "設定…";
"main_menu.visualizers" = "ビジュアライザー";
"main_menu.quit" = "%@を終了";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "창 닫기";
"main_menu.file" = "파일";
"main_menu.settings" = "설정…";
"main_menu.visualizers" = "시각화 도구";
"main_menu.quit" = "%@ 종료";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Venster sluiten";
"main_menu.file" = "Archief";
"main_menu.settings" = "Instellingen…";
"main_menu.visualizers" = "Visualisaties";
"main_menu.quit" = "%@ stoppen";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Zamknij okno";
"main_menu.file" = "Plik";
"main_menu.settings" = "Ustawienia…";
"main_menu.visualizers" = "Wizualizatory";
"main_menu.quit" = "Zakończ %@";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Fechar Janela";
"main_menu.file" = "Arquivo";
"main_menu.settings" = "Ajustes…";
"main_menu.visualizers" = "Visualizadores";
"main_menu.quit" = "Sair de %@";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Pencereyi Kapat";
"main_menu.file" = "Dosya";
"main_menu.settings" = "Ayarlar…";
"main_menu.visualizers" = "Görselleştiriciler";
"main_menu.quit" = "%@’den Çık";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "Закрити вікно";
"main_menu.file" = "Файл";
"main_menu.settings" = "Параметри…";
"main_menu.visualizers" = "Візуалізатори";
"main_menu.quit" = "Завершити роботу %@";

/* About window */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"main_menu.close_window" = "关闭窗口";
"main_menu.file" = "文件";
"main_menu.settings" = "设置…";
"main_menu.visualizers" = "可视化器";
"main_menu.quit" = "退出 %@";

/* About window */
Expand Down
23 changes: 23 additions & 0 deletions Apps/Keyty/Tests/KeytyTests/App/Shell/MenuControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ import XCTest
@testable import Keyty

final class MenuControllerTests: XCTestCase {
func testStatusMenuVisualizersReflectCurrentSettings() throws {
let settings = AppSettingsContainer(store: InMemoryKeyValueStore())
let controller = MenuController()

let menu = controller.makeStatusMenu(
shortcutItem: controller.makeStatusShortcutMenuItem(),
settings: settings
)
menu.delegate?.menuNeedsUpdate?(menu)

let visualizersMenu = try XCTUnwrap(menu.items[1].submenu)
XCTAssertEqual(visualizersMenu.items.map(\.title), ["Keyboard", "Ring", "Ripples", "Icon"])
XCTAssertEqual(visualizersMenu.items.map(\.state), [.on, .off, .off, .off])

settings.keyboardVisualizerSettings.isEnabled = false
settings.pointerRingSettings.isEnabled = false
settings.pointerRipplesSettings.isEnabled = false
settings.pointerIconSettings.isEnabled = true
menu.delegate?.menuNeedsUpdate?(menu)

XCTAssertEqual(visualizersMenu.items.map(\.state), [.off, .off, .off, .on])
}

func testMainMenuIncludesOnlyRequiredCommandQAndCommandWShortcuts() throws {
let controller = MenuController()

Expand Down
Loading