14 phase updated

This commit is contained in:
yunchan8804 2026-04-08 00:14:12 +09:00
parent 023714442d
commit dd68fc9ccd
3 changed files with 1003 additions and 25 deletions

View file

@ -476,28 +476,32 @@ class HotkeyService extends EventEmitter {
// 핫키에 지정된 수정자가 모두 눌려 있어야 하고,
// 지정되지 않은 수정자는 눌려 있으면 안 된다.
//
// 단, 핫키의 주 키 자체가 수정자 키인 경우(예: Right Alt)에는
// 해당 수정자의 altKey 등이 true로 올 수 있으므로, 주 키가 수정자인 경우
// 해당 수정자 검사를 건너뛴다.
const isKeyModifier = this._isModifierKeyCode(config.keyCode)
// 단, 핫키의 주 키 자체가 수정자 키인 경우(예: Right Alt 단독):
// - 해당 수정자의 altKey 등이 true로 올 수 있으므로,
// modifiers에 해당 modifier가 **없을 때만** 이벤트의 해당 플래그를 무시한다.
// - modifiers에 해당 modifier가 **있으면** (예: Alt+Right Alt은 불가하므로)
// 해당 플래그가 true여야 매칭 성공.
//
// 예: Right Alt 단독 (keyCode=AltRight, modifiers=[])
// → e.altKey가 true여도 wantsAlt=false이므로 bypass → match
// 예: Ctrl+Right Alt (keyCode=AltRight, modifiers=['ctrl'])
// → e.altKey bypass, e.ctrlKey===true 체크 → match only with Ctrl held
const wantsCtrl = config.modifiers.includes('ctrl')
const wantsAlt = config.modifiers.includes('alt')
const wantsShift = config.modifiers.includes('shift')
const wantsMeta = config.modifiers.includes('meta')
const ctrlMatch = isKeyModifier && this._isCtrlKeyCode(config.keyCode)
? true
: e.ctrlKey === wantsCtrl
const altMatch = isKeyModifier && this._isAltKeyCode(config.keyCode)
? true
: e.altKey === wantsAlt
const shiftMatch = isKeyModifier && this._isShiftKeyCode(config.keyCode)
? true
: e.shiftKey === wantsShift
const metaMatch = isKeyModifier && this._isMetaKeyCode(config.keyCode)
? true
: e.metaKey === wantsMeta
// 주 키 자체가 해당 modifier인 경우, 그 modifier의 이벤트 플래그는 무시
const skipCtrl = this._isCtrlKeyCode(config.keyCode) && !wantsCtrl
const skipAlt = this._isAltKeyCode(config.keyCode) && !wantsAlt
const skipShift = this._isShiftKeyCode(config.keyCode) && !wantsShift
const skipMeta = this._isMetaKeyCode(config.keyCode) && !wantsMeta
const ctrlMatch = skipCtrl || (e.ctrlKey === wantsCtrl)
const altMatch = skipAlt || (e.altKey === wantsAlt)
const shiftMatch = skipShift || (e.shiftKey === wantsShift)
const metaMatch = skipMeta || (e.metaKey === wantsMeta)
if (ctrlMatch && altMatch && shiftMatch && metaMatch) {
return config

View file

@ -85,6 +85,10 @@ export function HotkeyRecordModal({
const [captured, setCaptured] = useState<Array<{ keyCode: number; name: string }> | null>(null)
const [error, setError] = useState<string | null>(null)
const pressedRef = useRef<Map<number, string>>(new Map())
// modifier-only 확정을 위한 타이머 (Alt만 눌렀을 때 바로 확정하지 않고 잠시 대기)
const modifierTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// 가장 최근 pressedKeys 스냅샷 (타이머 콜백에서 stale closure 방지)
const lastPressedRef = useRef<Array<{ keyCode: number; name: string }>>([])
// Modal 열릴 때 리셋
useEffect(() => {
@ -93,6 +97,11 @@ export function HotkeyRecordModal({
setCaptured(null)
setError(null)
pressedRef.current.clear()
lastPressedRef.current = []
if (modifierTimerRef.current) {
clearTimeout(modifierTimerRef.current)
modifierTimerRef.current = null
}
}
}, [open])
@ -101,6 +110,12 @@ export function HotkeyRecordModal({
e.preventDefault()
e.stopPropagation()
// 새 키가 눌렸으므로 modifier-only 타이머 취소
if (modifierTimerRef.current) {
clearTimeout(modifierTimerRef.current)
modifierTimerRef.current = null
}
const keyCode = e.keyCode || e.which
if (pressedRef.current.has(keyCode)) return
@ -109,8 +124,9 @@ export function HotkeyRecordModal({
const keys = Array.from(pressedRef.current.entries()).map(([kc, n]) => ({ keyCode: kc, name: n }))
setPressedKeys(keys)
lastPressedRef.current = keys
// modifier가 아닌 키가 눌리면 → 조합 확정
// modifier가 아닌 키가 눌리면 → 조합 즉시 확정 (Alt+1, Ctrl+F5 등)
if (!isModifier(keyCode)) {
setCaptured(keys)
}
@ -122,14 +138,20 @@ export function HotkeyRecordModal({
const keyCode = e.keyCode || e.which
pressedRef.current.delete(keyCode)
// 모든 키를 놓았고 modifier만 눌렀었다면 → 단일 modifier 확정
if (pressedRef.current.size === 0 && pressedKeys.length > 0 && pressedKeys.every(k => isModifier(k.keyCode))) {
setCaptured(pressedKeys)
// 모든 키를 놓았고 modifier만 눌렀었다면 → 500ms 대기 후 확정
// 이 대기 시간 동안 추가 키를 누르면 타이머가 취소되어 조합키로 확장 가능
if (pressedRef.current.size === 0 && lastPressedRef.current.length > 0 && lastPressedRef.current.every(k => isModifier(k.keyCode))) {
if (modifierTimerRef.current) clearTimeout(modifierTimerRef.current)
modifierTimerRef.current = setTimeout(() => {
// 타이머 만료: 여전히 confirmed 안됐고 추가 키 입력 없으면 단일 modifier로 확정
setCaptured(lastPressedRef.current)
modifierTimerRef.current = null
}, 500)
}
const keys = Array.from(pressedRef.current.entries()).map(([kc, n]) => ({ keyCode: kc, name: n }))
setPressedKeys(keys)
}, [captured, pressedKeys])
}, [captured])
useEffect(() => {
if (!open) return
@ -159,15 +181,27 @@ export function HotkeyRecordModal({
}
// HotkeyBinding 생성
// 주 키(main key) = modifier가 아닌 키가 있으면 그것, 없으면 첫 번째 modifier
const mainKey = captured.find(k => !isModifier(k.keyCode))
const primaryKeyCode = mainKey?.keyCode ?? captured[0].keyCode
// modifier 플래그: 주 키 자체가 해당 modifier인 경우 false 유지
// 예: Right Alt 단독 → keyCode=165, alt=false (주 키가 Alt 자체이므로)
// 예: Ctrl+A → keyCode=A, ctrl=true (Ctrl은 modifier로 사용)
const isCtrlKey = (kc: number) => kc === 17 || kc === 162 || kc === 163
const isAltKey = (kc: number) => kc === 18 || kc === 164 || kc === 165
const isShiftKey = (kc: number) => kc === 16 || kc === 160 || kc === 161
const isMetaKey = (kc: number) => kc === 91 || kc === 92
// 주 키를 제외한 나머지 키들만 modifier로 취급
const modifierKeys = captured.filter(k => k.keyCode !== primaryKeyCode)
const binding: HotkeyBinding = {
keyCode: primaryKeyCode,
ctrl: captured.some(k => k.keyCode === 17 || k.keyCode === 162 || k.keyCode === 163),
alt: captured.some(k => k.keyCode === 18 || k.keyCode === 164 || k.keyCode === 165),
shift: captured.some(k => k.keyCode === 16 || k.keyCode === 160 || k.keyCode === 161),
meta: captured.some(k => k.keyCode === 91 || k.keyCode === 92),
ctrl: modifierKeys.some(k => isCtrlKey(k.keyCode)),
alt: modifierKeys.some(k => isAltKey(k.keyCode)),
shift: modifierKeys.some(k => isShiftKey(k.keyCode)),
meta: modifierKeys.some(k => isMetaKey(k.keyCode)),
displayLabel: label,
}