mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-02 19:40:03 +00:00
Compare commits
1 Commits
fix/codera
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf399a556f |
@@ -1,7 +1,8 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { downloadFile, openFileInNewTab } from '@/base/common/downloadUtil'
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useMediaAssetGalleryStore } from '@/platform/assets/composables/useMediaAssetGalleryStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
import type { MenuOption } from './useMoreOptionsMenu'
|
||||
@@ -23,7 +24,7 @@ export function useImageMenuOptions() {
|
||||
if (!img) return
|
||||
const url = new URL(img.src)
|
||||
url.searchParams.delete('preview')
|
||||
void openFileInNewTab(url.toString())
|
||||
useMediaAssetGalleryStore().openUrl(url.toString())
|
||||
}
|
||||
|
||||
const copyImage = async (node: LGraphNode) => {
|
||||
@@ -87,7 +88,7 @@ export function useImageMenuOptions() {
|
||||
},
|
||||
{
|
||||
label: t('contextMenu.Open Image'),
|
||||
icon: 'icon-[lucide--external-link]',
|
||||
icon: 'icon-[lucide--maximize]',
|
||||
action: () => openImage(node)
|
||||
},
|
||||
{
|
||||
|
||||
@@ -905,14 +905,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
app.canvas.pasteFromClipboard()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.PasteFromClipboardWithConnect',
|
||||
icon: 'icon-[lucide--clipboard-paste]',
|
||||
label: () => t('Paste with Connect'),
|
||||
function: () => {
|
||||
app.canvas.pasteFromClipboard({ connectInputs: true })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.SelectAll',
|
||||
icon: 'icon-[lucide--lasso-select]',
|
||||
@@ -927,12 +919,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Delete Selected Items',
|
||||
versionAdded: '1.10.5',
|
||||
function: () => {
|
||||
if (app.canvas.selectedItems.size === 0) {
|
||||
app.canvas.canvas.dispatchEvent(
|
||||
new CustomEvent('litegraph:no-items-selected', { bubbles: true })
|
||||
)
|
||||
return
|
||||
}
|
||||
app.canvas.deleteSelected()
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
@@ -189,10 +189,11 @@ export function useWorkflowActionsMenu(
|
||||
|
||||
addItem({
|
||||
id: 'share',
|
||||
label: t('breadcrumbsMenu.share'),
|
||||
label: t('menuLabels.Share'),
|
||||
icon: 'icon-[comfy--send]',
|
||||
command: async () => {},
|
||||
visible: false
|
||||
disabled: true,
|
||||
visible: isRoot
|
||||
})
|
||||
|
||||
addItem({
|
||||
|
||||
@@ -3791,6 +3791,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
return
|
||||
}
|
||||
|
||||
private _noItemsSelected(): void {
|
||||
const event = new CustomEvent('litegraph:no-items-selected', {
|
||||
bubbles: true
|
||||
})
|
||||
this.canvas.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* process a key event
|
||||
*/
|
||||
@@ -3835,6 +3842,31 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.node_panel?.close()
|
||||
this.options_panel?.close()
|
||||
if (this.node_panel || this.options_panel) block_default = true
|
||||
} else if (e.keyCode === 65 && e.ctrlKey) {
|
||||
// select all Control A
|
||||
this.selectItems()
|
||||
block_default = true
|
||||
} else if (e.keyCode === 67 && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
||||
// copy
|
||||
if (this.selected_nodes) {
|
||||
this.copyToClipboard()
|
||||
block_default = true
|
||||
}
|
||||
} else if (e.keyCode === 86 && (e.metaKey || e.ctrlKey)) {
|
||||
// paste
|
||||
this.pasteFromClipboard({ connectInputs: e.shiftKey })
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
// delete or backspace
|
||||
// @ts-expect-error EventTarget.localName is not in standard types
|
||||
if (e.target.localName != 'input' && e.target.localName != 'textarea') {
|
||||
if (this.selectedItems.size === 0) {
|
||||
this._noItemsSelected()
|
||||
return
|
||||
}
|
||||
|
||||
this.deleteSelected()
|
||||
block_default = true
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -5494,12 +5526,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @todo Split tooltip from hover, so it can be drawn / eased separately
|
||||
*/
|
||||
drawLinkTooltip(ctx: CanvasRenderingContext2D, link: LinkSegment): void {
|
||||
ctx.save()
|
||||
const pos = link._pos
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.beginPath()
|
||||
if (this.linkMarkerShape === LinkMarkerShape.Arrow) {
|
||||
ctx.save()
|
||||
const transform = ctx.getTransform()
|
||||
ctx.translate(pos[0], pos[1])
|
||||
// Assertion: Number.isFinite guarantees this is a number.
|
||||
if (Number.isFinite(link._centreAngle))
|
||||
@@ -5507,7 +5538,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
ctx.moveTo(-2, -3)
|
||||
ctx.lineTo(+4, 0)
|
||||
ctx.lineTo(-2, +3)
|
||||
ctx.restore()
|
||||
ctx.setTransform(transform)
|
||||
} else if (
|
||||
this.linkMarkerShape == null ||
|
||||
this.linkMarkerShape === LinkMarkerShape.Circle
|
||||
@@ -5518,16 +5549,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
// @ts-expect-error TODO: Better value typing
|
||||
const { data } = link
|
||||
if (data == null) {
|
||||
ctx.restore()
|
||||
return
|
||||
}
|
||||
if (data == null) return
|
||||
|
||||
// @ts-expect-error TODO: Better value typing
|
||||
if (this.onDrawLinkTooltip?.(ctx, link, this) == true) {
|
||||
ctx.restore()
|
||||
return
|
||||
}
|
||||
if (this.onDrawLinkTooltip?.(ctx, link, this) == true) return
|
||||
|
||||
let text: string | null = null
|
||||
|
||||
@@ -5537,10 +5562,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
else if (data.toToolTip) text = data.toToolTip()
|
||||
else text = `[${data.constructor.name}]`
|
||||
|
||||
if (text == null) {
|
||||
ctx.restore()
|
||||
return
|
||||
}
|
||||
if (text == null) return
|
||||
|
||||
// Hard-coded tooltip limit
|
||||
text = text.substring(0, 30)
|
||||
@@ -5564,7 +5586,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillStyle = '#CEC'
|
||||
ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,7 +91,7 @@ export function strokeShape(
|
||||
}
|
||||
|
||||
// Set up context
|
||||
ctx.save()
|
||||
const { lineWidth, strokeStyle } = ctx
|
||||
ctx.lineWidth = thickness
|
||||
ctx.globalAlpha = 0.8
|
||||
ctx.strokeStyle = color
|
||||
@@ -138,7 +138,12 @@ export function strokeShape(
|
||||
// Stroke the shape
|
||||
ctx.stroke()
|
||||
|
||||
ctx.restore()
|
||||
// Reset context
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.strokeStyle = strokeStyle
|
||||
|
||||
// TODO: Store and reset value properly. Callers currently expect this behaviour (e.g. muted nodes).
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,24 +216,18 @@ export function drawTextInArea({
|
||||
}: IDrawTextInAreaOptions) {
|
||||
const { left, right, bottom, width, centreX } = area
|
||||
|
||||
ctx.save()
|
||||
|
||||
// Text already fits
|
||||
const fullWidth = ctx.measureText(text).width
|
||||
if (fullWidth <= width) {
|
||||
ctx.textAlign = align
|
||||
const x = align === 'left' ? left : align === 'right' ? right : centreX
|
||||
ctx.fillText(text, x, bottom)
|
||||
ctx.restore()
|
||||
return
|
||||
}
|
||||
|
||||
// Need to truncate text
|
||||
const truncated = truncateTextToWidth(ctx, text, width)
|
||||
if (truncated.length === 0) {
|
||||
ctx.restore()
|
||||
return
|
||||
}
|
||||
if (truncated.length === 0) return
|
||||
|
||||
// Draw text - left-aligned to prevent bouncing during resize
|
||||
ctx.textAlign = 'left'
|
||||
@@ -239,6 +238,4 @@ export function drawTextInArea({
|
||||
ctx.textAlign = 'right'
|
||||
const ellipsis = truncated.at(-1)!
|
||||
ctx.fillText(ellipsis, right, bottom, ctx.measureText(ellipsis).width * 0.75)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
@@ -318,9 +318,15 @@ export abstract class SubgraphIONodeBase<
|
||||
| SubgraphOutput,
|
||||
editorAlpha?: number
|
||||
): void {
|
||||
ctx.save()
|
||||
const { lineWidth, strokeStyle, fillStyle, font, textBaseline } = ctx
|
||||
this.drawProtected(ctx, colorContext, fromSlot, editorAlpha)
|
||||
ctx.restore()
|
||||
Object.assign(ctx, {
|
||||
lineWidth,
|
||||
strokeStyle,
|
||||
fillStyle,
|
||||
font,
|
||||
textBaseline
|
||||
})
|
||||
}
|
||||
|
||||
/** @internal Leaves {@link ctx} dirty. */
|
||||
|
||||
@@ -39,8 +39,8 @@ export class AssetWidget
|
||||
options: DrawWidgetOptions
|
||||
) {
|
||||
const { width, showText = true } = options
|
||||
|
||||
ctx.save()
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
|
||||
this.drawWidgetShape(ctx, options)
|
||||
|
||||
@@ -48,7 +48,8 @@ export class AssetWidget
|
||||
this.drawTruncatingText({ ctx, width, leftPadding: 0, rightPadding: 0 })
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
// Restore original context attributes
|
||||
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
|
||||
}
|
||||
|
||||
override onClick() {
|
||||
|
||||
@@ -62,7 +62,8 @@ export abstract class BaseSteppedWidget<
|
||||
ctx: CanvasRenderingContext2D,
|
||||
options: DrawWidgetOptions
|
||||
) {
|
||||
ctx.save()
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
|
||||
this.drawWidgetShape(ctx, options)
|
||||
if (options.showText) {
|
||||
@@ -71,6 +72,7 @@ export abstract class BaseSteppedWidget<
|
||||
this.drawTruncatingText({ ctx, width: options.width })
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
// Restore original context attributes
|
||||
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ export class BooleanWidget
|
||||
ctx: CanvasRenderingContext2D,
|
||||
options: DrawWidgetOptions
|
||||
) {
|
||||
ctx.save()
|
||||
|
||||
const { width, showText = true } = options
|
||||
const { height, y } = this
|
||||
const { margin } = BaseWidget
|
||||
@@ -30,8 +28,6 @@ export class BooleanWidget
|
||||
this.drawLabel(ctx, margin * 2)
|
||||
this.drawValue(ctx, width - 40)
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
drawLabel(ctx: CanvasRenderingContext2D, x: number): void {
|
||||
|
||||
@@ -25,7 +25,8 @@ export class ButtonWidget
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, showText = true, suppressPromotedOutline }: DrawWidgetOptions
|
||||
) {
|
||||
ctx.save()
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
|
||||
const { height, y } = this
|
||||
const { margin } = BaseWidget
|
||||
@@ -47,7 +48,8 @@ export class ButtonWidget
|
||||
// Draw button text
|
||||
if (showText) this.drawLabel(ctx, width * 0.5)
|
||||
|
||||
ctx.restore()
|
||||
// Restore original context attributes
|
||||
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
|
||||
}
|
||||
|
||||
drawLabel(ctx: CanvasRenderingContext2D, x: number): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IChartWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -13,7 +15,32 @@ export class ChartWidget
|
||||
override type = 'chart' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'Chart')
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `Chart: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
|
||||
@@ -29,7 +29,7 @@ export class ColorWidget
|
||||
override type = 'color' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
ctx.save()
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
|
||||
this.drawWidgetShape(ctx, options)
|
||||
|
||||
@@ -62,7 +62,7 @@ export class ColorWidget
|
||||
ctx.textAlign = 'right'
|
||||
ctx.fillText(this.value || '#000000', swatchX - 8, y + height * 0.7)
|
||||
|
||||
ctx.restore()
|
||||
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
|
||||
}
|
||||
|
||||
onClick({ e, node, canvas }: WidgetEventOptions): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IFileUploadWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -13,7 +15,32 @@ export class FileUploadWidget
|
||||
override type = 'fileupload' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'Fileupload')
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `Fileupload: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IGalleriaWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -13,7 +15,32 @@ export class GalleriaWidget
|
||||
override type = 'galleria' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'Galleria')
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `Galleria: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IImageCompareWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -13,7 +15,32 @@ export class ImageCompareWidget
|
||||
override type = 'imagecompare' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'ImageCompare')
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `ImageCompare: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
|
||||
@@ -35,7 +35,8 @@ export class KnobWidget extends BaseWidget<IKnobWidget> implements IKnobWidget {
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, showText = true, suppressPromotedOutline }: DrawWidgetOptions
|
||||
): void {
|
||||
ctx.save()
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
|
||||
const { y } = this
|
||||
const { margin } = BaseWidget
|
||||
@@ -176,7 +177,8 @@ export class KnobWidget extends BaseWidget<IKnobWidget> implements IKnobWidget {
|
||||
)
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
// Restore original context attributes
|
||||
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
|
||||
}
|
||||
|
||||
onClick(): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IMarkdownWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -13,7 +15,32 @@ export class MarkdownWidget
|
||||
override type = 'markdown' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'Markdown')
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `Markdown: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IMultiSelectWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -13,7 +15,32 @@ export class MultiSelectWidget
|
||||
override type = 'multiselect' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'MultiSelect')
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `MultiSelect: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ISelectButtonWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -13,7 +15,32 @@ export class SelectButtonWidget
|
||||
override type = 'selectbutton' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'SelectButton')
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `SelectButton: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
|
||||
@@ -22,7 +22,8 @@ export class SliderWidget
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, showText = true, suppressPromotedOutline }: DrawWidgetOptions
|
||||
) {
|
||||
ctx.save()
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
|
||||
const { height, y } = this
|
||||
const { margin } = BaseWidget
|
||||
@@ -66,7 +67,8 @@ export class SliderWidget
|
||||
)
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
// Restore original context attributes
|
||||
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,8 +24,8 @@ export class TextWidget
|
||||
options: DrawWidgetOptions
|
||||
) {
|
||||
const { width, showText = true } = options
|
||||
|
||||
ctx.save()
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
|
||||
this.drawWidgetShape(ctx, options)
|
||||
|
||||
@@ -33,7 +33,8 @@ export class TextWidget
|
||||
this.drawTruncatingText({ ctx, width, leftPadding: 0, rightPadding: 0 })
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
// Restore original context attributes
|
||||
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
|
||||
}
|
||||
|
||||
override onClick({ e, node, canvas }: WidgetEventOptions) {
|
||||
|
||||
@@ -1262,7 +1262,6 @@
|
||||
"Move Selected Nodes Right": "Move Selected Nodes Right",
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Paste": "Paste",
|
||||
"Paste with Connect": "Paste with Connect",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Select All": "Select All",
|
||||
@@ -2604,8 +2603,7 @@
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"deleteBlueprint": "Delete Blueprint",
|
||||
"enterNewName": "Enter new name",
|
||||
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red).",
|
||||
"share": "Share"
|
||||
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red)."
|
||||
},
|
||||
"shortcuts": {
|
||||
"shortcuts": "Shortcuts",
|
||||
|
||||
@@ -160,6 +160,37 @@ describe('useMediaAssetGalleryStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('openUrl', () => {
|
||||
it('should create a ResultItemImpl with overridden url getter', () => {
|
||||
const store = useMediaAssetGalleryStore()
|
||||
const testUrl = 'https://example.com/node-image.png'
|
||||
|
||||
store.openUrl(testUrl)
|
||||
|
||||
expect(ResultItemImpl).toHaveBeenCalledWith({
|
||||
filename: 'node-image.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '0',
|
||||
mediaType: 'images'
|
||||
})
|
||||
expect(store.items).toHaveLength(1)
|
||||
expect(store.items[0].url).toBe(testUrl)
|
||||
expect(store.activeIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle urls without a filename path', () => {
|
||||
const store = useMediaAssetGalleryStore()
|
||||
|
||||
store.openUrl('https://example.com/')
|
||||
|
||||
expect(ResultItemImpl).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filename: '' })
|
||||
)
|
||||
expect(store.items[0].url).toBe('https://example.com/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('close', () => {
|
||||
it('should reset activeIndex to -1', () => {
|
||||
const store = useMediaAssetGalleryStore()
|
||||
|
||||
@@ -37,11 +37,32 @@ export const useMediaAssetGalleryStore = defineStore(
|
||||
activeIndex.value = 0
|
||||
}
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
const resultItem = new ResultItemImpl({
|
||||
filename: url.split('/').pop() ?? '',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '0',
|
||||
mediaType: 'images'
|
||||
})
|
||||
|
||||
Object.defineProperty(resultItem, 'url', {
|
||||
get() {
|
||||
return url
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
|
||||
items.value = [resultItem]
|
||||
activeIndex.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
activeIndex,
|
||||
items,
|
||||
close,
|
||||
openSingle
|
||||
openSingle,
|
||||
openUrl
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -208,52 +208,5 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 'Escape'
|
||||
},
|
||||
commandId: 'Comfy.Graph.ExitSubgraph'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'a'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.SelectAll',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'c'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.CopySelected',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.PasteFromClipboard',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.PasteFromClipboardWithConnect',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Delete'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.DeleteSelectedItems',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Backspace'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.DeleteSelectedItems',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
vi.mock('@/scripts/app', () => {
|
||||
return {
|
||||
app: {
|
||||
canvas: {
|
||||
processKey: vi.fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
@@ -25,15 +36,13 @@ function createTestKeyboardEvent(
|
||||
ctrlKey?: boolean
|
||||
altKey?: boolean
|
||||
metaKey?: boolean
|
||||
shiftKey?: boolean
|
||||
} = {}
|
||||
): KeyboardEvent {
|
||||
const {
|
||||
target = document.body,
|
||||
ctrlKey = false,
|
||||
altKey = false,
|
||||
metaKey = false,
|
||||
shiftKey = false
|
||||
metaKey = false
|
||||
} = options
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
@@ -41,7 +50,6 @@ function createTestKeyboardEvent(
|
||||
ctrlKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
@@ -52,10 +60,8 @@ function createTestKeyboardEvent(
|
||||
return event
|
||||
}
|
||||
|
||||
describe('keybindingService - Canvas Keybindings', () => {
|
||||
describe('keybindingService - Event Forwarding', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let canvasContainer: HTMLDivElement
|
||||
let canvasChild: HTMLCanvasElement
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -70,156 +76,94 @@ describe('keybindingService - Canvas Keybindings', () => {
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
canvasContainer = document.createElement('div')
|
||||
canvasContainer.id = 'graph-canvas-container'
|
||||
canvasChild = document.createElement('canvas')
|
||||
canvasContainer.appendChild(canvasChild)
|
||||
document.body.appendChild(canvasContainer)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
canvasContainer.remove()
|
||||
})
|
||||
|
||||
it('should execute DeleteSelectedItems for Delete key on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: canvasChild
|
||||
})
|
||||
it('should forward Delete key to canvas when no keybinding exists', async () => {
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.DeleteSelectedItems'
|
||||
)
|
||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute DeleteSelectedItems for Backspace key on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('Backspace', {
|
||||
target: canvasChild
|
||||
})
|
||||
it('should forward Backspace key to canvas when no keybinding exists', async () => {
|
||||
const event = createTestKeyboardEvent('Backspace')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.DeleteSelectedItems'
|
||||
)
|
||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not execute DeleteSelectedItems when typing in input field', async () => {
|
||||
it('should not forward Delete key when typing in input field', async () => {
|
||||
const inputElement = document.createElement('input')
|
||||
const event = createTestKeyboardEvent('Delete', { target: inputElement })
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not execute DeleteSelectedItems when typing in textarea', async () => {
|
||||
it('should not forward Delete key when typing in textarea', async () => {
|
||||
const textareaElement = document.createElement('textarea')
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: textareaElement
|
||||
})
|
||||
const event = createTestKeyboardEvent('Delete', { target: textareaElement })
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute SelectAll for Ctrl+A on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('a', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
it('should not forward Delete key when canvas processKey is not available', async () => {
|
||||
// Temporarily replace processKey with undefined - testing edge case
|
||||
const originalProcessKey = vi.mocked(app.canvas).processKey
|
||||
vi.mocked(app.canvas).processKey = undefined!
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.SelectAll'
|
||||
)
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore processKey for other tests
|
||||
vi.mocked(app.canvas).processKey = originalProcessKey
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute CopySelected for Ctrl+C on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('c', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
it('should not forward Delete key when canvas is not available', async () => {
|
||||
const originalCanvas = vi.mocked(app).canvas
|
||||
vi.mocked(app).canvas = null!
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.CopySelected'
|
||||
)
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore canvas for other tests
|
||||
vi.mocked(app).canvas = originalCanvas
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute PasteFromClipboard for Ctrl+V on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('v', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.PasteFromClipboard'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute PasteFromClipboardWithConnect for Ctrl+Shift+V on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('v', {
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.PasteFromClipboardWithConnect'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute graph-canvas bindings by normalizing to graph-canvas-container', async () => {
|
||||
const event = createTestKeyboardEvent('=', {
|
||||
altKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.ZoomIn'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not execute graph-canvas bindings when target is outside canvas', async () => {
|
||||
const outsideDiv = document.createElement('div')
|
||||
document.body.appendChild(outsideDiv)
|
||||
|
||||
const event = createTestKeyboardEvent('=', {
|
||||
altKey: true,
|
||||
target: outsideDiv
|
||||
})
|
||||
it('should not forward non-canvas keys', async () => {
|
||||
const event = createTestKeyboardEvent('Enter')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
outsideDiv.remove()
|
||||
})
|
||||
|
||||
it('should not execute canvas commands when target is outside canvas container', async () => {
|
||||
const outsideDiv = document.createElement('div')
|
||||
document.body.appendChild(outsideDiv)
|
||||
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: outsideDiv
|
||||
})
|
||||
it('should not forward when modifier keys are pressed', async () => {
|
||||
const event = createTestKeyboardEvent('Delete', { ctrlKey: true })
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
outsideDiv.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -14,6 +15,16 @@ export function useKeybindingService() {
|
||||
const settingStore = useSettingStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function shouldForwardToCanvas(event: KeyboardEvent): boolean {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
const canvasKeys = ['Delete', 'Backspace']
|
||||
|
||||
return canvasKeys.includes(event.key)
|
||||
}
|
||||
|
||||
async function keybindHandler(event: KeyboardEvent) {
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
if (keyCombo.isModifier) {
|
||||
@@ -33,17 +44,7 @@ export function useKeybindingService() {
|
||||
}
|
||||
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding) {
|
||||
const targetElementId =
|
||||
keybinding.targetElementId === 'graph-canvas'
|
||||
? 'graph-canvas-container'
|
||||
: keybinding.targetElementId
|
||||
if (targetElementId) {
|
||||
const container = document.getElementById(targetElementId)
|
||||
if (!container?.contains(target)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
!event.ctrlKey &&
|
||||
@@ -73,6 +74,18 @@ export function useKeybindingService() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!keybinding && shouldForwardToCanvas(event)) {
|
||||
const canvas = app.canvas
|
||||
if (
|
||||
canvas &&
|
||||
canvas.processKey &&
|
||||
typeof canvas.processKey === 'function'
|
||||
) {
|
||||
canvas.processKey(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ export class CanvasPathRenderer {
|
||||
const angle = Math.atan2(posB.y - posA.y, posB.x - posA.x)
|
||||
|
||||
// Draw arrow triangle (matching original shape)
|
||||
ctx.save()
|
||||
const transform = ctx.getTransform()
|
||||
ctx.translate(posA.x, posA.y)
|
||||
ctx.rotate(angle)
|
||||
ctx.fillStyle = color
|
||||
@@ -441,7 +441,7 @@ export class CanvasPathRenderer {
|
||||
ctx.lineTo(0, +7)
|
||||
ctx.lineTo(+5, -3)
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
ctx.setTransform(transform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,19 +803,20 @@ export class CanvasPathRenderer {
|
||||
): void {
|
||||
if (!link.centerPos) return
|
||||
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
|
||||
if (
|
||||
context.style.centerMarkerShape === 'arrow' &&
|
||||
link.centerAngle !== undefined
|
||||
) {
|
||||
const transform = ctx.getTransform()
|
||||
ctx.translate(link.centerPos.x, link.centerPos.y)
|
||||
ctx.rotate(link.centerAngle)
|
||||
// The math is off, but it currently looks better in chromium (from original)
|
||||
ctx.moveTo(-3.2, -5)
|
||||
ctx.lineTo(7, 0)
|
||||
ctx.lineTo(-3.2, 5)
|
||||
ctx.setTransform(transform)
|
||||
} else {
|
||||
// Default to circle
|
||||
ctx.arc(link.centerPos.x, link.centerPos.y, 5, 0, Math.PI * 2)
|
||||
@@ -823,14 +824,15 @@ export class CanvasPathRenderer {
|
||||
|
||||
// Apply disabled pattern or color
|
||||
if (link.disabled && context.patterns?.disabled) {
|
||||
const { fillStyle, globalAlpha } = ctx
|
||||
ctx.fillStyle = context.patterns.disabled
|
||||
ctx.globalAlpha = 0.75
|
||||
ctx.fill()
|
||||
ctx.globalAlpha = globalAlpha
|
||||
ctx.fillStyle = fillStyle
|
||||
} else {
|
||||
ctx.fillStyle = color
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,6 +676,20 @@ export class ComfyApp {
|
||||
e.stopImmediatePropagation()
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+C Copy
|
||||
if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+V Paste
|
||||
if (
|
||||
(e.key === 'v' || e.key == 'V') &&
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
!e.shiftKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fall through to Litegraph defaults
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { downloadFile, openFileInNewTab } from '@/base/common/downloadUtil'
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
|
||||
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
ISerialisedNode
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useMediaAssetGalleryStore } from '@/platform/assets/composables/useMediaAssetGalleryStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -687,7 +688,7 @@ export const useLitegraphService = () => {
|
||||
callback: () => {
|
||||
const url = new URL(img.src)
|
||||
url.searchParams.delete('preview')
|
||||
void openFileInNewTab(url.toString())
|
||||
useMediaAssetGalleryStore().openUrl(url.toString())
|
||||
}
|
||||
},
|
||||
...getCopyImageOption(img),
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ResultGallery
|
||||
v-model:active-index="mediaAssetGalleryStore.activeIndex"
|
||||
:all-gallery-items="mediaAssetGalleryStore.items"
|
||||
/>
|
||||
|
||||
<GlobalToast />
|
||||
<InviteAcceptedToast />
|
||||
<RerouteMigrationToast />
|
||||
@@ -50,6 +55,7 @@ import { runWhenGlobalIdle } from '@/base/common/async'
|
||||
import MenuHamburger from '@/components/MenuHamburger.vue'
|
||||
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
|
||||
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
|
||||
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
||||
import GlobalToast from '@/components/toast/GlobalToast.vue'
|
||||
import InviteAcceptedToast from '@/platform/workspace/components/toasts/InviteAcceptedToast.vue'
|
||||
import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue'
|
||||
@@ -57,6 +63,7 @@ import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
import { useCoreCommands } from '@/composables/useCoreCommands'
|
||||
import { useQueuePolling } from '@/platform/remote/comfyui/useQueuePolling'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useMediaAssetGalleryStore } from '@/platform/assets/composables/useMediaAssetGalleryStore'
|
||||
import { useProgressFavicon } from '@/composables/useProgressFavicon'
|
||||
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
|
||||
import type { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
|
||||
@@ -109,6 +116,7 @@ const colorPaletteStore = useColorPaletteStore()
|
||||
const queueStore = useQueueStore()
|
||||
const assetsStore = useAssetsStore()
|
||||
const versionCompatibilityStore = useVersionCompatibilityStore()
|
||||
const mediaAssetGalleryStore = useMediaAssetGalleryStore()
|
||||
const graphCanvasContainerRef = ref<HTMLDivElement | null>(null)
|
||||
const { isBuilderMode } = useAppMode()
|
||||
const { linearMode } = storeToRefs(useCanvasStore())
|
||||
|
||||
Reference in New Issue
Block a user