Compare commits
8 Commits
v1.46.9
...
glary/widg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dbd437a51 | ||
|
|
6f7ae72646 | ||
|
|
7891439a12 | ||
|
|
9cb9dd9f1c | ||
|
|
8de8031ecb | ||
|
|
6dc819b15b | ||
|
|
5bf8ea01d5 | ||
|
|
bc966a39b7 |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
@@ -39,6 +39,10 @@ import type { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { app } from '@/scripts/app'
|
||||
import { getExecutionIdByNode } from '@/utils/graphTraversalUtil'
|
||||
import type { WidgetGridOverrides } from '@/utils/widgetGridOverrides'
|
||||
import { readGridOverrides } from '@/utils/widgetGridOverrides'
|
||||
|
||||
export type { WidgetGridOverrides }
|
||||
|
||||
export interface WidgetSlotMetadata {
|
||||
index: number
|
||||
@@ -115,6 +119,7 @@ export interface VueNodeData {
|
||||
ghost?: boolean
|
||||
pinned?: boolean
|
||||
}
|
||||
gridOverrides?: WidgetGridOverrides
|
||||
hasErrors?: boolean
|
||||
inputs?: INodeInputSlot[]
|
||||
outputs?: INodeOutputSlot[]
|
||||
@@ -133,6 +138,9 @@ export interface GraphNodeManager {
|
||||
// Access to original LiteGraph nodes (non-reactive)
|
||||
getNode(id: string): LGraphNode | undefined
|
||||
|
||||
// Re-extract VueNodeData for fields not covered by tracked-property events
|
||||
refreshNode(id: string): void
|
||||
|
||||
// Lifecycle methods
|
||||
cleanup(): void
|
||||
}
|
||||
@@ -513,6 +521,7 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
flags: node.flags ? { ...node.flags } : undefined,
|
||||
color: node.color || undefined,
|
||||
bgcolor: node.bgcolor || undefined,
|
||||
gridOverrides: readGridOverrides(node),
|
||||
resizable: node.resizable,
|
||||
shape: node.shape,
|
||||
showAdvanced: node.showAdvanced
|
||||
@@ -858,9 +867,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
})
|
||||
}
|
||||
|
||||
const refreshNode = (id: string) => {
|
||||
const nodeRef = nodeRefs.get(id)
|
||||
if (nodeRef) vueNodeData.set(id, extractVueNodeData(nodeRef))
|
||||
}
|
||||
|
||||
return {
|
||||
vueNodeData,
|
||||
getNode,
|
||||
refreshNode,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import './slotDefaults'
|
||||
import './uploadAudio'
|
||||
import './uploadImage'
|
||||
import './webcamCapture'
|
||||
import './widgetGridOverrides'
|
||||
import './widgetInputs'
|
||||
|
||||
// Cloud-only extensions - tree-shaken in OSS builds
|
||||
|
||||
140
src/extensions/core/widgetGridOverrides.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
clearAllGridOverrides,
|
||||
clearGridOverride,
|
||||
readGridOverrides,
|
||||
setGridOverride
|
||||
} from '@/utils/widgetGridOverrides'
|
||||
|
||||
const DEFAULT_SIZE = '200px'
|
||||
|
||||
function refreshVueNode(nodeId: string): void {
|
||||
const manager = useVueNodeLifecycle().nodeManager.value
|
||||
manager?.refreshNode(nodeId)
|
||||
}
|
||||
|
||||
function applyOverrideAndRefresh(
|
||||
node: LGraphNode,
|
||||
widgetName: string,
|
||||
value: string
|
||||
): void {
|
||||
setGridOverride(node, widgetName, value)
|
||||
refreshVueNode(String(node.id))
|
||||
app.canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
function removeOverrideAndRefresh(node: LGraphNode, widgetName: string): void {
|
||||
clearGridOverride(node, widgetName)
|
||||
refreshVueNode(String(node.id))
|
||||
app.canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
function removeAllOverridesAndRefresh(node: LGraphNode): void {
|
||||
clearAllGridOverrides(node)
|
||||
refreshVueNode(String(node.id))
|
||||
app.canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
function openSizeDialog(
|
||||
currentValue: string | undefined,
|
||||
onSubmit: (value: string) => void
|
||||
): void {
|
||||
useDialogStore().showDialog({
|
||||
key: 'widget-grid-size',
|
||||
title: t('widgetGridOverrides.sizeLabel'),
|
||||
component: PromptDialogContent,
|
||||
props: {
|
||||
message: t('widgetGridOverrides.prompt'),
|
||||
defaultValue: currentValue ?? DEFAULT_SIZE,
|
||||
placeholder: '200px',
|
||||
onConfirm: (value: string) => {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed.length > 0) {
|
||||
onSubmit(trimmed)
|
||||
}
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
modal: true,
|
||||
closable: true,
|
||||
dismissableMask: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildWidgetMenuItem(
|
||||
node: LGraphNode,
|
||||
widgetName: string
|
||||
): IContextMenuValue {
|
||||
const overrides = readGridOverrides(node) ?? {}
|
||||
const current = overrides[widgetName]
|
||||
const label = current
|
||||
? `${widgetName} → ${current}`
|
||||
: `${widgetName} → ${t('widgetGridOverrides.auto')}`
|
||||
|
||||
const openSetSize = () => {
|
||||
openSizeDialog(current, (value) =>
|
||||
applyOverrideAndRefresh(node, widgetName, value)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
content: label,
|
||||
has_submenu: true,
|
||||
callback: openSetSize,
|
||||
submenu: {
|
||||
options: [
|
||||
{
|
||||
content: t('widgetGridOverrides.setSize'),
|
||||
callback: openSetSize
|
||||
},
|
||||
{
|
||||
content: t('widgetGridOverrides.clearOverride'),
|
||||
disabled: !current,
|
||||
callback: () => removeOverrideAndRefresh(node, widgetName)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.WidgetGridOverrides',
|
||||
getNodeMenuItems(node: LGraphNode): (IContextMenuValue | null)[] {
|
||||
const widgets = node.widgets ?? []
|
||||
if (widgets.length === 0) return []
|
||||
|
||||
const overrides = readGridOverrides(node) ?? {}
|
||||
const hasAny = Object.keys(overrides).length > 0
|
||||
|
||||
const widgetItems: (IContextMenuValue | null)[] = widgets.map((widget) =>
|
||||
buildWidgetMenuItem(node, widget.name)
|
||||
)
|
||||
|
||||
if (hasAny) {
|
||||
widgetItems.push(null, {
|
||||
content: t('widgetGridOverrides.clearAll'),
|
||||
callback: () => removeAllOverridesAndRefresh(node)
|
||||
})
|
||||
}
|
||||
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: t('widgetGridOverrides.menuLabel'),
|
||||
has_submenu: true,
|
||||
callback: () => {},
|
||||
submenu: {
|
||||
options: widgetItems
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -3816,5 +3816,14 @@
|
||||
"training": "Training…",
|
||||
"processingVideo": "Processing video…",
|
||||
"running": "Running…"
|
||||
},
|
||||
"widgetGridOverrides": {
|
||||
"menuLabel": "Widget Grid Sizes",
|
||||
"setSize": "Set size…",
|
||||
"clearOverride": "Clear override",
|
||||
"clearAll": "Clear all overrides",
|
||||
"auto": "(auto)",
|
||||
"sizeLabel": "Row height (CSS grid value)",
|
||||
"prompt": "Grid row size (e.g. 200px, minmax(150px, 300px), 1fr, auto)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,19 @@
|
||||
<div
|
||||
v-if="!widget.hidden && (!widget.advanced || showAdvanced)"
|
||||
data-testid="node-widget"
|
||||
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
|
||||
:class="
|
||||
cn(
|
||||
'lg-node-widget group relative col-span-full grid grid-cols-subgrid',
|
||||
widget.hasGridOverride ? 'items-center' : 'items-stretch'
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0 h-1 cursor-ns-resize opacity-0 transition-opacity hover:bg-node-stroke hover:opacity-50"
|
||||
@pointerdown="
|
||||
handleResizePointerDown($event, widget.slotName ?? widget.name)
|
||||
"
|
||||
/>
|
||||
<!-- Widget Input Slot Dot -->
|
||||
<div
|
||||
:class="
|
||||
@@ -91,6 +102,7 @@ import AppInput from '@/renderer/extensions/linearMode/AppInput.vue'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
import { useProcessedWidgets } from '@/renderer/extensions/vueNodes/composables/useProcessedWidgets'
|
||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||
import { useWidgetRowResize } from '@/renderer/extensions/vueNodes/composables/useWidgetRowResize'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import InputSlot from './InputSlot.vue'
|
||||
@@ -140,4 +152,18 @@ const {
|
||||
if (nodeData?.id != null) {
|
||||
useVueElementTracking(String(nodeData.id), 'widgets-grid')
|
||||
}
|
||||
|
||||
const { startResize } = useWidgetRowResize()
|
||||
|
||||
function handleResizePointerDown(
|
||||
event: PointerEvent,
|
||||
widgetOverrideKey: string
|
||||
) {
|
||||
const handle = event.currentTarget as HTMLElement
|
||||
const rowElement = handle.closest(
|
||||
"[data-testid='node-widget']"
|
||||
) as HTMLElement
|
||||
if (!rowElement || nodeData?.id == null) return
|
||||
startResize(event, String(nodeData.id), widgetOverrideKey, rowElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -42,15 +42,18 @@ import {
|
||||
getExecutionIdFromNodeData,
|
||||
getLocatorIdFromNodeData
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { isValidGridTrack } from '@/utils/widgetGridOverrides'
|
||||
|
||||
interface ProcessedWidget {
|
||||
advanced: boolean
|
||||
handleContextMenu: (e: PointerEvent) => void
|
||||
hasGridOverride: boolean
|
||||
hasLayoutSize: boolean
|
||||
hasError: boolean
|
||||
hidden: boolean
|
||||
id: string
|
||||
name: string
|
||||
slotName?: string
|
||||
renderKey: string
|
||||
simplified: SimplifiedWidget
|
||||
tooltipConfig: TooltipOptions
|
||||
@@ -332,9 +335,13 @@ export function computeProcessedWidgets({
|
||||
)
|
||||
}
|
||||
|
||||
const widgetKey = widget.slotName ?? widget.name
|
||||
const hasGridOverride = !!nodeData.gridOverrides?.[widgetKey]
|
||||
|
||||
result.push({
|
||||
advanced: mergedOptions.advanced ?? false,
|
||||
handleContextMenu,
|
||||
hasGridOverride,
|
||||
hasLayoutSize: widget.hasLayoutSize ?? false,
|
||||
hasError: hasWidgetError(
|
||||
widget,
|
||||
@@ -346,6 +353,7 @@ export function computeProcessedWidgets({
|
||||
hidden: mergedOptions.hidden ?? false,
|
||||
id: String(bareWidgetId),
|
||||
name: widget.name,
|
||||
slotName: widget.slotName,
|
||||
renderKey,
|
||||
type: widget.type,
|
||||
vueComponent,
|
||||
@@ -412,13 +420,16 @@ export function useProcessedWidgets(
|
||||
)
|
||||
)
|
||||
|
||||
const gridTemplateRows = computed((): string =>
|
||||
visibleWidgets.value
|
||||
.map((w) =>
|
||||
shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content'
|
||||
)
|
||||
const gridTemplateRows = computed((): string => {
|
||||
const overrides = nodeDataGetter()?.gridOverrides
|
||||
return visibleWidgets.value
|
||||
.map((w) => {
|
||||
const override = overrides?.[w.slotName ?? w.name]
|
||||
if (override && isValidGridTrack(override)) return override
|
||||
return shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content'
|
||||
})
|
||||
.join(' ')
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
canSelectInputs,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { app } from '@/scripts/app'
|
||||
import { setGridOverride } from '@/utils/widgetGridOverrides'
|
||||
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
|
||||
|
||||
const MIN_ROW_HEIGHT = 24
|
||||
|
||||
export function useWidgetRowResize() {
|
||||
const transformState = useTransformState()
|
||||
const isResizing = ref(false)
|
||||
const resizeStartY = ref(0)
|
||||
const resizeStartHeight = ref(0)
|
||||
const activeNodeId = ref<string | null>(null)
|
||||
const activeWidgetName = ref<string | null>(null)
|
||||
|
||||
function startResize(
|
||||
event: PointerEvent,
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
rowElement: HTMLElement
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const target = event.currentTarget
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
|
||||
target.setPointerCapture(event.pointerId)
|
||||
|
||||
const safeZoom = () => transformState.camera.z || 1
|
||||
|
||||
isResizing.value = true
|
||||
resizeStartY.value = event.clientY
|
||||
resizeStartHeight.value =
|
||||
rowElement.getBoundingClientRect().height / safeZoom()
|
||||
activeNodeId.value = nodeId
|
||||
activeWidgetName.value = widgetName
|
||||
|
||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
||||
if (!isResizing.value) return
|
||||
|
||||
const deltaY = (moveEvent.clientY - resizeStartY.value) / safeZoom()
|
||||
const newHeight = Math.max(
|
||||
MIN_ROW_HEIGHT,
|
||||
resizeStartHeight.value + deltaY
|
||||
)
|
||||
|
||||
rowElement.style.height = `${newHeight}px`
|
||||
}
|
||||
|
||||
const handlePointerUp = () => {
|
||||
if (!isResizing.value || !activeNodeId.value || !activeWidgetName.value)
|
||||
return
|
||||
|
||||
const finalHeight = rowElement.getBoundingClientRect().height / safeZoom()
|
||||
const heightPx = `${Math.round(finalHeight)}px`
|
||||
|
||||
const node = app.graph?.getNodeById(activeNodeId.value)
|
||||
if (node) {
|
||||
setGridOverride(node, activeWidgetName.value, heightPx)
|
||||
const manager = useVueNodeLifecycle().nodeManager.value
|
||||
manager?.refreshNode(activeNodeId.value)
|
||||
app.canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
rowElement.style.height = ''
|
||||
isResizing.value = false
|
||||
activeNodeId.value = null
|
||||
activeWidgetName.value = null
|
||||
|
||||
target.removeEventListener('pointermove', handlePointerMove)
|
||||
target.removeEventListener('pointerup', handlePointerUp)
|
||||
target.removeEventListener('pointercancel', handlePointerUp)
|
||||
}
|
||||
|
||||
target.addEventListener('pointermove', handlePointerMove)
|
||||
target.addEventListener('pointerup', handlePointerUp)
|
||||
target.addEventListener('pointercancel', handlePointerUp)
|
||||
}
|
||||
|
||||
return { isResizing, startResize }
|
||||
}
|
||||
67
src/utils/widgetGridOverrides.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
|
||||
const GRID_OVERRIDES_PROPERTY_KEY = 'gridOverrides'
|
||||
|
||||
const gridTrackValidityCache = new Map<string, boolean>()
|
||||
|
||||
export function isValidGridTrack(value: string): boolean {
|
||||
const cached = gridTrackValidityCache.get(value)
|
||||
if (cached !== undefined) return cached
|
||||
const valid =
|
||||
typeof CSS !== 'undefined' &&
|
||||
typeof CSS.supports === 'function' &&
|
||||
CSS.supports('grid-template-rows', value)
|
||||
gridTrackValidityCache.set(value, valid)
|
||||
return valid
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps widget name -> CSS grid-template-rows track value
|
||||
* (e.g. '200px', 'minmax(150px, 300px)', '1fr', 'auto').
|
||||
* Persisted on `node.properties.gridOverrides`.
|
||||
*/
|
||||
export type WidgetGridOverrides = Record<string, string>
|
||||
|
||||
export function readGridOverrides(
|
||||
node: LGraphNode
|
||||
): WidgetGridOverrides | undefined {
|
||||
const raw = node.properties?.[GRID_OVERRIDES_PROPERTY_KEY]
|
||||
if (!raw || typeof raw !== 'object') return undefined
|
||||
const entries = Object.entries(raw as Record<string, unknown>).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
||||
)
|
||||
if (entries.length === 0) return undefined
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
function writeGridOverrides(
|
||||
node: LGraphNode,
|
||||
overrides: WidgetGridOverrides
|
||||
): void {
|
||||
if (Object.keys(overrides).length === 0) {
|
||||
if (node.properties) delete node.properties[GRID_OVERRIDES_PROPERTY_KEY]
|
||||
return
|
||||
}
|
||||
node.properties ??= {}
|
||||
node.properties[GRID_OVERRIDES_PROPERTY_KEY] = overrides
|
||||
}
|
||||
|
||||
export function setGridOverride(
|
||||
node: LGraphNode,
|
||||
widgetName: string,
|
||||
value: string
|
||||
): void {
|
||||
const current = readGridOverrides(node) ?? {}
|
||||
current[widgetName] = value
|
||||
writeGridOverrides(node, current)
|
||||
}
|
||||
|
||||
export function clearGridOverride(node: LGraphNode, widgetName: string): void {
|
||||
const current = readGridOverrides(node) ?? {}
|
||||
delete current[widgetName]
|
||||
writeGridOverrides(node, current)
|
||||
}
|
||||
|
||||
export function clearAllGridOverrides(node: LGraphNode): void {
|
||||
writeGridOverrides(node, {})
|
||||
}
|
||||