mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-09-11 05:59:57 -05:00
fix(desktop): fix keyboard issues
compatible with IME composition compatible with Windows AltGr key
This commit is contained in:
@@ -4,26 +4,26 @@ import { useAtomValue } from 'jotai'
|
||||
import { IpcEvents } from '@common/ipc-events'
|
||||
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard'
|
||||
import { KeyboardReport } from '@renderer/libs/keyboard/keyboard'
|
||||
import { isModifier } from "@renderer/libs/keyboard/keymap"
|
||||
import { isModifier } from '@renderer/libs/keyboard/keymap'
|
||||
|
||||
interface AltGrState {
|
||||
active: boolean
|
||||
ctrlLeftTimestamp: number
|
||||
}
|
||||
|
||||
const ALTGR_THRESHOLD_MS = 10
|
||||
|
||||
export const Keyboard = (): ReactElement => {
|
||||
const isKeyboardEnabled = useAtomValue(isKeyboardEnableAtom)
|
||||
|
||||
const keyboardRef = useRef(new KeyboardReport())
|
||||
const pressedKeys = useRef(new Set<string>())
|
||||
|
||||
// Keyboard handler
|
||||
async function handleKeyEvent(event: { type: 'keydown' | 'keyup'; code: string }): Promise<void> {
|
||||
const kb = keyboardRef.current
|
||||
const report = event.type === 'keydown' ? kb.keyDown(event.code) : kb.keyUp(event.code)
|
||||
await sendReport(report)
|
||||
}
|
||||
|
||||
async function sendReport(report: number[]): Promise<void> {
|
||||
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_KEYBOARD, report)
|
||||
}
|
||||
const altGrState = useRef<AltGrState | null>(null)
|
||||
const isComposing = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
initAltGr()
|
||||
|
||||
if (!isKeyboardEnabled) {
|
||||
releaseKeys()
|
||||
return
|
||||
@@ -31,6 +31,8 @@ export const Keyboard = (): ReactElement => {
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('keyup', handleKeyUp)
|
||||
document.addEventListener('compositionstart', handleCompositionStart)
|
||||
document.addEventListener('compositionend', handleCompositionEnd)
|
||||
window.addEventListener('blur', handleBlur)
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
@@ -38,6 +40,9 @@ export const Keyboard = (): ReactElement => {
|
||||
async function handleKeyDown(event: KeyboardEvent): Promise<void> {
|
||||
if (!isKeyboardEnabled) return
|
||||
|
||||
// Skip during IME composition
|
||||
if (isComposing.current || event.isComposing) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -46,6 +51,20 @@ export const Keyboard = (): ReactElement => {
|
||||
return
|
||||
}
|
||||
|
||||
// When AltGr is pressed, browsers send ControlLeft followed immediately by AltRight
|
||||
if (altGrState.current) {
|
||||
if (code === 'ControlLeft') {
|
||||
altGrState.current.ctrlLeftTimestamp = event.timeStamp
|
||||
} else if (code === 'AltRight') {
|
||||
const timeDiff = event.timeStamp - altGrState.current.ctrlLeftTimestamp
|
||||
if (timeDiff < ALTGR_THRESHOLD_MS && pressedKeys.current.has('ControlLeft')) {
|
||||
pressedKeys.current.delete('ControlLeft')
|
||||
handleKeyEvent({ type: 'keyup', code: 'ControlLeft' })
|
||||
altGrState.current.active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pressedKeys.current.add(code)
|
||||
await handleKeyEvent({ type: 'keydown', code })
|
||||
}
|
||||
@@ -54,24 +73,51 @@ export const Keyboard = (): ReactElement => {
|
||||
async function handleKeyUp(event: KeyboardEvent): Promise<void> {
|
||||
if (!isKeyboardEnabled) return
|
||||
|
||||
if (isComposing.current || event.isComposing) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const code = event.code
|
||||
|
||||
// Handle AltGr state for Windows
|
||||
if (altGrState.current?.active) {
|
||||
if (code === 'ControlLeft') return
|
||||
|
||||
if (code === 'AltRight') {
|
||||
altGrState.current.active = false
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible with macOS's command key combinations
|
||||
if (code === 'MetaLeft' || code === 'MetaRight') {
|
||||
const keysToRelease: string[] = []
|
||||
pressedKeys.current.forEach((pressedCode) => {
|
||||
if (!isModifier(pressedCode)) {
|
||||
handleKeyEvent({ type: 'keyup', code: pressedCode });
|
||||
pressedKeys.current.delete(pressedCode);
|
||||
keysToRelease.push(pressedCode)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
for (const key of keysToRelease) {
|
||||
await handleKeyEvent({ type: 'keyup', code: key })
|
||||
pressedKeys.current.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
pressedKeys.current.delete(code)
|
||||
await handleKeyEvent({ type: 'keyup', code })
|
||||
}
|
||||
|
||||
// Composition start event
|
||||
function handleCompositionStart(): void {
|
||||
isComposing.current = true
|
||||
}
|
||||
|
||||
// Composition end event
|
||||
function handleCompositionEnd(): void {
|
||||
isComposing.current = false
|
||||
}
|
||||
|
||||
// Release all keys when window loses focus
|
||||
async function handleBlur(): Promise<void> {
|
||||
await releaseKeys()
|
||||
@@ -92,6 +138,12 @@ export const Keyboard = (): ReactElement => {
|
||||
|
||||
pressedKeys.current.clear()
|
||||
|
||||
// Reset AltGr state
|
||||
if (altGrState.current) {
|
||||
altGrState.current.active = false
|
||||
altGrState.current.ctrlLeftTimestamp = 0
|
||||
}
|
||||
|
||||
const report = keyboardRef.current.reset()
|
||||
await sendReport(report)
|
||||
}
|
||||
@@ -99,10 +151,33 @@ export const Keyboard = (): ReactElement => {
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('keyup', handleKeyUp)
|
||||
document.removeEventListener('compositionstart', handleCompositionStart)
|
||||
document.removeEventListener('compositionend', handleCompositionEnd)
|
||||
window.removeEventListener('blur', handleBlur)
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
releaseKeys()
|
||||
}
|
||||
}, [isKeyboardEnabled])
|
||||
|
||||
async function initAltGr(): Promise<void> {
|
||||
const platform = await window.electron.ipcRenderer.invoke(IpcEvents.GET_PLATFORM)
|
||||
const isWin = platform.toLowerCase().startsWith('win')
|
||||
if (isWin && !altGrState.current) {
|
||||
altGrState.current = { active: false, ctrlLeftTimestamp: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard handler
|
||||
async function handleKeyEvent(event: { type: 'keydown' | 'keyup'; code: string }): Promise<void> {
|
||||
const kb = keyboardRef.current
|
||||
const report = event.type === 'keydown' ? kb.keyDown(event.code) : kb.keyUp(event.code)
|
||||
await sendReport(report)
|
||||
}
|
||||
|
||||
async function sendReport(report: number[]): Promise<void> {
|
||||
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_KEYBOARD, report)
|
||||
}
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user