Files
ComfyUI_frontend/src/components/graph/modals/ZoomControlsModal.vue
Johnpaul Chiwetelu 84379d9522 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>
2025-08-21 11:16:29 -07:00

238 lines
7.1 KiB
Vue

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