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
13 changes: 10 additions & 3 deletions Loop/Core/Observers/KeybindTrigger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,10 @@ final class KeybindTrigger {
private func performKeybind(keyCode: CGKeyCode, type: CGEventType, isARepeat: Bool, flags: CGEventFlags, isLoopOpen: Bool) -> PerformKeybindResult {
let flagKeys = sideDependentTriggerKey ? flags.keyCodes : flags.keyCodes.baseModifiers
let allPressedKeys: Set<CGKeyCode> = pressedKeys.union(flagKeys)

let actionKeys: Set<CGKeyCode> = Set(allPressedKeys.subtracting(triggerKey).map(\.baseModifier))
let containsTrigger = allPressedKeys.isSuperset(of: triggerKey)
let allPressedKeysBaseModifiers: Set<CGKeyCode> = Set(allPressedKeys.map(\.baseModifier))

if isLoopOpen {
if pressedKeys.contains(.kVK_Escape) {
Expand All @@ -175,10 +177,9 @@ final class KeybindTrigger {
if containsTrigger {
// Try an match directly with the action keys first, then fallback to just the key code.
// This prevents failures when the user is tapping the keys in rapid succession.
let initalMatch = windowActionCache.actionsByKeybind[actionKeys]
let fallbackMatch = windowActionCache.actionsByKeybind[[keyCode]]
let match = windowActionCache.actionsByKeybind[actionKeys] ?? windowActionCache.actionsByKeybind[[keyCode]]

if let action = initalMatch ?? fallbackMatch {
if let action = match {
if !isARepeat || action.canRepeat {
openLoop(startingAction: action, overrideExistingTriggerDelayTimerAction: true)
}
Expand All @@ -196,6 +197,12 @@ final class KeybindTrigger {
)
return .opening
}
} else if let bypassedAction = windowActionCache.bypassedActionsByKeybind[allPressedKeysBaseModifiers] {
if !isARepeat || bypassedAction.canRepeat {
openLoop(startingAction: bypassedAction, overrideExistingTriggerDelayTimerAction: true)
}

return checkIfLoopOpen() ? .consume : .opening
} else {
if allPressedKeys.isEmpty {
doubleClickTimer.handleKeyUp()
Expand Down
101 changes: 19 additions & 82 deletions Loop/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -3302,6 +3302,9 @@
}
}
}
},
"Clear Keybind" : {

},
"Close" : {
"comment" : "Label for a button that closes a modal window",
Expand Down Expand Up @@ -12469,6 +12472,10 @@
}
}
},
"Link Trigger Key" : {
"comment" : "A button that links a trigger key to an action's keybind.",
"isCommentAutoGenerated" : true
},
"Locked icon alert title" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -21513,6 +21520,10 @@
}
}
},
"Please include at least one modifier key." : {
"comment" : "An error message displayed when a custom keybind is created but does not include at least one modifier key.",
"isCommentAutoGenerated" : true
},
"Position" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -30016,6 +30027,10 @@
}
}
},
"Unlink Trigger Key" : {
"comment" : "A menu item that removes the trigger key from the keybind.",
"isCommentAutoGenerated" : true
},
"Unstash" : {
"comment" : "Window action",
"localizations" : {
Expand Down Expand Up @@ -32000,87 +32015,9 @@
}
}
},
"You can only use up to %lld keys in a keybind, including the trigger key." : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "يمكنك استخدام حتى %lld مفاتيح في ربط المفتاح، بما في ذلك مفتاح التشغيل."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Du kannst in einem Tastenkürzel nur bis zu %lld Tasten verwenden, einschließlich der Aktivierungstaste."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "You can only use up to %lld keys in a keybind, including the trigger key."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Solo se pueden usar hasta %lld teclas en una combinación, incluyendo la tecla de activación"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vous pouvez utiliser un maximum de %lld touches par raccourci, touches d'activation incluses."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Puoi usare un massimo di %lld tasti nelle scorciatoie, compresi i tasti di attivazione."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "キー設定では、トリガーキーも含めて %lld キーまで使えます。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "트리거 키를 포함하여 키바인드에는 최대 %lld 개의 키만 사용할 수 있습니다."
}
},
"nl-BE" : {
"stringUnit" : {
"state" : "translated",
"value" : "Je kunt maximaal %lld toetsen gebruiken in een toetscombinatie, inclusief de activatie knop."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Você pode usar até %lld teclas em um atalho, incluindo a tecla de gatilho."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Вы можете использовать до %lld клавиш в этом сочетании, включая клавишу активации."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "在一个快捷键中,你最多只能使用%lld个按键(包括触发键)。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "在一個按鍵綁定中,你最多只能使用%lld個按鍵(包括觸發鍵)。"
}
}
}
"You can only use up to %lld keys in a keybind." : {
"comment" : "An error message displayed when a user tries to create a keybind with more than the maximum number of keys allowed. The placeholder is replaced with the actual number of keys allowed.",
"isCommentAutoGenerated" : true
},
"You can only use up to %lld keys in your trigger key." : {
"localizations" : {
Expand Down Expand Up @@ -32412,5 +32349,5 @@
}
}
},
"version" : "1.0"
"version" : "1.1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct Keycorder: View {
@Binding private var validCurrentKeybind: Set<CGKeyCode>
@State private var selectionKeybind: Set<CGKeyCode>
@Binding private var direction: WindowDirection
@Binding private var bypassTriggerKey: Bool?

@State private var eventMonitor: LocalEventMonitor?
@State private var shouldShake: Bool = false
Expand All @@ -33,6 +34,7 @@ struct Keycorder: View {
init(_ keybind: Binding<WindowAction>) {
self._validCurrentKeybind = keybind.keybind
self._direction = keybind.direction
self._bypassTriggerKey = keybind.bypassTriggerKey
self._selectionKeybind = State(initialValue: keybind.wrappedValue.keybind)
}

Expand Down Expand Up @@ -82,7 +84,7 @@ struct Keycorder: View {
isHovering = hovering
}
.onChange(of: model.currentEventMonitor) { _ in
if model.currentEventMonitor != eventMonitor {
if let eventMonitor, model.currentEventMonitor != eventMonitor {
finishedObservingKeys(wasForced: true)
}
}
Expand All @@ -104,6 +106,9 @@ struct Keycorder: View {
func startObservingKeys() {
selectionKeybind = []
isActive = true

LoopManager.shared.keybindTrigger.stop()

eventMonitor = LocalEventMonitor(events: [.keyDown, .keyUp]) { event in
// Handle regular key presses first
if event.type == .keyDown, !event.isARepeat {
Expand Down Expand Up @@ -133,61 +138,43 @@ struct Keycorder: View {
let currentKeys = selectionKeybind + [event.keyCode]
.map { $0.baseKey(flags: event.modifierFlags) }

var flags = CGEventFlags(cocoaFlags: event.modifierFlags)
var flags = CGEventFlags(
cocoaFlags: event.modifierFlags
.intersection(.deviceIndependentFlagsMask) // Prevents right/left dependence
)

if event.keyCode.isFnSpecialKey {
flags.remove(.maskSecondaryFn)
}

// Filter out trigger keys from flags
let validModifiers = flags.keyCodes.map(\.baseModifier).filter {
!Defaults[.triggerKey]
.map(\.baseModifier)
.contains($0)
let validModifiers = if bypassTriggerKey == true {
flags.keyCodes
} else {
flags.keyCodes.filter {
!Defaults[.triggerKey]
.map(\.baseModifier)
.contains($0)
}
}

let finalKeys = Set(currentKeys + validModifiers)

shouldError = false

/// Make sure we don't go over the key limit
guard finalKeys.count < keyLimit else {
errorMessage = "You can only use up to \(keyLimit) keys in a keybind, including the trigger key."
shouldShake.toggle()
guard finalKeys.count <= keyLimit else {
errorMessage = "You can only use up to \(keyLimit) keys in a keybind."
shake()
shouldError = true
return
}

shouldError = false
selectionKeybind = finalKeys
}

func finishedObservingKeys(wasForced: Bool = false) {
isActive = false
var willSet = !wasForced

if validCurrentKeybind == selectionKeybind {
willSet = false
}

if willSet {
for keybind in Defaults[.keybinds] where
keybind.keybind == selectionKeybind {
willSet = false

if let name = keybind.name, !name.isEmpty {
self.errorMessage = "That keybind is already being used by \(name)."
} else if keybind.direction == .custom {
self.errorMessage = "That keybind is already being used by another custom keybind."
} else if keybind.direction == .stash {
self.errorMessage = "That keybind is already being used by another stash keybind."
} else {
self.errorMessage = "That keybind is already being used by \(keybind.direction.name.lowercased())."
}

self.shouldShake.toggle()
self.shouldError = true
break
}
}
let willSet = !wasForced && checkValidKeybindConditions()

if willSet {
// Set the valid keybind to the current selected one
Expand All @@ -199,5 +186,56 @@ struct Keycorder: View {

eventMonitor?.stop()
eventMonitor = nil

LoopManager.shared.keybindTrigger.start()
}

private func checkValidKeybindConditions() -> Bool {
if validCurrentKeybind == selectionKeybind {
return false
}

// Validate keybind requirements when in bypass mode
if bypassTriggerKey == true,
selectionKeybind.filter(\.isModifier).isEmpty {
errorMessage = "Please include at least one modifier key."
shake()
shouldError = true
return false
}

let effectiveSelection = bypassTriggerKey == true
? selectionKeybind
: triggerKey.union(selectionKeybind)

for keybind in Defaults[.keybinds] {
let effectiveExisting = keybind.bypassTriggerKey == true
? keybind.keybind
: triggerKey.union(keybind.keybind)

guard effectiveSelection == effectiveExisting else { continue }

if let name = keybind.name, !name.isEmpty {
errorMessage = "That keybind is already being used by \(name)."
} else if keybind.direction == .custom {
errorMessage = "That keybind is already being used by another custom keybind."
} else if keybind.direction == .stash {
errorMessage = "That keybind is already being used by another stash keybind."
} else {
errorMessage = "That keybind is already being used by \(keybind.direction.name.lowercased())."
}

shake()
shouldError = true
return false
}

return true
}

private func shake() {
Task {
shouldShake.toggle()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ struct TriggerKeycorder: View {
isHovering = hovering
}
.onChange(of: model.currentEventMonitor) { _ in
if model.currentEventMonitor != eventMonitor {
if let eventMonitor, model.currentEventMonitor != eventMonitor {
finishedObservingKeys(wasForced: true)
}
}
Expand Down Expand Up @@ -129,7 +129,6 @@ struct TriggerKeycorder: View {
selectionKey = []
isActive = true

// So that if doesn't interfere with the key detection here
LoopManager.shared.keybindTrigger.stop()

eventMonitor = LocalEventMonitor(events: [.keyDown, .flagsChanged]) { event in
Expand All @@ -148,7 +147,7 @@ struct TriggerKeycorder: View {
}

if !keycodes.isEmpty, selectionKey.isEmpty {
shouldShake.toggle()
shake()
}

return nil
Expand All @@ -163,7 +162,7 @@ struct TriggerKeycorder: View {

if selectionKey.count > keyLimit {
willSet = false
shouldShake.toggle()
shake()
tooManyKeysPopup = true
}

Expand All @@ -182,6 +181,12 @@ struct TriggerKeycorder: View {

LoopManager.shared.keybindTrigger.start()
}

private func shake() {
Task {
shouldShake.toggle()
}
}
}

struct TriggerKeycorderKeyView: View {
Expand Down
Loading