Hide placeholders (#9246)

* New menuVisible logic

* Hide placeholders unless sole selected node, required argument, or requested by dynamic config
This commit is contained in:
Kaz Wesley 2024-03-05 09:16:38 -05:00 committed by GitHub
parent f02213ae2b
commit b0842feea2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 68 additions and 27 deletions

View File

@ -60,6 +60,10 @@ test('Selection widgets in Data.read node', async ({ page }) => {
// Check initially visible arguments // Check initially visible arguments
const node = locate.graphNodeByBinding(page, 'data') const node = locate.graphNodeByBinding(page, 'data')
const argumentNames = node.locator('.WidgetArgumentName') const argumentNames = node.locator('.WidgetArgumentName')
await expect(argumentNames).toHaveCount(1)
// Check arguments after selecting node
await node.click()
await expect(argumentNames).toHaveCount(3) await expect(argumentNames).toHaveCount(3)
// Set value on `on_problems` (static drop-down) // Set value on `on_problems` (static drop-down)
@ -129,6 +133,10 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
// Check initially visible arguments // Check initially visible arguments
const node = locate.graphNodeByBinding(page, 'aggregated') const node = locate.graphNodeByBinding(page, 'aggregated')
const argumentNames = node.locator('.WidgetArgumentName') const argumentNames = node.locator('.WidgetArgumentName')
await expect(argumentNames).toHaveCount(1)
// Check arguments after selecting node
await node.click()
await expect(argumentNames).toHaveCount(3) await expect(argumentNames).toHaveCount(3)
// Add first aggregate // Add first aggregate

View File

@ -30,6 +30,7 @@ export const graphSelection: GraphSelection = {
hoveredNode: undefined, hoveredNode: undefined,
hoveredPort: undefined, hoveredPort: undefined,
isSelected: () => false, isSelected: () => false,
isChanging: false,
mouseHandler: () => false, mouseHandler: () => false,
selectAll: () => {}, selectAll: () => {},
selected: new Set(), selected: new Set(),

View File

@ -88,14 +88,6 @@ const contentNode = ref<HTMLElement>()
const nodeSize = useResizeObserver(rootNode) const nodeSize = useResizeObserver(rootNode)
const baseNodeSize = computed(() => new Vec2(contentNode.value?.scrollWidth ?? 0, nodeSize.value.y)) const baseNodeSize = computed(() => new Vec2(contentNode.value?.scrollWidth ?? 0, nodeSize.value.y))
/// Menu can be full, partial or off
enum MenuState {
Full,
Partial,
Off,
}
const menuVisible = ref(MenuState.Off)
const error = computed(() => { const error = computed(() => {
const externalId = graph.db.idToExternal(nodeId.value) const externalId = graph.db.idToExternal(nodeId.value)
if (!externalId) return if (!externalId) return
@ -122,12 +114,19 @@ const warning = computed(() => {
}) })
const isSelected = computed(() => nodeSelection?.isSelected(nodeId.value) ?? false) const isSelected = computed(() => nodeSelection?.isSelected(nodeId.value) ?? false)
const isOnlyOneSelected = computed(() => isSelected.value && nodeSelection?.selected.size === 1) const isOnlyOneSelected = computed(
watch(isSelected, (selected) => { () => isSelected.value && nodeSelection?.selected.size === 1 && !nodeSelection.isChanging,
if (!selected) { )
menuVisible.value = MenuState.Off
} const menuVisible = isOnlyOneSelected
const menuFull = ref(false)
watch(menuVisible, (visible) => {
if (!visible) menuFull.value = false
}) })
function openFullMenu() {
menuFull.value = true
nodeSelection?.setSelection(new Set([nodeId.value]))
}
const isDocsVisible = ref(false) const isDocsVisible = ref(false)
const visualizationWidth = computed(() => props.node.vis?.width ?? null) const visualizationWidth = computed(() => props.node.vis?.width ?? null)
@ -181,7 +180,6 @@ const dragPointer = usePointer((pos, event, type) => {
) { ) {
nodeSelection?.handleSelectionOf(event, new Set([nodeId.value])) nodeSelection?.handleSelectionOf(event, new Set([nodeId.value]))
handleNodeClick(event) handleNodeClick(event)
menuVisible.value = MenuState.Partial
} }
startEvent = null startEvent = null
startEpochMs.value = 0 startEpochMs.value = 0
@ -363,13 +361,6 @@ function portGroupStyle(port: PortData) {
} }
} }
function openFullMenu() {
if (!nodeSelection?.isSelected(nodeId.value)) {
nodeSelection?.setSelection(new Set([nodeId.value]))
}
menuVisible.value = MenuState.Full
}
const editingComment = ref(false) const editingComment = ref(false)
const documentation = computed<string | undefined>({ const documentation = computed<string | undefined>({
@ -419,12 +410,12 @@ const documentation = computed<string | undefined>({
{{ node.pattern?.code() ?? '' }} {{ node.pattern?.code() ?? '' }}
</div> </div>
<CircularMenu <CircularMenu
v-if="menuVisible === MenuState.Full || menuVisible === MenuState.Partial" v-if="menuVisible"
v-model:isOutputContextOverridden="isOutputContextOverridden" v-model:isOutputContextOverridden="isOutputContextOverridden"
v-model:isDocsVisible="isDocsVisible" v-model:isDocsVisible="isDocsVisible"
:isOutputContextEnabledGlobally="projectStore.isOutputContextEnabled" :isOutputContextEnabledGlobally="projectStore.isOutputContextEnabled"
:isVisualizationVisible="isVisualizationVisible" :isVisualizationVisible="isVisualizationVisible"
:isFullMenuVisible="menuVisible === MenuState.Full" :isFullMenuVisible="menuVisible && menuFull"
@update:isVisualizationVisible="emit('update:visualizationVisible', $event)" @update:isVisualizationVisible="emit('update:visualizationVisible', $event)"
@startEditing="startEditingNode" @startEditing="startEditingNode"
@startEditingComment="editingComment = true" @startEditingComment="editingComment = true"
@ -436,7 +427,7 @@ const documentation = computed<string | undefined>({
:nodeSize="baseNodeSize" :nodeSize="baseNodeSize"
:scale="navigator?.scale ?? 1" :scale="navigator?.scale ?? 1"
:nodePosition="props.node.position" :nodePosition="props.node.position"
:isCircularMenuVisible="menuVisible === MenuState.Full || menuVisible === MenuState.Partial" :isCircularMenuVisible="menuVisible"
:currentType="node.vis?.identifier" :currentType="node.vis?.identifier"
:isFullscreen="isVisualizationFullscreen" :isFullscreen="isVisualizationFullscreen"
:dataSource="{ type: 'node', nodeId: externalId }" :dataSource="{ type: 'node', nodeId: externalId }"
@ -474,6 +465,7 @@ const documentation = computed<string | undefined>({
:icon="icon" :icon="icon"
:connectedSelfArgumentId="connectedSelfArgumentId" :connectedSelfArgumentId="connectedSelfArgumentId"
:potentialSelfArgumentId="potentialSelfArgumentId" :potentialSelfArgumentId="potentialSelfArgumentId"
:extended="isOnlyOneSelected"
@openFullMenu="openFullMenu" @openFullMenu="openFullMenu"
/> />
</div> </div>
@ -484,7 +476,7 @@ const documentation = computed<string | undefined>({
<GraphNodeError <GraphNodeError
v-if="warning && (nodeHovered || isSelected)" v-if="warning && (nodeHovered || isSelected)"
class="afterNode warning" class="afterNode warning"
:class="{ messageWithMenu: menuVisible !== MenuState.Off }" :class="{ messageWithMenu: menuVisible }"
:message="warning" :message="warning"
icon="warning" icon="warning"
type="warning" type="warning"

View File

@ -15,6 +15,7 @@ const props = defineProps<{
icon: Icon icon: Icon
connectedSelfArgumentId: Ast.AstId | undefined connectedSelfArgumentId: Ast.AstId | undefined
potentialSelfArgumentId: Ast.AstId | undefined potentialSelfArgumentId: Ast.AstId | undefined
extended: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
openFullMenu: [] openFullMenu: []
@ -69,6 +70,7 @@ provideWidgetTree(
toRef(props, 'icon'), toRef(props, 'icon'),
toRef(props, 'connectedSelfArgumentId'), toRef(props, 'connectedSelfArgumentId'),
toRef(props, 'potentialSelfArgumentId'), toRef(props, 'potentialSelfArgumentId'),
toRef(props, 'extended'),
layoutTransitions.active, layoutTransitions.active,
() => { () => {
emit('openFullMenu') emit('openFullMenu')

View File

@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue' import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
import { WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry' import { WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
import { injectWidgetTree } from '@/providers/widgetTree'
import { Ast } from '@/util/ast' import { Ast } from '@/util/ast'
import { ArgumentApplication, ArgumentApplicationKey } from '@/util/callTree' import { ArgumentApplication, ArgumentApplicationKey } from '@/util/callTree'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps(widgetProps(widgetDefinition)) const props = defineProps(widgetProps(widgetDefinition))
const tree = injectWidgetTree()
const application = computed(() => props.input[ArgumentApplicationKey]) const application = computed(() => props.input[ArgumentApplicationKey])
const targetMaybePort = computed(() => { const targetMaybePort = computed(() => {
const target = application.value.target const target = application.value.target
@ -45,7 +47,13 @@ export const widgetDefinition = defineWidget(ArgumentApplicationKey, {
<div v-if="application.infixOperator" class="infixOp" :style="operatorStyle"> <div v-if="application.infixOperator" class="infixOp" :style="operatorStyle">
<NodeWidget :input="WidgetInput.FromAst(application.infixOperator)" /> <NodeWidget :input="WidgetInput.FromAst(application.infixOperator)" />
</div> </div>
<NodeWidget :input="application.argument.toWidgetInput()" nest /> <div
v-if="tree.extended || !application.argument.hideByDefault"
class="argument"
:class="{ animateWhenShown: application.argument.hideByDefault }"
>
<NodeWidget :input="application.argument.toWidgetInput()" nest />
</div>
</span> </span>
</template> </template>
@ -75,4 +83,22 @@ export const widgetDefinition = defineWidget(ArgumentApplicationKey, {
white-space: pre; white-space: pre;
} }
} }
.argument {
display: flex;
flex-direction: row;
place-items: center;
}
.animateWhenShown {
animation: show 4800ms 100ms cubic-bezier(0.38, 0.97, 0.56, 0.76) forwards;
max-width: 0;
overflow-x: clip;
}
@keyframes show {
100% {
max-width: 2000px;
}
}
</style> </style>

View File

@ -148,6 +148,7 @@ export function useSelection<T>(
}, },
deselectAll: () => selected.clear(), deselectAll: () => selected.clear(),
isSelected: (element: T) => selected.has(element), isSelected: (element: T) => selected.has(element),
isChanging: computed(() => anchor.value != null),
setSelection, setSelection,
handleSelectionOf, handleSelectionOf,
hoveredNode, hoveredNode,

View File

@ -14,6 +14,7 @@ const { provideFn, injectFn } = createContextStore(
icon: Ref<Icon>, icon: Ref<Icon>,
connectedSelfArgumentId: Ref<Ast.AstId | undefined>, connectedSelfArgumentId: Ref<Ast.AstId | undefined>,
potentialSelfArgumentId: Ref<Ast.AstId | undefined>, potentialSelfArgumentId: Ref<Ast.AstId | undefined>,
extended: Ref<boolean>,
hasActiveAnimations: Ref<boolean>, hasActiveAnimations: Ref<boolean>,
emitOpenFullMenu: () => void, emitOpenFullMenu: () => void,
) => { ) => {
@ -25,6 +26,7 @@ const { provideFn, injectFn } = createContextStore(
icon, icon,
connectedSelfArgumentId, connectedSelfArgumentId,
potentialSelfArgumentId, potentialSelfArgumentId,
extended,
nodeSpanStart, nodeSpanStart,
hasActiveAnimations, hasActiveAnimations,
emitOpenFullMenu, emitOpenFullMenu,

View File

@ -2,6 +2,7 @@ import type { PortId } from '@/providers/portInfo'
import { WidgetInput } from '@/providers/widgetRegistry' import { WidgetInput } from '@/providers/widgetRegistry'
import type { WidgetConfiguration } from '@/providers/widgetRegistry/configuration' import type { WidgetConfiguration } from '@/providers/widgetRegistry/configuration'
import * as widgetCfg from '@/providers/widgetRegistry/configuration' import * as widgetCfg from '@/providers/widgetRegistry/configuration'
import { DisplayMode } from '@/providers/widgetRegistry/configuration'
import type { SuggestionEntry, SuggestionEntryArgument } from '@/stores/suggestionDatabase/entry' import type { SuggestionEntry, SuggestionEntryArgument } from '@/stores/suggestionDatabase/entry'
import { Ast } from '@/util/ast' import { Ast } from '@/util/ast'
import { findLastIndex, tryGetIndex } from '@/util/data/array' import { findLastIndex, tryGetIndex } from '@/util/data/array'
@ -23,7 +24,7 @@ export class ArgumentPlaceholder {
public argInfo: SuggestionEntryArgument, public argInfo: SuggestionEntryArgument,
public kind: ApplicationKind, public kind: ApplicationKind,
public insertAsNamed: boolean, public insertAsNamed: boolean,
public dynamicConfig?: WidgetConfiguration | undefined, public dynamicConfig?: (WidgetConfiguration & { display?: DisplayMode }) | undefined,
) {} ) {}
static WithRetrievedConfig( static WithRetrievedConfig(
@ -52,6 +53,10 @@ export class ArgumentPlaceholder {
get portId(): PortId { get portId(): PortId {
return `${this.callId}[${this.index}]` as PortId return `${this.callId}[${this.index}]` as PortId
} }
get hideByDefault(): boolean {
return this.argInfo.hasDefault && this.dynamicConfig?.display !== DisplayMode.Always
}
} }
export class ArgumentAst { export class ArgumentAst {
@ -88,6 +93,10 @@ export class ArgumentAst {
get portId(): PortId { get portId(): PortId {
return this.ast.id return this.ast.id
} }
get hideByDefault(): boolean {
return false
}
} }
type InterpretedCall = InterpretedInfix | InterpretedPrefix type InterpretedCall = InterpretedInfix | InterpretedPrefix