diff --git a/src/components/rightSidePanel/settings/SetNodeState.vue b/src/components/rightSidePanel/settings/SetNodeState.vue index 7d64fefef..aec680c3c 100644 --- a/src/components/rightSidePanel/settings/SetNodeState.vue +++ b/src/components/rightSidePanel/settings/SetNodeState.vue @@ -2,23 +2,18 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' +import { useNodeMode } from '@/composables/canvas/useNodeMode' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { LGraphEventMode } from '@/lib/litegraph/src/litegraph' import FormSelectButton from '@/renderer/extensions/vueNodes/widgets/components/form/FormSelectButton.vue' import LayoutField from './LayoutField.vue' -/** - * Good design limits dependencies and simplifies the interface of the abstraction layer. - * Here, we only care about the mode method, - * and do not concern ourselves with other methods. - */ -type PickedNode = Pick - -const { nodes } = defineProps<{ nodes: PickedNode[] }>() +const { nodes } = defineProps<{ nodes: LGraphNode[] }>() const emit = defineEmits<{ (e: 'changed'): void }>() const { t } = useI18n() +const { setNodesMode } = useNodeMode() const nodeState = computed({ get() { @@ -39,9 +34,7 @@ const nodeState = computed({ return mode }, set(value: LGraphNode['mode']) { - nodes.forEach((node) => { - node.mode = value - }) + setNodesMode(nodes, value) emit('changed') } }) diff --git a/src/composables/canvas/useNodeMode.ts b/src/composables/canvas/useNodeMode.ts new file mode 100644 index 000000000..6d3e870a1 --- /dev/null +++ b/src/composables/canvas/useNodeMode.ts @@ -0,0 +1,39 @@ +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LGraphEventMode } from '@/lib/litegraph/src/litegraph' + +/** + * Composable for managing node execution modes. + * Provides a centralized way to change node modes while ensuring + * proper graph state updates and side effects. + */ +export function useNodeMode() { + /** + * Sets the execution mode for a single node. + * Uses the node's changeMode method to handle mode-specific side effects + * and notifies the graph of the change. + */ + function setNodeMode(node: LGraphNode, mode: LGraphEventMode): void { + node.changeMode(mode) + node.graph?.change() + } + + /** + * Sets the execution mode for multiple nodes. + * Applies the mode to all nodes and notifies the graph once. + */ + function setNodesMode(nodes: LGraphNode[], mode: LGraphEventMode): void { + if (nodes.length === 0) return + + nodes.forEach((node) => { + node.changeMode(mode) + }) + + // Only call change once for all nodes + nodes[0].graph?.change() + } + + return { + setNodeMode, + setNodesMode + } +} diff --git a/src/composables/canvas/useSelectedLiteGraphItems.test.ts b/src/composables/canvas/useSelectedLiteGraphItems.test.ts index 23e1e8dd3..cb9bcbef4 100644 --- a/src/composables/canvas/useSelectedLiteGraphItems.test.ts +++ b/src/composables/canvas/useSelectedLiteGraphItems.test.ts @@ -224,8 +224,23 @@ describe('useSelectedLiteGraphItems', () => { it('toggleSelectedNodesMode should toggle node modes correctly', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const node1 = { id: 1, mode: LGraphEventMode.ALWAYS } as LGraphNode - const node2 = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode + const mockGraph = { change: vi.fn() } + const node1 = { + id: 1, + mode: LGraphEventMode.ALWAYS, + changeMode: vi.fn((mode) => { + node1.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode + const node2 = { + id: 2, + mode: LGraphEventMode.NEVER, + changeMode: vi.fn((mode) => { + node2.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode app.canvas.selected_nodes = { '0': node1, '1': node2 } @@ -236,11 +251,22 @@ describe('useSelectedLiteGraphItems', () => { // node2 should stay NEVER (since a selected node exists which is not NEVER) expect(node1.mode).toBe(LGraphEventMode.NEVER) expect(node2.mode).toBe(LGraphEventMode.NEVER) + expect(node1.changeMode).toHaveBeenCalledWith(LGraphEventMode.NEVER) + expect(node2.changeMode).toHaveBeenCalledWith(LGraphEventMode.NEVER) + expect(mockGraph.change).toHaveBeenCalled() }) it('toggleSelectedNodesMode should set mode to ALWAYS when already in target mode', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const node = { id: 1, mode: LGraphEventMode.BYPASS } as LGraphNode + const mockGraph = { change: vi.fn() } + const node = { + id: 1, + mode: LGraphEventMode.BYPASS, + changeMode: vi.fn((mode) => { + node.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode app.canvas.selected_nodes = { '0': node } @@ -249,6 +275,8 @@ describe('useSelectedLiteGraphItems', () => { // Should change to ALWAYS expect(node.mode).toBe(LGraphEventMode.ALWAYS) + expect(node.changeMode).toHaveBeenCalledWith(LGraphEventMode.ALWAYS) + expect(mockGraph.change).toHaveBeenCalled() }) it('getSelectedNodes should include nodes from subgraphs', () => { @@ -277,17 +305,43 @@ describe('useSelectedLiteGraphItems', () => { it('toggleSelectedNodesMode should apply unified state to subgraph children', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode - const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode + const mockGraph = { change: vi.fn() } + const subNode1 = { + id: 11, + mode: LGraphEventMode.ALWAYS, + changeMode: vi.fn((mode) => { + subNode1.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode + const subNode2 = { + id: 12, + mode: LGraphEventMode.NEVER, + changeMode: vi.fn((mode) => { + subNode2.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode const subgraphNode = { id: 1, mode: LGraphEventMode.ALWAYS, isSubgraphNode: () => true, subgraph: { nodes: [subNode1, subNode2] - } + }, + changeMode: vi.fn((mode) => { + subgraphNode.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode + const regularNode = { + id: 2, + mode: LGraphEventMode.BYPASS, + changeMode: vi.fn((mode) => { + regularNode.mode = mode + }), + graph: mockGraph } as unknown as LGraphNode - const regularNode = { id: 2, mode: LGraphEventMode.BYPASS } as LGraphNode app.canvas.selected_nodes = { '0': subgraphNode, '1': regularNode } @@ -308,15 +362,34 @@ describe('useSelectedLiteGraphItems', () => { it('toggleSelectedNodesMode should toggle to ALWAYS when subgraph is already in target mode', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode - const subNode2 = { id: 12, mode: LGraphEventMode.BYPASS } as LGraphNode + const mockGraph = { change: vi.fn() } + const subNode1 = { + id: 11, + mode: LGraphEventMode.ALWAYS, + changeMode: vi.fn((mode) => { + subNode1.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode + const subNode2 = { + id: 12, + mode: LGraphEventMode.BYPASS, + changeMode: vi.fn((mode) => { + subNode2.mode = mode + }), + graph: mockGraph + } as unknown as LGraphNode const subgraphNode = { id: 1, mode: LGraphEventMode.NEVER, // Already in NEVER mode isSubgraphNode: () => true, subgraph: { nodes: [subNode1, subNode2] - } + }, + changeMode: vi.fn((mode) => { + subgraphNode.mode = mode + }), + graph: mockGraph } as unknown as LGraphNode app.canvas.selected_nodes = { '0': subgraphNode } diff --git a/src/composables/canvas/useSelectedLiteGraphItems.ts b/src/composables/canvas/useSelectedLiteGraphItems.ts index a4a93b9fb..3f42698e6 100644 --- a/src/composables/canvas/useSelectedLiteGraphItems.ts +++ b/src/composables/canvas/useSelectedLiteGraphItems.ts @@ -7,6 +7,8 @@ import { traverseNodesDepthFirst } from '@/utils/graphTraversalUtil' +import { useNodeMode } from './useNodeMode' + /** * Composable for handling selected LiteGraph items filtering and operations. * This provides utilities for working with selected items on the canvas, @@ -114,6 +116,8 @@ export function useSelectedLiteGraphItems() { const selectedNodes = app.canvas.selected_nodes if (!selectedNodes) return + const { setNodeMode } = useNodeMode() + // Convert selected_nodes object to array const selectedNodeArray: LGraphNode[] = [] for (const i in selectedNodes) { @@ -127,8 +131,7 @@ export function useSelectedLiteGraphItems() { // Process each selected node independently to determine its target state and apply to children selectedNodeArray.forEach((selectedNode) => { // Apply standard toggle logic to the selected node itself - - selectedNode.mode = newModeForSelectedNode + setNodeMode(selectedNode, newModeForSelectedNode) // If this selected node is a subgraph, apply the same mode uniformly to all its children // This ensures predictable behavior: all children get the same state as their parent @@ -139,7 +142,7 @@ export function useSelectedLiteGraphItems() { if (node === selectedNode) return undefined // Apply the parent's new mode to all children uniformly - node.mode = newModeForSelectedNode + setNodeMode(node, newModeForSelectedNode) return undefined } })