mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Support selection of app inputs and outputs from vue mode (#9259)
- The input and output indicators are now plugged directly into the `LGraphNode.vue` template. Care was taken to make implementation to have low cost for performance and complexity when not in app mode setup. - Context menu event handlers are added to each widget in vue mode instead of resolving the target widget of an event - Swap the nodeId passed by `useGraphNodeManager` to not include the locator id. This id was never used and was incorrect since it didn't resolve across nested subgraphs. - Continued bug fixes for app mode as a whole. Known issue: There is disparity of nodeId between litegraph (which references the widget in the root graph) and vue (which promotes the original widget). Efforts to reconcile are ongoing. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9259-Support-selection-app-inputs-and-outputs-from-vue-mode-3136d73d365081ae8e56e35bf6322409) by [Unito](https://www.unito.io) --------- Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
This commit is contained in:
@@ -155,7 +155,7 @@ const unifiedWidth = computed(() =>
|
|||||||
|
|
||||||
const { focusMode } = storeToRefs(workspaceStore)
|
const { focusMode } = storeToRefs(workspaceStore)
|
||||||
|
|
||||||
const { isSelectMode } = useAppMode()
|
const { isSelectMode, isBuilderMode } = useAppMode()
|
||||||
const { activeSidebarTabId, activeSidebarTab } = storeToRefs(sidebarTabStore)
|
const { activeSidebarTabId, activeSidebarTab } = storeToRefs(sidebarTabStore)
|
||||||
const { bottomPanelVisible } = storeToRefs(useBottomPanelStore())
|
const { bottomPanelVisible } = storeToRefs(useBottomPanelStore())
|
||||||
const { isOpen: rightSidePanelVisible } = storeToRefs(rightSidePanelStore)
|
const { isOpen: rightSidePanelVisible } = storeToRefs(rightSidePanelStore)
|
||||||
@@ -163,7 +163,9 @@ const showOffsideSplitter = computed(
|
|||||||
() => rightSidePanelVisible.value || isSelectMode.value
|
() => rightSidePanelVisible.value || isSelectMode.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const sidebarPanelVisible = computed(() => activeSidebarTab.value !== null)
|
const sidebarPanelVisible = computed(
|
||||||
|
() => activeSidebarTab.value !== null && !isBuilderMode.value
|
||||||
|
)
|
||||||
|
|
||||||
const sidebarStateKey = computed(() => {
|
const sidebarStateKey = computed(() => {
|
||||||
return unifiedWidth.value
|
return unifiedWidth.value
|
||||||
|
|||||||
@@ -38,20 +38,28 @@ const workflowStore = useWorkflowStore()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const canvas: LGraphCanvas = canvasStore.getCanvas()
|
const canvas: LGraphCanvas = canvasStore.getCanvas()
|
||||||
|
|
||||||
const { mode, isArrangeMode } = useAppMode()
|
const { isSelectMode, isArrangeMode } = useAppMode()
|
||||||
const hoveringSelectable = ref(false)
|
const hoveringSelectable = ref(false)
|
||||||
|
|
||||||
provide(HideLayoutFieldKey, true)
|
provide(HideLayoutFieldKey, true)
|
||||||
|
|
||||||
workflowStore.activeWorkflow?.changeTracker?.reset()
|
workflowStore.activeWorkflow?.changeTracker?.reset()
|
||||||
|
|
||||||
|
function resolveNode(nodeId: NodeId) {
|
||||||
|
return (
|
||||||
|
app.rootGraph.getNodeById(nodeId) ??
|
||||||
|
[...app.rootGraph.subgraphs.values()]
|
||||||
|
.flatMap((sg) => sg.nodes)
|
||||||
|
.find((n) => n.id == nodeId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Prune stale entries whose node/widget no longer exists, so the
|
// Prune stale entries whose node/widget no longer exists, so the
|
||||||
// DraggableList model always matches the rendered items.
|
// DraggableList model always matches the rendered items.
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const valid = appModeStore.selectedInputs.filter(([nodeId, widgetName]) => {
|
const valid = appModeStore.selectedInputs.filter(([nodeId, widgetName]) =>
|
||||||
const node = app.rootGraph.getNodeById(nodeId)
|
resolveNode(nodeId)?.widgets?.some((w) => w.name === widgetName)
|
||||||
return node?.widgets?.some((w) => w.name === widgetName)
|
)
|
||||||
})
|
|
||||||
if (valid.length < appModeStore.selectedInputs.length) {
|
if (valid.length < appModeStore.selectedInputs.length) {
|
||||||
appModeStore.selectedInputs = valid
|
appModeStore.selectedInputs = valid
|
||||||
}
|
}
|
||||||
@@ -60,7 +68,7 @@ watchEffect(() => {
|
|||||||
const arrangeInputs = computed(() =>
|
const arrangeInputs = computed(() =>
|
||||||
appModeStore.selectedInputs
|
appModeStore.selectedInputs
|
||||||
.map(([nodeId, widgetName]) => {
|
.map(([nodeId, widgetName]) => {
|
||||||
const node = app.rootGraph.getNodeById(nodeId)
|
const node = resolveNode(nodeId)
|
||||||
const widget = node?.widgets?.find((w) => w.name === widgetName)
|
const widget = node?.widgets?.find((w) => w.name === widgetName)
|
||||||
if (!node || !widget) return null
|
if (!node || !widget) return null
|
||||||
return { nodeId, widgetName, node, widget }
|
return { nodeId, widgetName, node, widget }
|
||||||
@@ -70,7 +78,7 @@ const arrangeInputs = computed(() =>
|
|||||||
|
|
||||||
const inputsWithState = computed(() =>
|
const inputsWithState = computed(() =>
|
||||||
appModeStore.selectedInputs.map(([nodeId, widgetName]) => {
|
appModeStore.selectedInputs.map(([nodeId, widgetName]) => {
|
||||||
const node = app.rootGraph.getNodeById(nodeId)
|
const node = resolveNode(nodeId)
|
||||||
const widget = node?.widgets?.find((w) => w.name === widgetName)
|
const widget = node?.widgets?.find((w) => w.name === widgetName)
|
||||||
if (!node || !widget) return { nodeId, widgetName }
|
if (!node || !widget) return { nodeId, widgetName }
|
||||||
|
|
||||||
@@ -168,14 +176,14 @@ function handleClick(e: MouseEvent) {
|
|||||||
if (!widget) {
|
if (!widget) {
|
||||||
if (!node.constructor.nodeData?.output_node)
|
if (!node.constructor.nodeData?.output_node)
|
||||||
return canvasInteractions.forwardEventToCanvas(e)
|
return canvasInteractions.forwardEventToCanvas(e)
|
||||||
const index = appModeStore.selectedOutputs.findIndex((id) => id === node.id)
|
const index = appModeStore.selectedOutputs.findIndex((id) => id == node.id)
|
||||||
if (index === -1) appModeStore.selectedOutputs.push(node.id)
|
if (index === -1) appModeStore.selectedOutputs.push(node.id)
|
||||||
else appModeStore.selectedOutputs.splice(index, 1)
|
else appModeStore.selectedOutputs.splice(index, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = appModeStore.selectedInputs.findIndex(
|
const index = appModeStore.selectedInputs.findIndex(
|
||||||
([nodeId, widgetName]) => node.id === nodeId && widget.name === widgetName
|
([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName
|
||||||
)
|
)
|
||||||
if (index === -1) appModeStore.selectedInputs.push([node.id, widget.name])
|
if (index === -1) appModeStore.selectedInputs.push([node.id, widget.name])
|
||||||
else appModeStore.selectedInputs.splice(index, 1)
|
else appModeStore.selectedInputs.splice(index, 1)
|
||||||
@@ -239,6 +247,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
|||||||
:disabled="!appModeStore.selectedInputs.length"
|
:disabled="!appModeStore.selectedInputs.length"
|
||||||
class="border-border-subtle border-b"
|
class="border-border-subtle border-b"
|
||||||
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
|
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
|
||||||
|
:tooltip-delay="100"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
@@ -274,7 +283,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
|||||||
() =>
|
() =>
|
||||||
remove(
|
remove(
|
||||||
appModeStore.selectedInputs,
|
appModeStore.selectedInputs,
|
||||||
([id, name]) => nodeId === id && widgetName === name
|
([id, name]) => nodeId == id && widgetName === name
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -286,6 +295,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
|||||||
enable-empty-state
|
enable-empty-state
|
||||||
:disabled="!appModeStore.selectedOutputs.length"
|
:disabled="!appModeStore.selectedOutputs.length"
|
||||||
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
|
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
|
||||||
|
:tooltip-delay="100"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
@@ -319,12 +329,15 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
|||||||
"
|
"
|
||||||
:title
|
:title
|
||||||
:sub-title="String(key)"
|
:sub-title="String(key)"
|
||||||
:remove="() => remove(appModeStore.selectedOutputs, (k) => k === key)"
|
:remove="() => remove(appModeStore.selectedOutputs, (k) => k == key)"
|
||||||
/>
|
/>
|
||||||
</DraggableList>
|
</DraggableList>
|
||||||
</PropertiesAccordionItem>
|
</PropertiesAccordionItem>
|
||||||
|
|
||||||
<Teleport v-if="mode === 'builder:select'" to="body">
|
<Teleport
|
||||||
|
v-if="isSelectMode && !settingStore.get('Comfy.VueNodes.Enabled')"
|
||||||
|
to="body"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
@@ -358,13 +371,19 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
|||||||
<div class="absolute top-0 right-0 size-8">
|
<div class="absolute top-0 right-0 size-8">
|
||||||
<div
|
<div
|
||||||
v-if="isSelected"
|
v-if="isSelected"
|
||||||
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg"
|
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg cursor-pointer pointer-events-auto"
|
||||||
|
@click.stop="
|
||||||
|
remove(appModeStore.selectedOutputs, (k) => k == key)
|
||||||
|
"
|
||||||
|
@pointerdown.stop
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--check] bg-text-foreground size-full" />
|
<i class="icon-[lucide--check] bg-text-foreground size-full" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg"
|
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg cursor-pointer pointer-events-auto"
|
||||||
|
@click.stop="appModeStore.selectedOutputs.push(key)"
|
||||||
|
@pointerdown.stop
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ const entries = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="p-2 my-2 rounded-lg flex items-center-safe">
|
<div class="p-2 my-2 rounded-lg flex items-center-safe">
|
||||||
<span class="mr-auto" v-text="title" />
|
<span class="mr-auto truncate shrink-1" v-text="title" />
|
||||||
<span class="text-muted-foreground mr-2 text-end" v-text="subTitle" />
|
<span
|
||||||
|
class="text-muted-foreground mr-2 text-end truncate shrink-3"
|
||||||
|
v-text="subTitle"
|
||||||
|
/>
|
||||||
<Popover :entries>
|
<Popover :entries>
|
||||||
<template #button>
|
<template #button>
|
||||||
<Button variant="muted-textonly">
|
<Button variant="muted-textonly">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const {
|
|||||||
enableEmptyState,
|
enableEmptyState,
|
||||||
tooltip,
|
tooltip,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
|
tooltipDelay = 1000,
|
||||||
class: className
|
class: className
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -18,6 +19,7 @@ const {
|
|||||||
enableEmptyState?: boolean
|
enableEmptyState?: boolean
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
size?: 'default' | 'lg'
|
size?: 'default' | 'lg'
|
||||||
|
tooltipDelay?: number
|
||||||
class?: string
|
class?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ const isExpanded = computed(() => !isCollapse.value && !disabled)
|
|||||||
|
|
||||||
const tooltipConfig = computed(() => {
|
const tooltipConfig = computed(() => {
|
||||||
if (!tooltip) return undefined
|
if (!tooltip) return undefined
|
||||||
return { value: tooltip, showDelay: 1000 }
|
return { value: tooltip, showDelay: tooltipDelay }
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||||
|
import type {
|
||||||
|
LGraphGroup,
|
||||||
|
LGraphNode,
|
||||||
|
NodeId
|
||||||
|
} from '@/lib/litegraph/src/litegraph'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
||||||
import { isLGraphGroup } from '@/utils/litegraphUtil'
|
import { isLGraphGroup } from '@/utils/litegraphUtil'
|
||||||
@@ -46,7 +51,7 @@ export enum BadgeVariant {
|
|||||||
// Global singleton for NodeOptions component reference
|
// Global singleton for NodeOptions component reference
|
||||||
let nodeOptionsInstance: null | NodeOptionsInstance = null
|
let nodeOptionsInstance: null | NodeOptionsInstance = null
|
||||||
|
|
||||||
const hoveredWidgetName = ref<string>()
|
const hoveredWidget = ref<[string, NodeId | undefined]>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the node options popover
|
* Toggle the node options popover
|
||||||
@@ -63,14 +68,12 @@ export function toggleNodeOptions(event: Event) {
|
|||||||
* Use this for contextmenu events where we always want to show at the new position
|
* Use this for contextmenu events where we always want to show at the new position
|
||||||
* @param event - The trigger event (must be MouseEvent for position)
|
* @param event - The trigger event (must be MouseEvent for position)
|
||||||
*/
|
*/
|
||||||
export function showNodeOptions(event: MouseEvent) {
|
export function showNodeOptions(
|
||||||
hoveredWidgetName.value = undefined
|
event: MouseEvent,
|
||||||
const target = event.target
|
widgetName?: string,
|
||||||
if (target instanceof HTMLElement) {
|
nodeId?: NodeId
|
||||||
const widgetEl = target.closest('.lg-node-widget')
|
) {
|
||||||
if (widgetEl instanceof HTMLElement)
|
hoveredWidget.value = widgetName ? [widgetName, nodeId] : undefined
|
||||||
hoveredWidgetName.value = widgetEl.dataset.widgetName
|
|
||||||
}
|
|
||||||
if (nodeOptionsInstance?.show) {
|
if (nodeOptionsInstance?.show) {
|
||||||
nodeOptionsInstance.show(event)
|
nodeOptionsInstance.show(event)
|
||||||
}
|
}
|
||||||
@@ -259,8 +262,16 @@ export function useMoreOptionsMenu() {
|
|||||||
options.push(...getImageMenuOptions(selectedNodes.value[0]))
|
options.push(...getImageMenuOptions(selectedNodes.value[0]))
|
||||||
options.push({ type: 'divider' })
|
options.push({ type: 'divider' })
|
||||||
}
|
}
|
||||||
const rawName = hoveredWidgetName.value
|
const [widgetName, nodeId] = hoveredWidget.value ?? []
|
||||||
const widget = node?.widgets?.find((w) => w.name === rawName)
|
const widget =
|
||||||
|
nodeId !== undefined
|
||||||
|
? node?.widgets?.find(
|
||||||
|
(w) =>
|
||||||
|
isPromotedWidgetView(w) &&
|
||||||
|
w.sourceWidgetName === widgetName &&
|
||||||
|
w.sourceNodeId === nodeId
|
||||||
|
)
|
||||||
|
: node?.widgets?.find((w) => w.name === widgetName)
|
||||||
if (widget) {
|
if (widget) {
|
||||||
const widgetOptions = convertContextMenuToOptions(
|
const widgetOptions = convertContextMenuToOptions(
|
||||||
getExtraOptionsForWidget(node, widget)
|
getExtraOptionsForWidget(node, widget)
|
||||||
|
|||||||
@@ -3056,11 +3056,11 @@
|
|||||||
"promptAddInputs": "Click on node parameters to add them here as inputs",
|
"promptAddInputs": "Click on node parameters to add them here as inputs",
|
||||||
"noInputs": "No inputs added yet",
|
"noInputs": "No inputs added yet",
|
||||||
"inputsDesc": "Users will interact and adjust these to generate their outputs.",
|
"inputsDesc": "Users will interact and adjust these to generate their outputs.",
|
||||||
"inputsExample": "Examples: “Load image”, “Text prompt”, “Steps”",
|
"inputsExample": "Examples: “Load\u00a0image”, “Text\u00a0prompt”, “Steps”",
|
||||||
"promptAddOutputs": "Click on output nodes to add them here. These will be the generated results.",
|
"promptAddOutputs": "Click on output nodes to add them here. These will be the generated results.",
|
||||||
"noOutputs": "No output nodes added yet",
|
"noOutputs": "No output nodes added yet",
|
||||||
"outputsDesc": "Connect at least one output node so users can see results after running.",
|
"outputsDesc": "Connect at least one output node so users can see results after running.",
|
||||||
"outputsExample": "Examples: “Save Image” or “Save Video”"
|
"outputsExample": "Examples: “Save\u00a0Image” or “Save\u00a0Video”"
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"clickToClear": "Click to clear queue",
|
"clickToClear": "Click to clear queue",
|
||||||
|
|||||||
62
src/renderer/extensions/linearMode/AppInput.vue
Normal file
62
src/renderer/extensions/linearMode/AppInput.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { remove } from 'es-toolkit'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||||
|
import { useAppModeStore } from '@/stores/appModeStore'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
const { id, name } = defineProps<{
|
||||||
|
id: string
|
||||||
|
isSelectMode: boolean
|
||||||
|
name: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const appModeStore = useAppModeStore()
|
||||||
|
const isPromoted = computed(() => appModeStore.selectedInputs.some(matchesThis))
|
||||||
|
|
||||||
|
function matchesThis([nodeId, widgetName]: [NodeId, string]) {
|
||||||
|
return id == nodeId && name === widgetName
|
||||||
|
}
|
||||||
|
function togglePromotion() {
|
||||||
|
if (isPromoted.value) remove(appModeStore.selectedInputs, matchesThis)
|
||||||
|
else appModeStore.selectedInputs.push([id, name])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isSelectMode"
|
||||||
|
class="col-span-2 flex flex-row pointer-events-auto cursor-pointer gap-1 relative"
|
||||||
|
@pointerdown.capture.stop.prevent="togglePromotion"
|
||||||
|
@click.capture.stop.prevent
|
||||||
|
@pointerup.capture.stop.prevent
|
||||||
|
@pointermove.capture.stop.prevent
|
||||||
|
@contextmenu.capture.stop.prevent
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'border-primary-background border rounded-sm size-4 self-center m-1',
|
||||||
|
isPromoted && 'bg-primary-background flex items-center'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="isPromoted"
|
||||||
|
class="icon-[lucide--check] bg-primary-foreground place-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'grid grid-cols-2 items-stretch ring-primary-background rounded-lg pointer-events-none flex-1',
|
||||||
|
isPromoted && 'ring-2'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div class="absolute size-full hover:bg-primary-background/10 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
<slot v-else />
|
||||||
|
</template>
|
||||||
51
src/renderer/extensions/linearMode/AppOutput.vue
Normal file
51
src/renderer/extensions/linearMode/AppOutput.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { remove } from 'es-toolkit'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||||
|
import { useAppModeStore } from '@/stores/appModeStore'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
const { id } = defineProps<{ id: string }>()
|
||||||
|
|
||||||
|
const appModeStore = useAppModeStore()
|
||||||
|
const isPromoted = computed(() =>
|
||||||
|
appModeStore.selectedOutputs.some(matchesThis)
|
||||||
|
)
|
||||||
|
|
||||||
|
function matchesThis(nodeId: NodeId) {
|
||||||
|
return id == nodeId
|
||||||
|
}
|
||||||
|
function togglePromotion() {
|
||||||
|
if (isPromoted.value) remove(appModeStore.selectedOutputs, matchesThis)
|
||||||
|
else appModeStore.selectedOutputs.push(id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'absolute w-full h-full pointer-events-auto ring-warning-background/50 ring-5 rounded-2xl',
|
||||||
|
isPromoted && 'ring-warning-background'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click.capture.stop.prevent
|
||||||
|
@pointerup.capture.stop.prevent
|
||||||
|
@pointermove.capture.stop.prevent
|
||||||
|
@pointerdown.capture.stop="togglePromotion"
|
||||||
|
@contextmenu.capture.stop.prevent
|
||||||
|
>
|
||||||
|
<div class="absolute top-0 right-0 size-8">
|
||||||
|
<div
|
||||||
|
v-if="isPromoted"
|
||||||
|
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--check] bg-text-foreground size-full" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -16,7 +16,6 @@ import { useTelemetry } from '@/platform/telemetry'
|
|||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
||||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||||
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
|
||||||
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
|
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
@@ -69,7 +68,11 @@ const mappedSelections = computed(() => {
|
|||||||
([id]) => id === nodeId
|
([id]) => id === nodeId
|
||||||
).map(([, widgetName]) => widgetName)
|
).map(([, widgetName]) => widgetName)
|
||||||
unprocessedInputs = unprocessedInputs.slice(inputGroup.length)
|
unprocessedInputs = unprocessedInputs.slice(inputGroup.length)
|
||||||
const node = app.rootGraph.getNodeById(nodeId)
|
const node =
|
||||||
|
app.rootGraph.getNodeById(nodeId) ??
|
||||||
|
[...app.rootGraph.subgraphs.values()]
|
||||||
|
.flatMap((sg) => sg.nodes)
|
||||||
|
.find((n) => n.id == nodeId)
|
||||||
if (!node) continue
|
if (!node) continue
|
||||||
|
|
||||||
const nodeData = nodeToNodeData(node)
|
const nodeData = nodeToNodeData(node)
|
||||||
@@ -239,7 +242,6 @@ defineExpose({ runButtonClick })
|
|||||||
/>
|
/>
|
||||||
<NodeWidgets
|
<NodeWidgets
|
||||||
:node-data
|
:node-data
|
||||||
:style="{ background: applyLightThemeColor(nodeData.bgcolor) }"
|
|
||||||
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 *:has-[textarea]:h-50 rounded-lg max-w-100"
|
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 *:has-[textarea]:h-50 rounded-lg max-w-100"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -279,7 +281,6 @@ defineExpose({ runButtonClick })
|
|||||||
'ring-2 ring-inset ring-node-stroke-error'
|
'ring-2 ring-inset ring-node-stroke-error'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:style="{ background: applyLightThemeColor(nodeData.bgcolor) }"
|
|
||||||
/>
|
/>
|
||||||
</DropZone>
|
</DropZone>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -50,6 +50,10 @@
|
|||||||
@dragleave="handleDragLeave"
|
@dragleave="handleDragLeave"
|
||||||
@drop.stop.prevent="handleDrop"
|
@drop.stop.prevent="handleDrop"
|
||||||
>
|
>
|
||||||
|
<AppOutput
|
||||||
|
v-if="lgraphNode?.constructor?.nodeData?.output_node && isSelectMode"
|
||||||
|
:id="nodeData.id"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="displayHeader"
|
v-if="displayHeader"
|
||||||
class="flex flex-col justify-center items-center relative"
|
class="flex flex-col justify-center items-center relative"
|
||||||
@@ -265,6 +269,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||||
|
import { useAppMode } from '@/composables/useAppMode'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { hasUnpromotedWidgets } from '@/core/graph/subgraph/unpromotedWidgetUtils'
|
import { hasUnpromotedWidgets } from '@/core/graph/subgraph/unpromotedWidgetUtils'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
@@ -284,6 +289,7 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
|||||||
import { usePromotedPreviews } from '@/composables/node/usePromotedPreviews'
|
import { usePromotedPreviews } from '@/composables/node/usePromotedPreviews'
|
||||||
import NodeBadges from '@/renderer/extensions/vueNodes/components/NodeBadges.vue'
|
import NodeBadges from '@/renderer/extensions/vueNodes/components/NodeBadges.vue'
|
||||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||||
|
import AppOutput from '@/renderer/extensions/linearMode/AppOutput.vue'
|
||||||
import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'
|
import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'
|
||||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||||
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||||
@@ -329,6 +335,7 @@ const { nodeData, error = null } = defineProps<LGraphNodeProps>()
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { isSelectMode } = useAppMode()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
|
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
<div
|
<div
|
||||||
v-if="!widget.hidden && (!widget.advanced || showAdvanced)"
|
v-if="!widget.hidden && (!widget.advanced || showAdvanced)"
|
||||||
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
|
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
|
||||||
:data-widget-name="widget.name"
|
|
||||||
>
|
>
|
||||||
<!-- Widget Input Slot Dot -->
|
<!-- Widget Input Slot Dot -->
|
||||||
<div
|
<div
|
||||||
@@ -54,21 +53,24 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Widget Component -->
|
<!-- Widget Component -->
|
||||||
<component
|
<AppInput :id="widget.id" :name="widget.name" :is-select-mode>
|
||||||
:is="widget.vueComponent"
|
<component
|
||||||
v-model="widget.value"
|
:is="widget.vueComponent"
|
||||||
v-tooltip.left="widget.tooltipConfig"
|
v-model="widget.value"
|
||||||
:widget="widget.simplified"
|
v-tooltip.left="widget.tooltipConfig"
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
:widget="widget.simplified"
|
||||||
:node-type="nodeType"
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
:class="
|
:node-type="nodeType"
|
||||||
cn(
|
:class="
|
||||||
'col-span-2',
|
cn(
|
||||||
widget.hasError && 'text-node-stroke-error font-bold'
|
'col-span-2',
|
||||||
)
|
widget.hasError && 'text-node-stroke-error font-bold'
|
||||||
"
|
)
|
||||||
@update:model-value="widget.updateHandler"
|
"
|
||||||
/>
|
@update:model-value="widget.updateHandler"
|
||||||
|
@contextmenu="widget.handleContextMenu"
|
||||||
|
/>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,12 +85,16 @@ import type {
|
|||||||
VueNodeData,
|
VueNodeData,
|
||||||
WidgetSlotMetadata
|
WidgetSlotMetadata
|
||||||
} from '@/composables/graph/useGraphNodeManager'
|
} from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useAppMode } from '@/composables/useAppMode'
|
||||||
|
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
|
import AppInput from '@/renderer/extensions/linearMode/AppInput.vue'
|
||||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||||
|
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||||
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
||||||
// Import widget components directly
|
// Import widget components directly
|
||||||
@@ -117,6 +123,7 @@ const { nodeData } = defineProps<NodeWidgetsProps>()
|
|||||||
|
|
||||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||||
useCanvasInteractions()
|
useCanvasInteractions()
|
||||||
|
const { isSelectMode } = useAppMode()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const { bringNodeToFront } = useNodeZIndex()
|
const { bringNodeToFront } = useNodeZIndex()
|
||||||
const promotionStore = usePromotionStore()
|
const promotionStore = usePromotionStore()
|
||||||
@@ -134,6 +141,8 @@ function handleBringToFront() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { handleNodeRightClick } = useNodeEventHandlers()
|
||||||
|
|
||||||
// Error boundary implementation
|
// Error boundary implementation
|
||||||
const renderError = ref<string | null>(null)
|
const renderError = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -159,9 +168,11 @@ const widgetValueStore = useWidgetValueStore()
|
|||||||
|
|
||||||
interface ProcessedWidget {
|
interface ProcessedWidget {
|
||||||
advanced: boolean
|
advanced: boolean
|
||||||
|
handleContextMenu: (e: PointerEvent) => void
|
||||||
hasLayoutSize: boolean
|
hasLayoutSize: boolean
|
||||||
hasError: boolean
|
hasError: boolean
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
simplified: SimplifiedWidget
|
simplified: SimplifiedWidget
|
||||||
tooltipConfig: TooltipOptions
|
tooltipConfig: TooltipOptions
|
||||||
@@ -240,15 +251,29 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
|
|
||||||
const tooltipText = getWidgetTooltip(widget)
|
const tooltipText = getWidgetTooltip(widget)
|
||||||
const tooltipConfig = createTooltipConfig(tooltipText)
|
const tooltipConfig = createTooltipConfig(tooltipText)
|
||||||
|
const handleContextMenu = (e: PointerEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
handleNodeRightClick(e, nodeId)
|
||||||
|
showNodeOptions(
|
||||||
|
e,
|
||||||
|
widget.name,
|
||||||
|
widget.nodeId !== undefined
|
||||||
|
? String(stripGraphPrefix(widget.nodeId))
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
advanced: widget.options?.advanced ?? false,
|
advanced: widget.options?.advanced ?? false,
|
||||||
|
handleContextMenu,
|
||||||
hasLayoutSize: widget.hasLayoutSize ?? false,
|
hasLayoutSize: widget.hasLayoutSize ?? false,
|
||||||
hasError:
|
hasError:
|
||||||
nodeErrors?.errors?.some(
|
nodeErrors?.errors?.some(
|
||||||
(error) => error.extra_info?.input_name === widget.name
|
(error) => error.extra_info?.input_name === widget.name
|
||||||
) ?? false,
|
) ?? false,
|
||||||
hidden: widget.options?.hidden ?? false,
|
hidden: widget.options?.hidden ?? false,
|
||||||
|
id: String(bareWidgetId),
|
||||||
name: widget.name,
|
name: widget.name,
|
||||||
type: widget.type,
|
type: widget.type,
|
||||||
vueComponent,
|
vueComponent,
|
||||||
|
|||||||
Reference in New Issue
Block a user