дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(keymap): open shortcut menu when activating an action (#2148)
Double-clicking an action in Settings > Keymap went straight to the Keyboard Shortcut dialog, where mouse clicks do nothing, so trying to bind Alt+Click there got no reaction. Add Mouse Shortcut was reachable only by right-click. Double-click and Enter now open the same menu as right-click, at the pointer or the selected row. Sibling covered: the keyboard dialog no longer inherits mouse recording mode from an earlier Add Mouse Shortcut. Fixes #2126
Цей коміт міститься в:
1 джерело
278936dd7d
коміт
9a4ef162bb
3 files changed
+118
-4
No files matched your search
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
|
||||
for (const activation of ["double-click", "Enter"] as const) {
|
||||
test(`Keymap ${activation} offers mouse and keyboard shortcuts`, async ({ workbench: page }) => {
|
||||
await page.keyboard.press("Meta+,");
|
||||
const settings = page.getByRole("dialog", { name: "Settings", exact: true });
|
||||
await settings.getByRole("treeitem", { name: "Keymap", exact: true }).click();
|
||||
await settings.getByRole("textbox", { name: "Search actions", exact: true }).fill("Find Action");
|
||||
const row = settings.getByRole("tree", { name: "Actions" }).getByRole("treeitem", { name: /Find Action/ });
|
||||
const activate = async () => {
|
||||
if (activation === "double-click") await row.dblclick();
|
||||
else {
|
||||
await row.click();
|
||||
await page.keyboard.press("Enter");
|
||||
}
|
||||
};
|
||||
await activate();
|
||||
const keyboard = page.getByRole("menuitem", { name: "Add Keyboard Shortcut", exact: true });
|
||||
await expect(keyboard).toBeFocused();
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await expect(page.getByRole("menuitem", { name: "Add Mouse Shortcut", exact: true })).toBeFocused();
|
||||
await page.keyboard.press("Enter");
|
||||
const mouseDialog = page.getByRole("dialog", { name: "Mouse Shortcut: Find Action…", exact: true });
|
||||
await mouseDialog.getByRole("textbox", { name: "Mouse shortcut" }).click({ modifiers: ["Alt"] });
|
||||
await expect(mouseDialog.getByRole("button", { name: "OK", exact: true })).toBeEnabled();
|
||||
await mouseDialog.getByRole("button", { name: "OK", exact: true }).click();
|
||||
await page
|
||||
.getByRole("dialog", { name: "Warning", exact: true })
|
||||
.getByRole("button", { name: "Leave", exact: true })
|
||||
.click();
|
||||
await expect(row).toContainText("Click");
|
||||
await activate();
|
||||
await keyboard.click();
|
||||
const keyboardDialog = page.getByRole("dialog", { name: "Keyboard Shortcut: Find Action…", exact: true });
|
||||
await expect(keyboardDialog.getByRole("textbox", { name: "First stroke" })).toBeFocused();
|
||||
await keyboardDialog.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await row.click({ button: "right" });
|
||||
await expect(keyboard).toBeVisible();
|
||||
await expect(page.getByRole("menuitem", { name: "Add Mouse Shortcut", exact: true })).toBeVisible();
|
||||
});
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import { registerAction, executeAction } from "@/platform/actions";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useKeymap } from "@/platform/keymap";
|
||||
import { ContextMenuHost } from "@/ui";
|
||||
import { ContextMenuHost, hideContextMenu, useContextMenuStore } from "@/ui";
|
||||
import { PageContext, useSetting } from "../components/context";
|
||||
import { DEFAULT_SETTINGS, setAt } from "../model/schema";
|
||||
import { openSettings, stage, useSettingsDialog } from "../model/staging";
|
||||
@@ -53,6 +53,7 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
act(() => useSettingsDialog.setState({ open: false, projectStructureOpen: false }));
|
||||
act(hideContextMenu);
|
||||
dispose();
|
||||
});
|
||||
|
||||
@@ -328,8 +329,10 @@ describe("Settings dialog", () => {
|
||||
});
|
||||
registerAction({ id: "t.mine", title: "My Action", category: "Code", when: "editorFocus", run: () => {} });
|
||||
const dialog = await openDialog("keymap");
|
||||
render(<ContextMenuHost />);
|
||||
const row = within(dialog).getByText("My Action");
|
||||
fireEvent.doubleClick(row);
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" }));
|
||||
const recorder = await screen.findByRole("dialog", { name: /Keyboard Shortcut: My Action/ });
|
||||
const first = within(recorder).getByRole("textbox", { name: "First stroke" });
|
||||
fireEvent.keyDown(first, { code: "KeyD", key: "d", metaKey: true });
|
||||
@@ -349,6 +352,67 @@ describe("Settings dialog", () => {
|
||||
expect(useSettingsDialog.getState().open).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["double-click", "Enter"])(
|
||||
"opens the shortcut menu on %s and clears mouse recording mode",
|
||||
async (activation) => {
|
||||
render(<ContextMenuHost />);
|
||||
registerAction({ id: "t.activate", title: "Activation Target", category: "Code", run: () => {} });
|
||||
const dialog = await openDialog("keymap");
|
||||
const row = within(dialog).getByText("Activation Target").closest('[role="treeitem"]')!;
|
||||
const tree = within(dialog).getByRole("tree", { name: "Actions" });
|
||||
const rect = vi.spyOn(row, "getBoundingClientRect").mockReturnValue({
|
||||
left: 100,
|
||||
bottom: 150,
|
||||
top: 126,
|
||||
right: 500,
|
||||
width: 400,
|
||||
height: 24,
|
||||
x: 100,
|
||||
y: 126,
|
||||
toJSON: () => ({}),
|
||||
});
|
||||
const activateRow = () => {
|
||||
fireEvent.mouseDown(row, { button: 0 });
|
||||
if (activation === "double-click") fireEvent.doubleClick(row, { clientX: 220, clientY: 138 });
|
||||
else fireEvent.keyDown(tree, { key: "Enter" });
|
||||
};
|
||||
activateRow();
|
||||
expect(useContextMenuStore.getState().menu).toMatchObject(
|
||||
activation === "double-click" ? { x: 220, y: 138 } : { x: 100, y: 150 },
|
||||
);
|
||||
expect(screen.queryByRole("dialog", { name: /Shortcut: Activation Target/ })).not.toBeInTheDocument();
|
||||
const keyboard = await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" });
|
||||
expect(keyboard).toHaveFocus();
|
||||
fireEvent.keyDown(keyboard, { key: "ArrowDown" });
|
||||
const mouse = screen.getByRole("menuitem", { name: "Add Mouse Shortcut" });
|
||||
expect(mouse).toHaveFocus();
|
||||
fireEvent.keyDown(mouse, { key: "Enter" });
|
||||
const recorder = await screen.findByRole("dialog", { name: "Mouse Shortcut: Activation Target" });
|
||||
fireEvent.mouseDown(within(recorder).getByRole("textbox", { name: "Mouse shortcut" }), {
|
||||
button: 0,
|
||||
altKey: true,
|
||||
});
|
||||
fireEvent.click(within(recorder).getByRole("button", { name: "OK" }));
|
||||
const overrides = useSettingsDialog.getState().staged["global|keymapOverrides"]?.value;
|
||||
expect(overrides).toEqual({ "t.activate": ["Alt+Button1"] });
|
||||
activateRow();
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" }));
|
||||
const keys = await screen.findByRole("dialog", { name: "Keyboard Shortcut: Activation Target" });
|
||||
expect(within(keys).getByRole("textbox", { name: "First stroke" })).toBeInTheDocument();
|
||||
fireEvent.click(within(keys).getByRole("button", { name: "Cancel" }));
|
||||
fireEvent.contextMenu(row);
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Mouse Shortcut" }));
|
||||
fireEvent.click(
|
||||
within(await screen.findByRole("dialog", { name: "Mouse Shortcut: Activation Target" })).getByRole("button", {
|
||||
name: "Cancel",
|
||||
}),
|
||||
);
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Add Keyboard Shortcut" }));
|
||||
expect(await screen.findByRole("dialog", { name: "Keyboard Shortcut: Activation Target" })).toBeInTheDocument();
|
||||
rect.mockRestore();
|
||||
},
|
||||
);
|
||||
|
||||
it("duplicates the keymap from the gear menu and edits the copy", async () => {
|
||||
render(<ContextMenuHost />);
|
||||
registerAction({ id: "t.dup", title: "Dup Target", category: "Code", run: () => {} });
|
||||
@@ -364,6 +428,7 @@ describe("Settings dialog", () => {
|
||||
"custom.macos-copy": { name: "macOS copy", parent: "intellij-macos", overrides: {} },
|
||||
});
|
||||
fireEvent.doubleClick(within(dialog).getByText("Dup Target"));
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" }));
|
||||
const recorder = await screen.findByRole("dialog", { name: /Keyboard Shortcut: Dup Target/ });
|
||||
fireEvent.keyDown(within(recorder).getByRole("textbox", { name: "First stroke" }), {
|
||||
code: "F9",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Keymap page (IDEA layout): keymap selector with a gear menu (Duplicate… /
|
||||
* Rename… / Restore Defaults / Delete), search by name or by recorded shortcut,
|
||||
* and the action tree grouped as Main Menu › File…Help, Editor Actions, Tool
|
||||
* Windows, Other, Plugins. Add / Remove / Reset via the context menu (Enter /
|
||||
* double-click adds). Edits are staged as the `keymap`, `keymapOverrides`
|
||||
* Windows, Other, Plugins. Enter / double-click opens the context menu to add,
|
||||
* remove or reset shortcuts. Edits are staged as the `keymap`, `keymapOverrides`
|
||||
* (bundled keymaps) and `customKeymaps` (user keymaps) settings, global scope.
|
||||
*/
|
||||
import { useMemo, useRef, useState, type MouseEvent } from "react";
|
||||
@@ -396,7 +396,15 @@ export function KeymapPage() {
|
||||
multiSelect={false}
|
||||
speedSearch={false}
|
||||
onSelectionChange={(_sel, node) => setLead(node?.kind === "action" ? node.key : null)}
|
||||
onActivate={(n) => n.kind === "action" && setRecordFor(n.action)}
|
||||
onActivate={(n, e) => {
|
||||
if (n.kind !== "action") return;
|
||||
if ("clientX" in e) showContextMenu(e, menuFor(n.action));
|
||||
else {
|
||||
const row = document.getElementById(e.currentTarget.getAttribute("aria-activedescendant") ?? "");
|
||||
const rect = row?.getBoundingClientRect();
|
||||
if (rect) showContextMenu({ x: rect.left, y: rect.bottom }, menuFor(n.action));
|
||||
}
|
||||
}}
|
||||
onContextMenu={(n, _sel, e: MouseEvent) => {
|
||||
if (n?.kind !== "action") return;
|
||||
e.preventDefault();
|
||||
|
||||
Reference in new issue
Заблокувати користувача