Compare commits

...

2 Commits

Author SHA1 Message Date
Connor Byrne
bd0c868b56 refactor: use canvas event for read-only sync instead of callback
Replace LGraphCanvas.onReadOnlyChanged callback property with a
'litegraph:read-only-changed' CustomEvent dispatched on the canvas
DOM element. canvasStore now subscribes via useEventListener,
matching the existing pattern used for litegraph:set-graph,
subgraph-opened, subgraph-converted, and litegraph:ghost-placement.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/8998#discussion_r2831838643
https://github.com/Comfy-Org/ComfyUI_frontend/pull/8998#discussion_r3184409804
2026-05-04 13:51:38 -07:00
bymyself
79bfb95d4d fix: space bar panning over Vue nodes in standard nav mode
Bridge LGraphCanvas.read_only to Vue reactivity via onReadOnlyChanged
callback so the existing CSS pointer-events-auto/none toggle on
LGraphNode.vue and NodeWidgets.vue re-evaluates when space key
toggles panning mode. Events then fall through to the LiteGraph
canvas naturally — no per-handler forwarding or force flags needed.

Fixes #7806

Amp-Thread-ID: https://ampcode.com/threads/T-019c796c-e83c-769d-85f4-20a349994bad
2026-05-04 00:33:14 -07:00
6 changed files with 88 additions and 4 deletions

View File

@@ -408,8 +408,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
set read_only(value: boolean) {
const changed = this.state.readOnly !== value
this.state.readOnly = value
this._updateCursorStyle()
if (changed) {
this.dispatchEvent('litegraph:read-only-changed', { readOnly: value })
}
}
get isDragging(): boolean {

View File

@@ -59,4 +59,9 @@ export interface LGraphCanvasEventMap {
active: boolean
nodeId: NodeId
}
/** The canvas read-only state has changed. */
'litegraph:read-only-changed': {
readOnly: boolean
}
}

View File

@@ -1,7 +1,9 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
vi.mock('@/composables/useAppMode', () => ({
@@ -35,6 +37,30 @@ vi.mock('@/scripts/app', () => ({
}
}))
vi.mock('@vueuse/core', async (importOriginal) => {
const actual = await importOriginal()
return {
...(actual as Record<string, unknown>),
useEventListener: vi.fn(
(
target: EventTarget,
event: string,
handler: EventListenerOrEventListenerObject
) => {
target.addEventListener(event, handler)
return () => target.removeEventListener(event, handler)
}
)
}
})
function createMockCanvas(readOnly = false): LGraphCanvas {
return {
read_only: readOnly,
canvas: document.createElement('canvas')
} as unknown as LGraphCanvas
}
describe('useCanvasStore', () => {
let store: ReturnType<typeof useCanvasStore>
@@ -84,4 +110,42 @@ describe('useCanvasStore', () => {
expect(originalHandler).toHaveBeenCalledWith(2.0, app.canvas.ds.offset)
})
})
describe('isReadOnly', () => {
it('syncs initial read_only value when canvas is set', async () => {
const mockCanvas = createMockCanvas(true)
store.canvas = mockCanvas as unknown as LGraphCanvas
await nextTick()
expect(store.isReadOnly).toBe(true)
})
it('updates isReadOnly when litegraph:read-only-changed event fires', async () => {
const mockCanvas = createMockCanvas(false)
store.canvas = mockCanvas as unknown as LGraphCanvas
await nextTick()
expect(store.isReadOnly).toBe(false)
// Simulate space key press → LGraphCanvas sets read_only = true
mockCanvas.canvas.dispatchEvent(
new CustomEvent('litegraph:read-only-changed', {
detail: { readOnly: true }
})
)
expect(store.isReadOnly).toBe(true)
// Simulate space key release
mockCanvas.canvas.dispatchEvent(
new CustomEvent('litegraph:read-only-changed', {
detail: { readOnly: false }
})
)
expect(store.isReadOnly).toBe(false)
})
})
})

View File

@@ -56,6 +56,7 @@ export const useCanvasStore = defineStore('canvas', () => {
setMode(val ? 'app' : 'graph')
}
})
const isReadOnly = ref(false)
// Set up scale synchronization when canvas is available
let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
@@ -131,6 +132,16 @@ export const useCanvasStore = defineStore('canvas', () => {
whenever(
() => canvas.value,
(newCanvas) => {
isReadOnly.value = newCanvas.read_only
useEventListener(
newCanvas.canvas,
'litegraph:read-only-changed',
(event: CustomEvent<{ readOnly: boolean }>) => {
isReadOnly.value = event.detail.readOnly
}
)
useEventListener(
newCanvas.canvas,
'litegraph:set-graph',
@@ -176,6 +187,7 @@ export const useCanvasStore = defineStore('canvas', () => {
rerouteSelected,
appScalePercentage,
linearMode,
isReadOnly,
updateSelectedItems,
getCanvas,
setAppZoomFromPercentage,

View File

@@ -12,7 +12,8 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => {
return {
useCanvasStore: vi.fn(() => ({
getCanvas,
setCursorStyle
setCursorStyle,
isReadOnly: false
}))
}
})

View File

@@ -22,9 +22,7 @@ export function useCanvasInteractions() {
* Whether Vue node components should handle pointer events.
* Returns false when canvas is in read-only/panning mode (e.g., space key held for panning).
*/
const shouldHandleNodePointerEvents = computed(
() => !(canvasStore.canvas?.read_only ?? false)
)
const shouldHandleNodePointerEvents = computed(() => !canvasStore.isReadOnly)
/**
* Returns true if the wheel event target is inside an element that should