diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts index 06de292e3..de21e674a 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts @@ -17,7 +17,26 @@ const mockCanvas = vi.hoisted(() => { const canvasElement = document.createElement('canvas') return { canvas: canvasElement, - processKey: vi.fn() + read_only: false, + dragging_canvas: false, + pointer: { isDown: false } + } +}) + +// Mock useMagicKeys and useActiveElement from VueUse +// Use vi.hoisted to store refs in an object that's available during mock hoisting +const vueUseMocks = vi.hoisted(() => ({ + spaceKey: null as { value: boolean } | null, + activeElement: null as { value: Element | null } | null +})) + +vi.mock('@vueuse/core', async () => { + const { ref: vueRef } = await import('vue') + vueUseMocks.spaceKey = vueRef(false) + vueUseMocks.activeElement = vueRef(null) + return { + useMagicKeys: () => ({ space: vueUseMocks.spaceKey }), + useActiveElement: () => vueUseMocks.activeElement } }) @@ -342,41 +361,79 @@ describe('useNodePointerInteractions', () => { ) }) - describe('keydown forwarding for spacebar panning', () => { - it('forwards keydown events to canvas.processKey when target is not the canvas', () => { - // Initialize the composable to set up keydown forwarding - useNodePointerInteractions('test-node-123') - - // Create a div to simulate a Vue node element - const vueNodeElement = document.createElement('div') - document.body.appendChild(vueNodeElement) - - // Dispatch keydown event on the Vue node element (not the canvas) - const keydownEvent = new KeyboardEvent('keydown', { - key: ' ', - bubbles: true - }) - vueNodeElement.dispatchEvent(keydownEvent) - - // Should forward to canvas.processKey - expect(mockCanvas.processKey).toHaveBeenCalledWith(keydownEvent) - - document.body.removeChild(vueNodeElement) + describe('spacebar panning via useMagicKeys', () => { + beforeEach(() => { + mockCanvas.read_only = false + mockCanvas.dragging_canvas = false + vueUseMocks.spaceKey!.value = false + vueUseMocks.activeElement!.value = null }) - it('does not forward keydown events when target is the canvas itself', () => { + it('sets read_only=true when spacebar is pressed on non-canvas element', async () => { + const vueNodeElement = document.createElement('div') + vueUseMocks.activeElement!.value = vueNodeElement + useNodePointerInteractions('test-node-123') - mockCanvas.processKey.mockClear() - // Dispatch keydown event directly on the canvas element - const keydownEvent = new KeyboardEvent('keydown', { - key: ' ', - bubbles: true - }) - mockCanvas.canvas.dispatchEvent(keydownEvent) + // Simulate spacebar press + vueUseMocks.spaceKey!.value = true + await nextTick() - // Should NOT forward (canvas handles it directly) - expect(mockCanvas.processKey).not.toHaveBeenCalled() + expect(mockCanvas.read_only).toBe(true) + }) + + it('resets read_only=false when spacebar is released', async () => { + const vueNodeElement = document.createElement('div') + vueUseMocks.activeElement!.value = vueNodeElement + + useNodePointerInteractions('test-node-123') + + // Press and release spacebar + vueUseMocks.spaceKey!.value = true + await nextTick() + vueUseMocks.spaceKey!.value = false + await nextTick() + + expect(mockCanvas.read_only).toBe(false) + expect(mockCanvas.dragging_canvas).toBe(false) + }) + + it('does not set read_only when canvas has focus', async () => { + vueUseMocks.activeElement!.value = mockCanvas.canvas + + useNodePointerInteractions('test-node-123') + + vueUseMocks.spaceKey!.value = true + await nextTick() + + // Should NOT change read_only (litegraph handles it directly) + expect(mockCanvas.read_only).toBe(false) + }) + + it('does not set read_only when input element has focus', async () => { + const inputElement = document.createElement('input') + vueUseMocks.activeElement!.value = inputElement + + useNodePointerInteractions('test-node-123') + + vueUseMocks.spaceKey!.value = true + await nextTick() + + // Should NOT change read_only (avoid interfering with text input) + expect(mockCanvas.read_only).toBe(false) + }) + + it('does not set read_only when textarea element has focus', async () => { + const textareaElement = document.createElement('textarea') + vueUseMocks.activeElement!.value = textareaElement + + useNodePointerInteractions('test-node-123') + + vueUseMocks.spaceKey!.value = true + await nextTick() + + // Should NOT change read_only (avoid interfering with text input) + expect(mockCanvas.read_only).toBe(false) }) }) }) diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts index 4bc74adda..28e447cd2 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts @@ -1,6 +1,8 @@ -import { onScopeDispose, ref, toValue } from 'vue' +import { onScopeDispose, ref, toValue, watch } from 'vue' import type { MaybeRefOrGetter } from 'vue' +import { useActiveElement, useMagicKeys } from '@vueuse/core' + import { isMiddlePointerInput } from '@/base/pointerUtils' import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' @@ -10,29 +12,51 @@ import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag' import { isMultiSelectKey } from '@/renderer/extensions/vueNodes/utils/selectionUtils' import { app } from '@/scripts/app' -// Forward keydown events to litegraph's processKey when Vue nodes have focus -let keydownForwardingInitialized = false +// Forward spacebar key events to litegraph for panning when Vue nodes have focus +let spacebarForwardingInitialized = false -function initKeydownForwarding() { - if (keydownForwardingInitialized) return - keydownForwardingInitialized = true +function isEditableElement(el: Element | null): boolean { + if (!el) return false + const tag = el.tagName.toUpperCase() + if (tag === 'INPUT' || tag === 'TEXTAREA') return true + if (el instanceof HTMLElement && el.isContentEditable) return true + return false +} - document.addEventListener( - 'keydown', - (e) => { - const canvas = app.canvas - if (!canvas) return - if (e.target === canvas.canvas) return - canvas.processKey(e) - }, - true - ) +function initSpacebarForwarding() { + if (spacebarForwardingInitialized) return + spacebarForwardingInitialized = true + + const { space } = useMagicKeys() + const activeElement = useActiveElement() + + watch(space, (isPressed) => { + const canvas = app.canvas + if (!canvas) return + + // Skip if canvas has focus (litegraph handles it) or if in editable element + if (activeElement.value === canvas.canvas) return + if (isEditableElement(activeElement.value!)) return + + // Mirror litegraph's processKey behavior for spacebar + if (isPressed) { + canvas.read_only = true + // Set dragging_canvas based on current pointer state + if (canvas.pointer?.isDown) { + canvas.dragging_canvas = true + } + } else { + canvas.read_only = false + canvas.dragging_canvas = false + } + }) } export function useNodePointerInteractions( nodeIdRef: MaybeRefOrGetter ) { - initKeydownForwarding() + // Initialize spacebar forwarding for panning when Vue nodes have focus + initSpacebarForwarding() const { startDrag, endDrag, handleDrag } = useNodeDrag() // Use canvas interactions for proper wheel event handling and pointer event capture control diff --git a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts index d827c3890..14a9ec005 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts @@ -293,9 +293,6 @@ export function useSlotLinkInteraction({ raf.cancel() dragContext.dispose() clearCompatible() - if (app.canvas?.pointer) { - app.canvas.pointer.isDown = false - } } const updatePointerState = (event: PointerEvent) => { @@ -414,7 +411,10 @@ export function useSlotLinkInteraction({ if (!pointerSession.matches(event)) return const canvas = app.canvas - if (canvas?.read_only && canvas.dragging_canvas) { + // When spacebar is held (read_only=true) and left mouse button is down, + // delegate to litegraph's processMouseMove for panning + const isLeftButtonDown = (event.buttons & 1) !== 0 + if (canvas?.read_only && isLeftButtonDown) { canvas.processMouseMove(event) } @@ -712,9 +712,7 @@ export function useSlotLinkInteraction({ ) pointerSession.begin(event.pointerId) - if (canvas.pointer) { - canvas.pointer.isDown = true - } + // Update last_mouse so panning delta calculations are correct canvas.last_mouse = [event.clientX, event.clientY] toCanvasPointerEvent(event)