diff --git a/Apps/Keyty/Sources/Keyty/App/Composition/AppSettingsContainer.swift b/Apps/Keyty/Sources/Keyty/App/Composition/AppSettingsContainer.swift index eb43030..b5326fa 100644 --- a/Apps/Keyty/Sources/Keyty/App/Composition/AppSettingsContainer.swift +++ b/Apps/Keyty/Sources/Keyty/App/Composition/AppSettingsContainer.swift @@ -35,4 +35,12 @@ final class AppSettingsContainer { self.shortcutSettings.registerDefaults() self.keyboardVisualizerSettings.registerDefaults() } + + func resetAllSettingsToDefaults() { + self.appSettings.resetToDefaults() + self.pointerRingSettings.resetToDefaults() + self.pointerIconSettings.resetToDefaults() + self.shortcutSettings.resetToDefaults() + self.keyboardVisualizerSettings.resetToDefaults() + } } diff --git a/Apps/Keyty/Sources/Keyty/App/Composition/AppUIContainer.swift b/Apps/Keyty/Sources/Keyty/App/Composition/AppUIContainer.swift index c30a60f..bb10da6 100644 --- a/Apps/Keyty/Sources/Keyty/App/Composition/AppUIContainer.swift +++ b/Apps/Keyty/Sources/Keyty/App/Composition/AppUIContainer.swift @@ -28,30 +28,15 @@ final class AppUIContainer { self.permissionsOnboardingWindowController = PermissionsOnboardingWindowController( permissionsService: services.permissionsService ) - let settingsWindowController = SettingsWindowController( + let settingsContext = SettingsContext( + settings: settings, shortcutManager: services.shortcutManager, - appSettings: settings.appSettings, pointerRingVisualizer: services.pointerVisualizersManager.ring, - pointerRingSettings: settings.pointerRingSettings, - pointerIconSettings: settings.pointerIconSettings, - keyboardVisualizerSettings: settings.keyboardVisualizerSettings, - startSettingKeyboardVisualizerPosition: { [weak keyboardVisualizerPlacementWindowController] onPlacementChanged in - keyboardVisualizerPlacementWindowController?.startSettingPosition(onPlacementChanged: onPlacementChanged) - }, - stopSettingKeyboardVisualizerPosition: { [weak keyboardVisualizerPlacementWindowController] in - keyboardVisualizerPlacementWindowController?.stopSettingPosition() - }, permissionsService: services.permissionsService, - updater: updater + updater: updater, + placementCoordinator: keyboardVisualizerPlacementWindowController ) - settingsWindowController.onClose = { [weak keyboardVisualizerPlacementWindowController, keyboardVisualizerSettings = settings.keyboardVisualizerSettings] in - guard let placement = keyboardVisualizerPlacementWindowController?.stopSettingPosition() else { return } - keyboardVisualizerSettings.applyCustomPlacement( - screenID: placement.screenID, - normalizedX: placement.positionX, - normalizedY: placement.positionY - ) - } + let settingsWindowController = SettingsWindowController(context: settingsContext) self.settingsWindowController = settingsWindowController } } diff --git a/Apps/Keyty/Sources/Keyty/Features/Settings/Displays/DisplaysSettingsPaneViewModel.swift b/Apps/Keyty/Sources/Keyty/Features/Settings/Displays/DisplaysSettingsPaneViewModel.swift index 36c192b..7febf14 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Settings/Displays/DisplaysSettingsPaneViewModel.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Settings/Displays/DisplaysSettingsPaneViewModel.swift @@ -13,8 +13,7 @@ import SwiftUI final class DisplaysSettingsPaneViewModel: ObservableObject { private let screensService: any ScreenServiceProvider private let keyboardVisualizerSettings: KeyboardVisualizerSettings - private let startSettingKeyboardVisualizerPosition: @MainActor (@escaping KeyboardVisualizerPlacementWindowController.PlacementChangeHandler) -> Void - private let stopSettingKeyboardVisualizerPosition: @MainActor () -> KeyboardVisualizerPlacementWindowController.Placement? + private let placementCoordinator: any KeyboardVisualizerPlacementCoordinating private var cancellables = Set() let paddingRange: ClosedRange = Double(KeyboardVisualizerSettings.minWindowPadding)...Double(KeyboardVisualizerSettings.maxWindowPadding) @@ -94,8 +93,7 @@ final class DisplaysSettingsPaneViewModel: ObservableObject { init( screensService: any ScreenServiceProvider = ScreensService.shared, keyboardVisualizerSettings: KeyboardVisualizerSettings = KeyboardVisualizerSettings(), - startSettingKeyboardVisualizerPosition: @escaping @MainActor (@escaping KeyboardVisualizerPlacementWindowController.PlacementChangeHandler) -> Void = { _ in }, - stopSettingKeyboardVisualizerPosition: @escaping @MainActor () -> KeyboardVisualizerPlacementWindowController.Placement? = { nil } + placementCoordinator: any KeyboardVisualizerPlacementCoordinating ) { guard let selectedScreen = Self.initialSelectedScreen( screensService: screensService, @@ -106,8 +104,7 @@ final class DisplaysSettingsPaneViewModel: ObservableObject { self.screensService = screensService self.keyboardVisualizerSettings = keyboardVisualizerSettings - self.startSettingKeyboardVisualizerPosition = startSettingKeyboardVisualizerPosition - self.stopSettingKeyboardVisualizerPosition = stopSettingKeyboardVisualizerPosition + self.placementCoordinator = placementCoordinator self.screens = screensService.screens self.selectedScreen = selectedScreen self.selectedAnchor = keyboardVisualizerSettings.anchor @@ -143,7 +140,7 @@ final class DisplaysSettingsPaneViewModel: ObservableObject { if self.isSettingCustomPosition { self.stopCustomPositionSetting() } else { - self.startSettingKeyboardVisualizerPosition { [weak self] placement in + self.placementCoordinator.startSettingPosition { [weak self] placement in self?.applyPlacement(placement) } self.isSettingCustomPosition = true @@ -176,7 +173,7 @@ extension DisplaysSettingsPaneViewModel { private extension DisplaysSettingsPaneViewModel { func stopCustomPositionSetting() { guard self.isSettingCustomPosition else { return } - if let placement = self.stopSettingKeyboardVisualizerPosition() { + if let placement = self.placementCoordinator.stopSettingPosition() { self.applyPlacement(placement) } self.isSettingCustomPosition = false diff --git a/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPane.swift b/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPane.swift index 6f746f3..4e577de 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPane.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPane.swift @@ -10,11 +10,17 @@ import SwiftUI struct GeneralSettingsPane: View { @StateObject private var model: GeneralSettingsPaneViewModel + @State private var isShowingResetConfirmation = false - init(shortcutManager: ShortcutManager, appSettings: any AppSettingsProtocol) { + init( + shortcutManager: ShortcutManager, + appSettings: any AppSettingsProtocol, + onResetAllSettingsToDefaults: @escaping @MainActor () -> Void + ) { _model = StateObject(wrappedValue: GeneralSettingsPaneViewModel( shortcutManager: shortcutManager, - appSettings: appSettings + appSettings: appSettings, + onResetAllSettingsToDefaults: onResetAllSettingsToDefaults )) } @@ -53,6 +59,29 @@ struct GeneralSettingsPane: View { } } } + + SettingsSectionView(title: L10n.General.settingsSectionTitle) { + SettingsControlRow( + title: L10n.General.resetAllSettingsTitle, + subtitle: L10n.General.resetAllSettingsSubtitle + ) { + Button(L10n.General.resetAllSettingsButton) { + self.isShowingResetConfirmation = true + } + .buttonStyle(.bordered) + .controlSize(.regular) + } + } + } + .alert(isPresented: self.$isShowingResetConfirmation) { + Alert( + title: Text(L10n.General.resetAllSettingsConfirmationTitle), + message: Text(L10n.General.resetAllSettingsConfirmationMessage), + primaryButton: .destructive(Text(L10n.General.resetAllSettingsConfirmationButton)) { + self.model.resetAllSettingsToDefaults() + }, + secondaryButton: .cancel(Text(L10n.General.resetAllSettingsCancelButton)) + ) } } } diff --git a/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPaneViewModel.swift b/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPaneViewModel.swift index e917ba7..e7da74d 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPaneViewModel.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Settings/General/GeneralSettingsPaneViewModel.swift @@ -12,6 +12,7 @@ final class GeneralSettingsPaneViewModel: ObservableObject { let shortcutManager: ShortcutManager private let appSettings: any AppSettingsProtocol + private let onResetAllSettingsToDefaults: @MainActor () -> Void @Published var visibleAtLaunch: Bool { didSet { self.appSettings.visibleAtLaunch = self.visibleAtLaunch } @@ -19,9 +20,14 @@ final class GeneralSettingsPaneViewModel: ObservableObject { @Published var shortcutValidationMessage: String? - init(shortcutManager: ShortcutManager, appSettings: any AppSettingsProtocol) { + init( + shortcutManager: ShortcutManager, + appSettings: any AppSettingsProtocol, + onResetAllSettingsToDefaults: @escaping @MainActor () -> Void + ) { self.shortcutManager = shortcutManager self.appSettings = appSettings + self.onResetAllSettingsToDefaults = onResetAllSettingsToDefaults self.visibleAtLaunch = self.appSettings.visibleAtLaunch self.shortcutValidationMessage = self.shortcutManager.shortcutValidationMessage @@ -29,4 +35,11 @@ final class GeneralSettingsPaneViewModel: ObservableObject { self?.shortcutValidationMessage = message } } + + @MainActor + func resetAllSettingsToDefaults() { + self.onResetAllSettingsToDefaults() + self.visibleAtLaunch = self.appSettings.visibleAtLaunch + self.shortcutValidationMessage = self.shortcutManager.shortcutValidationMessage + } } diff --git a/Apps/Keyty/Sources/Keyty/Features/Settings/KeyboardVisualizerPlacementCoordinating.swift b/Apps/Keyty/Sources/Keyty/Features/Settings/KeyboardVisualizerPlacementCoordinating.swift new file mode 100644 index 0000000..6d6cd79 --- /dev/null +++ b/Apps/Keyty/Sources/Keyty/Features/Settings/KeyboardVisualizerPlacementCoordinating.swift @@ -0,0 +1,18 @@ +// +// KeyboardVisualizerPlacementCoordinating.swift +// Keyty +// +// SPDX-FileCopyrightText: 2026 Serhii Bykov +// SPDX-License-Identifier: BSD-3-Clause +// + +import Foundation + +@MainActor +protocol KeyboardVisualizerPlacementCoordinating: AnyObject { + func startSettingPosition( + onPlacementChanged: @escaping KeyboardVisualizerPlacementWindowController.PlacementChangeHandler + ) + + func stopSettingPosition() -> KeyboardVisualizerPlacementWindowController.Placement? +} diff --git a/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsContext.swift b/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsContext.swift new file mode 100644 index 0000000..2edecb5 --- /dev/null +++ b/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsContext.swift @@ -0,0 +1,44 @@ +// +// SettingsContext.swift +// Keyty +// +// SPDX-FileCopyrightText: 2026 Serhii Bykov +// SPDX-License-Identifier: BSD-3-Clause +// + +import Sparkle + +@MainActor +final class SettingsContext { + let settings: AppSettingsContainer + let shortcutManager: ShortcutManager + let pointerRingVisualizer: PointerRingVisualizer + let permissionsService: any PermissionsService + let updater: SPUUpdater + let placementCoordinator: any KeyboardVisualizerPlacementCoordinating + + var appSettings: any AppSettingsProtocol { self.settings.appSettings } + var pointerRingSettings: any PointerRingSettingsProtocol { self.settings.pointerRingSettings } + var pointerIconSettings: any PointerIconSettingsProtocol { self.settings.pointerIconSettings } + var keyboardVisualizerSettings: KeyboardVisualizerSettings { self.settings.keyboardVisualizerSettings } + + init( + settings: AppSettingsContainer, + shortcutManager: ShortcutManager, + pointerRingVisualizer: PointerRingVisualizer, + permissionsService: any PermissionsService, + updater: SPUUpdater, + placementCoordinator: any KeyboardVisualizerPlacementCoordinating + ) { + self.settings = settings + self.shortcutManager = shortcutManager + self.pointerRingVisualizer = pointerRingVisualizer + self.permissionsService = permissionsService + self.updater = updater + self.placementCoordinator = placementCoordinator + } + + func resetAllSettingsToDefaults() { + self.settings.resetAllSettingsToDefaults() + } +} diff --git a/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsPaneRegistry.swift b/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsPaneRegistry.swift index d8e94cb..86bfaf9 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsPaneRegistry.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsPaneRegistry.swift @@ -19,27 +19,8 @@ struct SettingsPaneEntry: Identifiable { @MainActor struct SettingsPaneRegistry { let entries: [SettingsPaneEntry] - private let displaysViewModel: DisplaysSettingsPaneViewModel - - init( - shortcutManager: ShortcutManager, - appSettings: any AppSettingsProtocol, - pointerRingVisualizer: PointerRingVisualizer, - pointerRingSettings: any PointerRingSettingsProtocol, - pointerIconSettings: any PointerIconSettingsProtocol, - keyboardVisualizerSettings: KeyboardVisualizerSettings, - startSettingKeyboardVisualizerPosition: @escaping @MainActor (@escaping KeyboardVisualizerPlacementWindowController.PlacementChangeHandler) -> Void, - stopSettingKeyboardVisualizerPosition: @escaping @MainActor () -> KeyboardVisualizerPlacementWindowController.Placement?, - permissionsService: any PermissionsService, - updater: SPUUpdater - ) { - let displaysViewModel = DisplaysSettingsPaneViewModel( - keyboardVisualizerSettings: keyboardVisualizerSettings, - startSettingKeyboardVisualizerPosition: startSettingKeyboardVisualizerPosition, - stopSettingKeyboardVisualizerPosition: stopSettingKeyboardVisualizerPosition - ) - self.displaysViewModel = displaysViewModel + init(context: SettingsContext, displaysViewModel: DisplaysSettingsPaneViewModel) { self.entries = [ SettingsPaneEntry( id: .general, @@ -48,8 +29,11 @@ struct SettingsPaneRegistry { makeView: { AnyView( GeneralSettingsPane( - shortcutManager: shortcutManager, - appSettings: appSettings + shortcutManager: context.shortcutManager, + appSettings: context.appSettings, + onResetAllSettingsToDefaults: { + context.resetAllSettingsToDefaults() + } ) ) } @@ -59,7 +43,7 @@ struct SettingsPaneRegistry { title: SettingsPaneIdentifier.keyboard.label, systemImageName: SettingsPaneIdentifier.keyboard.sfSymbolName, makeView: { - AnyView(KeyboardSettingsPane(settings: keyboardVisualizerSettings)) + AnyView(KeyboardSettingsPane(settings: context.keyboardVisualizerSettings)) } ), SettingsPaneEntry( @@ -69,9 +53,9 @@ struct SettingsPaneRegistry { makeView: { AnyView( MouseSettingsPane( - pointerRingVisualizer: pointerRingVisualizer, - pointerRingSettings: pointerRingSettings, - pointerIconSettings: pointerIconSettings + pointerRingVisualizer: context.pointerRingVisualizer, + pointerRingSettings: context.pointerRingSettings, + pointerIconSettings: context.pointerIconSettings ) ) } @@ -93,7 +77,7 @@ struct SettingsPaneRegistry { title: SettingsPaneIdentifier.permissions.label, systemImageName: SettingsPaneIdentifier.permissions.sfSymbolName, makeView: { - AnyView(PermissionsSettingsPane(permissionsService: permissionsService)) + AnyView(PermissionsSettingsPane(permissionsService: context.permissionsService)) } ), SettingsPaneEntry( @@ -101,7 +85,7 @@ struct SettingsPaneRegistry { title: SettingsPaneIdentifier.update.label, systemImageName: SettingsPaneIdentifier.update.sfSymbolName, makeView: { - AnyView(UpdateSettingsPane(updater: updater)) + AnyView(UpdateSettingsPane(updater: context.updater)) } ), SettingsPaneEntry( @@ -118,8 +102,4 @@ struct SettingsPaneRegistry { func entry(for identifier: SettingsPaneIdentifier) -> SettingsPaneEntry? { self.entries.first { $0.id == identifier } } - - func finishTransientWork() { - self.displaysViewModel.finishCustomPositionSetting() - } } diff --git a/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsWindowController.swift b/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsWindowController.swift index a623cea..0930ac1 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsWindowController.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Settings/SettingsWindowController.swift @@ -14,36 +14,19 @@ import SwiftUI @MainActor final class SettingsWindowController: NSWindowController { let sidebarViewModel = SettingsSidebarViewModel() - var onClose: (@MainActor () -> Void)? private let registry: SettingsPaneRegistry + private let displaysViewModel: DisplaysSettingsPaneViewModel private var cancellables = Set() private var permissionObservationToken: PermissionObservationToken? - init( - shortcutManager: ShortcutManager, - appSettings: any AppSettingsProtocol, - pointerRingVisualizer: PointerRingVisualizer, - pointerRingSettings: any PointerRingSettingsProtocol, - pointerIconSettings: any PointerIconSettingsProtocol, - keyboardVisualizerSettings: KeyboardVisualizerSettings, - startSettingKeyboardVisualizerPosition: @escaping @MainActor (@escaping KeyboardVisualizerPlacementWindowController.PlacementChangeHandler) -> Void, - stopSettingKeyboardVisualizerPosition: @escaping @MainActor () -> KeyboardVisualizerPlacementWindowController.Placement?, - permissionsService: any PermissionsService, - updater: SPUUpdater - ) { - self.registry = SettingsPaneRegistry( - shortcutManager: shortcutManager, - appSettings: appSettings, - pointerRingVisualizer: pointerRingVisualizer, - pointerRingSettings: pointerRingSettings, - pointerIconSettings: pointerIconSettings, - keyboardVisualizerSettings: keyboardVisualizerSettings, - startSettingKeyboardVisualizerPosition: startSettingKeyboardVisualizerPosition, - stopSettingKeyboardVisualizerPosition: stopSettingKeyboardVisualizerPosition, - permissionsService: permissionsService, - updater: updater + init(context: SettingsContext) { + let displaysViewModel = DisplaysSettingsPaneViewModel( + keyboardVisualizerSettings: context.keyboardVisualizerSettings, + placementCoordinator: context.placementCoordinator ) + self.displaysViewModel = displaysViewModel + self.registry = SettingsPaneRegistry(context: context, displaysViewModel: displaysViewModel) let window = Window() let rootView = SettingsRootView(registry: self.registry, sidebarViewModel: self.sidebarViewModel) @@ -52,14 +35,14 @@ final class SettingsWindowController: NSWindowController { super.init(window: window) window.delegate = self self.bindWindowTitle() - self.bindSidebarBadges(permissionsService: permissionsService) + self.bindSidebarBadges(permissionsService: context.permissionsService) self.updateWindowTitle(for: self.sidebarViewModel.selectedPaneID) window.center() } @available(*, unavailable) required init?(coder: NSCoder) { - fatalError("Use init(shortcutManager:appSettings:pointerRingVisualizer:pointerRingSettings:pointerIconSettings:keyboardVisualizerSettings:startSettingKeyboardVisualizerPosition:stopSettingKeyboardVisualizerPosition:permissionsService:updater:) instead.") + fatalError("Use the designated SettingsWindowController initializer instead.") } override func showWindow(_ sender: Any?) { @@ -105,8 +88,7 @@ final class SettingsWindowController: NSWindowController { // MARK: - NSWindowDelegate extension SettingsWindowController: NSWindowDelegate { func windowWillClose(_ notification: Notification) { - self.registry.finishTransientWork() - self.onClose?() + self.displaysViewModel.finishCustomPositionSetting() } } diff --git a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerPlacementWindowController.swift b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerPlacementWindowController.swift index 9bef767..80877a8 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerPlacementWindowController.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerPlacementWindowController.swift @@ -9,7 +9,7 @@ import AppKit @MainActor -final class KeyboardVisualizerPlacementWindowController: NSWindowController { +final class KeyboardVisualizerPlacementWindowController: NSWindowController, KeyboardVisualizerPlacementCoordinating { struct Placement { let screenID: CGDirectDisplayID let positionX: CGFloat diff --git a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerSettings.swift b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerSettings.swift index 037fca9..29634bf 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerSettings.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Keyboard/KeyboardVisualizerSettings.swift @@ -49,6 +49,7 @@ protocol KeyboardVisualizerSettingsProtocol: AnyObject { var showMouseEvents: Bool { get set } func registerDefaults() + func resetToDefaults() func applyCustomPlacement(screenID: CGDirectDisplayID, normalizedX: CGFloat, normalizedY: CGFloat) } @@ -76,6 +77,12 @@ final class KeyboardVisualizerSettings: KeyboardVisualizerSettingsProtocol, HasS self.registerStoredDefaults() } + func resetToDefaults() { + self.resetStoredSettingsToDefaults() + self.isEnabledChangesSubject.send(self.isEnabled) + self.placementChangesSubject.send(()) + } + func applyCustomPlacement(screenID: CGDirectDisplayID, normalizedX: CGFloat, normalizedY: CGFloat) { self.screenID = screenID self.customPositionNormalizedX = normalizedX diff --git a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerIcon/PointerIconSettings.swift b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerIcon/PointerIconSettings.swift index f442d6e..f99ee54 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerIcon/PointerIconSettings.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerIcon/PointerIconSettings.swift @@ -20,6 +20,7 @@ protocol PointerIconSettingsProtocol: AnyObject { var iconSize: NSSize { get } func registerDefaults() + func resetToDefaults() } final class PointerIconSettings: PointerIconSettingsProtocol, ReactiveSettings, HasSettingsStore { @@ -86,6 +87,10 @@ final class PointerIconSettings: PointerIconSettingsProtocol, ReactiveSettings, self.registerStoredDefaults() } + func resetToDefaults() { + self.resetStoredSettingsToDefaults() + } + private func storeDidChange() { self.changesSubject.send() } diff --git a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerRing/PointerRingSettings.swift b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerRing/PointerRingSettings.swift index 6a850a0..070f585 100644 --- a/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerRing/PointerRingSettings.swift +++ b/Apps/Keyty/Sources/Keyty/Features/Visualizers/Mouse/PointerRing/PointerRingSettings.swift @@ -18,6 +18,7 @@ protocol PointerRingSettingsProtocol: AnyObject { var shape: PointerRingShape { get set } func registerDefaults() + func resetToDefaults() } private struct PointerRingVisualSettingsSnapshot: Equatable { @@ -98,6 +99,10 @@ final class PointerRingSettings: PointerRingSettingsProtocol, ReactiveSettings, self.visualSettingsSnapshot = self.currentVisualSettingsSnapshot } + func resetToDefaults() { + self.resetStoredSettingsToDefaults() + } + private func storeDidChange() { let snapshot = self.currentVisualSettingsSnapshot guard snapshot != self.visualSettingsSnapshot else { return } diff --git a/Apps/Keyty/Sources/Keyty/Resources/de.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/de.lproj/Localizable.strings index 3a225ac..8fea132 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/de.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/de.lproj/Localizable.strings @@ -63,6 +63,14 @@ "general.stop_capturing" = "Deaktivieren"; "general.show_settings_at_launch" = "Einstellungen beim Start anzeigen"; "general.show_settings_at_launch_subtitle" = "Das Einstellungsfenster beim Start von Keyty automatisch wieder öffnen."; +"general.settings_section_title" = "Einstellungen"; +"general.reset_all_settings_title" = "Alle Einstellungen zurücksetzen"; +"general.reset_all_settings_subtitle" = "Tastatur-, Maus-, Anzeige-, App- und Tastaturkurzbefehl-Einstellungen auf die Standardwerte zurücksetzen."; +"general.reset_all_settings_button" = "Zurücksetzen"; +"general.reset_all_settings_confirmation_title" = "Alle Einstellungen zurücksetzen?"; +"general.reset_all_settings_confirmation_message" = "Dadurch werden die Tastatur-, Maus-, Anzeige-, App- und Tastaturkurzbefehl-Einstellungen auf ihre Standardwerte zurückgesetzt."; +"general.reset_all_settings_confirmation_button" = "Einstellungen zurücksetzen"; +"general.reset_all_settings_cancel_button" = "Abbrechen"; /* Event capture */ "event_tap.key_tap_creation_failed" = "Die Tastaturerfassung konnte nicht gestartet werden. Die Berechtigung für Bedienungshilfen ist erforderlich."; diff --git a/Apps/Keyty/Sources/Keyty/Resources/en.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/en.lproj/Localizable.strings index 5fbe5e1..824d674 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/en.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/en.lproj/Localizable.strings @@ -56,6 +56,7 @@ /* General settings pane */ "general.appearance_section_title" = "App"; "general.shortcut_section_title" = "Shortcut"; +"general.settings_section_title" = "Settings"; "general.toggle_capturing_label" = "Toggle capturing"; "general.toggle_capturing_subtitle" = "Global shortcut used to start or stop capturing from anywhere."; "general.shortcut_validation_fallback" = "This shortcut cannot be used."; @@ -63,6 +64,13 @@ "general.stop_capturing" = "Disable"; "general.show_settings_at_launch" = "Show settings at launch"; "general.show_settings_at_launch_subtitle" = "Reopen the settings window automatically when Keyty starts."; +"general.reset_all_settings_title" = "Reset All Settings"; +"general.reset_all_settings_subtitle" = "Reset keyboard, mouse, display, app, and shortcut settings to their defaults."; +"general.reset_all_settings_button" = "Reset"; +"general.reset_all_settings_confirmation_title" = "Reset All Settings?"; +"general.reset_all_settings_confirmation_message" = "This will reset keyboard, mouse, display, app, and shortcut settings to their defaults."; +"general.reset_all_settings_confirmation_button" = "Reset Settings"; +"general.reset_all_settings_cancel_button" = "Cancel"; /* Event capture */ "event_tap.key_tap_creation_failed" = "Could not create key event tap. Accessibility permission is required."; diff --git a/Apps/Keyty/Sources/Keyty/Resources/es.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/es.lproj/Localizable.strings index ba839bf..f29b59b 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/es.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/es.lproj/Localizable.strings @@ -63,6 +63,14 @@ "general.stop_capturing" = "Desactivar"; "general.show_settings_at_launch" = "Mostrar los ajustes al iniciar"; "general.show_settings_at_launch_subtitle" = "Volver a abrir automáticamente la ventana de ajustes al iniciar Keyty."; +"general.settings_section_title" = "Ajustes"; +"general.reset_all_settings_title" = "Restablecer todos los ajustes"; +"general.reset_all_settings_subtitle" = "Restablece la configuración de teclado, ratón, pantalla, aplicación y atajos a sus valores predeterminados."; +"general.reset_all_settings_button" = "Restablecer"; +"general.reset_all_settings_confirmation_title" = "¿Restablecer todos los ajustes?"; +"general.reset_all_settings_confirmation_message" = "Esto restablecerá la configuración de teclado, ratón, pantalla, aplicación y atajos a sus valores predeterminados."; +"general.reset_all_settings_confirmation_button" = "Restablecer ajustes"; +"general.reset_all_settings_cancel_button" = "Cancelar"; /* Event capture */ "event_tap.key_tap_creation_failed" = "No se ha podido iniciar la captura del teclado. Se necesita el permiso de Accesibilidad."; diff --git a/Apps/Keyty/Sources/Keyty/Resources/fr.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/fr.lproj/Localizable.strings index 0c49222..b0f917f 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/fr.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/fr.lproj/Localizable.strings @@ -63,6 +63,14 @@ "general.stop_capturing" = "Désactiver"; "general.show_settings_at_launch" = "Afficher les réglages au démarrage"; "general.show_settings_at_launch_subtitle" = "Rouvrir automatiquement la fenêtre des réglages au démarrage de Keyty."; +"general.settings_section_title" = "Réglages"; +"general.reset_all_settings_title" = "Réinitialiser tous les réglages"; +"general.reset_all_settings_subtitle" = "Réinitialise les réglages du clavier, de la souris, de l’affichage, de l’app et des raccourcis à leurs valeurs par défaut."; +"general.reset_all_settings_button" = "Réinitialiser"; +"general.reset_all_settings_confirmation_title" = "Réinitialiser tous les réglages ?"; +"general.reset_all_settings_confirmation_message" = "Cela réinitialisera les réglages du clavier, de la souris, de l’affichage, de l’app et des raccourcis à leurs valeurs par défaut."; +"general.reset_all_settings_confirmation_button" = "Réinitialiser les réglages"; +"general.reset_all_settings_cancel_button" = "Annuler"; /* Event capture */ "event_tap.key_tap_creation_failed" = "Impossible de démarrer la détection du clavier. L’autorisation d’accessibilité est requise."; diff --git a/Apps/Keyty/Sources/Keyty/Resources/ja.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/ja.lproj/Localizable.strings index 4bfa209..0ff0631 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/ja.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/ja.lproj/Localizable.strings @@ -63,6 +63,14 @@ "general.stop_capturing" = "無効にする"; "general.show_settings_at_launch" = "起動時に設定を表示"; "general.show_settings_at_launch_subtitle" = "Keytyの起動時に設定ウインドウを自動的に開きます。"; +"general.settings_section_title" = "設定"; +"general.reset_all_settings_title" = "すべての設定をリセット"; +"general.reset_all_settings_subtitle" = "キーボード、マウス、ディスプレイ、アプリ、ショートカットの設定をデフォルトに戻します。"; +"general.reset_all_settings_button" = "リセット"; +"general.reset_all_settings_confirmation_title" = "すべての設定をリセットしますか?"; +"general.reset_all_settings_confirmation_message" = "キーボード、マウス、ディスプレイ、アプリ、ショートカットの設定がデフォルトに戻ります。"; +"general.reset_all_settings_confirmation_button" = "設定をリセット"; +"general.reset_all_settings_cancel_button" = "キャンセル"; /* Event capture */ "event_tap.key_tap_creation_failed" = "キーボード入力の検出を開始できませんでした。アクセシビリティへのアクセス権が必要です。"; diff --git a/Apps/Keyty/Sources/Keyty/Resources/pl.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/pl.lproj/Localizable.strings index ea4757f..621da85 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/pl.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/pl.lproj/Localizable.strings @@ -63,6 +63,14 @@ "general.stop_capturing" = "Wyłącz"; "general.show_settings_at_launch" = "Pokaż ustawienia przy uruchomieniu"; "general.show_settings_at_launch_subtitle" = "Automatycznie ponownie otwieraj okno ustawień po uruchomieniu Keyty."; +"general.settings_section_title" = "Ustawienia"; +"general.reset_all_settings_title" = "Resetuj wszystkie ustawienia"; +"general.reset_all_settings_subtitle" = "Resetuje ustawienia klawiatury, myszy, ekranu, aplikacji i skrótów do wartości domyślnych."; +"general.reset_all_settings_button" = "Resetuj"; +"general.reset_all_settings_confirmation_title" = "Zresetować wszystkie ustawienia?"; +"general.reset_all_settings_confirmation_message" = "Spowoduje to zresetowanie ustawień klawiatury, myszy, ekranu, aplikacji i skrótów do wartości domyślnych."; +"general.reset_all_settings_confirmation_button" = "Zresetuj ustawienia"; +"general.reset_all_settings_cancel_button" = "Anuluj"; /* Event capture */ "event_tap.key_tap_creation_failed" = "Nie udało się utworzyć przechwytywania zdarzeń klawiatury. Wymagane jest uprawnienie Dostępność."; diff --git a/Apps/Keyty/Sources/Keyty/Resources/uk.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/uk.lproj/Localizable.strings index acd6c9e..dfc5f69 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/uk.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/uk.lproj/Localizable.strings @@ -63,6 +63,14 @@ "general.stop_capturing" = "Вимкнути"; "general.show_settings_at_launch" = "Показувати параметри після запуску"; "general.show_settings_at_launch_subtitle" = "Автоматично відкривати вікно параметрів після запуску Keyty."; +"general.settings_section_title" = "Параметри"; +"general.reset_all_settings_title" = "Скинути всі параметри"; +"general.reset_all_settings_subtitle" = "Скидає параметри клавіатури, миші, дисплея, програми та клавіатурних скорочень до стандартних значень."; +"general.reset_all_settings_button" = "Скинути"; +"general.reset_all_settings_confirmation_title" = "Скинути всі параметри?"; +"general.reset_all_settings_confirmation_message" = "Буде скинуто параметри клавіатури, миші, дисплея, програми та клавіатурних скорочень до стандартних значень."; +"general.reset_all_settings_confirmation_button" = "Скинути параметри"; +"general.reset_all_settings_cancel_button" = "Скасувати"; /* Event capture */ "event_tap.key_tap_creation_failed" = "Не вдалося почати відстеження клавіатури. Потрібен дозвіл «Доступність»."; diff --git a/Apps/Keyty/Sources/Keyty/Resources/zh-Hans.lproj/Localizable.strings b/Apps/Keyty/Sources/Keyty/Resources/zh-Hans.lproj/Localizable.strings index 7f00ca0..85cec53 100644 --- a/Apps/Keyty/Sources/Keyty/Resources/zh-Hans.lproj/Localizable.strings +++ b/Apps/Keyty/Sources/Keyty/Resources/zh-Hans.lproj/Localizable.strings @@ -63,6 +63,14 @@ "general.stop_capturing" = "停用"; "general.show_settings_at_launch" = "启动时显示设置"; "general.show_settings_at_launch_subtitle" = "Keyty 启动时自动重新打开设置窗口。"; +"general.settings_section_title" = "设置"; +"general.reset_all_settings_title" = "重置所有设置"; +"general.reset_all_settings_subtitle" = "将键盘、鼠标、显示、应用和快捷键设置恢复为默认值。"; +"general.reset_all_settings_button" = "重置"; +"general.reset_all_settings_confirmation_title" = "要重置所有设置吗?"; +"general.reset_all_settings_confirmation_message" = "这将把键盘、鼠标、显示、应用和快捷键设置恢复为默认值。"; +"general.reset_all_settings_confirmation_button" = "重置设置"; +"general.reset_all_settings_cancel_button" = "取消"; /* Event capture */ "event_tap.key_tap_creation_failed" = "无法开始监测键盘输入。需要“辅助功能”权限。"; diff --git a/Apps/Keyty/Sources/Keyty/Services/Settings/AppSettings.swift b/Apps/Keyty/Sources/Keyty/Services/Settings/AppSettings.swift index e193b6d..4d4099c 100644 --- a/Apps/Keyty/Sources/Keyty/Services/Settings/AppSettings.swift +++ b/Apps/Keyty/Sources/Keyty/Services/Settings/AppSettings.swift @@ -12,6 +12,7 @@ protocol AppSettingsProtocol: AnyObject { var visibleAtLaunch: Bool { get set } func registerDefaults() + func resetToDefaults() } final class AppSettings: AppSettingsProtocol, HasSettingsStore { @@ -29,4 +30,8 @@ final class AppSettings: AppSettingsProtocol, HasSettingsStore { func registerDefaults() { self.registerStoredDefaults() } + + func resetToDefaults() { + self.resetStoredSettingsToDefaults() + } } diff --git a/Apps/Keyty/Sources/Keyty/Services/Settings/StoredSetting.swift b/Apps/Keyty/Sources/Keyty/Services/Settings/StoredSetting.swift index 7f546e2..7b5eb6c 100644 --- a/Apps/Keyty/Sources/Keyty/Services/Settings/StoredSetting.swift +++ b/Apps/Keyty/Sources/Keyty/Services/Settings/StoredSetting.swift @@ -24,6 +24,7 @@ protocol PlacementReactiveSettings: AnyObject { protocol AnyStoredSetting { var defaultRegistration: (key: String, value: Any)? { get } + func reset(in store: KeyValueStore) } private protocol AnyOptional { @@ -65,6 +66,10 @@ struct Stored: AnyStoredSetting { return (self.descriptor.key, self.descriptor.registrationValue) } + func reset(in store: KeyValueStore) { + store.removeObject(forKey: self.descriptor.key) + } + @available(*, unavailable, message: "@Stored can only be used on reference types that conform to HasSettingsStore.") var wrappedValue: Value { get { fatalError() } @@ -101,6 +106,12 @@ extension HasSettingsStore { func registerStoredDefaults() { StoredDefaults.register(from: self, into: self.store) } + + func resetStoredSettingsToDefaults() { + Mirror(reflecting: self).children.forEach { child in + (child.value as? AnyStoredSetting)?.reset(in: self.store) + } + } } extension StoredDescriptor where Value == Bool { diff --git a/Apps/Keyty/Sources/Keyty/Services/Shortcuts/ShortcutSettings.swift b/Apps/Keyty/Sources/Keyty/Services/Shortcuts/ShortcutSettings.swift index d390558..b3c3b7f 100644 --- a/Apps/Keyty/Sources/Keyty/Services/Shortcuts/ShortcutSettings.swift +++ b/Apps/Keyty/Sources/Keyty/Services/Shortcuts/ShortcutSettings.swift @@ -12,6 +12,7 @@ protocol ShortcutSettingsProtocol: AnyObject { var capturingHotKeyData: Data? { get set } func registerDefaults() + func resetToDefaults() } final class ShortcutSettings: ShortcutSettingsProtocol, HasSettingsStore { @@ -29,4 +30,8 @@ final class ShortcutSettings: ShortcutSettingsProtocol, HasSettingsStore { func registerDefaults() { self.registerStoredDefaults() } + + func resetToDefaults() { + self.resetStoredSettingsToDefaults() + } } diff --git a/Apps/Keyty/Tests/KeytyTests/App/Composition/AppSettingsContainerTests.swift b/Apps/Keyty/Tests/KeytyTests/App/Composition/AppSettingsContainerTests.swift new file mode 100644 index 0000000..d0596b8 --- /dev/null +++ b/Apps/Keyty/Tests/KeytyTests/App/Composition/AppSettingsContainerTests.swift @@ -0,0 +1,52 @@ +// +// AppSettingsContainerTests.swift +// KeytyTests +// +// SPDX-FileCopyrightText: 2026 Serhii Bykov +// SPDX-License-Identifier: BSD-3-Clause +// + +import AppKit +import XCTest +@testable import Keyty + +final class AppSettingsContainerTests: XCTestCase { + private var store: InMemoryKeyValueStore! + private var container: AppSettingsContainer! + + override func setUp() { + super.setUp() + self.store = InMemoryKeyValueStore() + self.container = AppSettingsContainer(store: self.store) + } + + override func tearDown() { + self.container = nil + self.store = nil + super.tearDown() + } + + func testResetAllSettingsToDefaultsRestoresAllSettingsGroups() { + self.container.appSettings.visibleAtLaunch = false + self.container.pointerRingSettings.isEnabled = true + self.container.pointerRingSettings.color = .systemRed + self.container.pointerIconSettings.isEnabled = true + self.container.pointerIconSettings.offset = 42 + self.container.shortcutSettings.capturingHotKeyData = nil + self.container.keyboardVisualizerSettings.isEnabled = false + self.container.keyboardVisualizerSettings.scale = 1.75 + self.container.keyboardVisualizerSettings.placementMode = .custom + + self.container.resetAllSettingsToDefaults() + + XCTAssertTrue(self.container.appSettings.visibleAtLaunch) + XCTAssertEqual(self.container.pointerRingSettings.isEnabled, PointerRingSettingsKeys.defaultIsEnabled) + XCTAssertEqual(self.container.pointerRingSettings.color.hexString, PointerRingSettingsKeys.automaticVisualizerColor.hexString) + XCTAssertFalse(self.container.pointerIconSettings.isEnabled) + XCTAssertEqual(self.container.pointerIconSettings.offset, PointerIconSettingsKeys.defaultOffset, accuracy: 0.0001) + XCTAssertEqual(self.container.shortcutSettings.capturingHotKeyData, ShortcutArchiver.defaultShortcutData()) + XCTAssertTrue(self.container.keyboardVisualizerSettings.isEnabled) + XCTAssertEqual(self.container.keyboardVisualizerSettings.scale, 1.0, accuracy: 0.0001) + XCTAssertEqual(self.container.keyboardVisualizerSettings.placementMode, .anchored) + } +} diff --git a/Apps/Keyty/Tests/KeytyTests/Features/Settings/Displays/DisplaysSettingsPaneViewModelTests.swift b/Apps/Keyty/Tests/KeytyTests/Features/Settings/Displays/DisplaysSettingsPaneViewModelTests.swift index 0b8b4cb..ef6184b 100644 --- a/Apps/Keyty/Tests/KeytyTests/Features/Settings/Displays/DisplaysSettingsPaneViewModelTests.swift +++ b/Apps/Keyty/Tests/KeytyTests/Features/Settings/Displays/DisplaysSettingsPaneViewModelTests.swift @@ -15,10 +15,7 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { private var store: InMemoryKeyValueStore! private var keyboardVisualizerSettings: KeyboardVisualizerSettings! private var screensService: TestScreenService! - private var startSettingCallCount = 0 - private var stopSettingCallCount = 0 - private var placementToReturn: KeyboardVisualizerPlacementWindowController.Placement? - private var placementChangeHandler: KeyboardVisualizerPlacementWindowController.PlacementChangeHandler? + private var placementCoordinator: FakeKeyboardVisualizerPlacementCoordinator! private var model: DisplaysSettingsPaneViewModel! override func setUp() { @@ -26,30 +23,17 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { self.store = InMemoryKeyValueStore() self.keyboardVisualizerSettings = KeyboardVisualizerSettings(store: self.store) self.screensService = TestScreenService() - self.startSettingCallCount = 0 - self.stopSettingCallCount = 0 - self.placementToReturn = nil - self.placementChangeHandler = nil + self.placementCoordinator = FakeKeyboardVisualizerPlacementCoordinator() self.model = DisplaysSettingsPaneViewModel( screensService: self.screensService, keyboardVisualizerSettings: self.keyboardVisualizerSettings, - startSettingKeyboardVisualizerPosition: { [weak self] onPlacementChanged in - self?.startSettingCallCount += 1 - self?.placementChangeHandler = onPlacementChanged - }, - stopSettingKeyboardVisualizerPosition: { [weak self] in - self?.stopSettingCallCount += 1 - return self?.placementToReturn - } + placementCoordinator: self.placementCoordinator ) } override func tearDown() { self.model = nil - self.placementChangeHandler = nil - self.placementToReturn = nil - self.stopSettingCallCount = 0 - self.startSettingCallCount = 0 + self.placementCoordinator = nil self.screensService = nil self.keyboardVisualizerSettings = nil self.store = nil @@ -77,14 +61,14 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { self.model.toggleCustomPositionSetting() XCTAssertTrue(self.model.isSettingCustomPosition) - XCTAssertEqual(self.startSettingCallCount, 1) - XCTAssertEqual(self.stopSettingCallCount, 0) + XCTAssertEqual(self.placementCoordinator.startSettingCallCount, 1) + XCTAssertEqual(self.placementCoordinator.stopSettingCallCount, 0) self.model.toggleCustomPositionSetting() XCTAssertFalse(self.model.isSettingCustomPosition) - XCTAssertEqual(self.startSettingCallCount, 1) - XCTAssertEqual(self.stopSettingCallCount, 1) + XCTAssertEqual(self.placementCoordinator.startSettingCallCount, 1) + XCTAssertEqual(self.placementCoordinator.stopSettingCallCount, 1) } func testChangingPlacementModeStopsPositionSetting() { @@ -94,7 +78,7 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { self.model.placementMode = .anchored XCTAssertFalse(self.model.isSettingCustomPosition) - XCTAssertEqual(self.stopSettingCallCount, 1) + XCTAssertEqual(self.placementCoordinator.stopSettingCallCount, 1) } func testAnchorSelectionReflectsPlacementMode() { @@ -140,7 +124,7 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { } func testFinishingCustomPositionSettingStopsAndAppliesReturnedPlacement() { - self.placementToReturn = KeyboardVisualizerPlacementWindowController.Placement( + self.placementCoordinator.placementToReturn = KeyboardVisualizerPlacementWindowController.Placement( screenID: 2, positionX: 0.3, positionY: 0.7 @@ -150,7 +134,7 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { self.model.finishCustomPositionSetting() XCTAssertFalse(self.model.isSettingCustomPosition) - XCTAssertEqual(self.stopSettingCallCount, 1) + XCTAssertEqual(self.placementCoordinator.stopSettingCallCount, 1) XCTAssertEqual(self.model.selectedScreenID, 2) XCTAssertEqual(self.keyboardVisualizerSettings.screenID, 2) XCTAssertEqual(self.model.customPositionNormalizedX, 0.3, accuracy: 0.0001) @@ -163,11 +147,11 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { self.model.finishCustomPositionSetting() XCTAssertFalse(self.model.isSettingCustomPosition) - XCTAssertEqual(self.stopSettingCallCount, 0) + XCTAssertEqual(self.placementCoordinator.stopSettingCallCount, 0) } func testStoppingCustomPositionSettingAppliesReturnedPlacement() { - self.placementToReturn = KeyboardVisualizerPlacementWindowController.Placement( + self.placementCoordinator.placementToReturn = KeyboardVisualizerPlacementWindowController.Placement( screenID: 2, positionX: 0.25, positionY: 0.75 @@ -187,7 +171,7 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { func testPlacementChangeWhileSettingAppliesPlacement() { self.model.toggleCustomPositionSetting() - self.placementChangeHandler?( + self.placementCoordinator.placementChangeHandler?( KeyboardVisualizerPlacementWindowController.Placement( screenID: 2, positionX: 0.4, @@ -205,6 +189,26 @@ final class DisplaysSettingsPaneViewModelTests: XCTestCase { } } +@MainActor +private final class FakeKeyboardVisualizerPlacementCoordinator: KeyboardVisualizerPlacementCoordinating { + private(set) var startSettingCallCount = 0 + private(set) var stopSettingCallCount = 0 + var placementToReturn: KeyboardVisualizerPlacementWindowController.Placement? + var placementChangeHandler: KeyboardVisualizerPlacementWindowController.PlacementChangeHandler? + + func startSettingPosition( + onPlacementChanged: @escaping KeyboardVisualizerPlacementWindowController.PlacementChangeHandler + ) { + self.startSettingCallCount += 1 + self.placementChangeHandler = onPlacementChanged + } + + func stopSettingPosition() -> KeyboardVisualizerPlacementWindowController.Placement? { + self.stopSettingCallCount += 1 + return self.placementToReturn + } +} + @MainActor final class KeyboardVisualizerPlacementWindowControllerTests: XCTestCase { func testFramePlacesHandleCenterAtNormalizedPosition() { diff --git a/Apps/Keyty/Tests/KeytyTests/Features/Settings/General/GeneralSettingsPaneViewModelTests.swift b/Apps/Keyty/Tests/KeytyTests/Features/Settings/General/GeneralSettingsPaneViewModelTests.swift new file mode 100644 index 0000000..3eb5c4b --- /dev/null +++ b/Apps/Keyty/Tests/KeytyTests/Features/Settings/General/GeneralSettingsPaneViewModelTests.swift @@ -0,0 +1,57 @@ +// +// GeneralSettingsPaneViewModelTests.swift +// KeytyTests +// +// SPDX-FileCopyrightText: 2026 Serhii Bykov +// SPDX-License-Identifier: BSD-3-Clause +// + +import ShortcutRecorder +import XCTest +@testable import Keyty + +@MainActor +final class GeneralSettingsPaneViewModelTests: XCTestCase { + func testResetAllSettingsToDefaultsRefreshesVisibleAtLaunchAfterReset() { + let store = InMemoryKeyValueStore() + let appSettings = AppSettings(store: store) + let shortcutSettings = ShortcutSettings(store: store) + let shortcutManager = ShortcutManager( + settings: shortcutSettings, + globalShortcutMonitor: FakeGlobalShortcutMonitor(), + shortcutValidator: FakeShortcutValidator(), + menuItemPresenter: FakeShortcutMenuItemPresenter(), + onToggleCapturingShortcut: {} + ) + + appSettings.registerDefaults() + shortcutSettings.registerDefaults() + appSettings.visibleAtLaunch = false + + let model = GeneralSettingsPaneViewModel( + shortcutManager: shortcutManager, + appSettings: appSettings, + onResetAllSettingsToDefaults: { + appSettings.resetToDefaults() + shortcutSettings.resetToDefaults() + } + ) + + model.resetAllSettingsToDefaults() + + XCTAssertTrue(model.visibleAtLaunch) + } +} + +private final class FakeGlobalShortcutMonitor: GlobalShortcutMonitoring { + func addAction(_ action: ShortcutAction, forKeyEvent keyEvent: KeyEventType) {} + func removeAction(_ action: ShortcutAction) {} +} + +private final class FakeShortcutValidator: ShortcutValidating { + func validationMessage(for shortcut: Shortcut) -> String? { nil } +} + +private final class FakeShortcutMenuItemPresenter: ShortcutMenuItemPresenting { + func displayShortcut(_ shortcut: Shortcut?) {} +} diff --git a/Apps/Keyty/Tests/KeytyTests/Features/Visualizers/Keyboard/Keycaps/KeyboardVisualizerSettingsTests.swift b/Apps/Keyty/Tests/KeytyTests/Features/Visualizers/Keyboard/Keycaps/KeyboardVisualizerSettingsTests.swift index 3ebb60b..c85d71f 100644 --- a/Apps/Keyty/Tests/KeytyTests/Features/Visualizers/Keyboard/Keycaps/KeyboardVisualizerSettingsTests.swift +++ b/Apps/Keyty/Tests/KeytyTests/Features/Visualizers/Keyboard/Keycaps/KeyboardVisualizerSettingsTests.swift @@ -216,6 +216,29 @@ final class KeyboardVisualizerSettingsTests: XCTestCase { cancellable.cancel() } + func testResetToDefaultsRestoresStoredValuesAndPublishesPlacementChange() { + self.settings.registerDefaults() + var placementChangeCount = 0 + let cancellable = self.settings.placementChanges.sink { _ in + placementChangeCount += 1 + } + + self.settings.placementMode = .custom + self.settings.customPositionNormalizedX = 0.25 + self.settings.customPositionNormalizedY = 0.75 + self.settings.customHorizontalAlignment = .leading + self.settings.scale = 1.5 + self.settings.resetToDefaults() + + XCTAssertEqual(self.settings.placementMode, .anchored) + XCTAssertEqual(self.settings.customPositionNormalizedX, 0.5, accuracy: 0.0001) + XCTAssertEqual(self.settings.customPositionNormalizedY, 0.5, accuracy: 0.0001) + XCTAssertEqual(self.settings.customHorizontalAlignment, .center) + XCTAssertEqual(self.settings.scale, 1.0, accuracy: 0.0001) + XCTAssertGreaterThanOrEqual(placementChangeCount, 5) + cancellable.cancel() + } + func testPersistsSharedTimingSettings() { self.settings.fadeDelay = 3.5 self.settings.fadeDuration = 0.45 diff --git a/Apps/Keyty/Tests/KeytyTests/Services/Settings/AppSettingsTests.swift b/Apps/Keyty/Tests/KeytyTests/Services/Settings/AppSettingsTests.swift index ea0766f..342e44d 100644 --- a/Apps/Keyty/Tests/KeytyTests/Services/Settings/AppSettingsTests.swift +++ b/Apps/Keyty/Tests/KeytyTests/Services/Settings/AppSettingsTests.swift @@ -37,4 +37,14 @@ final class AppSettingsTests: XCTestCase { XCTAssertFalse(settings.visibleAtLaunch) XCTAssertFalse(store.bool(forKey: AppSettings.visibleAtLaunchKey)) } + + func testResetToDefaultsRemovesStoredValue() { + settings.registerDefaults() + settings.visibleAtLaunch = false + + settings.resetToDefaults() + + XCTAssertTrue(settings.visibleAtLaunch) + XCTAssertTrue(store.bool(forKey: AppSettings.visibleAtLaunchKey)) + } }