Skip to content

Commit c7e4412

Browse files
EscoComclaude
andcommitted
Retry key event tap until accessibility permission is granted
Der CGEventTap fuer Tasten-Kombos (z.B. fn + R) wurde nur einmal beim App-Start erstellt. Fehlte die Bedienungshilfen-Berechtigung in dem Moment, blieben Tasten-Kombos bis zum Neustart tot. Jetzt wird die Tap-Erstellung alle 3 Sekunden erneut versucht und die Einstellungen warnen, wenn Tasten-Kombos mangels Freigabe nicht funktionieren. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f05a38b commit c7e4412

2 files changed

Lines changed: 52 additions & 7 deletions

File tree

BlitztextMac/Features/Settings/SettingsContentView.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,13 @@ struct CustomizeSettingsView: View {
639639
.fixedSize(horizontal: false, vertical: true)
640640
}
641641

642+
if hotkeysNeedAccessibilityPermission {
643+
Text("Tastenk\u{00FC}rzel mit normaler Taste (z.B. fn + R) brauchen die Bedienungshilfen-Freigabe oben. Ohne sie wird die Taste ins Textfeld getippt statt Blitztext zu starten.")
644+
.font(.system(size: 10.5))
645+
.foregroundStyle(.orange)
646+
.fixedSize(horizontal: false, vertical: true)
647+
}
648+
642649
if !appState.hotkeyCombosAreDefault {
643650
Button("Standard-Tastenk\u{00FC}rzel wiederherstellen") {
644651
hotkeyErrorText = nil
@@ -832,6 +839,11 @@ struct CustomizeSettingsView: View {
832839

833840
// MARK: - Hotkey Recording
834841

842+
private var hotkeysNeedAccessibilityPermission: Bool {
843+
guard !appState.hotkeyService.keyEventTapActive else { return false }
844+
return WorkflowType.allCases.contains { appState.hotkeyCombo(for: $0).keyCode != nil }
845+
}
846+
835847
private func hotkeyRowLabel(for type: WorkflowType) -> String {
836848
guard recordingHotkeyType == type else {
837849
return appState.hotkeyLabel(for: type)

BlitztextMac/Services/HotkeyService.swift

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,14 @@ final class HotkeyService {
119119
private var fallbackKeyUpMonitors: [Any] = []
120120
private var eventTap: CFMachPort?
121121
private var eventTapRunLoopSource: CFRunLoopSource?
122+
private var eventTapRetryTimer: Timer?
122123
private var activeCombo: WorkflowType? // Which combo is currently held
123124
private var activeKeyCode: UInt16? // Set when the active combo includes a regular key
124125

126+
/// false, solange der CGEventTap mangels Accessibility-Berechtigung nicht
127+
/// laeuft -- Kombos mit normaler Taste funktionieren dann nicht.
128+
private(set) var keyEventTapActive = false
129+
125130
var combos: [WorkflowType: HotkeyCombo] = HotkeyCombo.defaults
126131

127132
/// Waehrend der Hotkey-Aufnahme in den Einstellungen pausiert,
@@ -167,10 +172,9 @@ final class HotkeyService {
167172
globalMonitor = nil
168173
localMonitor = nil
169174
keyMonitor = nil
170-
fallbackKeyDownMonitors.forEach { NSEvent.removeMonitor($0) }
171-
fallbackKeyUpMonitors.forEach { NSEvent.removeMonitor($0) }
172-
fallbackKeyDownMonitors = []
173-
fallbackKeyUpMonitors = []
175+
stopFallbackKeyMonitors()
176+
eventTapRetryTimer?.invalidate()
177+
eventTapRetryTimer = nil
174178
if let eventTapRunLoopSource {
175179
CFRunLoopRemoveSource(CFRunLoopGetMain(), eventTapRunLoopSource, .commonModes)
176180
}
@@ -179,14 +183,34 @@ final class HotkeyService {
179183
}
180184
eventTap = nil
181185
eventTapRunLoopSource = nil
186+
keyEventTapActive = false
182187
}
183188

184189
// MARK: - Key Event Tap (fuer Kombos mit normaler Taste, z.B. fn + R)
185190

186191
/// Ein CGEventTap schluckt die normale Taste beim Ausloesen, damit sie nicht
187192
/// zusaetzlich in das fokussierte Textfeld getippt wird. Ohne Accessibility-
188-
/// Berechtigung faellt der Service auf passive NSEvent-Monitore zurueck.
193+
/// Berechtigung faellt der Service auf passive NSEvent-Monitore zurueck und
194+
/// versucht periodisch erneut, den Tap zu erstellen -- sonst blieben
195+
/// Tasten-Kombos nach spaeter erteilter Berechtigung bis zum Neustart tot.
189196
private func startKeyEventTap() {
197+
guard !createKeyEventTap() else { return }
198+
startFallbackKeyMonitors()
199+
eventTapRetryTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
200+
Task { @MainActor in
201+
self?.retryKeyEventTap()
202+
}
203+
}
204+
}
205+
206+
private func retryKeyEventTap() {
207+
guard createKeyEventTap() else { return }
208+
eventTapRetryTimer?.invalidate()
209+
eventTapRetryTimer = nil
210+
stopFallbackKeyMonitors()
211+
}
212+
213+
private func createKeyEventTap() -> Bool {
190214
let mask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
191215
let callback: CGEventTapCallBack = { _, type, cgEvent, userInfo in
192216
guard let userInfo else { return Unmanaged.passUnretained(cgEvent) }
@@ -207,13 +231,15 @@ final class HotkeyService {
207231
)
208232

209233
guard let eventTap else {
210-
startFallbackKeyMonitors()
211-
return
234+
keyEventTapActive = false
235+
return false
212236
}
213237

214238
eventTapRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
215239
CFRunLoopAddSource(CFRunLoopGetMain(), eventTapRunLoopSource, .commonModes)
216240
CGEvent.tapEnable(tap: eventTap, enable: true)
241+
keyEventTapActive = true
242+
return true
217243
}
218244

219245
private func startFallbackKeyMonitors() {
@@ -241,6 +267,13 @@ final class HotkeyService {
241267
} as Any)
242268
}
243269

270+
private func stopFallbackKeyMonitors() {
271+
fallbackKeyDownMonitors.forEach { NSEvent.removeMonitor($0) }
272+
fallbackKeyUpMonitors.forEach { NSEvent.removeMonitor($0) }
273+
fallbackKeyDownMonitors = []
274+
fallbackKeyUpMonitors = []
275+
}
276+
244277
private func handleKeyTapEvent(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
245278
switch type {
246279
case .tapDisabledByTimeout, .tapDisabledByUserInput:

0 commit comments

Comments
 (0)