import { useEffect, useMemo, useState } from 'react' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { getConflictingShortcutActionIds, formatKeyBinding, isSpaceBinding, keyBindingFromKeyboardEvent, SHORTCUT_DEFINITIONS, SHORTCUT_SCOPE_LABELS, } from '@/lib/keyboardShortcuts' import { getEffectiveKeyBindings } from '@/lib/settings' import { useKeyboardShortcutSettings } from '@/lib/useKeyboardShortcuts' import type { ShortcutActionId } from '@/lib/keyboardShortcuts' import type { ClientKeyboardSettings } from '@/lib/settings' import type { FC } from 'react' type Props = { sectionClassName: string onDirtyStateChange?: ( dirty: boolean, discard: () => void, ) => void } const KeyboardSettingsSection: FC = ( { sectionClassName, onDirtyStateChange }, ) => { const { keyboardSettings, saveKeyboardSettings, } = useKeyboardShortcutSettings () const [capturingActionId, setCapturingActionId] = useState (null) const [draftKeyboardSettings, setDraftKeyboardSettings] = useState (keyboardSettings) useEffect (() => { setDraftKeyboardSettings (keyboardSettings) }, [keyboardSettings]) const draftEffectiveBindings = useMemo ( () => getEffectiveKeyBindings (draftKeyboardSettings.bindings), [draftKeyboardSettings.bindings], ) const draftConflictActionIds = useMemo ( () => getConflictingShortcutActionIds (draftEffectiveBindings), [draftEffectiveBindings], ) const hasUnsavedChanges = useMemo ( () => JSON.stringify (draftKeyboardSettings) !== JSON.stringify (keyboardSettings), [draftKeyboardSettings, keyboardSettings], ) useEffect (() => { if (capturingActionId == null) return const onKeyDown = (event: KeyboardEvent) => { if (event.isComposing || event.key === 'Process') return event.preventDefault () event.stopPropagation () if (event.key === 'Escape') { setCapturingActionId (null) return } if (event.key === 'Backspace' || event.key === 'Delete') { setDraftKeyboardSettings (current => ({ ...current, bindings: { ...(current.bindings ?? { }), [capturingActionId]: null, }, })) setCapturingActionId (null) return } const binding = keyBindingFromKeyboardEvent (event) if (binding == null) return if (isSpaceBinding (binding)) { toast ({ title: 'Space は今は割り当てられません.' }) return } setDraftKeyboardSettings (current => ({ ...current, bindings: { ...(current.bindings ?? { }), [capturingActionId]: binding, }, })) setCapturingActionId (null) } document.addEventListener ('keydown', onKeyDown, true) return () => document.removeEventListener ('keydown', onKeyDown, true) }, [capturingActionId]) const rows = useMemo (() => SHORTCUT_DEFINITIONS.map (definition => ({ ...definition, currentBinding: draftEffectiveBindings[definition.id], hasConflict: draftConflictActionIds.has (definition.id), hasCustomBinding: Object.prototype.hasOwnProperty.call ( draftKeyboardSettings.bindings ?? { }, definition.id, ), })), [ draftConflictActionIds, draftEffectiveBindings, draftKeyboardSettings.bindings, ]) const handleSave = () => { saveKeyboardSettings (draftKeyboardSettings) toast ({ title: 'キーボード設定を保存しました' }) } const handleDiscard = () => { setDraftKeyboardSettings (keyboardSettings) setCapturingActionId (null) } useEffect (() => { onDirtyStateChange?.(hasUnsavedChanges, handleDiscard) return () => { onDirtyStateChange?.(false, () => { }) } }, [handleDiscard, hasUnsavedChanges, onDirtyStateChange]) return (

キーボード

{capturingActionId != null && (

キー入力待ちです.Escape で取消し,Backspace / Delete で未割当にできます.

)}
{rows.map (row => ( ))}
操作 適用範囲 現在のキー 競合状態
{row.label} {SHORTCUT_SCOPE_LABELS[row.scope]} {formatKeyBinding (row.currentBinding)} {row.hasConflict ? 競合あり : 'なし'}
) } export default KeyboardSettingsSection