/** * IDEA's "Keyboard Shortcut" dialog: records a first (and optional second) * stroke and lists conflicts live; OK with conflicts asks whether to remove the * other assignments (Remove / Leave / Cancel). With `mouse` it is the "Mouse * Shortcut" dialog: a click on its pad records the button and modifiers. */ import { useEffect, useMemo, useState, type MouseEvent } from "react"; import { Checkbox, Dialog, Icon, Kbd, T } from "@/ui"; import type { Action } from "@/platform/actions"; import { formatBinding, mouseStrokeOf, normalizeBinding, suspendKeyboardDispatch } from "@/platform/keymap"; import { ask } from "@/workbench/messages"; import { findConflicts, resolveBindings, type Conflict, type KeymapDraft } from "./model"; import { groupPath } from "./groups"; import { StrokeRecorder } from "./StrokeRecorder"; import styles from "./Keymap.module.css"; import { tx } from "@/platform/i18n"; export interface RecordShortcutDialogProps { action: Pick; actions: readonly Pick[]; draft: KeymapDraft; onCancel: () => void; /** `removeConflicts`: also unbind the conflicting assignments. */ onSubmit: (binding: string, conflicts: Conflict[], removeConflicts: boolean) => void; /** Record a mouse shortcut (modified left clicks or side buttons) instead of keys. */ mouse?: boolean; } export function RecordShortcutDialog({ action, actions, draft, onCancel, onSubmit, mouse }: RecordShortcutDialogProps) { const [first, setFirst] = useState(""); const [second, setSecond] = useState(""); const [useSecond, setUseSecond] = useState(false); const binding = mouse ? first : first && useSecond ? (second ? `${first} ${second}` : "") : first; const conflicts = useMemo( () => (binding ? findConflicts(draft, binding, action.id, actions) : []), [binding, draft, action.id, actions], ); const already = useMemo( () => !!binding && resolveBindings(draft, action.id).includes(normalizeBinding(binding)), [binding, draft, action.id], ); const ok = async () => { if (!binding || already) return; if (!conflicts.length) return onSubmit(binding, conflicts, false); const { button } = await ask({ title: "Warning", icon: "warning", body: ( <> The shortcut is already assigned to other actions. Do you want to remove other assignments? ), buttons: [ { id: "remove", label: "Remove", primary: true }, { id: "leave", label: "Leave" }, ], }); if (button !== "cancel") onSubmit(binding, conflicts, button === "remove"); }; return ( void ok() }, { label: "Cancel", role: "cancel" }, ]} >
{mouse ? ( ) : ( )}
Conflicts:
{already && ( The action already has this shortcut )} {!already && binding && conflicts.length === 0 && ( No conflicts )} {!binding && ( Press a shortcut )} {conflicts.length > 0 && ( <>
is already assigned to:
)}
); } function ConflictList({ conflicts }: { conflicts: readonly Conflict[] }) { return (
    {conflicts.map((c) => (
  • {c.title} ({groupPath({ id: c.actionId, category: c.category }).join(" | ")}) {c.kind === "prefix" && ( chord prefix )}
  • ))}
); } interface KeyStrokesProps { first: string; second: string; useSecond: boolean; setFirst: (v: string) => void; setSecond: (v: string) => void; setUseSecond: (v: boolean) => void; } function KeyStrokes({ first, second, useSecond, setFirst, setSecond, setUseSecond }: KeyStrokesProps) { return ( <>
First stroke:
Keys are recorded as pressed, including Tab and Esc. Use the buttons to finish.
); } /** Record modified left clicks and side buttons; shortcuts stay off while the dialog is open. */ function MouseRecorder({ value, onChange }: { value: string; onChange: (binding: string) => void }) { useEffect(() => suspendKeyboardDispatch(), []); const record = (e: MouseEvent) => { const stroke = mouseStrokeOf(e.nativeEvent); if (!stroke) return; e.preventDefault(); e.stopPropagation(); onChange(stroke); }; return (
e.preventDefault()} onContextMenu={(e) => e.preventDefault()} > {value ? ( {formatBinding(value)} ) : ( Click here with Ctrl, Alt or Command held, or use a mouse side button )}
); }