fix(desktop): fix keyboard issues

compatible with IME composition
compatible with Windows AltGr key
This commit is contained in:
wj-xiao
2026-01-19 16:42:42 +08:00
parent cd8ef315bc
commit e665814798

View File

@@ -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 <></>
}