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

@@ -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>