Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,9 @@ export class InputHandler {
this.eventBus.emit(new WarshipSelectionBoxCancelEvent());
}
}

// Modifier menus first: on Win/Linux Ctrl is the default build-menu
// key, so a ctrl+left must still reach ShowBuildMenuEvent.
if (this.activeKeys.has(this.keybinds.buildMenuModifier)) {
this.suppressNextTap = false;
this.eventBus.emit(new ShowBuildMenuEvent(event.clientX, event.clientY));
Expand All @@ -880,6 +883,16 @@ export class InputHandler {
return;
}

// macOS treats Ctrl+Left as secondary-click (context menu). Skip the
// primary-click path so we don't also fire an attack (#4918). Mac-only:
// on Win/Linux event.ctrlKey is also true for Right Ctrl, which is not
// the default build-menu bind and must still attack. Spawn-phase
// Ctrl+click still needs MouseUpEvent — contextmenu is ignored then.
if (Platform.isMac && event.ctrlKey && !this.gameView.inSpawnPhase()) {
this.suppressNextTap = false;
return;
}

const dist =
Math.abs(event.x - this.lastPointerDownX) +
Math.abs(event.y - this.lastPointerDownY);
Expand Down
137 changes: 137 additions & 0 deletions tests/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
WarshipSelectionBoxCompleteEvent,
WarshipSelectionBoxUpdateEvent,
} from "../src/client/InputHandler";
import { Platform } from "../src/client/Platform";
import { UIState } from "../src/client/UIState";
import { GameView, PlayerView, UnitView } from "../src/client/view";
import { EventBus } from "../src/core/EventBus";
Expand All @@ -23,6 +24,8 @@ class MockPointerEvent {
pointerId: number;
type: string;
pointerType: string;
ctrlKey: boolean;
shiftKey: boolean;
preventDefault: () => void;

constructor(type: string, init: any) {
Expand All @@ -34,6 +37,8 @@ class MockPointerEvent {
this.y = init.y ?? init.clientY;
this.pointerId = init.pointerId;
this.pointerType = init.pointerType ?? "mouse";
this.ctrlKey = init.ctrlKey ?? false;
this.shiftKey = init.shiftKey ?? false;
this.preventDefault = vi.fn();
}
}
Expand Down Expand Up @@ -290,6 +295,138 @@ describe("InputHandler AutoUpgrade", () => {
});
});

describe("Ctrl+left click (#4918)", () => {
let isMacDescriptor: PropertyDescriptor | undefined;

function setIsMac(value: boolean) {
Object.defineProperty(Platform, "isMac", {
configurable: true,
value,
});
}

function fireLeftPointerUp(ctrlKey: boolean) {
const pointerEvent = new PointerEvent("pointerup", {
button: 0,
clientX: 150,
clientY: 250,
ctrlKey,
});
inputHandler["lastPointerDownX"] = 149;
inputHandler["lastPointerDownY"] = 249;
inputHandler["onPointerUp"](pointerEvent);
}

beforeEach(() => {
isMacDescriptor = Object.getOwnPropertyDescriptor(Platform, "isMac");
inputHandler["userSettings"].leftClickOpensMenu = () => false;
});

afterEach(() => {
if (isMacDescriptor) {
Object.defineProperty(Platform, "isMac", isMacDescriptor);
}
});

test("on Mac, should not emit MouseUpEvent on ctrl+left release (secondary-click)", () => {
setIsMac(true);
const mockEmit = vi.spyOn(eventBus, "emit");

fireLeftPointerUp(true);

const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).not.toContain("MouseUpEvent");
expect(emittedTypes).not.toContain("ContextMenuEvent");
expect(emittedTypes).not.toContain("ShowBuildMenuEvent");
});

test("should still emit MouseUpEvent on plain left release", () => {
const mockEmit = vi.spyOn(eventBus, "emit");

fireLeftPointerUp(false);

const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("MouseUpEvent");
});

test("Win/Linux: ctrl+left still opens the build menu when Control is held", () => {
setIsMac(false);
inputHandler["keybinds"].buildMenuModifier = "ControlLeft";
inputHandler["activeKeys"].add("ControlLeft");

const mockEmit = vi.spyOn(eventBus, "emit");
fireLeftPointerUp(true);

const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("ShowBuildMenuEvent");
expect(emittedTypes).not.toContain("MouseUpEvent");
});

test("Win/Linux: Right Ctrl+left still attacks (not a dead click)", () => {
setIsMac(false);
inputHandler["keybinds"].buildMenuModifier = "ControlLeft";
inputHandler["activeKeys"].add("ControlRight");

const mockEmit = vi.spyOn(eventBus, "emit");
fireLeftPointerUp(true);

const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("MouseUpEvent");
expect(emittedTypes).not.toContain("ShowBuildMenuEvent");
});

test("Mac: cmd+left still opens the build menu", () => {
setIsMac(true);
inputHandler["keybinds"].buildMenuModifier = "MetaLeft";
inputHandler["activeKeys"].add("MetaLeft");

const mockEmit = vi.spyOn(eventBus, "emit");
fireLeftPointerUp(false);

const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("ShowBuildMenuEvent");
expect(emittedTypes).not.toContain("MouseUpEvent");
});

test("Mac: ctrl+left during spawn still emits MouseUpEvent", () => {
setIsMac(true);
mockGameView.inSpawnPhase = () => true;
const mockEmit = vi.spyOn(eventBus, "emit");

fireLeftPointerUp(true);

const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("MouseUpEvent");
});

test("onContextMenu still opens the radial after ctrl+left", () => {
const mockEmit = vi.spyOn(eventBus, "emit");

const mouseEvent = new MouseEvent("contextmenu", {
clientX: 150,
clientY: 250,
});
inputHandler["onContextMenu"](mouseEvent);

const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("ContextMenuEvent");
});
});

describe("Left-click menu with ghost structure (#4789)", () => {
test("should emit MouseUpEvent and not ContextMenuEvent when placing a ghost structure with left-click menu enabled", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
Expand Down
Loading