Skip to content

Commit 17bdf5e

Browse files
authored
Merge pull request #215 from ospfranco/fix-keyboard-recorder
Fix keyboard recorder
2 parents 5b9c586 + 673b681 commit 17bdf5e

File tree

6 files changed

+95
-28
lines changed

6 files changed

+95
-28
lines changed

macos/sol-macOS/lib/KeyboardShortcutRecorder.swift

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import Cocoa
44
class KeyboardShortcutRecorder {
55
private var isActive = false
66
var onShortcut: (([String]) -> Void)?
7+
var onCancel: (() -> Void)?
8+
private var hyperDown: Bool = false
79
private var eventTap: CFMachPort?
810
private var runLoopSource: CFRunLoopSource?
911

@@ -57,27 +59,51 @@ class KeyboardShortcutRecorder {
5759
CGEvent
5860
>? {
5961
if type == .keyDown {
62+
let code = UInt8(event.getIntegerValueField(.keyboardEventKeycode))
63+
// Ignore Tab key
64+
if code == UInt8(kVK_Tab) {
65+
return nil
66+
}
67+
// Handle Escape key: close component
68+
if code == UInt8(kVK_Escape) {
69+
DispatchQueue.main.async {
70+
self.onCancel?()
71+
}
72+
return nil
73+
}
74+
if code == UInt8(kVK_F18) {
75+
if type == .keyDown {
76+
hyperDown = true
77+
} else {
78+
hyperDown = false
79+
}
80+
return nil
81+
}
6082
// Convert CGEvent to NSEvent to use our existing parsing logic
6183
if let nsEvent = NSEvent(cgEvent: event) {
6284
let keys = keysFrom(event: nsEvent)
63-
64-
// Call the callback directly on main thread if already on main, otherwise dispatch
6585
self.onShortcut?(keys)
6686
}
67-
6887
// Consume the event to prevent it from triggering system shortcuts
6988
return nil
7089
}
71-
7290
return Unmanaged.passUnretained(event)
7391
}
7492

7593
private func keysFrom(event: NSEvent) -> [String] {
7694
var keys: [String] = []
95+
if hyperDown {
96+
keys.append("")
97+
keys.append("")
98+
keys.append("")
99+
keys.append("")
100+
}
101+
77102
if event.modifierFlags.contains(.command) { keys.append("") }
78103
if event.modifierFlags.contains(.option) { keys.append("") }
79104
if event.modifierFlags.contains(.control) { keys.append("") }
80105
if event.modifierFlags.contains(.shift) { keys.append("") }
106+
81107
if let chars = event.charactersIgnoringModifiers {
82108
keys.append(chars.uppercased())
83109
}

macos/sol-macOS/views/KeyboardShortcutRecorderView.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
@interface RCT_EXTERN_MODULE (KeyboardShortcutRecorderViewManager,
44
RCTViewManager)
55
RCT_EXPORT_VIEW_PROPERTY(onShortcutChange, RCTBubblingEventBlock)
6+
RCT_EXPORT_VIEW_PROPERTY(onCancel, RCTBubblingEventBlock)
67
@end

macos/sol-macOS/views/KeyboardShortcutRecorderView.swift

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import Cocoa
22

33
class KeyboardShortcutRecorderView: NSView {
4-
private let textField = NSTextField(labelWithString: "Click to record shortcut")
4+
private let instructionLabel = NSTextField(labelWithString: "Enter Key Combination")
5+
private let valueLabel = NSTextField(labelWithString: "waiting...")
56
private var isRecording = false
67
private let recorder = KeyboardShortcutRecorder()
78

8-
// Add the callback property
9+
// Add the callback properties
910
@objc var onShortcutChange: RCTBubblingEventBlock?
11+
@objc var onCancel: RCTBubblingEventBlock?
1012

1113
override init(frame frameRect: NSRect) {
1214
super.init(frame: frameRect)
@@ -21,37 +23,57 @@ class KeyboardShortcutRecorderView: NSView {
2123
private func setupView() {
2224
wantsLayer = true
2325
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
24-
layer?.cornerRadius = 6
25-
26-
textField.alignment = .center
27-
textField.translatesAutoresizingMaskIntoConstraints = false
28-
addSubview(textField)
26+
layer?.cornerRadius = 10
27+
layer?.shadowColor = NSColor.black.cgColor
28+
layer?.shadowOpacity = 0.08
29+
layer?.shadowRadius = 4
30+
layer?.shadowOffset = CGSize(width: 0, height: 2)
31+
32+
instructionLabel.alignment = .center
33+
instructionLabel.font = NSFont.boldSystemFont(ofSize: 13)
34+
instructionLabel.translatesAutoresizingMaskIntoConstraints = false
35+
addSubview(instructionLabel)
36+
37+
valueLabel.alignment = .center
38+
valueLabel.font = NSFont.monospacedSystemFont(ofSize: 13, weight: .medium)
39+
valueLabel.textColor = NSColor.labelColor
40+
valueLabel.backgroundColor = NSColor.controlBackgroundColor
41+
valueLabel.wantsLayer = true
42+
valueLabel.translatesAutoresizingMaskIntoConstraints = false
43+
valueLabel.drawsBackground = true
44+
addSubview(valueLabel)
2945

3046
NSLayoutConstraint.activate([
31-
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
32-
textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
33-
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
47+
instructionLabel.topAnchor.constraint(equalTo: topAnchor, constant: 18),
48+
instructionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
49+
instructionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
50+
51+
valueLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 10),
52+
valueLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
53+
valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
54+
valueLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -18),
3455
])
3556

36-
// let clickGesture = NSClickGestureRecognizer(target: self, action: #selector(toggleRecording))
37-
// addGestureRecognizer(clickGesture)
38-
3957
recorder.onShortcut = { [weak self] keys in
40-
self?.textField.stringValue = keys.joined(separator: " + ")
41-
// self?.stopRecording()
42-
// Call the callback with the shortcut
58+
self?.valueLabel.stringValue = keys.joined(separator: " + ")
4359
if let onShortcutChange = self?.onShortcutChange {
4460
onShortcutChange([
4561
"shortcut": keys
4662
])
4763
}
4864
}
4965

66+
recorder.onCancel = { [weak self] in
67+
self?.valueLabel.stringValue = "waiting..."
68+
if let onCancel = self?.onCancel {
69+
onCancel([:])
70+
}
71+
}
72+
5073
startRecording()
5174
}
5275

5376
deinit {
54-
// CRITICAL: Stop recording when view is deallocated
5577
recorder.stopRecording()
5678
}
5779

@@ -63,7 +85,7 @@ class KeyboardShortcutRecorderView: NSView {
6385

6486
private func startRecording() {
6587
isRecording = true
66-
textField.stringValue = "Recording..."
88+
valueLabel.stringValue = "waiting..."
6789
recorder.startRecording()
6890
}
6991

src/components/KeyboardShortcutRecorderView.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import {requireNativeComponent} from 'react-native'
2-
import {FC} from 'react'
3-
import {cssInterop} from 'nativewind'
1+
import { requireNativeComponent } from 'react-native'
2+
import { FC } from 'react'
3+
import { cssInterop } from 'nativewind'
44

55
type Props = {
66
onShortcutChange: (e: any) => void
7+
onCancel: () => void
78
style?: any
89
className?: string
910
}

src/stores/ui.store.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,14 @@ export const createUIStore = (root: IRootStore) => {
927927
},
928928

929929
setShortcut(id: string, shortcut: string) {
930+
// Check for duplicate shortcut
931+
if (shortcut !== "") {
932+
const isDuplicate = Object.entries(store.shortcuts).some(([key, value]) => value === shortcut && key !== id)
933+
if (isDuplicate) {
934+
solNative.showToast('Shortcut already exists', 'error', 4)
935+
return
936+
}
937+
}
930938
store.shortcuts[id] = shortcut
931939
solNative.updateHotkeys(toJS(store.shortcuts))
932940
},
@@ -962,14 +970,20 @@ export const createUIStore = (root: IRootStore) => {
962970
applicationsChanged: () => {
963971
store.getApps()
964972
},
973+
closeKeyboardRecorder: () => {
974+
store.showKeyboardRecorder = false
975+
store.keyboardRecorderSelectedItem = null
976+
},
965977
setShowKeyboardRecorderForItem: (show: boolean, itemId: string) => {
966978
store.showKeyboardRecorder = show
967979
store.keyboardRecorderSelectedItem = itemId
968980
},
969981
setShortcutFromUI: (shortcut: string[]) => {
970982
setTimeout(() => {
971-
store.showKeyboardRecorder = false
972-
}, 1000)
983+
runInAction(() => {
984+
store.showKeyboardRecorder = false
985+
})
986+
}, 2000)
973987

974988
let itemId = store.keyboardRecorderSelectedItem
975989
store.keyboardRecorderSelectedItem = null

src/widgets/settings.widget.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ export const SettingsWidget: FC = observer(() => {
2929
{showKeyboardRecorder && (
3030
<View className="absolute top-0 bottom-0 left-0 right-0 bg-black/80 items-center justify-center">
3131
<KeyboardShortcutRecorderView
32-
className={'w-80 h-20'}
32+
className={'w-80 h-24'}
3333
onShortcutChange={e => {
3434
store.ui.setShortcutFromUI(e.nativeEvent.shortcut)
3535
}}
36+
onCancel={() => {
37+
store.ui.closeKeyboardRecorder()
38+
}}
3639
/>
3740
</View>
3841
)}

0 commit comments

Comments
 (0)