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
34 changes: 30 additions & 4 deletions macos/sol-macOS/lib/KeyboardShortcutRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Cocoa
class KeyboardShortcutRecorder {
private var isActive = false
var onShortcut: (([String]) -> Void)?
var onCancel: (() -> Void)?
private var hyperDown: Bool = false
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?

Expand Down Expand Up @@ -57,27 +59,51 @@ class KeyboardShortcutRecorder {
CGEvent
>? {
if type == .keyDown {
let code = UInt8(event.getIntegerValueField(.keyboardEventKeycode))
// Ignore Tab key
if code == UInt8(kVK_Tab) {
return nil
}
// Handle Escape key: close component
if code == UInt8(kVK_Escape) {
DispatchQueue.main.async {
self.onCancel?()
}
return nil
}
if code == UInt8(kVK_F18) {
if type == .keyDown {
hyperDown = true
} else {
hyperDown = false
}
return nil
}
// Convert CGEvent to NSEvent to use our existing parsing logic
if let nsEvent = NSEvent(cgEvent: event) {
let keys = keysFrom(event: nsEvent)

// Call the callback directly on main thread if already on main, otherwise dispatch
self.onShortcut?(keys)
}

// Consume the event to prevent it from triggering system shortcuts
return nil
}

return Unmanaged.passUnretained(event)
}

private func keysFrom(event: NSEvent) -> [String] {
var keys: [String] = []
if hyperDown {
keys.append("⌘")
keys.append("⌥")
keys.append("⌃")
keys.append("⇧")
}

if event.modifierFlags.contains(.command) { keys.append("⌘") }
if event.modifierFlags.contains(.option) { keys.append("⌥") }
if event.modifierFlags.contains(.control) { keys.append("⌃") }
if event.modifierFlags.contains(.shift) { keys.append("⇧") }

if let chars = event.charactersIgnoringModifiers {
keys.append(chars.uppercased())
}
Expand Down
1 change: 1 addition & 0 deletions macos/sol-macOS/views/KeyboardShortcutRecorderView.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
@interface RCT_EXTERN_MODULE (KeyboardShortcutRecorderViewManager,
RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(onShortcutChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCancel, RCTBubblingEventBlock)
@end
58 changes: 40 additions & 18 deletions macos/sol-macOS/views/KeyboardShortcutRecorderView.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import Cocoa

class KeyboardShortcutRecorderView: NSView {
private let textField = NSTextField(labelWithString: "Click to record shortcut")
private let instructionLabel = NSTextField(labelWithString: "Enter Key Combination")
private let valueLabel = NSTextField(labelWithString: "waiting...")
private var isRecording = false
private let recorder = KeyboardShortcutRecorder()

// Add the callback property
// Add the callback properties
@objc var onShortcutChange: RCTBubblingEventBlock?
@objc var onCancel: RCTBubblingEventBlock?

override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
Expand All @@ -21,37 +23,57 @@ class KeyboardShortcutRecorderView: NSView {
private func setupView() {
wantsLayer = true
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
layer?.cornerRadius = 6

textField.alignment = .center
textField.translatesAutoresizingMaskIntoConstraints = false
addSubview(textField)
layer?.cornerRadius = 10
layer?.shadowColor = NSColor.black.cgColor
layer?.shadowOpacity = 0.08
layer?.shadowRadius = 4
layer?.shadowOffset = CGSize(width: 0, height: 2)

instructionLabel.alignment = .center
instructionLabel.font = NSFont.boldSystemFont(ofSize: 13)
instructionLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(instructionLabel)

valueLabel.alignment = .center
valueLabel.font = NSFont.monospacedSystemFont(ofSize: 13, weight: .medium)
valueLabel.textColor = NSColor.labelColor
valueLabel.backgroundColor = NSColor.controlBackgroundColor
valueLabel.wantsLayer = true
valueLabel.translatesAutoresizingMaskIntoConstraints = false
valueLabel.drawsBackground = true
addSubview(valueLabel)

NSLayoutConstraint.activate([
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
instructionLabel.topAnchor.constraint(equalTo: topAnchor, constant: 18),
instructionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
instructionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),

valueLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 10),
valueLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
valueLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -18),
])

// let clickGesture = NSClickGestureRecognizer(target: self, action: #selector(toggleRecording))
// addGestureRecognizer(clickGesture)

recorder.onShortcut = { [weak self] keys in
self?.textField.stringValue = keys.joined(separator: " + ")
// self?.stopRecording()
// Call the callback with the shortcut
self?.valueLabel.stringValue = keys.joined(separator: " + ")
if let onShortcutChange = self?.onShortcutChange {
onShortcutChange([
"shortcut": keys
])
}
}

recorder.onCancel = { [weak self] in
self?.valueLabel.stringValue = "waiting..."
if let onCancel = self?.onCancel {
onCancel([:])
}
}

startRecording()
}

deinit {
// CRITICAL: Stop recording when view is deallocated
recorder.stopRecording()
}

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

private func startRecording() {
isRecording = true
textField.stringValue = "Recording..."
valueLabel.stringValue = "waiting..."
recorder.startRecording()
}

Expand Down
7 changes: 4 additions & 3 deletions src/components/KeyboardShortcutRecorderView.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {requireNativeComponent} from 'react-native'
import {FC} from 'react'
import {cssInterop} from 'nativewind'
import { requireNativeComponent } from 'react-native'
import { FC } from 'react'
import { cssInterop } from 'nativewind'

type Props = {
onShortcutChange: (e: any) => void
onCancel: () => void
style?: any
className?: string
}
Expand Down
18 changes: 16 additions & 2 deletions src/stores/ui.store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,14 @@ export const createUIStore = (root: IRootStore) => {
},

setShortcut(id: string, shortcut: string) {
// Check for duplicate shortcut
if (shortcut !== "") {
const isDuplicate = Object.entries(store.shortcuts).some(([key, value]) => value === shortcut && key !== id)
if (isDuplicate) {
solNative.showToast('Shortcut already exists', 'error', 4)
return
}
}
store.shortcuts[id] = shortcut
solNative.updateHotkeys(toJS(store.shortcuts))
},
Expand Down Expand Up @@ -962,14 +970,20 @@ export const createUIStore = (root: IRootStore) => {
applicationsChanged: () => {
store.getApps()
},
closeKeyboardRecorder: () => {
store.showKeyboardRecorder = false
store.keyboardRecorderSelectedItem = null
},
setShowKeyboardRecorderForItem: (show: boolean, itemId: string) => {
store.showKeyboardRecorder = show
store.keyboardRecorderSelectedItem = itemId
},
setShortcutFromUI: (shortcut: string[]) => {
setTimeout(() => {
store.showKeyboardRecorder = false
}, 1000)
runInAction(() => {
store.showKeyboardRecorder = false
})
}, 2000)

let itemId = store.keyboardRecorderSelectedItem
store.keyboardRecorderSelectedItem = null
Expand Down
5 changes: 4 additions & 1 deletion src/widgets/settings.widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ export const SettingsWidget: FC = observer(() => {
{showKeyboardRecorder && (
<View className="absolute top-0 bottom-0 left-0 right-0 bg-black/80 items-center justify-center">
<KeyboardShortcutRecorderView
className={'w-80 h-20'}
className={'w-80 h-24'}
onShortcutChange={e => {
store.ui.setShortcutFromUI(e.nativeEvent.shortcut)
}}
onCancel={() => {
store.ui.closeKeyboardRecorder()
}}
/>
</View>
)}
Expand Down