From 1d74bcbdc32451c1b29fcb08b30ed005eb070821 Mon Sep 17 00:00:00 2001 From: Kaz Date: Tue, 27 Feb 2024 16:51:30 -0800 Subject: [PATCH 1/4] Close dropdown when another interaction starts, somewhere else is clicked, or Esc is pressed. --- app/gui2/src/bindings.ts | 1 - app/gui2/src/components/ComponentBrowser.vue | 42 ++++--- app/gui2/src/components/GraphEditor.vue | 119 +++++------------- .../src/components/GraphEditor/GraphEdge.vue | 2 +- .../src/components/GraphEditor/GraphEdges.vue | 2 +- .../GraphEditor/widgets/WidgetSelection.vue | 32 ++++- app/gui2/src/providers/interactionHandler.ts | 26 ++-- 7 files changed, 102 insertions(+), 122 deletions(-) diff --git a/app/gui2/src/bindings.ts b/app/gui2/src/bindings.ts index 0f96b8ee5101..000de151e3b3 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 29b8a9e99cdd..3bc810b6d580 100644 --- a/app/gui2/src/components/ComponentBrowser.vue +++ b/app/gui2/src/components/ComponentBrowser.vue @@ -11,6 +11,7 @@ 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' @@ -35,6 +36,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 +49,26 @@ const emit = defineEmits<{ canceled: [] }>() +const cbOpen: Interaction = { + cancel: () => { + emit('canceled') + }, + click: (e: PointerEvent) => { + if (cbRoot.value && e.target instanceof Element && !cbRoot.value.contains(e.target)) { + if (input.anyChange.value) { + acceptInput() + } else { + interaction.cancel(cbOpen) + } + return true + } else { + return false + } + }, +} + onMounted(() => { + interaction.setCurrent(cbOpen) input.reset(props.usage) if (inputField.value != null) { inputField.value.focus({ preventScroll: true }) @@ -159,23 +180,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 +361,7 @@ function acceptSuggestion(index: Opt = null) { function acceptInput() { emit('accepted', input.code.value.trim(), input.importsToAdd()) + interaction.end(cbOpen) } // === Key Events Handler === @@ -386,9 +391,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 789926de7f96..9ab0f3a4f32e 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() { @@ -317,11 +318,10 @@ const graphBindingsHandler = graphBindings.handler({ }) const { handleClick } = useDoubleClick( - (e: MouseEvent) => { + (e: PointerEvent) => { graphBindingsHandler(e) }, () => { - if (keyboardBusy()) return false stackNavigator.exitNode() }, ) @@ -360,62 +360,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. @@ -432,13 +395,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 @@ -446,15 +403,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() } }, ) @@ -599,30 +554,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 }) } @@ -634,7 +582,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) { :style="groupColors" v-on.="graphNavigator.events" v-on..="nodeSelection.events" - @click="handleClick" + @pointerdown="handleClick" @dragover.prevent @drop.prevent="handleFileDrop($event)" > @@ -652,8 +600,8 @@ function handleEdgeDrop(source: AstId, position: Vec2) { :navigator="graphNavigator" :nodePosition="componentBrowserNodePosition" :usage="componentBrowserUsage" - @accepted="onComponentBrowserCommit" - @canceled="onComponentBrowserCancel" + @accepted="commitComponentBrowser" + @canceled="hideComponentBrowser" /> - + @@ -688,6 +632,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..2baca679e156 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, @@ -24,11 +25,15 @@ import { tryQualifiedName, type IdentifierOrOperatorIdentifier, } from '@/util/qualifiedName' +import { defineKeybinds } from '@/util/shortcuts' +import * as random from 'lib0/random' 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 +124,23 @@ const innerWidgetInput = computed(() => { return { ...props.input, dynamicConfig: singleChoiceConfiguration(config) } }) const showDropdownWidget = ref(false) +interaction.setWhen(showDropdownWidget, { + cancel: () => { + showDropdownWidget.value = false + }, + click: (e: PointerEvent) => { + if (widgetRoot.value && e.target instanceof Element && !widgetRoot.value.contains(e.target)) { + showDropdownWidget.value = false + return true + } else { + return false + } + }, +}) + +const handleClick = defineKeybinds(`dropdown-${random.uint53()}`, { + toggleDropdownWidget: ['PointerMain'], +}).handler({ toggleDropdownWidget }) function toggleDropdownWidget() { showDropdownWidget.value = !showDropdownWidget.value @@ -172,7 +194,13 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {