diff --git a/app/gui2/src/bindings.ts b/app/gui2/src/bindings.ts index 4926981c114a..c7a21460aaa9 100644 --- a/app/gui2/src/bindings.ts +++ b/app/gui2/src/bindings.ts @@ -13,7 +13,6 @@ export const componentBrowserBindings = defineKeybinds('component-browser', { applySuggestion: ['Tab'], acceptSuggestion: ['Enter'], acceptInput: ['Mod+Enter'], - cancelEditing: ['Escape'], moveUp: ['ArrowUp'], moveDown: ['ArrowDown'], }) diff --git a/app/gui2/src/components/ComponentBrowser.vue b/app/gui2/src/components/ComponentBrowser.vue index 1d0fa30ec395..5977af926122 100644 --- a/app/gui2/src/components/ComponentBrowser.vue +++ b/app/gui2/src/components/ComponentBrowser.vue @@ -11,12 +11,14 @@ import ToggleIcon from '@/components/ToggleIcon.vue' import { useApproach } from '@/composables/animation' import { useEvent, useResizeObserver } from '@/composables/events' import type { useNavigator } from '@/composables/navigator' +import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler' import { useGraphStore } from '@/stores/graph' import type { RequiredImport } from '@/stores/graph/imports' import { useProjectStore } from '@/stores/project' import { groupColorStyle, useSuggestionDbStore } from '@/stores/suggestionDatabase' import { SuggestionKind, type SuggestionEntry } from '@/stores/suggestionDatabase/entry' import type { VisualizationDataSource } from '@/stores/visualization' +import { targetIsOutside } from '@/util/autoBlur' import { tryGetIndex } from '@/util/data/array' import type { Opt } from '@/util/data/opt' import { allRanges } from '@/util/data/range' @@ -35,6 +37,7 @@ const COMPONENT_BROWSER_TO_NODE_OFFSET = new Vec2(-4, -4) const projectStore = useProjectStore() const suggestionDbStore = useSuggestionDbStore() const graphStore = useGraphStore() +const interaction = injectInteractionHandler() const props = defineProps<{ nodePosition: Vec2 @@ -47,7 +50,24 @@ const emit = defineEmits<{ canceled: [] }>() +const cbOpen: Interaction = { + cancel: () => { + emit('canceled') + }, + click: (e: PointerEvent) => { + if (targetIsOutside(e, cbRoot)) { + if (input.anyChange.value) { + acceptInput() + } else { + interaction.cancel(cbOpen) + } + } + return false + }, +} + onMounted(() => { + interaction.setCurrent(cbOpen) input.reset(props.usage) if (inputField.value != null) { inputField.value.focus({ preventScroll: true }) @@ -159,23 +179,6 @@ function preventNonInputDefault(e: Event) { } } -useEvent( - window, - 'pointerdown', - (event) => { - if (event.button !== 0) return - if (!(event.target instanceof Element)) return - if (!cbRoot.value?.contains(event.target)) { - if (input.anyChange.value) { - emit('accepted', input.code.value, input.importsToAdd()) - } else { - emit('canceled') - } - } - }, - { capture: true }, -) - const inputElement = ref() const inputSize = useResizeObserver(inputElement, false) @@ -357,6 +360,7 @@ function acceptSuggestion(index: Opt = null) { function acceptInput() { emit('accepted', input.code.value.trim(), input.importsToAdd()) + interaction.end(cbOpen) } // === Key Events Handler === @@ -386,9 +390,6 @@ const handler = componentBrowserBindings.handler({ } scrolling.scrollWithTransition({ type: 'selected' }) }, - cancelEditing() { - emit('canceled') - }, }) diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue index 0f263f71fc54..13061c6e20e3 100644 --- a/app/gui2/src/components/GraphEditor.vue +++ b/app/gui2/src/components/GraphEditor.vue @@ -21,7 +21,7 @@ import { keyboardBusy, keyboardBusyExceptIn, useEvent } from '@/composables/even import { useStackNavigator } from '@/composables/stackNavigator' import { provideGraphNavigator } from '@/providers/graphNavigator' import { provideGraphSelection } from '@/providers/graphSelection' -import { provideInteractionHandler, type Interaction } from '@/providers/interactionHandler' +import { provideInteractionHandler } from '@/providers/interactionHandler' import { provideWidgetRegistry } from '@/providers/widgetRegistry' import { useGraphStore, type NodeId } from '@/stores/graph' import type { RequiredImport } from '@/stores/graph/imports' @@ -110,7 +110,7 @@ const nodeSelection = provideGraphSelection(graphNavigator, graphStore.nodeRects const interactionBindingsHandler = interactionBindings.handler({ cancel: () => interaction.handleCancel(), - click: (e) => (e instanceof MouseEvent ? interaction.handleClick(e, graphNavigator) : false), + click: (e) => (e instanceof PointerEvent ? interaction.handleClick(e, graphNavigator) : false), }) // Return the environment for the placement of a new node. The passed nodes should be the nodes that are @@ -162,7 +162,8 @@ function sourcePortForSelection() { } useEvent(window, 'keydown', (event) => { - ;(!keyboardBusy() && (interactionBindingsHandler(event) || graphBindingsHandler(event))) || + interactionBindingsHandler(event) || + (!keyboardBusy() && graphBindingsHandler(event)) || (!keyboardBusyExceptIn(codeEditorArea.value) && codeEditorHandler(event)) }) useEvent(window, 'pointerdown', interactionBindingsHandler, { capture: true }) @@ -208,7 +209,7 @@ const graphBindingsHandler = graphBindings.handler({ openComponentBrowser() { if (keyboardBusy()) return false if (graphNavigator.sceneMousePos != null && !componentBrowserVisible.value) { - interaction.setCurrent(creatingNode) + showComponentBrowser() } }, newNode() { @@ -331,7 +332,6 @@ const { handleClick } = useDoubleClick( } }, () => { - if (keyboardBusy()) return false stackNavigator.exitNode() }, ) @@ -370,62 +370,25 @@ const groupColors = computed(() => { return styles }) -const editingNode: Interaction = { - init: () => { - // component browser usage is set in `graphStore.editedNodeInfo` watch - componentBrowserNodePosition.value = targetComponentBrowserNodePosition() - }, - cancel: () => { - hideComponentBrowser() - graphStore.editedNodeInfo = undefined - }, -} -const nodeIsBeingEdited = computed(() => graphStore.editedNodeInfo != null) -interaction.setWhen(nodeIsBeingEdited, editingNode) - -const creatingNode: Interaction = { - init: () => { - componentBrowserUsage.value = { type: 'newNode', sourcePort: sourcePortForSelection() } - componentBrowserNodePosition.value = targetComponentBrowserNodePosition() - componentBrowserVisible.value = true - }, - cancel: hideComponentBrowser, -} - -const creatingNodeFromButton: Interaction = { - init: () => { - componentBrowserUsage.value = { type: 'newNode', sourcePort: sourcePortForSelection() } - let targetPos = placementPositionForSelection() - if (targetPos == undefined) { - targetPos = nonDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value).position - } - componentBrowserNodePosition.value = targetPos - componentBrowserVisible.value = true - }, - cancel: hideComponentBrowser, -} - -const creatingNodeFromPortDoubleClick: Interaction = { - init: () => { - // component browser usage is set in event handler - componentBrowserVisible.value = true - }, - cancel: hideComponentBrowser, +function showComponentBrowser(nodePosition?: Vec2, usage?: Usage) { + componentBrowserUsage.value = usage ?? { type: 'newNode', sourcePort: sourcePortForSelection() } + componentBrowserNodePosition.value = nodePosition ?? targetComponentBrowserNodePosition() + componentBrowserVisible.value = true } -const creatingNodeFromEdgeDrop: Interaction = { - init: () => { - // component browser usage is set in event handler - componentBrowserVisible.value = true - }, - cancel: hideComponentBrowser, +function startCreatingNodeFromButton() { + const targetPos = + placementPositionForSelection() ?? + nonDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment.value).position + showComponentBrowser(targetPos) } function hideComponentBrowser() { + graphStore.editedNodeInfo = undefined componentBrowserVisible.value = false } -function onComponentBrowserCommit(content: string, requiredImports: RequiredImport[]) { +function commitComponentBrowser(content: string, requiredImports: RequiredImport[]) { if (content != null) { if (graphStore.editedNodeInfo) { // We finish editing a node. @@ -442,13 +405,7 @@ function onComponentBrowserCommit(content: string, requiredImports: RequiredImpo if (createdNode) nodeSelection.setSelection(new Set([createdNode])) } } - // Finish interaction. This should also hide component browser. - interaction.setCurrent(undefined) -} - -function onComponentBrowserCancel() { - // Finish interaction. This should also hide component browser. - interaction.setCurrent(undefined) + hideComponentBrowser() } // Watch the `editedNode` in the graph store @@ -456,15 +413,13 @@ watch( () => graphStore.editedNodeInfo, (editedInfo) => { if (editedInfo) { - componentBrowserNodePosition.value = targetComponentBrowserNodePosition() - componentBrowserUsage.value = { + showComponentBrowser(undefined, { type: 'editNode', node: editedInfo.id, cursorPos: editedInfo.initialCursorPos, - } - componentBrowserVisible.value = true + }) } else { - componentBrowserVisible.value = false + hideComponentBrowser() } }, ) @@ -609,30 +564,23 @@ async function readNodeFromExcelClipboard( } function handleNodeOutputPortDoubleClick(id: AstId) { - componentBrowserUsage.value = { type: 'newNode', sourcePort: id } const srcNode = graphStore.db.getPatternExpressionNodeId(id) if (srcNode == null) { console.error('Impossible happened: Double click on port not belonging to any node: ', id) return } const placementEnvironment = environmentForNodes([srcNode].values()) - componentBrowserNodePosition.value = previousNodeDictatedPlacement( - DEFAULT_NODE_SIZE, - placementEnvironment, - { - horizontalGap: gapBetweenNodes, - verticalGap: gapBetweenNodes, - }, - ).position - interaction.setCurrent(creatingNodeFromPortDoubleClick) + const position = previousNodeDictatedPlacement(DEFAULT_NODE_SIZE, placementEnvironment, { + horizontalGap: gapBetweenNodes, + verticalGap: gapBetweenNodes, + }).position + showComponentBrowser(position, { type: 'newNode', sourcePort: id }) } const stackNavigator = useStackNavigator() function handleEdgeDrop(source: AstId, position: Vec2) { - componentBrowserUsage.value = { type: 'newNode', sourcePort: source } - componentBrowserNodePosition.value = position - interaction.setCurrent(creatingNodeFromEdgeDrop) + showComponentBrowser(position, { type: 'newNode', sourcePort: source }) } @@ -662,8 +610,8 @@ function handleEdgeDrop(source: AstId, position: Vec2) { :navigator="graphNavigator" :nodePosition="componentBrowserNodePosition" :usage="componentBrowserUsage" - @accepted="onComponentBrowserCommit" - @canceled="onComponentBrowserCancel" + @accepted="commitComponentBrowser" + @canceled="hideComponentBrowser" /> - + @@ -698,6 +642,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) { position: relative; contain: layout; overflow: clip; + user-select: none; --group-color-fallback: #006b8a; --node-color-no-type: #596b81; } diff --git a/app/gui2/src/components/GraphEditor/GraphEdge.vue b/app/gui2/src/components/GraphEditor/GraphEdge.vue index 39ebc0ca44bb..d475e581f8a0 100644 --- a/app/gui2/src/components/GraphEditor/GraphEdge.vue +++ b/app/gui2/src/components/GraphEditor/GraphEdge.vue @@ -509,7 +509,7 @@ const connected = computed(() => isConnected(props.edge)) class="edge io" :data-source-node-id="sourceNode" :data-target-node-id="targetNode" - @pointerdown="click" + @pointerdown.stop="click" @pointerenter="hovered = true" @pointerleave="hovered = false" /> diff --git a/app/gui2/src/components/GraphEditor/GraphEdges.vue b/app/gui2/src/components/GraphEditor/GraphEdges.vue index 958503be431b..f4327c181b1f 100644 --- a/app/gui2/src/components/GraphEditor/GraphEdges.vue +++ b/app/gui2/src/components/GraphEditor/GraphEdges.vue @@ -26,7 +26,7 @@ const editingEdge: Interaction = { cancel() { graph.clearUnconnected() }, - click(_e: MouseEvent, graphNavigator: GraphNavigator): boolean { + click(_e: PointerEvent, graphNavigator: GraphNavigator): boolean { if (graph.unconnectedEdge == null) return false let source: AstId | undefined let sourceNode: NodeId | undefined diff --git a/app/gui2/src/components/GraphEditor/widgets/WidgetSelection.vue b/app/gui2/src/components/GraphEditor/widgets/WidgetSelection.vue index 7c60187801f0..9480942534ce 100644 --- a/app/gui2/src/components/GraphEditor/widgets/WidgetSelection.vue +++ b/app/gui2/src/components/GraphEditor/widgets/WidgetSelection.vue @@ -2,7 +2,8 @@ import NodeWidget from '@/components/GraphEditor/NodeWidget.vue' import SvgIcon from '@/components/SvgIcon.vue' import DropdownWidget from '@/components/widgets/DropdownWidget.vue' -import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry' +import { injectInteractionHandler } from '@/providers/interactionHandler' +import { defineWidget, Score, WidgetInput, widgetProps } from '@/providers/widgetRegistry' import { singleChoiceConfiguration, type ArgumentWidgetConfiguration, @@ -16,6 +17,7 @@ import { } from '@/stores/suggestionDatabase/entry.ts' import { Ast } from '@/util/ast' import type { TokenId } from '@/util/ast/abstract.ts' +import { targetIsOutside } from '@/util/autoBlur' import { ArgumentInfoKey } from '@/util/callTree' import { arrayEquals } from '@/util/data/array' import { asNot } from '@/util/data/types.ts' @@ -29,6 +31,8 @@ import { computed, ref, watch } from 'vue' const props = defineProps(widgetProps(widgetDefinition)) const suggestions = useSuggestionDbStore() const graph = useGraphStore() +const interaction = injectInteractionHandler() +const widgetRoot = ref() interface Tag { /** If not set, the label is same as expression */ @@ -119,6 +123,15 @@ const innerWidgetInput = computed(() => { return { ...props.input, dynamicConfig: singleChoiceConfiguration(config) } }) const showDropdownWidget = ref(false) +interaction.setWhen(showDropdownWidget, { + cancel: () => { + showDropdownWidget.value = false + }, + click: (e: PointerEvent) => { + if (targetIsOutside(e, widgetRoot)) showDropdownWidget.value = false + return false + }, +}) function toggleDropdownWidget() { showDropdownWidget.value = !showDropdownWidget.value @@ -172,7 +185,13 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {