diff --git a/app/gui2/src/components/GraphEditor/GraphNode.vue b/app/gui2/src/components/GraphEditor/GraphNode.vue index b8d3ac093fba..c24eeb44bd68 100644 --- a/app/gui2/src/components/GraphEditor/GraphNode.vue +++ b/app/gui2/src/components/GraphEditor/GraphNode.vue @@ -400,6 +400,7 @@ const documentation = computed({ transform, minWidth: isVisualizationVisible ? `${visualizationWidth}px` : undefined, '--node-group-color': color, + ...(node.zIndex ? { 'z-index': node.zIndex } : {}), }" :class="{ edited: props.edited, diff --git a/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue b/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue index 6bbcedbe82d0..8b5dee0a713b 100644 --- a/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue +++ b/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue @@ -2,12 +2,14 @@ import NodeWidget from '@/components/GraphEditor/NodeWidget.vue' import SvgIcon from '@/components/SvgIcon.vue' import { useTransitioning } from '@/composables/animation' +import { injectGraphSelection } from '@/providers/graphSelection' import { WidgetInput, type WidgetUpdate } from '@/providers/widgetRegistry' +import { WidgetEditHandler } from '@/providers/widgetRegistry/editHandler' import { provideWidgetTree } from '@/providers/widgetTree' import { useGraphStore, type NodeId } from '@/stores/graph' import { Ast } from '@/util/ast' import type { Icon } from '@/util/iconName' -import { computed, ref, toRef } from 'vue' +import { computed, ref, toRef, watch } from 'vue' const props = defineProps<{ ast: Ast.Ast @@ -30,6 +32,7 @@ const rootPort = computed(() => { } return input }) +const selection = injectGraphSelection() const observedLayoutTransitions = new Set([ 'margin-left', @@ -44,7 +47,12 @@ const observedLayoutTransitions = new Set([ 'height', ]) +function selectNode() { + selection.setSelection(new Set([props.nodeId])) +} + function handleWidgetUpdates(update: WidgetUpdate) { + selectNode() const edit = update.edit ?? graph.startEdit() if (update.portUpdate) { const { value, origin } = update.portUpdate @@ -67,6 +75,9 @@ function handleWidgetUpdates(update: WidgetUpdate) { return true } +const currentEdit = ref() +watch(currentEdit, (edit) => edit && selectNode()) + /** * We have two goals for our DOM/CSS that are somewhat in conflict: * - We position widget dialogs drawn outside the widget, like dropdowns, relative to their parents. If we teleported @@ -97,6 +108,7 @@ provideWidgetTree( toRef(props, 'conditionalPorts'), toRef(props, 'extended'), layoutTransitions.active, + currentEdit, () => { emit('openFullMenu') }, diff --git a/app/gui2/src/components/GraphEditor/widgets/WidgetNumber.vue b/app/gui2/src/components/GraphEditor/widgets/WidgetNumber.vue index 2966d69b9ad8..f69c3cb15417 100644 --- a/app/gui2/src/components/GraphEditor/widgets/WidgetNumber.vue +++ b/app/gui2/src/components/GraphEditor/widgets/WidgetNumber.vue @@ -1,10 +1,12 @@ diff --git a/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts b/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts index 1e4319603da7..07cd36d5c9a6 100644 --- a/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts +++ b/app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts @@ -11,6 +11,7 @@ function editHandlerTree( widgets: string[], interactionHandler: InteractionHandler, createInteraction: (name: string) => Record, + widgetTree: { currentEdit: WidgetEditHandler | undefined }, ): Map }> { const handlers = new Map() for (const id of widgets) { @@ -24,6 +25,7 @@ function editHandlerTree( interaction, parent ? handlers.get(parent)?.handler : undefined, interactionHandler, + widgetTree, ) handlers.set(id, { handler, interaction }) } @@ -42,12 +44,18 @@ test.each` 'Edit interaction propagation starting from $edited in $widgets tree', ({ widgets, edited, expectedPropagation }) => { const interactionHandler = new InteractionHandler() - const handlers = editHandlerTree(widgets, interactionHandler, () => ({ - start: vi.fn(), - edit: vi.fn(), - end: vi.fn(), - cancel: vi.fn(), - })) + const widgetTree = { currentEdit: undefined } + const handlers = editHandlerTree( + widgets, + interactionHandler, + () => ({ + start: vi.fn(), + edit: vi.fn(), + end: vi.fn(), + cancel: vi.fn(), + }), + widgetTree, + ) const expectedPropagationSet = new Set(expectedPropagation) const checkCallbackCall = (callback: string, ...args: any[]) => { for (const [id, { interaction }] of handlers) { @@ -64,6 +72,7 @@ test.each` assert(editedHandler != null) editedHandler.handler.start() + expect(widgetTree.currentEdit).toBe(editedHandler.handler) checkCallbackCall('start', edited) for (const [id, { handler }] of handlers) { expect(handler.isActive()).toBe(expectedPropagationSet.has(id)) @@ -76,21 +85,27 @@ test.each` const endedHandler = handlers.get(ended)?.handler editedHandler.handler.start() + expect(widgetTree.currentEdit).toBe(editedHandler.handler) expect(editedHandler.handler.isActive()).toBeTruthy() endedHandler?.end() + expect(widgetTree.currentEdit).toBeUndefined() checkCallbackCall('end', ended) expect(editedHandler.handler.isActive()).toBeFalsy() editedHandler.handler.start() + expect(widgetTree.currentEdit).toBe(editedHandler.handler) expect(editedHandler.handler.isActive()).toBeTruthy() endedHandler?.cancel() + expect(widgetTree.currentEdit).toBeUndefined() checkCallbackCall('cancel') expect(editedHandler.handler.isActive()).toBeFalsy() } editedHandler.handler.start() + expect(widgetTree.currentEdit).toBe(editedHandler.handler) expect(editedHandler.handler.isActive()).toBeTruthy() interactionHandler.setCurrent(undefined) + expect(widgetTree.currentEdit).toBeUndefined() checkCallbackCall('cancel') expect(editedHandler.handler.isActive()).toBeFalsy() }, @@ -110,28 +125,33 @@ test.each` const event = new MouseEvent('click') as PointerEvent const navigator = {} as GraphNavigator const interactionHandler = new InteractionHandler() + const widgetTree = { currentEdit: undefined } const propagatingHandlersSet = new Set(propagatingHandlers) const nonPropagatingHandlersSet = new Set(nonPropagatingHandlers) const expectedHandlerCallsSet = new Set(expectedHandlerCalls) - const handlers = editHandlerTree(widgets, interactionHandler, (id) => - propagatingHandlersSet.has(id) ? - { - click: vi.fn((e, nav, childHandler) => { - expect(e).toBe(event) - expect(nav).toBe(navigator) - childHandler?.() - }), - } - : nonPropagatingHandlersSet.has(id) ? - { - click: vi.fn((e, nav) => { - expect(e).toBe(event) - expect(nav).toBe(navigator) - }), - } - : {}, + const handlers = editHandlerTree( + widgets, + interactionHandler, + (id) => + propagatingHandlersSet.has(id) ? + { + click: vi.fn((e, nav, childHandler) => { + expect(e).toBe(event) + expect(nav).toBe(navigator) + childHandler?.() + }), + } + : nonPropagatingHandlersSet.has(id) ? + { + click: vi.fn((e, nav) => { + expect(e).toBe(event) + expect(nav).toBe(navigator) + }), + } + : {}, + widgetTree, ) handlers.get(edited)?.handler.start() interactionHandler.handleClick(event, navigator) diff --git a/app/gui2/src/providers/widgetRegistry/editHandler.ts b/app/gui2/src/providers/widgetRegistry/editHandler.ts index e02a5a7417cb..c913d93e372e 100644 --- a/app/gui2/src/providers/widgetRegistry/editHandler.ts +++ b/app/gui2/src/providers/widgetRegistry/editHandler.ts @@ -1,12 +1,10 @@ import type { PortId } from '@/providers//portInfo' import type { GraphNavigator } from '@/providers/graphNavigator' -import { - injectInteractionHandler, - type Interaction, - type InteractionHandler, -} from '@/providers/interactionHandler' +import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler' import type { WidgetInput } from '@/providers/widgetRegistry' import type { Ast } from '@/util/ast' +import { markRaw } from 'vue' +import { injectWidgetTree } from '../widgetTree' /** An extend {@link Interaction} used in {@link WidgetEditHandler} */ export interface WidgetEditInteraction extends Interaction { @@ -61,11 +59,18 @@ export class WidgetEditHandler { private portId: PortId, innerInteraction: WidgetEditInteraction, private parent?: WidgetEditHandler, - private interactionHandler: InteractionHandler = injectInteractionHandler(), + private interactionHandler = injectInteractionHandler(), + private widgetTree: { currentEdit: WidgetEditHandler | undefined } = injectWidgetTree(), ) { + const noLongerActive = () => { + this.activeInteraction = undefined + if (widgetTree.currentEdit === this) { + widgetTree.currentEdit = undefined + } + } this.interaction = { cancel: () => { - this.activeInteraction = undefined + noLongerActive() innerInteraction.cancel?.() parent?.interaction.cancel?.() }, @@ -88,7 +93,7 @@ export class WidgetEditHandler { parent?.interaction.edit?.(portId, value) }, end: (portId) => { - this.activeInteraction = undefined + noLongerActive() innerInteraction.end?.(portId) parent?.interaction.end?.(portId) }, @@ -110,6 +115,7 @@ export class WidgetEditHandler { start() { this.interactionHandler.setCurrent(this.interaction) + this.widgetTree.currentEdit = markRaw(this) for ( let handler: WidgetEditHandler | undefined = this; handler != null; diff --git a/app/gui2/src/providers/widgetTree.ts b/app/gui2/src/providers/widgetTree.ts index d8bf01fb2f8c..f7f5833c77d2 100644 --- a/app/gui2/src/providers/widgetTree.ts +++ b/app/gui2/src/providers/widgetTree.ts @@ -4,6 +4,7 @@ import { type NodeId } from '@/stores/graph/graphDatabase' import { Ast } from '@/util/ast' import type { Icon } from '@/util/iconName' import { computed, proxyRefs, type Ref } from 'vue' +import type { WidgetEditHandler } from './widgetRegistry/editHandler' function makeExistenceRegistry(onChange: (anyExist: boolean) => void) { const registered = new Set() @@ -35,6 +36,7 @@ const { provideFn, injectFn } = createContextStore( conditionalPorts: Ref>, extended: Ref, hasActiveAnimations: Ref, + currentEdit: Ref, emitOpenFullMenu: () => void, clippingInhibitorsChanged: (anyExist: boolean) => void, ) => { @@ -51,6 +53,7 @@ const { provideFn, injectFn } = createContextStore( extended, nodeSpanStart, hasActiveAnimations, + currentEdit, emitOpenFullMenu, inhibitClipping, }) diff --git a/app/gui2/src/stores/graph/graphDatabase.ts b/app/gui2/src/stores/graph/graphDatabase.ts index 857733ca55ef..425838785665 100644 --- a/app/gui2/src/stores/graph/graphDatabase.ts +++ b/app/gui2/src/stores/graph/graphDatabase.ts @@ -121,6 +121,7 @@ export class BindingsDb { export class GraphDb { nodeIdToNode = new ReactiveDb() + private highestZIndex = 0 private readonly idToExternalMap = reactive(new Map()) private readonly idFromExternalMap = reactive(new Map()) private bindings = new BindingsDb() @@ -302,7 +303,10 @@ export class GraphDb { } moveNodeToTop(id: NodeId) { - this.nodeIdToNode.moveToLast(id) + const node = this.nodeIdToNode.get(id) + if (!node) return + node.zIndex = this.highestZIndex + 1 + this.highestZIndex++ } /** Get the method name from the stack item. */ @@ -358,7 +362,7 @@ export class GraphDb { vis: nodeMeta.get('visualization'), } } - this.nodeIdToNode.set(nodeId, { ...newNode, ...metadataFields }) + this.nodeIdToNode.set(nodeId, { ...newNode, ...metadataFields, zIndex: this.highestZIndex }) } else { const { outerExpr, @@ -477,6 +481,7 @@ export class GraphDb { pattern, rootExpr: Ast.parse(code ?? '0'), innerExpr: Ast.parse(code ?? '0'), + zIndex: this.highestZIndex, } const bindingId = pattern.id this.nodeIdToNode.set(asNodeId(id), node) @@ -517,7 +522,9 @@ export interface NodeDataFromMetadata { vis: Opt } -export interface Node extends NodeDataFromAst, NodeDataFromMetadata {} +export interface Node extends NodeDataFromAst, NodeDataFromMetadata { + zIndex: number +} const baseMockNode = { position: Vec2.Zero,