diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 8a459222..1814b446 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 4C6B93E82C1DCF6E00AFF832 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B93E22C1DCF6E00AFF832 /* Updater.swift */; }; 4C6B93E92C1DCF6E00AFF832 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B93E32C1DCF6E00AFF832 /* UpdateView.swift */; }; 4CD883A42C30F0D7009A132A /* WallpaperColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD883A32C30F0D7009A132A /* WallpaperColors.swift */; }; - A80554C22D234E5D009238C7 /* Luminare in Frameworks */ = {isa = PBXBuildFile; productRef = A80554C12D234E5D009238C7 /* Luminare */; }; A8055EC22AFEDE0B00459D13 /* Keycorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8055EC12AFEDE0B00459D13 /* Keycorder.swift */; }; A80900D52AA3F9F30085C63B /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80900D32AA3F9F20085C63B /* VisualEffectView.swift */; }; A80D49BB2BAE479900493B67 /* Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D49BA2BAE479900493B67 /* Migrator.swift */; }; @@ -484,7 +483,6 @@ name = Loop; packageProductDependencies = ( A8DCC97A2980D5F500D41065 /* Defaults */, - A80554C42D235233009238C7 /* Luminare */, ); productName = WindowManager; productReference = A8E59C35297F5E9A0064D4BA /* Loop.app */; @@ -913,7 +911,6 @@ /* Begin XCSwiftPackageProductDependency section */ A860324E2CB34A6C005742EB /* Luminare */ = { isa = XCSwiftPackageProductDependency; - package = A860324D2CB34A6C005742EB /* XCRemoteSwiftPackageReference "Luminare" */; productName = Luminare; }; A8DCC97A2980D5F500D41065 /* Defaults */ = { diff --git a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift index d81996b8..6ccafb5e 100644 --- a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift +++ b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift @@ -12,6 +12,7 @@ import SwiftUI struct Keycorder: View { @EnvironmentObject private var model: KeybindsConfigurationModel + @Environment(\.appearsActive) private var appearsActive let keyLimit: Int = 6 @@ -49,7 +50,14 @@ struct Keycorder: View { .modifier(LuminareBordered()) } else { HStack(spacing: 5) { - ForEach(selectionKeybind.sorted(), id: \.self) { key in + // First show modifiers in order + let sortedKeys = selectionKeybind.sorted { (a: CGKeyCode, b: CGKeyCode) in + if a.isModifier, !b.isModifier { return true } + if !a.isModifier, b.isModifier { return false } + return a < b + } + + ForEach(sortedKeys, id: \.self) { key in if let systemImage = key.systemImage { Text("\(Image(systemName: systemImage))") } else if let humanReadable = key.humanReadable { @@ -77,12 +85,17 @@ struct Keycorder: View { finishedObservingKeys(wasForced: true) } } + .onChange(of: appearsActive) { _ in + if appearsActive { + finishedObservingKeys(wasForced: true) + } + } .onChange(of: validCurrentKeybind) { _ in if selectionKeybind != validCurrentKeybind { selectionKeybind = validCurrentKeybind } } - .buttonStyle(PlainButtonStyle()) + .buttonStyle(.plain) // Don't allow the button to be pressed if more than one keybind is selected in the list .allowsHitTesting(model.selectedKeybinds.count <= 1) } @@ -90,43 +103,21 @@ struct Keycorder: View { func startObservingKeys() { selectionKeybind = [] isActive = true - eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .keyUp, .flagsChanged]) { event in - if event.type == .flagsChanged { - if !Defaults[.triggerKey].contains(where: { $0.baseModifier == event.keyCode.baseModifier }) { - shouldError = false - selectionKeybind.insert(event.keyCode.baseModifier) - } else { - if let systemImage = event.keyCode.baseModifier.systemImage { - errorMessage = "\(Image(systemName: systemImage)) is already used as your trigger key." - } else { - errorMessage = "That key is already used as your trigger key." - } - - shouldShake.toggle() - shouldError = true - } - } - - if event.type == .keyUp || - (event.type == .flagsChanged && !selectionKeybind.isEmpty && event.modifierFlags.rawValue == 256) { - finishedObservingKeys() - return nil - } + eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .keyUp]) { event in + // Handle regular key presses first if event.type == .keyDown, !event.isARepeat { - if event.keyCode == CGKeyCode.kVK_Escape { + if event.keyCode == .kVK_Escape { finishedObservingKeys(wasForced: true) return nil } - if (selectionKeybind.count + triggerKey.count) >= keyLimit { - errorMessage = "You can only use up to \(keyLimit) keys in a keybind, including the trigger key." - shouldShake.toggle() - shouldError = true - } else { - shouldError = false - selectionKeybind.insert(event.keyCode) - } + handleKeyDown(with: event) + } + + if event.type == .keyUp { + finishedObservingKeys() + return nil } return nil @@ -136,6 +127,35 @@ struct Keycorder: View { model.currentEventMonitor = eventMonitor } + /// Handles key presses and updates the current keybind + func handleKeyDown(with event: NSEvent) { + /// Get current selected keys that aren't modifiers + let currentKeys = selectionKeybind + [event.keyCode] + .filter { !$0.isModifier } + + /// Get current modifiers that aren't trigger keys + let currentModifiers = event.modifierFlags + .convertToCGKeyCode() + .filter { + !Defaults[.triggerKey] + .map(\.baseModifier) + .contains($0) + } + + let newSelection = Set(currentKeys + currentModifiers) + + /// Make sure we don't go over the key limit + guard newSelection.count < keyLimit else { + errorMessage = "You can only use up to \(keyLimit) keys in a keybind, including the trigger key." + shouldShake.toggle() + shouldError = true + return + } + + shouldError = false + selectionKeybind = newSelection + } + func finishedObservingKeys(wasForced: Bool = false) { isActive = false var willSet = !wasForced diff --git a/Loop/Managers/LoopManager.swift b/Loop/Managers/LoopManager.swift index a415ee78..372700c0 100644 --- a/Loop/Managers/LoopManager.swift +++ b/Loop/Managers/LoopManager.swift @@ -71,7 +71,7 @@ class LoopManager: ObservableObject { } // This is called when setting the trigger key, so that there aren't conflicting event monitors - func setFlagsObservers(scope: NSEventMonitor.Scope = .all) { + func setFlagsObservers(scope: NSEventMonitor.Scope) { flagsChangedEventMonitor?.stop() flagsChangedEventMonitor = NSEventMonitor( diff --git a/Loop/Utilities/EventMonitor.swift b/Loop/Utilities/EventMonitor.swift index 69e4ed16..dc367fe0 100644 --- a/Loop/Utilities/EventMonitor.swift +++ b/Loop/Utilities/EventMonitor.swift @@ -30,7 +30,13 @@ class NSEventMonitor: EventMonitor, Identifiable, Equatable { } deinit { - stop() + if isEnabled { + stop() + } + + // Clear references + localEventMonitor = nil + globalEventMonitor = nil } init(scope: Scope, eventMask: NSEvent.EventTypeMask, handler: @escaping (NSEvent) -> (NSEvent?)) { @@ -62,12 +68,16 @@ class NSEventMonitor: EventMonitor, Identifiable, Equatable { } func stop() { + guard isEnabled else { return } + if let localEventMonitor { NSEvent.removeMonitor(localEventMonitor) + self.localEventMonitor = nil } if let globalEventMonitor { NSEvent.removeMonitor(globalEventMonitor) + self.globalEventMonitor = nil } isEnabled = false } @@ -110,7 +120,19 @@ class CGEventMonitor: EventMonitor, Identifiable, Equatable { } deinit { - stop() + if isEnabled { + stop() + } + + // Clean up run loop source and event tap + if let runLoopSource { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) + self.runLoopSource = nil + } + + if eventTap != nil { + self.eventTap = nil + } } private func handleEvent(event: CGEvent) -> Unmanaged? { @@ -118,16 +140,16 @@ class CGEventMonitor: EventMonitor, Identifiable, Equatable { } func start() { - if let eventTap { - CGEvent.tapEnable(tap: eventTap, enable: true) - } + guard let eventTap else { return } + CGEvent.tapEnable(tap: eventTap, enable: true) isEnabled = true } func stop() { - if let eventTap { - CGEvent.tapEnable(tap: eventTap, enable: false) - } + guard isEnabled else { return } + + guard let eventTap else { return } + CGEvent.tapEnable(tap: eventTap, enable: false) isEnabled = false } diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index 49200d68..037b1c69 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -132,9 +132,14 @@ class Window { /// Activate the window. This will bring it to the front and focus it if possible func activate() { do { - try self.axWindow.setValue(.main, value: true) + // First activate the application to ensure proper window management context if let runningApplication = self.nsRunningApplication { - runningApplication.activate() + runningApplication.activate(options: .activateIgnoringOtherApps) + } + + // Then set the window as main after a brief delay to ensure proper ordering + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + try? self.axWindow.setValue(.main, value: true) } } catch { print("Failed to activate window: \(error.localizedDescription)")