mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): add menu items to copy canvas/bbox to clipboard
This commit is contained in:
committed by
Kent Keirsey
parent
dfb9e300d4
commit
c5e5641f0e
@@ -3,6 +3,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { CanvasContextMenuItemsCropCanvasToBbox } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import {
|
||||
useCopyCanvasToClipboard,
|
||||
useNewControlLayerFromBbox,
|
||||
useNewGlobalReferenceImageFromBbox,
|
||||
useNewRasterLayerFromBbox,
|
||||
@@ -13,12 +14,13 @@ import {
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFloppyDiskBold } from 'react-icons/pi';
|
||||
import { PiCopyBold, PiFloppyDiskBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasContextMenuGlobalMenuItems = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const saveSubMenu = useSubMenu();
|
||||
const newSubMenu = useSubMenu();
|
||||
const copySubMenu = useSubMenu();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const saveCanvasToGallery = useSaveCanvasToGallery();
|
||||
const saveBboxToGallery = useSaveBboxToGallery();
|
||||
@@ -26,6 +28,8 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
|
||||
const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox();
|
||||
const newRasterLayerFromBbox = useNewRasterLayerFromBbox();
|
||||
const newControlLayerFromBbox = useNewControlLayerFromBbox();
|
||||
const copyCanvasToClipboard = useCopyCanvasToClipboard('canvas');
|
||||
const copyBboxToClipboard = useCopyCanvasToClipboard('bbox');
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -67,6 +71,21 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</MenuItem>
|
||||
<MenuItem {...copySubMenu.parentMenuItemProps} icon={<PiCopyBold />}>
|
||||
<Menu {...copySubMenu.menuProps}>
|
||||
<MenuButton {...copySubMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('controlLayers.canvasContextMenu.copyToClipboard')} />
|
||||
</MenuButton>
|
||||
<MenuList {...copySubMenu.menuListProps}>
|
||||
<MenuItem icon={<PiCopyBold />} isDisabled={isBusy} onClick={copyCanvasToClipboard}>
|
||||
{t('controlLayers.canvasContextMenu.copyCanvasToClipboard')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiCopyBold />} isDisabled={isBusy} onClick={copyBboxToClipboard}>
|
||||
{t('controlLayers.canvasContextMenu.copyBboxToClipboard')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { deepClone } from 'common/util/deepClone';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { canvasToBlob, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
@@ -27,7 +27,9 @@ import type {
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
@@ -150,6 +152,42 @@ export const useSaveBboxToGallery = () => {
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useCopyCanvasToClipboard = (region: 'canvas' | 'bbox') => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useCanvasManager();
|
||||
const copyCanvasToClipboard = useCallback(async () => {
|
||||
const rect =
|
||||
region === 'bbox'
|
||||
? canvasManager.stateApi.getBbox().rect
|
||||
: canvasManager.compositor.getVisibleRectOfType('raster_layer');
|
||||
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
toast({
|
||||
title: t('controlLayers.copyRegionError', { region: startCase(region) }),
|
||||
description: t('controlLayers.regionIsEmpty'),
|
||||
status: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await withResultAsync(async () => {
|
||||
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const canvasElement = canvasManager.compositor.getCompositeCanvas(rasterAdapters, rect);
|
||||
const blob = await canvasToBlob(canvasElement);
|
||||
copyBlobToClipboard(blob);
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
toast({ title: t('controlLayers.regionCopiedToClipboard', { region: startCase(region) }) });
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
|
||||
toast({ title: t('controlLayers.copyRegionError', { region: startCase(region) }), status: 'error' });
|
||||
}
|
||||
}, [canvasManager.compositor, canvasManager.stateApi, region, t]);
|
||||
|
||||
return copyCanvasToClipboard;
|
||||
};
|
||||
|
||||
export const useNewRegionalReferenceImageFromBbox = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
|
||||
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
@@ -26,17 +27,21 @@ export const useCopyLayerToClipboard = () => {
|
||||
if (!adapter) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
const result = await withResultAsync(async () => {
|
||||
const canvas = adapter.getCanvas();
|
||||
const blob = await canvasToBlob(canvas);
|
||||
copyBlobToClipboard(blob);
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
log.trace('Layer copied to clipboard');
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('toast.layerCopiedToClipboard'),
|
||||
});
|
||||
} catch (error) {
|
||||
log.error({ error: serializeError(error) }, 'Problem copying layer to clipboard');
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Problem copying layer to clipboard');
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('toast.problemCopyingLayer'),
|
||||
|
||||
Reference in New Issue
Block a user