Feature/expanded minimap (#4902)

* [feat] Add formatKeySequence function to format keybindings for commands

* [feat] Add lock and unlock canvas commands with keybindings and update localization

* feat: Implement canvas scale synchronization and zoom level adjustment

* feat: Enhance GraphCanvasMenu with zoom controls and improved button functionality

* feat: Refactor MiniMap component layout and remove unused bottomPanelStore

* feat: Update zoom control shortcuts to use formatted key sequences

* feat: Add tests for ZoomControlsModal and enhance GraphCanvasMenu tests

* Update locales [skip ci]

* Fix browser tests

* ui: align minimap properly

* Update locales [skip ci]

* feat: focus zoom input when zoom modal loads

* style: improve styling of zoom controls and add focus effect

* fix styling and tests

* styling: add divider to graph canvas menu

* styling: position minimap properly

* styling: add close button for minimap

* styling: add horizontal divider to minimap

* styling: update minimap toggle button text and remove old styles

* Update locales [skip ci]

* Update locales [skip ci]

* feat: disable canvas menu in viewport settings after zoom adjustments

* Update test expectations [skip ci]

* fix: update canvas read-only property access to use state object

* Update locales [skip ci]

* fix: adjust button group and minimap positioning

* feat: enhance zoom controls and adjust minimap positioning per PR comments

* feat: implement zoom controls composable

* feat: add timeout delays for headless tests

* fix: update zoom input validation range in applyZoom function

* [refactor] Update positioning and styles for GraphCanvasMenu, MiniMap, and ZoomControlsModal components

* [refactor] Adjust z-index and positioning for GraphCanvasMenu, MiniMap, and ZoomControlsModal components

* [style] Adjust margin for minimap button styles in GraphCanvasMenu component

* [refactor] minimap should show on focus mode

* [refactor] Update LiteGraphCanvasSplitterOverlay to conditionally render side and bottom panels based on focus mode

* [style] Adjust right positioning for MiniMap and ZoomControlsModal components

* [style] Adjust right positioning for MiniMap and ZoomControlsModal components

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
Johnpaul Chiwetelu
2025-08-21 19:16:29 +01:00
committed by GitHub
parent 23b3914714
commit 84379d9522
37 changed files with 1062 additions and 147 deletions

View File

@@ -65,6 +65,7 @@ export class Topbar {
}
async openTopbarMenu() {
await this.page.waitForTimeout(1000)
await this.page.locator('.comfyui-logo-wrapper').click()
const menu = this.page.locator('.comfy-command-menu')
await menu.waitFor({ state: 'visible' })

View File

@@ -7,13 +7,11 @@ test.describe('Graph Canvas Menu', () => {
// Set link render mode to spline to make sure it's not affected by other tests'
// side effects.
await comfyPage.setSetting('Comfy.LinkRenderMode', 2)
// Enable canvas menu for all tests
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
test('Can toggle link visibility', async ({ comfyPage }) => {
// Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup.
// so no cleanup is needed.
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
@@ -36,4 +34,45 @@ test.describe('Graph Canvas Menu', () => {
hiddenLinkRenderMode
)
})
test('Focus mode button is clickable and has correct test id', async ({
comfyPage
}) => {
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
await expect(focusButton).toBeVisible()
await expect(focusButton).toBeEnabled()
// Test that the button can be clicked without error
await focusButton.click()
await comfyPage.nextFrame()
})
test('Zoom controls popup opens and closes', async ({ comfyPage }) => {
// Find the zoom button by its percentage text content
const zoomButton = comfyPage.page.locator('button').filter({
hasText: '%'
})
await expect(zoomButton).toBeVisible()
// Click to open zoom controls
await zoomButton.click()
await comfyPage.nextFrame()
// Zoom controls modal should be visible
const zoomModal = comfyPage.page
.locator('div')
.filter({
hasText: 'Zoom To Fit'
})
.first()
await expect(zoomModal).toBeVisible()
// Click backdrop to close
const backdrop = comfyPage.page.locator('.fixed.inset-0').first()
await backdrop.click()
await comfyPage.nextFrame()
// Modal should be hidden
await expect(zoomModal).not.toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -780,9 +780,18 @@ test.describe('Viewport settings', () => {
// Screenshot the canvas element
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await toggleButton.click()
// close zoom menu
await zoomControlsButton.click()
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame()

View File

@@ -35,34 +35,44 @@ test.describe('Minimap', () => {
})
test('Validate minimap toggle button state', async ({ comfyPage }) => {
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(toggleButton).toBeVisible()
await expect(toggleButton).toHaveClass(/minimap-active/)
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
await expect(minimapContainer).toBeVisible()
})
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapContainer).toBeVisible()
await expect(toggleButton).toHaveClass(/minimap-active/)
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).not.toBeVisible()
await expect(toggleButton).not.toHaveClass(/minimap-active/)
await expect(toggleButton).toContainText('Show Minimap')
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).toBeVisible()
await expect(toggleButton).toHaveClass(/minimap-active/)
await expect(toggleButton).toContainText('Hide Minimap')
})
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {

View File

@@ -193,6 +193,7 @@ test.describe('Workflows sidebar', () => {
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.confirmDialog.click('overwrite')
await comfyPage.page.waitForTimeout(200)
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
])

View File

@@ -256,6 +256,7 @@ test.describe('Animated image widget', () => {
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await comfyPage.page.waitForTimeout(200)
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)

View File

@@ -2,13 +2,11 @@
<!-- Load splitter overlay only after comfyApp is ready. -->
<!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay
v-if="comfyAppReady && betaMenuEnabled && !workspaceStore.focusMode"
>
<template #side-bar-panel>
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
<SideToolbar />
</template>
<template #bottom-panel>
<template v-if="!workspaceStore.focusMode" #bottom-panel>
<BottomPanel />
</template>
<template #graph-canvas-panel>

View File

@@ -1,125 +1,278 @@
<template>
<ButtonGroup
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
@wheel="canvasInteractions.handleWheel"
>
<Button
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
severity="secondary"
icon="pi pi-plus"
:aria-label="$t('graphCanvasMenu.zoomIn')"
@mousedown="repeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
/>
<Button
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
severity="secondary"
icon="pi pi-minus"
:aria-label="$t('graphCanvasMenu.zoomOut')"
@mousedown="repeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
/>
<Button
v-tooltip.left="t('graphCanvasMenu.fitView')"
severity="secondary"
icon="pi pi-expand"
:aria-label="$t('graphCanvasMenu.fitView')"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
/>
<Button
v-tooltip.left="
t(
'graphCanvasMenu.' +
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
) + ' (Space)'
"
severity="secondary"
:aria-label="
t(
'graphCanvasMenu.' +
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
)
"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
<div>
<ZoomControlsModal :visible="isModalVisible" />
<!-- Backdrop -->
<div
v-if="hasActivePopup"
class="fixed inset-0 z-[1200]"
@click="hideModal"
></div>
<ButtonGroup
class="p-buttongroup-vertical p-1 absolute bottom-4 right-2 md:right-4"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>
<template #icon>
<i-material-symbols:pan-tool-outline
v-if="canvasStore.canvas?.read_only"
/>
<i-simple-line-icons:cursor v-else />
</template>
</Button>
<Button
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
severity="secondary"
:icon="linkHidden ? 'pi pi-eye-slash' : 'pi pi-eye'"
:aria-label="$t('graphCanvasMenu.toggleLinkVisibility')"
data-testid="toggle-link-visibility-button"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
/>
<Button
v-tooltip.left="minimapTooltip"
severity="secondary"
:icon="'pi pi-map'"
:aria-label="$t('graphCanvasMenu.toggleMinimap')"
:class="{ 'minimap-active': minimapVisible }"
data-testid="toggle-minimap-button"
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
/>
</ButtonGroup>
<Button
v-tooltip.top="selectTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
severity="secondary"
:aria-label="selectTooltip"
:pressed="isCanvasReadOnly"
icon="i-material-symbols:pan-tool-outline"
:class="selectButtonClass"
@click="() => commandStore.execute('Comfy.Canvas.Unlock')"
>
<template #icon>
<i-lucide:mouse-pointer-2 />
</template>
</Button>
<Button
v-tooltip.top="handTooltip"
severity="secondary"
:aria-label="handTooltip"
:pressed="isCanvasUnlocked"
:class="handButtonClass"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.Lock')"
>
<template #icon>
<i-lucide:hand />
</template>
</Button>
<!-- vertical line with bg E1DED5 -->
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
v-tooltip.top="fitViewTooltip"
severity="secondary"
icon="pi pi-expand"
:aria-label="fitViewTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
class="hover:dark-theme:!bg-[#444444] hover:!bg-[#E7E6E6]"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
<i-lucide:focus />
</template>
</Button>
<Button
ref="zoomButton"
v-tooltip.top="t('zoomControls.label')"
severity="secondary"
:label="t('zoomControls.label')"
:class="zoomButtonClass"
:aria-label="t('zoomControls.label')"
data-testid="zoom-controls-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="toggleModal"
>
<span class="inline-flex text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i-lucide:chevron-down />
</span>
</Button>
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
ref="focusButton"
v-tooltip.top="focusModeTooltip"
severity="secondary"
:aria-label="focusModeTooltip"
data-testid="focus-mode-button"
:style="stringifiedMinimapStyles.buttonStyles"
:class="focusButtonClass"
@click="() => commandStore.execute('Workspace.ToggleFocusMode')"
>
<template #icon>
<i-lucide:lightbulb />
</template>
</Button>
<Button
v-tooltip.top="{
value: linkVisibilityTooltip,
pt: {
root: {
style: 'z-index: 2; transform: translateY(-20px);'
}
}
}"
severity="secondary"
:class="linkVisibleClass"
:aria-label="linkVisibilityAriaLabel"
data-testid="toggle-link-visibility-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i-lucide:route-off />
</template>
</Button>
</ButtonGroup>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ButtonGroup from 'primevue/buttongroup'
import { computed } from 'vue'
import { computed, onBeforeUnmount, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useZoomControls } from '@/composables/useZoomControls'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import ZoomControlsModal from './modals/ZoomControlsModal.vue'
const { t } = useI18n()
const commandStore = useCommandStore()
const { formatKeySequence } = useCommandStore()
const canvasStore = useCanvasStore()
const keybindingStore = useKeybindingStore()
const settingStore = useSettingStore()
const canvasInteractions = useCanvasInteractions()
const workspaceStore = useWorkspaceStore()
const minimap = useMinimap()
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
const minimapTooltip = computed(() => {
const baseText = t('graphCanvasMenu.toggleMinimap')
const keybinding = keybindingStore.getKeybindingByCommandId(
'Comfy.Canvas.ToggleMinimap'
)
return keybinding ? `${baseText} (${keybinding.combo.toString()})` : baseText
const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
useZoomControls()
const stringifiedMinimapStyles = computed(() => {
const buttonGroupKeys = ['backgroundColor', 'borderRadius', '']
const buttonKeys = ['backgroundColor', 'borderRadius']
const additionalButtonStyles = {
border: 'none',
width: '35px',
height: '35px',
'margin-right': '2px',
'margin-left': '2px'
}
const containerStyles = minimap.containerStyles.value
const buttonStyles = {
...Object.fromEntries(
Object.entries(containerStyles).filter(([key]) =>
buttonKeys.includes(key)
)
),
...additionalButtonStyles
}
const buttonGroupStyles = Object.entries(containerStyles)
.filter(([key]) => buttonGroupKeys.includes(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
return { buttonStyles, buttonGroupStyles }
})
// Computed properties for reactive states
const isCanvasReadOnly = computed(() => canvasStore.canvas?.read_only ?? false)
const isCanvasUnlocked = computed(() => !isCanvasReadOnly.value)
const linkHidden = computed(
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
)
let interval: number | null = null
const repeat = async (command: string) => {
if (interval) return
const cmd = () => commandStore.execute(command)
await cmd()
interval = window.setInterval(cmd, 100)
}
const stopRepeat = () => {
if (interval) {
clearInterval(interval)
interval = null
}
}
// Computed properties for command text
const unlockCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Comfy.Canvas.Unlock')
).toUpperCase()
)
const lockCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.Lock')).toUpperCase()
)
const fitViewCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Comfy.Canvas.FitView')
).toUpperCase()
)
const focusCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Workspace.ToggleFocusMode')
).toUpperCase()
)
// Computed properties for button classes and states
const selectButtonClass = computed(() =>
isCanvasUnlocked.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: ''
)
const handButtonClass = computed(() =>
isCanvasReadOnly.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: ''
)
const zoomButtonClass = computed(() => [
'!w-16',
isModalVisible.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: '',
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]'
])
const focusButtonClass = computed(() => ({
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]': true,
'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]':
workspaceStore.focusMode
}))
// Computed properties for tooltip and aria-label texts
const selectTooltip = computed(
() => `${t('graphCanvasMenu.select')} (${unlockCommandText.value})`
)
const handTooltip = computed(
() => `${t('graphCanvasMenu.hand')} (${lockCommandText.value})`
)
const fitViewTooltip = computed(
() => `${t('graphCanvasMenu.fitView')} (${fitViewCommandText.value})`
)
const focusModeTooltip = computed(
() => `${t('graphCanvasMenu.focusMode')} (${focusCommandText.value})`
)
const linkVisibilityTooltip = computed(() =>
linkHidden.value
? t('graphCanvasMenu.showLinks')
: t('graphCanvasMenu.hideLinks')
)
const linkVisibilityAriaLabel = computed(() =>
linkHidden.value
? t('graphCanvasMenu.showLinks')
: t('graphCanvasMenu.hideLinks')
)
const linkVisibleClass = computed(() => [
linkHidden.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: '',
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]'
])
onMounted(() => {
canvasStore.initScaleSync()
})
onBeforeUnmount(() => {
canvasStore.cleanupScaleSync()
})
</script>
<style scoped>
.p-buttongroup-vertical {
display: flex;
flex-direction: column;
flex-direction: row;
z-index: 1200;
border-radius: var(--p-button-border-radius);
overflow: hidden;
border: 1px solid var(--p-panel-border-color);
@@ -129,15 +282,4 @@ const stopRepeat = () => {
margin: 0;
border-radius: 0;
}
.p-button.minimap-active {
background-color: var(--p-button-primary-background);
border-color: var(--p-button-primary-border-color);
color: var(--p-button-primary-color);
}
.p-button.minimap-active:hover {
background-color: var(--p-button-primary-hover-background);
border-color: var(--p-button-primary-hover-border-color);
}
</style>

View File

@@ -2,7 +2,7 @@
<div
v-if="visible && initialized"
ref="minimapRef"
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
class="minimap-main-container flex absolute bottom-[66px] right-2 md:right-11 z-[1000]"
>
<MiniMapPanel
v-if="showOptionsPanel"
@@ -31,6 +31,25 @@
<i-lucide:settings-2 />
</template>
</Button>
<Button
class="absolute z-10 right-0"
size="small"
text
severity="secondary"
data-testid="close-minmap-button"
@click.stop="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
>
<template #icon>
<i-lucide:x />
</template>
</Button>
<hr
class="absolute top-5 bg-[#E1DED5] dark-theme:bg-[#262729] h-[1px] border-0"
:style="{
width: containerStyles.width
}"
/>
<canvas
ref="canvasRef"
@@ -58,9 +77,12 @@ import Button from 'primevue/button'
import { onMounted, onUnmounted, ref } from 'vue'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import MiniMapPanel from './MiniMapPanel.vue'
const commandStore = useCommandStore()
const minimapRef = ref<HTMLDivElement>()
const {

View File

@@ -0,0 +1,237 @@
<template>
<div
v-if="visible"
class="w-[250px] absolute flex justify-center right-2 md:right-11 z-[1300] bottom-[66px] !bg-inherit !border-0"
>
<div
class="bg-white dark-theme:bg-[#2b2b2b] border border-gray-200 dark-theme:border-gray-700 rounded-lg shadow-lg p-4 w-4/5"
:style="filteredMinimapStyles"
@click.stop
>
<div>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@mousedown="startRepeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<template #default>
<span class="text-sm font-medium block">{{
$t('graphCanvasMenu.zoomIn')
}}</span>
<span class="text-sm text-gray-500 block">{{
zoomInCommandText
}}</span>
</template>
</Button>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@mousedown="startRepeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<template #default>
<span class="text-sm font-medium block">{{
$t('graphCanvasMenu.zoomOut')
}}</span>
<span class="text-sm text-gray-500 block">{{
zoomOutCommandText
}}</span>
</template>
</Button>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@click="executeCommand('Comfy.Canvas.FitView')"
>
<template #default>
<span class="text-sm font-medium block">{{
$t('zoomControls.zoomToFit')
}}</span>
<span class="text-sm text-gray-500 block">{{
zoomToFitCommandText
}}</span>
</template>
</Button>
<hr class="border-[#E1DED5] mb-1 dark-theme:border-[#2E3037]" />
<Button
severity="secondary"
text
data-testid="toggle-minimap-button"
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@click="executeCommand('Comfy.Canvas.ToggleMinimap')"
>
<template #default>
<span class="text-sm font-medium block">{{
minimapToggleText
}}</span>
<span class="text-sm text-gray-500 block">{{
showMinimapCommandText
}}</span>
</template>
</Button>
<hr class="border-[#E1DED5] mt-1 dark-theme:border-[#2E3037]" />
<div
ref="zoomInputContainer"
class="flex items-center px-2 bg-[#E7E6E6] focus-within:bg-[#F3F3F3] dark-theme:bg-[#8282821A] rounded p-2 zoomInputContainer"
>
<InputNumber
ref="zoomInput"
:default-value="canvasStore.appScalePercentage"
:min="1"
:max="1000"
:show-buttons="false"
:use-grouping="false"
:unstyled="true"
input-class="flex-1 bg-transparent border-none outline-none text-sm shadow-none my-0 "
fluid
@input="applyZoom"
@keyup.enter="applyZoom"
/>
<span class="text-sm text-gray-500 -ml-4">%</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, InputNumber, InputNumberInputEvent } from 'primevue'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { useSettingStore } from '@/stores/settingStore'
const { t } = useI18n()
const minimap = useMinimap()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const { formatKeySequence } = useCommandStore()
interface Props {
visible: boolean
}
const props = defineProps<Props>()
const interval = ref<number | null>(null)
// Computed properties for reactive states
const minimapToggleText = computed(() =>
settingStore.get('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
)
const applyZoom = (val: InputNumberInputEvent) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
return
}
canvasStore.setAppZoomFromPercentage(inputValue)
}
const executeCommand = (command: string) => {
void commandStore.execute(command)
}
const startRepeat = (command: string) => {
if (interval.value) return
const cmd = () => commandStore.execute(command)
void cmd()
interval.value = window.setInterval(cmd, 100)
}
const stopRepeat = () => {
if (interval.value) {
clearInterval(interval.value)
interval.value = null
}
}
const filteredMinimapStyles = computed(() => {
return {
...minimap.containerStyles.value,
height: undefined,
width: undefined
}
})
const zoomInCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomIn'))
)
const zoomOutCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomOut'))
)
const zoomToFitCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))
)
const showMinimapCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ToggleMinimap'))
)
const zoomInput = ref<InstanceType<typeof InputNumber> | null>(null)
const zoomInputContainer = ref<HTMLDivElement | null>(null)
watch(
() => props.visible,
async (newVal) => {
if (newVal) {
await nextTick()
const input = zoomInputContainer.value?.querySelector(
'input'
) as HTMLInputElement
input?.focus()
}
}
)
</script>
<style>
.zoomInputContainer:focus-within {
border: 1px solid rgb(204, 204, 204);
}
.dark-theme .zoomInputContainer:focus-within {
border: 1px solid rgb(204, 204, 204);
}
</style>

View File

@@ -1,11 +1,6 @@
<template>
<SidebarIcon
:tooltip="
$t('shortcuts.keyboardShortcuts') +
' (' +
formatKeySequence(command.keybinding!.combo.getKeySequences()) +
')'
"
:tooltip="tooltipText"
:selected="isShortcutsPanelVisible"
@click="toggleShortcutsPanel"
>
@@ -17,28 +12,28 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import SidebarIcon from './SidebarIcon.vue'
const { t } = useI18n()
const bottomPanelStore = useBottomPanelStore()
const command = useCommandStore().getCommand(
'Workspace.ToggleBottomPanel.Shortcuts'
)
const commandStore = useCommandStore()
const command = commandStore.getCommand('Workspace.ToggleBottomPanel.Shortcuts')
const { formatKeySequence } = commandStore
const isShortcutsPanelVisible = computed(
() => bottomPanelStore.activePanel === 'shortcuts'
)
const tooltipText = computed(
() => `${t('shortcuts.keyboardShortcuts')} (${formatKeySequence(command)})`
)
const toggleShortcutsPanel = () => {
bottomPanelStore.togglePanel('shortcuts')
}
const formatKeySequence = (sequences: string[]): string => {
return sequences
.map((seq) => seq.replace(/Control/g, 'Ctrl').replace(/Shift/g, 'Shift'))
.join(' + ')
}
</script>

View File

@@ -298,8 +298,26 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.ToggleLock',
icon: 'pi pi-lock',
label: 'Canvas Toggle Lock',
category: 'view-controls' as const,
function: () => {
app.canvas['read_only'] = !app.canvas['read_only']
app.canvas.state.readOnly = !app.canvas.state.readOnly
}
},
{
id: 'Comfy.Canvas.Lock',
icon: 'pi pi-lock',
label: 'Lock Canvas',
category: 'view-controls' as const,
function: () => {
app.canvas.state.readOnly = true
}
},
{
id: 'Comfy.Canvas.Unlock',
icon: 'pi pi-lock-open',
label: 'Unlock Canvas',
function: () => {
app.canvas.state.readOnly = false
}
},
{

View File

@@ -0,0 +1,27 @@
import { computed, ref } from 'vue'
export function useZoomControls() {
const isModalVisible = ref(false)
const showModal = () => {
isModalVisible.value = true
}
const hideModal = () => {
isModalVisible.value = false
}
const toggleModal = () => {
isModalVisible.value = !isModalVisible.value
}
const hasActivePopup = computed(() => isModalVisible.value)
return {
isModalVisible,
showModal,
hideModal,
toggleModal,
hasActivePopup
}
}

View File

@@ -191,6 +191,18 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
},
{
combo: {
key: 'v'
},
commandId: 'Comfy.Canvas.Unlock'
},
{
combo: {
key: 'h'
},
commandId: 'Comfy.Canvas.Lock'
},
{
combo: {
key: 'Escape'

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "تعديل العرض ليناسب العقد المحددة"
},
"Comfy_Canvas_Lock": {
"label": "قفل اللوحة"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "تحريك العقد المحددة للأسفل"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "تثبيت/إلغاء تثبيت العناصر المحددة"
},
"Comfy_Canvas_Unlock": {
"label": "فتح اللوحة"
},
"Comfy_Canvas_ZoomIn": {
"label": "تكبير"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "ملائمة العرض",
"focusMode": "وضع التركيز",
"hand": "يد",
"hideLinks": "إخفاء الروابط",
"panMode": "وضع التحريك",
"resetView": "إعادة تعيين العرض",
"select": "تحديد",
"selectMode": "وضع التحديد",
"toggleLinkVisibility": "تبديل ظهور الروابط",
"showLinks": "إظهار الروابط",
"toggleMinimap": "تبديل الخريطة المصغرة",
"zoomIn": "تكبير",
"zoomOptions": "خيارات التكبير",
"zoomOut": "تصغير"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "زيادة حجم الفرشاة في محرر القناع",
"Interrupt": "إيقاف مؤقت",
"Load Default Workflow": "تحميل سير العمل الافتراضي",
"Lock Canvas": "قفل اللوحة",
"Manage group nodes": "إدارة عقد المجموعة",
"Manager": "المدير",
"Minimap": "خريطة مصغرة",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "تبديل شريط تقدم مدير العقد المخصصة",
"Undo": "تراجع",
"Ungroup selected group nodes": "فك تجميع عقد المجموعة المحددة",
"Unlock Canvas": "فتح قفل اللوحة",
"Unpack the selected Subgraph": "فك تجميع الرسم البياني الفرعي المحدد",
"Workflows": "سير العمل",
"Zoom In": "تكبير",
@@ -1694,5 +1701,11 @@
"enterFilename": "أدخل اسم الملف",
"exportWorkflow": "تصدير سير العمل",
"saveWorkflow": "حفظ سير العمل"
},
"zoomControls": {
"hideMinimap": "إخفاء الخريطة المصغرة",
"label": "عناصر التحكم في التكبير",
"showMinimap": "إظهار الخريطة المصغرة",
"zoomToFit": "تكبير لتناسب الشاشة"
}
}

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "Fit view to selected nodes"
},
"Comfy_Canvas_Lock": {
"label": "Lock Canvas"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "Move Selected Nodes Down"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "Pin/Unpin Selected Nodes"
},
"Comfy_Canvas_Unlock": {
"label": "Unlock Canvas"
},
"Comfy_Canvas_ZoomIn": {
"label": "Zoom In"
},

View File

@@ -894,8 +894,19 @@
"fitView": "Fit View",
"selectMode": "Select Mode",
"panMode": "Pan Mode",
"toggleLinkVisibility": "Toggle Link Visibility",
"toggleMinimap": "Toggle Minimap"
"toggleMinimap": "Toggle Minimap",
"select": "Select",
"hand": "Hand",
"zoomOptions": "Zoom Options",
"focusMode": "Focus Mode",
"hideLinks": "Hide Links",
"showLinks": "Show Links"
},
"zoomControls": {
"label": "Zoom Controls",
"zoomToFit": "Zoom To Fit",
"showMinimap": "Show Minimap",
"hideMinimap": "Hide Minimap"
},
"groupNode": {
"create": "Create group node",
@@ -963,6 +974,7 @@
"Browse Templates": "Browse Templates",
"Delete Selected Items": "Delete Selected Items",
"Zoom to fit": "Zoom to fit",
"Lock Canvas": "Lock Canvas",
"Move Selected Nodes Down": "Move Selected Nodes Down",
"Move Selected Nodes Left": "Move Selected Nodes Left",
"Move Selected Nodes Right": "Move Selected Nodes Right",
@@ -977,6 +989,7 @@
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
"Mute/Unmute Selected Nodes": "Mute/Unmute Selected Nodes",
"Pin/Unpin Selected Nodes": "Pin/Unpin Selected Nodes",
"Unlock Canvas": "Unlock Canvas",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Clear Pending Tasks": "Clear Pending Tasks",

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "Ajustar vista a los nodos seleccionados"
},
"Comfy_Canvas_Lock": {
"label": "Bloquear lienzo"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "Mover nodos seleccionados hacia abajo"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Anclar/Desanclar elementos seleccionados"
},
"Comfy_Canvas_Unlock": {
"label": "Desbloquear lienzo"
},
"Comfy_Canvas_ZoomIn": {
"label": "Acercar"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "Ajustar vista",
"focusMode": "Modo de enfoque",
"hand": "Mano",
"hideLinks": "Ocultar enlaces",
"panMode": "Modo de desplazamiento",
"resetView": "Restablecer vista",
"select": "Seleccionar",
"selectMode": "Modo de selección",
"toggleLinkVisibility": "Alternar visibilidad de enlace",
"showLinks": "Mostrar enlaces",
"toggleMinimap": "Alternar minimapa",
"zoomIn": "Acercar",
"zoomOptions": "Opciones de zoom",
"zoomOut": "Alejar"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
"Interrupt": "Interrumpir",
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
"Lock Canvas": "Bloquear lienzo",
"Manage group nodes": "Gestionar nodos de grupo",
"Manager": "Administrador",
"Minimap": "Minimapa",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
"Undo": "Deshacer",
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
"Unlock Canvas": "Desbloquear lienzo",
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
"Workflows": "Flujos de trabajo",
"Zoom In": "Acercar",
@@ -1694,5 +1701,11 @@
"enterFilename": "Introduzca el nombre del archivo",
"exportWorkflow": "Exportar flujo de trabajo",
"saveWorkflow": "Guardar flujo de trabajo"
},
"zoomControls": {
"hideMinimap": "Ocultar minimapa",
"label": "Controles de zoom",
"showMinimap": "Mostrar minimapa",
"zoomToFit": "Ajustar al zoom"
}
}

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "Ajuster la vue aux nœuds sélectionnés"
},
"Comfy_Canvas_Lock": {
"label": "Verrouiller la toile"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "Déplacer les nœuds sélectionnés vers le bas"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Épingler/Désépingler les éléments sélectionnés"
},
"Comfy_Canvas_Unlock": {
"label": "Déverrouiller le Canvas"
},
"Comfy_Canvas_ZoomIn": {
"label": "Zoom avant"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "Adapter la vue",
"focusMode": "Mode focus",
"hand": "Main",
"hideLinks": "Masquer les liens",
"panMode": "Mode panoramique",
"resetView": "Réinitialiser la vue",
"select": "Sélectionner",
"selectMode": "Mode sélection",
"toggleLinkVisibility": "Basculer la visibilité des liens",
"showLinks": "Afficher les liens",
"toggleMinimap": "Afficher/Masquer la mini-carte",
"zoomIn": "Zoom avant",
"zoomOptions": "Options de zoom",
"zoomOut": "Zoom arrière"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
"Interrupt": "Interrompre",
"Load Default Workflow": "Charger le flux de travail par défaut",
"Lock Canvas": "Verrouiller le canevas",
"Manage group nodes": "Gérer les nœuds de groupe",
"Manager": "Gestionnaire",
"Minimap": "Minicarte",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
"Undo": "Annuler",
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
"Unlock Canvas": "Déverrouiller le canevas",
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
"Workflows": "Flux de travail",
"Zoom In": "Zoom avant",
@@ -1694,5 +1701,11 @@
"enterFilename": "Entrez le nom du fichier",
"exportWorkflow": "Exporter le flux de travail",
"saveWorkflow": "Enregistrer le flux de travail"
},
"zoomControls": {
"hideMinimap": "Masquer la mini-carte",
"label": "Contrôles de zoom",
"showMinimap": "Afficher la mini-carte",
"zoomToFit": "Ajuster à lécran"
}
}

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "選択したノードにビューを合わせる"
},
"Comfy_Canvas_Lock": {
"label": "キャンバスをロック"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "選択したノードを下に移動"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "選択したアイテムのピン留め/ピン留め解除"
},
"Comfy_Canvas_Unlock": {
"label": "キャンバスをロック解除"
},
"Comfy_Canvas_ZoomIn": {
"label": "ズームイン"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "ビューに合わせる",
"focusMode": "フォーカスモード",
"hand": "手のひら",
"hideLinks": "リンクを非表示",
"panMode": "パンモード",
"resetView": "ビューをリセット",
"select": "選択",
"selectMode": "選択モード",
"toggleLinkVisibility": "リンク表示切り替え",
"showLinks": "リンク表示",
"toggleMinimap": "ミニマップの切り替え",
"zoomIn": "拡大",
"zoomOptions": "ズームオプション",
"zoomOut": "縮小"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
"Interrupt": "中断",
"Load Default Workflow": "デフォルトワークフローを読み込む",
"Lock Canvas": "キャンバスをロック",
"Manage group nodes": "グループノードを管理",
"Manager": "マネージャー",
"Minimap": "ミニマップ",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
"Undo": "元に戻す",
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
"Unlock Canvas": "キャンバスのロックを解除",
"Unpack the selected Subgraph": "選択したサブグラフを展開",
"Workflows": "ワークフロー",
"Zoom In": "ズームイン",
@@ -1694,5 +1701,11 @@
"enterFilename": "ファイル名を入力",
"exportWorkflow": "ワークフローをエクスポート",
"saveWorkflow": "ワークフローを保存"
},
"zoomControls": {
"hideMinimap": "ミニマップを非表示",
"label": "ズームコントロール",
"showMinimap": "ミニマップを表示",
"zoomToFit": "全体表示にズーム"
}
}

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "선택한 노드에 뷰 맞추기"
},
"Comfy_Canvas_Lock": {
"label": "캔버스 잠금"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "선택한 노드 아래로 이동"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "선택한 항목 고정/고정 해제"
},
"Comfy_Canvas_Unlock": {
"label": "캔버스 잠금 해제"
},
"Comfy_Canvas_ZoomIn": {
"label": "확대"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "보기 맞춤",
"focusMode": "포커스 모드",
"hand": "손 도구",
"hideLinks": "링크 숨기기",
"panMode": "팬 모드",
"resetView": "보기 재설정",
"select": "선택",
"selectMode": "선택 모드",
"toggleLinkVisibility": "링크 가시성 전환",
"showLinks": "링크 표시",
"toggleMinimap": "미니맵 전환",
"zoomIn": "확대",
"zoomOptions": "확대/축소 옵션",
"zoomOut": "축소"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
"Interrupt": "중단",
"Load Default Workflow": "기본 워크플로 불러오기",
"Lock Canvas": "캔버스 잠금",
"Manage group nodes": "그룹 노드 관리",
"Manager": "매니저",
"Minimap": "미니맵",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
"Undo": "실행 취소",
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
"Unlock Canvas": "캔버스 잠금 해제",
"Unpack the selected Subgraph": "선택한 서브그래프 풀기",
"Workflows": "워크플로우",
"Zoom In": "확대",
@@ -1694,5 +1701,11 @@
"enterFilename": "파일 이름 입력",
"exportWorkflow": "워크플로 내보내기",
"saveWorkflow": "워크플로 저장"
},
"zoomControls": {
"hideMinimap": "미니맵 숨기기",
"label": "확대/축소 컨트롤",
"showMinimap": "미니맵 표시",
"zoomToFit": "화면에 맞게 확대"
}
}

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "Подогнать вид к выбранным нодам"
},
"Comfy_Canvas_Lock": {
"label": "Заблокировать холст"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "Переместить выбранные узлы вниз"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Закрепить/Открепить выбранных нод"
},
"Comfy_Canvas_Unlock": {
"label": "Разблокировать Canvas"
},
"Comfy_Canvas_ZoomIn": {
"label": "Увеличить"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "Подгонять под выделенные",
"focusMode": "Режим фокуса",
"hand": "Рука",
"hideLinks": "Скрыть связи",
"panMode": "Режим панорамирования",
"resetView": "Сбросить вид",
"select": "Выбрать",
"selectMode": "Выбрать режим",
"toggleLinkVisibility": "Переключить видимость ссылок",
"showLinks": "Показать связи",
"toggleMinimap": "Показать/скрыть миникарту",
"zoomIn": "Увеличить",
"zoomOptions": "Параметры масштабирования",
"zoomOut": "Уменьшить"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
"Interrupt": "Прервать",
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
"Lock Canvas": "Заблокировать холст",
"Manage group nodes": "Управление групповыми нодами",
"Manager": "Менеджер",
"Minimap": "Мини-карта",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
"Unlock Canvas": "Разблокировать холст",
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
"Workflows": "Рабочие процессы",
"Zoom In": "Увеличить",
@@ -1694,5 +1701,11 @@
"enterFilename": "Введите название файла",
"exportWorkflow": "Экспорт рабочего процесса",
"saveWorkflow": "Сохранить рабочий процесс"
},
"zoomControls": {
"hideMinimap": "Скрыть миникарту",
"label": "Элементы управления масштабом",
"showMinimap": "Показать миникарту",
"zoomToFit": "Масштабировать по размеру"
}
}

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "將視圖適應至所選節點"
},
"Comfy_Canvas_Lock": {
"label": "鎖定畫布"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "將選取的節點下移"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "釘選/取消釘選已選項目"
},
"Comfy_Canvas_Unlock": {
"label": "解鎖畫布"
},
"Comfy_Canvas_ZoomIn": {
"label": "放大"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "適合視窗",
"focusMode": "專注模式",
"hand": "拖曳",
"hideLinks": "隱藏連結",
"panMode": "平移模式",
"resetView": "重設視圖",
"select": "選取",
"selectMode": "選取模式",
"toggleLinkVisibility": "切換連結顯示",
"showLinks": "顯示連結",
"toggleMinimap": "切換小地圖",
"zoomIn": "放大",
"zoomOptions": "縮放選項",
"zoomOut": "縮小"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
"Interrupt": "中斷",
"Load Default Workflow": "載入預設工作流程",
"Lock Canvas": "鎖定畫布",
"Manage group nodes": "管理群組節點",
"Manager": "管理員",
"Minimap": "縮圖地圖",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
"Undo": "復原",
"Ungroup selected group nodes": "取消群組選取的群組節點",
"Unlock Canvas": "解除鎖定畫布",
"Unpack the selected Subgraph": "解包所選子圖",
"Workflows": "工作流程",
"Zoom In": "放大",
@@ -1694,5 +1701,11 @@
"enterFilename": "輸入檔案名稱",
"exportWorkflow": "匯出工作流程",
"saveWorkflow": "儲存工作流程"
},
"zoomControls": {
"hideMinimap": "隱藏小地圖",
"label": "縮放控制",
"showMinimap": "顯示小地圖",
"zoomToFit": "縮放至適合大小"
}
}

View File

@@ -47,6 +47,9 @@
"Comfy_Canvas_FitView": {
"label": "适应视图到选中节点"
},
"Comfy_Canvas_Lock": {
"label": "鎖定畫布"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "下移选中的节点"
},
@@ -89,6 +92,9 @@
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "固定/取消固定选中项"
},
"Comfy_Canvas_Unlock": {
"label": "解鎖畫布"
},
"Comfy_Canvas_ZoomIn": {
"label": "放大"
},

View File

@@ -410,12 +410,17 @@
},
"graphCanvasMenu": {
"fitView": "适应视图",
"focusMode": "專注模式",
"hand": "拖曳",
"hideLinks": "隱藏連結",
"panMode": "平移模式",
"resetView": "重置视图",
"select": "選取",
"selectMode": "选择模式",
"toggleLinkVisibility": "切换连线可见性",
"showLinks": "顯示連結",
"toggleMinimap": "切换小地图",
"zoomIn": "放大",
"zoomOptions": "縮放選項",
"zoomOut": "缩小"
},
"groupNode": {
@@ -801,6 +806,7 @@
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大笔刷大小",
"Interrupt": "中断",
"Load Default Workflow": "加载默认工作流",
"Lock Canvas": "鎖定畫布",
"Manage group nodes": "管理组节点",
"Manager": "管理器",
"Minimap": "小地图",
@@ -855,6 +861,7 @@
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
"Undo": "撤销",
"Ungroup selected group nodes": "解散选中组节点",
"Unlock Canvas": "解除鎖定畫布",
"Unpack the selected Subgraph": "解包选中子图",
"Workflows": "工作流",
"Zoom In": "放大画面",
@@ -1694,5 +1701,11 @@
"enterFilename": "输入文件名",
"exportWorkflow": "导出工作流",
"saveWorkflow": "保存工作流"
},
"zoomControls": {
"hideMinimap": "隱藏小地圖",
"label": "縮放控制",
"showMinimap": "顯示小地圖",
"zoomToFit": "適合畫面"
}
}

View File

@@ -120,6 +120,13 @@ export const useCommandStore = defineStore('command', () => {
}
}
const formatKeySequence = (command: ComfyCommandImpl): string => {
const sequences = command.keybinding?.combo.getKeySequences() || []
return sequences
.map((seq) => seq.replace(/Control/g, 'Ctrl').replace(/Shift/g, 'Shift'))
.join(' + ')
}
return {
commands,
execute,
@@ -127,6 +134,7 @@ export const useCommandStore = defineStore('command', () => {
registerCommand,
registerCommands,
isRegistered,
loadExtensionCommands
loadExtensionCommands,
formatKeySequence
}
})

View File

@@ -1,12 +1,13 @@
import { defineStore } from 'pinia'
import { type Raw, computed, markRaw, ref, shallowRef } from 'vue'
import type { Positionable } from '@/lib/litegraph/src/interfaces'
import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
import type {
LGraphCanvas,
LGraphGroup,
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { isLGraphGroup, isLGraphNode, isReroute } from '@/utils/litegraphUtil'
export const useTitleEditorStore = defineStore('titleEditor', () => {
@@ -33,6 +34,36 @@ export const useCanvasStore = defineStore('canvas', () => {
selectedItems.value = items.map((item) => markRaw(item))
}
// Reactive scale percentage that syncs with app.canvas.ds.scale
const appScalePercentage = ref(100)
// Set up scale synchronization when canvas is available
let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
undefined
const initScaleSync = () => {
if (app.canvas?.ds) {
// Initial sync
originalOnChanged = app.canvas.ds.onChanged
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
// Set up continuous sync
app.canvas.ds.onChanged = () => {
if (app.canvas?.ds?.scale) {
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
}
// Call original handler if exists
originalOnChanged?.(app.canvas.ds.scale, app.canvas.ds.offset)
}
}
}
const cleanupScaleSync = () => {
if (app.canvas?.ds) {
app.canvas.ds.onChanged = originalOnChanged
originalOnChanged = undefined
}
}
const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
const rerouteSelected = computed(() => selectedItems.value.some(isReroute))
@@ -42,13 +73,38 @@ export const useCanvasStore = defineStore('canvas', () => {
return canvas.value
}
/**
* Sets the canvas zoom level from a percentage value
* @param percentage - Zoom percentage value (1-1000, where 1000 = 1000% zoom)
*/
const setAppZoomFromPercentage = (percentage: number) => {
if (!app.canvas?.ds || percentage <= 0) return
// Convert percentage to scale (1000% = 10.0 scale)
const newScale = percentage / 100
const ds = app.canvas.ds
ds.changeScale(
newScale,
ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined
)
app.canvas.setDirty(true, true)
// Update reactive value immediately for UI consistency
appScalePercentage.value = Math.round(newScale * 100)
}
return {
canvas,
selectedItems,
nodeSelected,
groupSelected,
rerouteSelected,
appScalePercentage,
updateSelectedItems,
getCanvas
getCanvas,
setAppZoomFromPercentage,
initScaleSync,
cleanupScaleSync
}
})

View File

@@ -0,0 +1,168 @@
import { describe, expect, it, vi } from 'vitest'
// Mock functions
const mockExecute = vi.fn()
const mockGetCommand = vi.fn().mockReturnValue({
keybinding: {
combo: {
getKeySequences: () => ['Ctrl', '+']
}
}
})
const mockFormatKeySequence = vi.fn().mockReturnValue('Ctrl+')
const mockSetAppZoom = vi.fn()
const mockSettingGet = vi.fn().mockReturnValue(true)
// Mock dependencies
vi.mock('@/renderer/extensions/minimap/composables/useMinimap', () => ({
useMinimap: () => ({
containerStyles: { value: { backgroundColor: '#fff', borderRadius: '8px' } }
})
}))
vi.mock('@/stores/commandStore', () => ({
useCommandStore: () => ({
execute: mockExecute,
getCommand: mockGetCommand,
formatKeySequence: mockFormatKeySequence
})
}))
vi.mock('@/stores/graphStore', () => ({
useCanvasStore: () => ({
appScalePercentage: 100,
setAppZoomFromPercentage: mockSetAppZoom
})
}))
vi.mock('@/stores/settingStore', () => ({
useSettingStore: () => ({
get: mockSettingGet
})
}))
describe('ZoomControlsModal', () => {
it('should have proper props interface', () => {
// Test that the component file structure and basic exports work
expect(mockExecute).toBeDefined()
expect(mockGetCommand).toBeDefined()
expect(mockFormatKeySequence).toBeDefined()
expect(mockSetAppZoom).toBeDefined()
expect(mockSettingGet).toBeDefined()
})
it('should call command store execute when executeCommand is invoked', () => {
mockExecute.mockClear()
// Simulate the executeCommand function behavior
const executeCommand = (command: string) => {
mockExecute(command)
}
executeCommand('Comfy.Canvas.FitView')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.FitView')
})
it('should validate zoom input ranges correctly', () => {
mockSetAppZoom.mockClear()
// Simulate the applyZoom function behavior
const applyZoom = (val: { value: number }) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
return
}
mockSetAppZoom(inputValue)
}
// Test invalid values
applyZoom({ value: 0 })
applyZoom({ value: 1010 })
applyZoom({ value: NaN })
expect(mockSetAppZoom).not.toHaveBeenCalled()
// Test valid value
applyZoom({ value: 50 })
expect(mockSetAppZoom).toHaveBeenCalledWith(50)
})
it('should return correct minimap toggle text based on setting', () => {
const t = (key: string) => {
const translations: Record<string, string> = {
'zoomControls.showMinimap': 'Show Minimap',
'zoomControls.hideMinimap': 'Hide Minimap'
}
return translations[key] || key
}
// Simulate the minimapToggleText computed property
const minimapToggleText = () =>
mockSettingGet('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
// Test when minimap is visible
mockSettingGet.mockReturnValue(true)
expect(minimapToggleText()).toBe('Hide Minimap')
// Test when minimap is hidden
mockSettingGet.mockReturnValue(false)
expect(minimapToggleText()).toBe('Show Minimap')
})
it('should format keyboard shortcuts correctly', () => {
mockFormatKeySequence.mockReturnValue('Ctrl+')
expect(mockFormatKeySequence()).toBe('Ctrl+')
expect(mockGetCommand).toBeDefined()
})
it('should handle repeat command functionality', () => {
mockExecute.mockClear()
let interval: number | null = null
// Simulate the repeat functionality
const startRepeat = (command: string) => {
if (interval) return
const cmd = () => mockExecute(command)
cmd() // Execute immediately
interval = 1 // Mock interval ID
}
const stopRepeat = () => {
if (interval) {
interval = null
}
}
startRepeat('Comfy.Canvas.ZoomIn')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.ZoomIn')
stopRepeat()
expect(interval).toBeNull()
})
it('should have proper filteredMinimapStyles computed property', () => {
const mockContainerStyles = {
backgroundColor: '#fff',
borderRadius: '8px',
height: '100px',
width: '200px'
}
// Simulate the filteredMinimapStyles computed property
const filteredMinimapStyles = () => {
return {
...mockContainerStyles,
height: undefined,
width: undefined
}
}
const result = filteredMinimapStyles()
expect(result.backgroundColor).toBe('#fff')
expect(result.borderRadius).toBe('8px')
expect(result.height).toBeUndefined()
expect(result.width).toBeUndefined()
})
})