From c1b15fcfdf68b086bf7985910cdf795f6ffd1b31 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 29 May 2025 03:08:04 -0700 Subject: [PATCH] Fix Ctrl+Space not closing the graph after opening the node creation menu --- .../components/floating-menus/MenuList.svelte | 2 +- frontend/src/components/views/Graph.svelte | 9 ++- .../src/components/widgets/WidgetSpan.svelte | 1 - frontend/src/io-managers/input.ts | 71 ++++++++++++------- 4 files changed, 53 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/floating-menus/MenuList.svelte b/frontend/src/components/floating-menus/MenuList.svelte index c25287677..0695d3830 100644 --- a/frontend/src/components/floating-menus/MenuList.svelte +++ b/frontend/src/components/floating-menus/MenuList.svelte @@ -566,7 +566,7 @@ background: var(--color-e-nearwhite); color: var(--color-2-mildblack); - svg { + > .icon-label { fill: var(--color-2-mildblack); } } diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 3cc63eac4..59575d314 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -137,7 +137,7 @@ await refreshWires(); } - function resolveWire(wire: FrontendNodeWire): { nodeOutput: SVGSVGElement | undefined; nodeInput: SVGSVGElement | undefined } { + function resolveWire(wire: FrontendNodeWire): { nodeOutput: SVGSVGElement; nodeInput: SVGSVGElement } | undefined { // TODO: Avoid the linear search const wireStartNodeIdIndex = Array.from($nodeGraph.nodes.keys()).findIndex((nodeId) => nodeId === (wire.wireStart as Node).nodeId); let nodeOutputConnectors = outputs[wireStartNodeIdIndex + 1]; @@ -146,6 +146,7 @@ } const indexOutput = Number(wire.wireStart.index); const nodeOutput = nodeOutputConnectors?.[indexOutput] as SVGSVGElement | undefined; + if (nodeOutput === undefined) return undefined; // TODO: Avoid the linear search const wireEndNodeIdIndex = Array.from($nodeGraph.nodes.keys()).findIndex((nodeId) => nodeId === (wire.wireEnd as Node).nodeId); @@ -155,6 +156,7 @@ } const indexInput = Number(wire.wireEnd.index); const nodeInput = nodeInputConnectors?.[indexInput] as SVGSVGElement | undefined; + if (nodeInput === undefined) return undefined; return { nodeOutput, nodeInput }; } @@ -177,8 +179,9 @@ nodeWirePaths = $nodeGraph.wires.flatMap((wire) => { // TODO: This call contains linear searches, which combined with the loop we're in, causes O(n^2) complexity as the graph grows - const { nodeOutput, nodeInput } = resolveWire(wire); - if (!nodeOutput || !nodeInput) return []; + const resolvedWires = resolveWire(wire); + if (!resolvedWires) return []; + const { nodeOutput, nodeInput } = resolvedWires; const wireStartNode = wire.wireStart.nodeId !== undefined ? $nodeGraph.nodes.get(wire.wireStart.nodeId) : undefined; const wireStart = wireStartNode?.isLayer || false; diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index a6331b593..831e1dbbc 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -217,5 +217,4 @@ } } } - // paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index c2956b498..bb7b035ce 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -104,6 +104,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (["KeyC", "KeyI", "KeyJ"].includes(key) && accelKey && e.shiftKey) return false; // Don't redirect tab or enter if not in canvas (to allow navigating elements) + potentiallyRestoreCanvasFocus(e); if (!canvasFocused && !targetIsTextField(e.target || undefined) && ["Tab", "Enter", "NumpadEnter", "Space", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(key)) return false; // Don't redirect if a MenuList is open @@ -145,6 +146,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // While any pointer button is already down, additional button down events are not reported, but they are sent as `pointermove` events and these are handled in the backend function onPointerMove(e: PointerEvent) { + potentiallyRestoreCanvasFocus(e); + if (!e.buttons) viewportPointerInteractionOngoing = false; // Don't redirect pointer movement to the backend if there's no ongoing interaction and it's over a floating menu, or the graph overlay, on top of the canvas @@ -155,25 +158,15 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const inGraphOverlay = get(document).graphViewOverlayOpen; if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return; - const { target } = e; - const newInCanvasArea = (target instanceof Element && target.closest("[data-viewport], [data-graph]")) instanceof Element && !targetIsTextField(window.document.activeElement || undefined); - if (newInCanvasArea && !canvasFocused) { - canvasFocused = true; - app?.focus(); - } - const modifiers = makeKeyboardModifiersBitfield(e); editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers); } - function onMouseDown(e: MouseEvent) { - // Block middle mouse button auto-scroll mode (the circlar gizmo that appears and allows quick scrolling by moving the cursor above or below it) - if (e.button === BUTTON_MIDDLE) e.preventDefault(); - } - function onPointerDown(e: PointerEvent) { + potentiallyRestoreCanvasFocus(e); + const { target } = e; - const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]")); + const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]"); const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]"); const inContextMenu = target instanceof Element && target.closest("[data-context-menu]"); const inTextInput = target === textToolInteractiveInputElement; @@ -185,18 +178,23 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli } if (!inTextInput && !inContextMenu) { - const isLeftOrRightClick = e.button === BUTTON_RIGHT || e.button === BUTTON_LEFT; - if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick); - else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element; + if (textToolInteractiveInputElement) { + const isLeftOrRightClick = e.button === BUTTON_RIGHT || e.button === BUTTON_LEFT; + editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick); + } else { + viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element; + } } - if (viewportPointerInteractionOngoing) { + if (viewportPointerInteractionOngoing && isTargetingCanvas instanceof Element) { const modifiers = makeKeyboardModifiersBitfield(e); editor.handle.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers); } } function onPointerUp(e: PointerEvent) { + potentiallyRestoreCanvasFocus(e); + // Don't let the browser navigate back or forward when using the buttons on some mice // TODO: This works in Chrome but not in Firefox // TODO: Possible workaround: use the browser's history API to block navigation: @@ -211,9 +209,16 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli editor.handle.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers); } + // Mouse events + function onPotentialDoubleClick(e: MouseEvent) { if (textToolInteractiveInputElement || inPointerLock) return; + // Allow only events within the viewport or node graph boundaries + const { target } = e; + const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]"); + if (!(isTargetingCanvas instanceof Element)) return; + // Allow only repeated increments of double-clicks (not 1, 3, 5, etc.) if (e.detail % 2 == 1) return; @@ -229,15 +234,26 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers); } + function onMouseDown(e: MouseEvent) { + // Block middle mouse button auto-scroll mode (the circlar gizmo that appears and allows quick scrolling by moving the cursor above or below it) + if (e.button === BUTTON_MIDDLE) e.preventDefault(); + } + + function onContextMenu(e: MouseEvent) { + if (!targetIsTextField(e.target || undefined) && e.target !== textToolInteractiveInputElement) { + e.preventDefault(); + } + } + function onPointerLockChange() { inPointerLock = Boolean(window.document.pointerLockElement); } - // Mouse events + // Wheel events function onWheelScroll(e: WheelEvent) { const { target } = e; - const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]")); + const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]"); // Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element // There seems to be no possible way to properly employ the browser's smooth scrolling interpolation @@ -254,12 +270,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli } } - function onContextMenu(e: MouseEvent) { - if (!targetIsTextField(e.target || undefined) && e.target !== textToolInteractiveInputElement) { - e.preventDefault(); - } - } - // Receives a custom event dispatched when the user begins interactively editing with the text tool. // We keep a copy of the text input element to check against when it's active for text entry. function onModifyInputField(e: CustomEvent) { @@ -420,6 +430,17 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli } }); + // Helper functions + + function potentiallyRestoreCanvasFocus(e: Event) { + const { target } = e; + const newInCanvasArea = (target instanceof Element && target.closest("[data-viewport], [data-graph]")) instanceof Element && !targetIsTextField(window.document.activeElement || undefined); + if (!canvasFocused && newInCanvasArea) { + canvasFocused = true; + app?.focus(); + } + } + // Initialization // Bind the event listeners