From a512fdc0f64fe54e8f7ebaa313f7eae14cd6c037 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 14 Aug 2023 14:36:14 -0400 Subject: [PATCH] update IAIDndImage to use children for icons, add UI for shift+delete to delete images from gallery --- .../web/src/common/components/IAIDndImage.tsx | 70 +++++-------------- .../src/common/components/IAIDndImageIcon.tsx | 46 ++++++++++++ .../components/ControlNetImagePreview.tsx | 38 +++++++--- .../components/GallerySettingsPopover.tsx | 25 +------ .../SingleSelectionMenuItems.tsx | 13 ++-- .../components/ImageGrid/GalleryImage.tsx | 65 ++++++++++++++--- .../components/ImageGrid/GalleryImageGrid.tsx | 22 +++++- .../fields/fieldTypes/ImageInputField.tsx | 5 +- 8 files changed, 177 insertions(+), 107 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index defe600b78..b7b2e35342 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -1,22 +1,10 @@ -import { - ChakraProps, - Flex, - Icon, - Image, - useColorMode, - useColorModeValue, -} from '@chakra-ui/react'; -import IAIIconButton from 'common/components/IAIIconButton'; +import { ChakraProps, Flex, Icon, Image, useColorMode } from '@chakra-ui/react'; import { IAILoadingImageFallback, IAINoContentFallback, } from 'common/components/IAIImageFallback'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { - TypesafeDraggableData, - TypesafeDroppableData, -} from 'features/dnd/types'; import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import { MouseEvent, @@ -26,22 +14,22 @@ import { useCallback, useState, } from 'react'; -import { FaImage, FaUndo, FaUpload } from 'react-icons/fa'; +import { FaImage, FaUpload } from 'react-icons/fa'; import { ImageDTO, PostUploadAction } from 'services/api/types'; import { mode } from 'theme/util/mode'; import IAIDraggable from './IAIDraggable'; import IAIDroppable from './IAIDroppable'; import SelectionOverlay from './SelectionOverlay'; +import { + TypesafeDraggableData, + TypesafeDroppableData, +} from 'features/dnd/types'; type IAIDndImageProps = { imageDTO: ImageDTO | undefined; onError?: (event: SyntheticEvent) => void; onLoad?: (event: SyntheticEvent) => void; onClick?: (event: MouseEvent) => void; - onClickReset?: (event: MouseEvent) => void; - withResetIcon?: boolean; - resetIcon?: ReactElement; - resetTooltip?: string; withMetadataOverlay?: boolean; isDragDisabled?: boolean; isDropDisabled?: boolean; @@ -58,15 +46,16 @@ type IAIDndImageProps = { noContentFallback?: ReactElement; useThumbailFallback?: boolean; withHoverOverlay?: boolean; + children?: JSX.Element; + onMouseOver?: () => void; + onMouseOut?: () => void; }; const IAIDndImage = (props: IAIDndImageProps) => { const { imageDTO, - onClickReset, onError, onClick, - withResetIcon = false, withMetadataOverlay = false, isDropDisabled = false, isDragDisabled = false, @@ -80,32 +69,30 @@ const IAIDndImage = (props: IAIDndImageProps) => { dropLabel, isSelected = false, thumbnail = false, - resetTooltip = 'Reset', - resetIcon = , noContentFallback = , useThumbailFallback, withHoverOverlay = false, + children, + onMouseOver, + onMouseOut, } = props; const { colorMode } = useColorMode(); const [isHovered, setIsHovered] = useState(false); const handleMouseOver = useCallback(() => { + if (onMouseOver) onMouseOver(); setIsHovered(true); - }, []); + }, [onMouseOver]); const handleMouseOut = useCallback(() => { + if (onMouseOut) onMouseOut(); setIsHovered(false); - }, []); + }, [onMouseOut]); const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ postUploadAction, isDisabled: isUploadDisabled, }); - const resetIconShadow = useColorModeValue( - `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`, - `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))` - ); - const uploadButtonStyles = isUploadDisabled ? {} : { @@ -212,30 +199,6 @@ const IAIDndImage = (props: IAIDndImageProps) => { onClick={onClick} /> )} - {onClickReset && withResetIcon && imageDTO && ( - - )} {!isDropDisabled && ( { dropLabel={dropLabel} /> )} + {children} )} diff --git a/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx b/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx new file mode 100644 index 0000000000..2e16377175 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx @@ -0,0 +1,46 @@ +import { JSXElementConstructor, ReactElement, memo, MouseEvent } from 'react'; +import IAIIconButton from './IAIIconButton'; +import { SystemStyleObject, useColorModeValue } from '@chakra-ui/react'; + +type Props = { + onClick: (event: MouseEvent) => void; + tooltip: string; + icon?: ReactElement>; + styleOverrides?: SystemStyleObject; +}; + +const IAIDndImageIcon = (props: Props) => { + const { onClick, tooltip, icon, styleOverrides } = props; + + const resetIconShadow = useColorModeValue( + `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`, + `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))` + ); + return ( + + ); +}; + +export default memo(IAIDndImageIcon); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx index 4fffb82275..901a86fd4e 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -1,4 +1,10 @@ -import { Box, Flex, Spinner, SystemStyleObject } from '@chakra-ui/react'; +import { + Box, + Flex, + Spinner, + SystemStyleObject, + useColorModeValue, +} from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/dist/query'; import { @@ -16,6 +22,8 @@ import { ControlNetConfig, controlNetImageChanged, } from '../store/controlNetSlice'; +import { FaUndo } from 'react-icons/fa'; +import IAIDndImageIcon from '../../../common/components/IAIDndImageIcon'; type Props = { controlNet: ControlNetConfig; @@ -93,6 +101,11 @@ const ControlNetImagePreview = (props: Props) => { [controlNetId] ); + const resetIconShadow = useColorModeValue( + `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`, + `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))` + ); + const shouldShowProcessedImage = controlImage && processedControlImage && @@ -119,11 +132,15 @@ const ControlNetImagePreview = (props: Props) => { droppableData={droppableData} imageDTO={controlImage} isDropDisabled={shouldShowProcessedImage || !isEnabled} - onClickReset={handleResetControlImage} postUploadAction={postUploadAction} - resetTooltip="Reset Control Image" - withResetIcon={Boolean(controlImage)} - /> + > + : undefined} + tooltip="Reset Control Image" + /> + + { imageDTO={processedControlImage} isUploadDisabled={true} isDropDisabled={!isEnabled} - onClickReset={handleResetControlImage} - resetTooltip="Reset Control Image" - withResetIcon={Boolean(controlImage)} - /> + > + : undefined} + tooltip="Reset Control Image" + /> + {pendingControlImages.includes(controlNetId) && ( { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const { - galleryImageMinimumWidth, - shouldAutoSwitch, - autoAssignBoardOnClick, - shouldShowDeleteButton, - } = useAppSelector(selector); + const { galleryImageMinimumWidth, shouldAutoSwitch, autoAssignBoardOnClick } = + useAppSelector(selector); const handleChangeGalleryImageMinimumWidth = useCallback( (v: number) => { @@ -68,13 +61,6 @@ const GallerySettingsPopover = () => { [dispatch] ); - const handleChangeShowDeleteButton = useCallback( - (e: ChangeEvent) => { - dispatch(shouldShowDeleteButtonChanged(e.target.checked)); - }, - [dispatch] - ); - return ( { { isChecked={shouldAutoSwitch} onChange={handleChangeAutoSwitch} /> - { Change Board {imageDTO.pinned ? ( - } onClickCapture={handleUnpinImage}> - Unpin Image + } onClickCapture={handleUnpinImage}> + Unstar Image ) : ( - } - onClickCapture={handlePinImage} - > - Pin Image + } onClickCapture={handlePinImage}> + Star Image )} { } }, [togglePin, imageDTO]); + const [isHovered, setIsHovered] = useState(false); + + const pinIcon = useMemo(() => { + if (imageDTO?.pinned) return ; + if (!imageDTO?.pinned && isHovered) return ; + }, [imageDTO?.pinned, isHovered]); + + const resetIconShadow = useColorModeValue( + `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`, + `drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))` + ); + + const iconButtonStyles = { + position: 'absolute', + top: 1, + insetInlineEnd: 1, + p: 0, + minW: 0, + svg: { + transitionProperty: 'common', + transitionDuration: 'normal', + fill: 'base.100', + _hover: { fill: 'base.50' }, + filter: resetIconShadow, + }, + }; + if (!imageDTO) { return ; } @@ -91,18 +120,34 @@ const GalleryImage = (props: HoverableImageProps) => { draggableData={draggableData} isSelected={isSelected} minSize={0} - onClickReset={togglePinnedState} imageSx={{ w: 'full', h: 'full' }} isDropDisabled={true} isUploadDisabled={true} thumbnail={true} withHoverOverlay - resetIcon={ - imageDTO.pinned ? : - } - resetTooltip="Pin image" - withResetIcon={true} - /> + onMouseOver={() => setIsHovered(true)} + onMouseOut={() => setIsHovered(false)} + > + <> + + + {isHovered && shouldShowDeleteButton && ( + } + tooltip={'Delete'} + styleOverrides={{ + bottom: 1, + top: 'auto', + }} + /> + )} + + ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx index bacd5c38ad..8951fc050a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx @@ -1,5 +1,5 @@ import { Box, Flex } from '@chakra-ui/react'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors'; @@ -20,6 +20,8 @@ import { useBoardTotal } from 'services/api/hooks/useBoardTotal'; import GalleryImage from './GalleryImage'; import ImageGridItemContainer from './ImageGridItemContainer'; import ImageGridListContainer from './ImageGridListContainer'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { shouldShowDeleteButtonChanged } from '../../store/gallerySlice'; const overlayScrollbarsConfig: UseOverlayScrollbarsParams = { defer: true, @@ -36,6 +38,7 @@ const overlayScrollbarsConfig: UseOverlayScrollbarsParams = { const GalleryImageGrid = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); const rootRef = useRef(null); const [scroller, setScroller] = useState(null); const [initialize, osInstance] = useOverlayScrollbars( @@ -85,6 +88,23 @@ const GalleryImageGrid = () => { return () => osInstance()?.destroy(); }, [scroller, initialize, osInstance]); + useHotkeys( + 'shift', + () => { + dispatch(shouldShowDeleteButtonChanged(true)); + }, + [shouldShowDeleteButtonChanged] + ); + + useHotkeys( + 'shift', + () => { + dispatch(shouldShowDeleteButtonChanged(false)); + }, + { keyup: true }, + [shouldShowDeleteButtonChanged] + ); + if (!currentData) { return ( );