mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-11-09 17:04:27 -06: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 { IpcEvents } from '@common/ipc-events'
|
||||||
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard'
|
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard'
|
||||||
import { KeyboardReport } from '@renderer/libs/keyboard/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 => {
|
export const Keyboard = (): ReactElement => {
|
||||||
const isKeyboardEnabled = useAtomValue(isKeyboardEnableAtom)
|
const isKeyboardEnabled = useAtomValue(isKeyboardEnableAtom)
|
||||||
|
|
||||||
const keyboardRef = useRef(new KeyboardReport())
|
const keyboardRef = useRef(new KeyboardReport())
|
||||||
const pressedKeys = useRef(new Set<string>())
|
const pressedKeys = useRef(new Set<string>())
|
||||||
|
const altGrState = useRef<AltGrState | null>(null)
|
||||||
// Keyboard handler
|
const isComposing = useRef(false)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
initAltGr()
|
||||||
|
|
||||||
if (!isKeyboardEnabled) {
|
if (!isKeyboardEnabled) {
|
||||||
releaseKeys()
|
releaseKeys()
|
||||||
return
|
return
|
||||||
@@ -31,6 +31,8 @@ export const Keyboard = (): ReactElement => {
|
|||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
document.addEventListener('keyup', handleKeyUp)
|
document.addEventListener('keyup', handleKeyUp)
|
||||||
|
document.addEventListener('compositionstart', handleCompositionStart)
|
||||||
|
document.addEventListener('compositionend', handleCompositionEnd)
|
||||||
window.addEventListener('blur', handleBlur)
|
window.addEventListener('blur', handleBlur)
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
|
||||||
@@ -38,6 +40,9 @@ export const Keyboard = (): ReactElement => {
|
|||||||
async function handleKeyDown(event: KeyboardEvent): Promise<void> {
|
async function handleKeyDown(event: KeyboardEvent): Promise<void> {
|
||||||
if (!isKeyboardEnabled) return
|
if (!isKeyboardEnabled) return
|
||||||
|
|
||||||
|
// Skip during IME composition
|
||||||
|
if (isComposing.current || event.isComposing) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
@@ -46,6 +51,20 @@ export const Keyboard = (): ReactElement => {
|
|||||||
return
|
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)
|
pressedKeys.current.add(code)
|
||||||
await handleKeyEvent({ type: 'keydown', code })
|
await handleKeyEvent({ type: 'keydown', code })
|
||||||
}
|
}
|
||||||
@@ -54,24 +73,51 @@ export const Keyboard = (): ReactElement => {
|
|||||||
async function handleKeyUp(event: KeyboardEvent): Promise<void> {
|
async function handleKeyUp(event: KeyboardEvent): Promise<void> {
|
||||||
if (!isKeyboardEnabled) return
|
if (!isKeyboardEnabled) return
|
||||||
|
|
||||||
|
if (isComposing.current || event.isComposing) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const code = event.code
|
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') {
|
if (code === 'MetaLeft' || code === 'MetaRight') {
|
||||||
|
const keysToRelease: string[] = []
|
||||||
pressedKeys.current.forEach((pressedCode) => {
|
pressedKeys.current.forEach((pressedCode) => {
|
||||||
if (!isModifier(pressedCode)) {
|
if (!isModifier(pressedCode)) {
|
||||||
handleKeyEvent({ type: 'keyup', code: pressedCode });
|
keysToRelease.push(pressedCode)
|
||||||
pressedKeys.current.delete(pressedCode);
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
|
for (const key of keysToRelease) {
|
||||||
|
await handleKeyEvent({ type: 'keyup', code: key })
|
||||||
|
pressedKeys.current.delete(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pressedKeys.current.delete(code)
|
pressedKeys.current.delete(code)
|
||||||
await handleKeyEvent({ type: 'keyup', 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
|
// Release all keys when window loses focus
|
||||||
async function handleBlur(): Promise<void> {
|
async function handleBlur(): Promise<void> {
|
||||||
await releaseKeys()
|
await releaseKeys()
|
||||||
@@ -92,6 +138,12 @@ export const Keyboard = (): ReactElement => {
|
|||||||
|
|
||||||
pressedKeys.current.clear()
|
pressedKeys.current.clear()
|
||||||
|
|
||||||
|
// Reset AltGr state
|
||||||
|
if (altGrState.current) {
|
||||||
|
altGrState.current.active = false
|
||||||
|
altGrState.current.ctrlLeftTimestamp = 0
|
||||||
|
}
|
||||||
|
|
||||||
const report = keyboardRef.current.reset()
|
const report = keyboardRef.current.reset()
|
||||||
await sendReport(report)
|
await sendReport(report)
|
||||||
}
|
}
|
||||||
@@ -99,10 +151,33 @@ export const Keyboard = (): ReactElement => {
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
document.removeEventListener('keyup', handleKeyUp)
|
document.removeEventListener('keyup', handleKeyUp)
|
||||||
|
document.removeEventListener('compositionstart', handleCompositionStart)
|
||||||
|
document.removeEventListener('compositionend', handleCompositionEnd)
|
||||||
window.removeEventListener('blur', handleBlur)
|
window.removeEventListener('blur', handleBlur)
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
|
||||||
|
releaseKeys()
|
||||||
}
|
}
|
||||||
}, [isKeyboardEnabled])
|
}, [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 <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user