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
82 changes: 54 additions & 28 deletions SwiftKey/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
private static var activeGalleryWindow: NSWindow?

var isOverlayVisible: Bool {
overlayWindow?.isVisible == true || notchContext?.presented == true || (facelessMenuController?.sessionActive == true)
overlayWindow?.isVisible == true || notchContext?
.presented == true || (facelessMenuController?.sessionActive == true)
}

func applicationDidFinishLaunching(_: Notification) {
logger.notice("SwiftKey application starting")

Expand Down Expand Up @@ -174,53 +175,67 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {

@MainActor
func toggleSession() async {

if isOverlayVisible {
logger.debug("Overlay is visible, hiding it on repeated trigger")
await hideWindow()
return
}

await configManager.refreshIfNeeded()

switch settings.overlayStyle {
case .faceless:
facelessMenuController?.startSession()

case .hud:
if notchContext == nil {
notchContext = NotchContext(
headerLeadingView: EmptyView(),
headerTrailingView: EmptyView(),
bodyView: AnyView(
MinimalHUDView(state: menuState)
.environmentObject(settings)
.environment(keyboardManager)
),
animated: true,
settingsStore: settings
)
}
setupNotchContextIfNeeded()
notchContext?.open()

case .panel:
if settings.menuStateResetDelay == 0 {
menuState.reset()
} else if let lastHide = lastHideTime,
Date().timeIntervalSince(lastHide) >= settings.menuStateResetDelay
Date().timeIntervalSince(lastHide) >= settings.menuStateResetDelay
{
menuState.reset()
}
presentOverlay()
}
}

@MainActor
private func setupNotchContextIfNeeded() {
if notchContext == nil {
notchContext = NotchContext(
headerLeadingView: EmptyView(),
headerTrailingView: EmptyView(),
bodyView: AnyView(
MinimalHUDView(state: menuState)
.environmentObject(settings)
.environment(keyboardManager)
),
animated: true,
settingsStore: settings
)
}
}

@MainActor
func hideWindow() async {
if !isOverlayVisible {
return
}

// Check if gallery window is visible - if so, don't hide the overlay
if let galleryWindow = Self.activeGalleryWindow,
galleryWindow.isVisible,
NSApp.keyWindow === galleryWindow
{
// Only skip hiding when gallery window is showing and active
logger.debug("hideWindow: skipping hide because gallery window is active")
return
}

logger.debug("hideWindow: hiding \(self.settings.overlayStyle.rawValue) overlay")

// Perform style-specific cleanup
Expand All @@ -241,12 +256,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
}
}

func windowDidResignKey(_: Notification) {
func windowDidResignKey(_ notification: Notification) {
// Check if we should suppress overlay hiding when snippets gallery is active
if let window = notification.object as? NSWindow,
window === overlayWindow,
let galleryWindow = Self.activeGalleryWindow,
galleryWindow.isVisible,
NSApp.keyWindow === galleryWindow
{
// Don't hide the overlay if it's losing focus to the gallery window
return
}

Task {
await hideWindow()
}
}

@objc func applicationDidResignActive(_: Notification) {
logger.debug("Application resigned active state")
// Only hide windows if app is already initialized
Expand Down Expand Up @@ -374,9 +400,9 @@ extension AppDelegate {
existingWindow.makeKeyAndOrderFront(nil)
return
}

Self.activeGalleryWindow = nil

let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
Expand All @@ -398,7 +424,7 @@ extension AppDelegate {
.environmentObject(container.settingsStore)
)
window.contentViewController = hostingController

NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification,
object: window,
Expand All @@ -408,9 +434,9 @@ extension AppDelegate {
Self.activeGalleryWindow = nil
}
}

Self.activeGalleryWindow = window

window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
Expand Down
18 changes: 18 additions & 0 deletions SwiftKey/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,21 @@ extension MenuItem {
return nil
}
}

// MARK: - MenuItem Array Extensions

extension Array where Element == MenuItem {
/// Recursively finds a menu item with the given ID
func findItem(with id: UUID) -> MenuItem? {
for item in self {
if item.id == id {
return item
}
if let submenu = item.submenu,
let found = submenu.findItem(with: id) {
return found
}
}
return nil
}
}
2 changes: 2 additions & 0 deletions SwiftKey/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ final class MenuState: ObservableObject {
@Published var menuStack: [[MenuItem]] = []
@Published var breadcrumbs: [String] = []
@Published var currentKey: String?
@Published var lastExecutedAction: (() -> Void)? = nil
@Published var lastActionTime: Date? = nil

func reset() {
menuStack = []
Expand Down
33 changes: 33 additions & 0 deletions SwiftKey/Core/ConfigManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Yams
/// Manages loading, parsing, and updating configuration files
class ConfigManager: DependencyInjectable, ObservableObject {
private let logger = AppLogger.config


/// Factory method to create a new ConfigManager instance
static func create() -> ConfigManager {
Expand Down Expand Up @@ -289,6 +290,38 @@ class ConfigManager: DependencyInjectable, ObservableObject {
)
}

// MARK: - Configuration Loading/Saving

func loadConfiguration() -> AnyPublisher<[MenuItem], ConfigError> {
Future { promise in
Task {
let result = await self.loadConfig()
switch result {
case .success(let items):
promise(.success(items))
case .failure(let error):
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}

func saveConfiguration(_ items: [MenuItem]) -> AnyPublisher<Void, ConfigError> {
Future { promise in
Task {
let result = await self.saveMenuItems(items)
switch result {
case .success:
promise(.success(()))
case .failure(let error):
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}

func importSnippet(menuItems: [MenuItem], strategy: MergeStrategy) async throws {
// Validate the menu items
let validationResult = await Task {
Expand Down
34 changes: 34 additions & 0 deletions SwiftKey/Core/KeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class KeyboardManager: DependencyInjectable, ObservableObject {
// Global key handlers map for menu hotkeys
private var hotkeyHandlers: [String: KeyboardShortcuts.Name] = [:]

private let lastActionRepeatWindow: TimeInterval = 30

// Default initialization for container creation
init() {
// Default initialization - will be properly set in injectDependencies
Expand Down Expand Up @@ -78,6 +80,13 @@ class KeyboardManager: DependencyInjectable, ObservableObject {
menuState.currentKey = normalizedKey
}

if normalizedKey == "." {
let result = await handleLastActionRepeat()
if result != .none {
return result
}
}

// Common navigation keys
if normalizedKey == "escape" {
return .escape
Expand Down Expand Up @@ -128,6 +137,11 @@ class KeyboardManager: DependencyInjectable, ObservableObject {
action()
}

await MainActor.run {
menuState.lastExecutedAction = action
menuState.lastActionTime = Date()
}

// Handle sticky flag for panel mode
let overlayStyle = settingsStore.overlayStyle

Expand Down Expand Up @@ -259,6 +273,26 @@ class KeyboardManager: DependencyInjectable, ObservableObject {
}
return nil
}

// MARK: - Last Action Repeat
private func handleLastActionRepeat() async -> KeyPressResult {
guard let lastAction = menuState.lastExecutedAction,
let lastActionTime = menuState.lastActionTime,
Date().timeIntervalSince(lastActionTime) <= lastActionRepeatWindow
else {
return .none
}

logger.debug("Repeating last action")

Task(priority: .userInitiated) {
lastAction()
await MainActor.run {
menuState.lastActionTime = Date()
}
}
return .actionExecuted(sticky: false)
}
}

// MARK: - KeyboardShortcuts Extension
Expand Down
9 changes: 9 additions & 0 deletions SwiftKey/SwiftKeyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ struct SwiftKeyApp: App {
.environmentObject(container.menuState)
.environmentObject(container.configManager)
.environmentObject(container.sparkleUpdater)
.onAppear {
// Show dock icon when settings window opens
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
.onDisappear {
// Hide dock icon when settings window closes
NSApp.setActivationPolicy(.accessory)
}
}
}
}
Loading