Skip to content

Commit

Permalink
Select node on any edit (#9536)
Browse files Browse the repository at this point in the history
  • Loading branch information
farmaazon authored Mar 26, 2024
1 parent 60fa83c commit 09a6ab7
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 44 deletions.
1 change: 1 addition & 0 deletions app/gui2/src/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ const documentation = computed<string | undefined>({
transform,
minWidth: isVisualizationVisible ? `${visualizationWidth}px` : undefined,
'--node-group-color': color,
...(node.zIndex ? { 'z-index': node.zIndex } : {}),
}"
:class="{
edited: props.edited,
Expand Down
14 changes: 13 additions & 1 deletion app/gui2/src/components/GraphEditor/NodeWidgetTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,6 +32,7 @@ const rootPort = computed(() => {
}
return input
})
const selection = injectGraphSelection()
const observedLayoutTransitions = new Set([
'margin-left',
Expand All @@ -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
Expand All @@ -67,6 +75,9 @@ function handleWidgetUpdates(update: WidgetUpdate) {
return true
}
const currentEdit = ref<WidgetEditHandler>()
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
Expand Down Expand Up @@ -97,6 +108,7 @@ provideWidgetTree(
toRef(props, 'conditionalPorts'),
toRef(props, 'extended'),
layoutTransitions.active,
currentEdit,
() => {
emit('openFullMenu')
},
Expand Down
14 changes: 13 additions & 1 deletion app/gui2/src/components/GraphEditor/widgets/WidgetNumber.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import NumericInputWidget from '@/components/widgets/NumericInputWidget.vue'
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
import { WidgetEditHandler } from '@/providers/widgetRegistry/editHandler'
import { Ast } from '@/util/ast'
import { computed } from 'vue'
import { computed, ref, type ComponentInstance } from 'vue'
const props = defineProps(widgetProps(widgetDefinition))
const inputComponent = ref<ComponentInstance<typeof NumericInputWidget>>()
const value = computed({
get() {
const valueStr = WidgetInput.valueRepr(props.input)
Expand All @@ -25,6 +27,12 @@ const limits = computed(() => {
return undefined
}
})
const editHandler = WidgetEditHandler.New(props.input, {
cancel: () => inputComponent.value?.cancel(),
start: () => inputComponent.value?.focus(),
end: () => inputComponent.value?.blur(),
})
</script>

<script lang="ts">
Expand Down Expand Up @@ -52,11 +60,15 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
<template>
<!-- See comment in GraphNode next to dragPointer definition about stopping pointerdown and pointerup -->
<NumericInputWidget
ref="inputComponent"
v-model="value"
class="WidgetNumber r-24"
:limits="limits"
@pointerdown.stop
@pointerup.stop
@focus="editHandler.start()"
@blur="editHandler.end()"
@input="editHandler.edit($event)"
/>
</template>

Expand Down
15 changes: 12 additions & 3 deletions app/gui2/src/components/GraphEditor/widgets/WidgetSelection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import DropdownWidget, { type DropdownEntry } from '@/components/widgets/DropdownWidget.vue'
import { unrefElement } from '@/composables/events'
import type { PortId } from '@/providers/portInfo'
import { defineWidget, Score, WidgetInput, widgetProps } from '@/providers/widgetRegistry'
import {
multipleChoiceConfiguration,
Expand Down Expand Up @@ -34,7 +35,8 @@ const tree = injectWidgetTree()
const dropdownElement = ref<ComponentInstance<typeof DropdownWidget>>()
const editedValue = ref<Ast.Ast | string | undefined>()
const editedWidget = ref<PortId>()
const editedValue = ref<Ast.Owned | string | undefined>()
const isHovered = ref(false)
class ExpressionTag {
Expand Down Expand Up @@ -185,22 +187,29 @@ const dropDownInteraction = WidgetEditHandler.New(props.input, {
click: (e, _, childHandler) => {
if (targetIsOutside(e, unrefElement(dropdownElement))) {
if (childHandler) return childHandler()
else dropdownVisible.value = false
else {
dropDownInteraction.end()
if (editedWidget.value)
props.onUpdate({ portUpdate: { origin: editedWidget.value, value: editedValue.value } })
}
}
return false
},
start: () => {
dropdownVisible.value = true
editedWidget.value = undefined
editedValue.value = undefined
},
edit: (_, value) => {
edit: (origin, value) => {
editedWidget.value = origin
editedValue.value = value
},
end: () => {
dropdownVisible.value = false
},
addItem: () => {
dropdownVisible.value = true
editedWidget.value = undefined
editedValue.value = undefined
return true
},
Expand Down
28 changes: 23 additions & 5 deletions app/gui2/src/components/widgets/NumericInputWidget.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
<script setup lang="ts">
import { PointerButtonMask, usePointer } from '@/composables/events'
import { WidgetEditHandler } from '@/providers/widgetRegistry/editHandler'
import { computed, ref, watch, type ComponentInstance, type StyleValue } from 'vue'
import AutoSizedInput from './AutoSizedInput.vue'
const props = defineProps<{
modelValue: number | string
limits?: { min: number; max: number } | undefined
}>()
const emit = defineEmits<{ 'update:modelValue': [modelValue: number | string] }>()
const emit = defineEmits<{
'update:modelValue': [modelValue: number | string]
blur: []
focus: []
input: [content: string]
}>()
const inputFieldActive = ref(false)
// Edited value reflects the `modelValue`, but does not update it until the user defocuses the field.
Expand Down Expand Up @@ -87,14 +93,25 @@ function emitUpdate() {
}
}
function blur() {
function blurred() {
inputFieldActive.value = false
emit('blur')
emitUpdate()
}
function focus() {
function focused() {
inputFieldActive.value = true
emit('focus')
}
defineExpose({
cancel: () => {
editedValue.value = `${props.modelValue}`
inputComponent.value?.blur()
},
blur: () => inputComponent.value?.blur(),
focus: () => inputComponent.value?.focus(),
})
</script>

<template>
Expand All @@ -106,8 +123,9 @@ function focus() {
autoSelect
:style="inputStyle"
v-on="dragPointer.events"
@blur="blur"
@focus="focus"
@blur="blurred"
@focus="focused"
@input="emit('input', editedValue)"
/>
</label>
</template>
Expand Down
66 changes: 43 additions & 23 deletions app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function editHandlerTree(
widgets: string[],
interactionHandler: InteractionHandler,
createInteraction: (name: string) => Record<string, Mock>,
widgetTree: { currentEdit: WidgetEditHandler | undefined },
): Map<string, { handler: WidgetEditHandler; interaction: Record<string, Mock> }> {
const handlers = new Map()
for (const id of widgets) {
Expand All @@ -24,6 +25,7 @@ function editHandlerTree(
interaction,
parent ? handlers.get(parent)?.handler : undefined,
interactionHandler,
widgetTree,
)
handlers.set(id, { handler, interaction })
}
Expand All @@ -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) {
Expand All @@ -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))
Expand All @@ -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()
},
Expand All @@ -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)
Expand Down
22 changes: 14 additions & 8 deletions app/gui2/src/providers/widgetRegistry/editHandler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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?.()
},
Expand All @@ -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)
},
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 09a6ab7

Please sign in to comment.