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
22 changes: 15 additions & 7 deletions Voxt/App/AppDelegate+HotkeyLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ extension AppDelegate {
guard let self else { return }
self.handleCustomPasteHotkeyDown()
}
hotkeyManager.onEscapeKeyDown = { [weak self] in
self?.handleEscapeShortcut() ?? false
}
hotkeyManager.start()
VoxtLog.hotkey("Hotkey callbacks configured.")
}
Expand Down Expand Up @@ -117,26 +120,31 @@ extension AppDelegate {
}

guard event.keyCode == UInt16(kVK_Escape) else { return event }
guard handleEscapeShortcut() else { return event }
return shouldConsume ? nil : event
}

func handleEscapeShortcut() -> Bool {
guard UserDefaults.standard.object(forKey: AppPreferenceKey.escapeKeyCancelsOverlaySession) as? Bool ?? true else {
return event
return false
}
if overlayState.displayMode == .answer {
dismissAnswerOverlay()
return shouldConsume ? nil : event
return true
}
if meetingSessionCoordinator.isActive {
if meetingSessionCoordinator.overlayState.isCloseConfirmationPresented {
dismissMeetingSessionCloseConfirmation()
} else {
requestMeetingSessionCloseConfirmation()
}
return shouldConsume ? nil : event
return true
}
guard HotkeyPreference.loadTriggerMode() == .tap else { return event }
guard isSessionActive else { return event }
guard !isSelectedTextTranslationFlow else { return event }
guard HotkeyPreference.loadTriggerMode() == .tap else { return false }
guard isSessionActive else { return false }
guard !isSelectedTextTranslationFlow else { return false }
cancelActiveRecordingSession()
return shouldConsume ? nil : event
return true
}

func shouldHandleAnswerOverlayContinueShortcut(_ event: NSEvent) -> Bool {
Expand Down
69 changes: 47 additions & 22 deletions Voxt/Hotkey/HotkeyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class HotkeyManager {
var onRewriteKeyUp: (() -> Void)?
var onMeetingKeyDown: (() -> Void)?
var onCustomPasteKeyDown: (() -> Void)?
var onEscapeKeyDown: (() -> Bool)?

private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
Expand Down Expand Up @@ -202,15 +203,15 @@ class HotkeyManager {
manager.recoverEventTapIfNeeded(disabledEventType: type)
return Unmanaged.passUnretained(event)
}
manager.handleEvent(type: type, event: event)
return Unmanaged.passUnretained(event)
let consumed = manager.handleEvent(type: type, event: event)
return consumed ? nil : Unmanaged.passUnretained(event)
}

for tapLocation in [CGEventTapLocation.cghidEventTap, .cgSessionEventTap] {
if let tap = CGEvent.tapCreate(
tap: tapLocation,
place: .tailAppendEventTap,
options: .listenOnly,
options: .defaultTap,
eventsOfInterest: eventMask,
callback: callback,
userInfo: Unmanaged.passUnretained(self).toOpaque()
Expand All @@ -222,23 +223,27 @@ class HotkeyManager {
return nil
}

private func handleEvent(type: CGEventType, event: CGEvent) {
private func handleEvent(type: CGEventType, event: CGEvent) -> Bool {
guard !UserDefaults.standard.bool(forKey: AppPreferenceKey.hotkeyCaptureInProgress) else {
return
return false
}
var eventWasConsumed = false
handleResolvedEvent(
type: type,
keyCode: UInt16(event.getIntegerValueField(.keyboardEventKeycode)),
flags: event.flags,
isAutoRepeat: event.getIntegerValueField(.keyboardEventAutorepeat) != 0
isAutoRepeat: event.getIntegerValueField(.keyboardEventAutorepeat) != 0,
eventWasConsumed: &eventWasConsumed
)
return eventWasConsumed
}

private func handleResolvedEvent(
type: CGEventType,
keyCode: UInt16,
flags: CGEventFlags,
isAutoRepeat: Bool
isAutoRepeat: Bool,
eventWasConsumed: inout Bool
) {
defer {
lastEventAt = Date()
Expand Down Expand Up @@ -349,28 +354,29 @@ class HotkeyManager {
switch type {
case .keyDown:
if keyCode == translationHotkey.keyCode, translationFlagsMatch, !isAutoRepeat {
activeTranslationKeyCode = keyCode
if triggerMode == .tap {
emitTranslationKeyDown()
} else if !isTranslationKeyDown {
isTranslationKeyDown = true
activeTranslationKeyCode = keyCode
emitTranslationKeyDown()
}
eventWasConsumed = true
return
}
case .keyUp:
if triggerMode == .tap {
if activeTranslationKeyCode == keyCode {
activeTranslationKeyCode = nil
}
if keyCode == translationHotkey.keyCode {
emitTranslationKeyUp()
eventWasConsumed = true
return
}
} else if isTranslationKeyDown, activeTranslationKeyCode == keyCode {
isTranslationKeyDown = false
activeTranslationKeyCode = nil
emitTranslationKeyUp()
eventWasConsumed = true
return
}
default:
Expand Down Expand Up @@ -402,28 +408,29 @@ class HotkeyManager {
switch type {
case .keyDown:
if keyCode == rewriteHotkey.keyCode, rewriteFlagsMatch, !isAutoRepeat {
activeRewriteKeyCode = keyCode
if triggerMode == .tap {
emitRewriteKeyDown()
} else if !isRewriteKeyDown {
isRewriteKeyDown = true
activeRewriteKeyCode = keyCode
emitRewriteKeyDown()
}
eventWasConsumed = true
return
}
case .keyUp:
if triggerMode == .tap {
if activeRewriteKeyCode == keyCode {
activeRewriteKeyCode = nil
}
if keyCode == rewriteHotkey.keyCode {
emitRewriteKeyUp()
eventWasConsumed = true
return
}
} else if isRewriteKeyDown, activeRewriteKeyCode == keyCode {
isRewriteKeyDown = false
activeRewriteKeyCode = nil
emitRewriteKeyUp()
eventWasConsumed = true
return
}
default:
Expand Down Expand Up @@ -459,13 +466,15 @@ class HotkeyManager {
isCustomPasteKeyDown = true
activeCustomPasteKeyCode = keyCode
}
eventWasConsumed = true
return
}
case .keyUp:
if isCustomPasteKeyDown, activeCustomPasteKeyCode == keyCode {
isCustomPasteKeyDown = false
activeCustomPasteKeyCode = nil
emitCustomPasteKeyDown()
eventWasConsumed = true
return
}
default:
Expand Down Expand Up @@ -498,32 +507,43 @@ class HotkeyManager {
switch type {
case .keyDown:
if keyCode == meetingHotkey.keyCode, meetingFlagsMatch, !isAutoRepeat {
activeMeetingKeyCode = keyCode
if triggerMode == .tap {
emitMeetingKeyDown()
} else if !isMeetingKeyDown {
isMeetingKeyDown = true
activeMeetingKeyCode = keyCode
emitMeetingKeyDown()
}
eventWasConsumed = true
return
}
case .keyUp:
if triggerMode == .tap {
if activeMeetingKeyCode == keyCode {
activeMeetingKeyCode = nil
eventWasConsumed = true
return
}
return
}
if isMeetingKeyDown, activeMeetingKeyCode == keyCode {
isMeetingKeyDown = false
activeMeetingKeyCode = nil
eventWasConsumed = true
return
}
default:
break
}
}

if type == .keyDown,
keyCode == UInt16(kVK_Escape),
!isAutoRepeat,
onEscapeKeyDown?() == true {
eventWasConsumed = true
return
}

// Transcription path runs after translation handling.
// This keeps fn+shift and fn responsibilities separated.
if HotkeyModifierInterpreter.isModifierOnly(transcriptionHotkey) {
Expand Down Expand Up @@ -557,29 +577,31 @@ class HotkeyManager {
switch type {
case .keyDown:
guard keyCode == transcriptionHotkey.keyCode, transcriptionFlagsMatch, !isAutoRepeat else { return }
activeKeyCode = keyCode
if triggerMode == .tap {
emitKeyDown()
eventWasConsumed = true
return
} else if !isKeyDown {
isKeyDown = true
activeKeyCode = keyCode
emitKeyDown()
eventWasConsumed = true
return
}
case .keyUp:
if triggerMode == .tap {
if activeKeyCode == keyCode {
activeKeyCode = nil
}
if keyCode == transcriptionHotkey.keyCode {
emitKeyUp()
eventWasConsumed = true
return
}
return
}
if isKeyDown, activeKeyCode == keyCode {
isKeyDown = false
activeKeyCode = nil
emitKeyUp()
eventWasConsumed = true
return
}
default:
Expand Down Expand Up @@ -1331,17 +1353,20 @@ extension HotkeyManager {
let currentSidedModifiers: SidedModifierFlags
}

@discardableResult
func testingHandleEvent(
type: CGEventType,
keyCode: UInt16,
flags: CGEventFlags,
isAutoRepeat: Bool = false
) {
) -> Bool {
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
_ = recoverEventTapIfNeeded(disabledEventType: type)
return
return false
}
handleResolvedEvent(type: type, keyCode: keyCode, flags: flags, isAutoRepeat: isAutoRepeat)
var eventWasConsumed = false
handleResolvedEvent(type: type, keyCode: keyCode, flags: flags, isAutoRepeat: isAutoRepeat, eventWasConsumed: &eventWasConsumed)
return eventWasConsumed
}

func testingSetTransientState(
Expand Down
Loading