From e82ece3f919d29506c0c1663a5234de1db4ffef9 Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 4 Jan 2025 22:42:06 +1000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9E=20Bug=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop.xcodeproj/project.pbxproj | 3 - .../Keybind Recorder/Keycorder.swift | 72 ++++++++++++------- Loop/Utilities/EventMonitor.swift | 38 +++++++--- Loop/Window Management/Window.swift | 9 ++- 4 files changed, 84 insertions(+), 38 deletions(-) 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..107a381e 100644 --- a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift +++ b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift @@ -49,7 +49,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 { @@ -91,44 +98,59 @@ struct Keycorder: View { 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 - } - + // 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 } + // Get current modifiers that aren't trigger keys + let currentModifiers = event.modifierFlags + .convertToCGKeyCode() + .filter { !Defaults[.triggerKey].contains($0) } + .sorted { a, b in + // Sort modifiers to ensure consistent order + // Command -> Option -> Control -> Shift -> Other keys + let modifierOrder: [CGKeyCode] = [ + .kVK_Command, + .kVK_Option, + .kVK_Control, + .kVK_Shift + ] + + let aIndex = modifierOrder.firstIndex(of: a.baseModifier) ?? modifierOrder.count + let bIndex = modifierOrder.firstIndex(of: b.baseModifier) ?? modifierOrder.count + return aIndex < bIndex + } + + // Clear existing selection and add modifiers first + selectionKeybind.removeAll() + + // Add modifiers in sorted order + for modifier in currentModifiers { + selectionKeybind.insert(modifier) + } + + // Then add the regular key if we're not at the limit 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) + // Only add non-modifier keys + if !event.keyCode.isModifier { + selectionKeybind.insert(event.keyCode) + } } } + if event.type == .keyUp { + finishedObservingKeys() + return nil + } + return nil } 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)") From 43d7b31e44588b04de12ca1a6d459bd3671f07da Mon Sep 17 00:00:00 2001 From: Kai Date: Sat, 4 Jan 2025 18:34:27 -0700 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9E=20Support=20more=20than=20one?= =?UTF-8?q?=20non-modifier=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Keybind Recorder/Keycorder.swift | 65 ++++++++----------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift index 107a381e..8f61b231 100644 --- a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift +++ b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift @@ -105,45 +105,7 @@ struct Keycorder: View { return nil } - // Get current modifiers that aren't trigger keys - let currentModifiers = event.modifierFlags - .convertToCGKeyCode() - .filter { !Defaults[.triggerKey].contains($0) } - .sorted { a, b in - // Sort modifiers to ensure consistent order - // Command -> Option -> Control -> Shift -> Other keys - let modifierOrder: [CGKeyCode] = [ - .kVK_Command, - .kVK_Option, - .kVK_Control, - .kVK_Shift - ] - - let aIndex = modifierOrder.firstIndex(of: a.baseModifier) ?? modifierOrder.count - let bIndex = modifierOrder.firstIndex(of: b.baseModifier) ?? modifierOrder.count - return aIndex < bIndex - } - - // Clear existing selection and add modifiers first - selectionKeybind.removeAll() - - // Add modifiers in sorted order - for modifier in currentModifiers { - selectionKeybind.insert(modifier) - } - - // Then add the regular key if we're not at the limit - 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 - // Only add non-modifier keys - if !event.keyCode.isModifier { - selectionKeybind.insert(event.keyCode) - } - } + handleKeyDown(with: event) } if event.type == .keyUp { @@ -158,6 +120,31 @@ 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].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 From b1e90fadf08b2a5ea0e389d7cf7e4603969f2dcf Mon Sep 17 00:00:00 2001 From: Kai Date: Sat, 4 Jan 2025 19:46:57 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20Stop=20event=20monitor=20on=20d?= =?UTF-8?q?efocus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Keybind Recorder/Keycorder.swift | 17 ++++++++++++++--- Loop/Managers/LoopManager.swift | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift index 8f61b231..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 @@ -84,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) } @@ -97,7 +103,8 @@ struct Keycorder: View { func startObservingKeys() { selectionKeybind = [] isActive = true - eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .keyUp, .flagsChanged]) { event in + eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .keyUp]) { event in + // Handle regular key presses first if event.type == .keyDown, !event.isARepeat { if event.keyCode == .kVK_Escape { @@ -129,7 +136,11 @@ struct Keycorder: View { /// Get current modifiers that aren't trigger keys let currentModifiers = event.modifierFlags .convertToCGKeyCode() - .filter { !Defaults[.triggerKey].contains($0) } + .filter { + !Defaults[.triggerKey] + .map(\.baseModifier) + .contains($0) + } let newSelection = Set(currentKeys + currentModifiers) 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(