diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index fa3f0f3c17..c6e269979b 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1654,7 +1654,8 @@ "storeNotInitialized": "Store is not initialized" }, "controlLayers": { - "saveCanvasToGallery": "Save Canvas To Gallery", + "bookmarkedForQuickSwitch": "Bookmarked for Quick Switch", + "notBookmarkedForQuickSwitch": "Not Bookmarked for Quick Switch", "saveBboxToGallery": "Save Bbox To Gallery", "savedToGalleryOk": "Saved to Gallery", "savedToGalleryError": "Error saving to gallery", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx index 7ae614cb09..2615771b0d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx @@ -1,6 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityIsBookmarkedForQuickSwitchToggle } from 'features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle'; import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { memo } from 'react'; @@ -10,6 +11,7 @@ export const CanvasEntityHeaderCommonActions = memo(() => { return ( + {entityIdentifier.type !== 'ip_adapter' && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx new file mode 100644 index 0000000000..6bafb04ecc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx @@ -0,0 +1,38 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntityIsBookmarkedForQuickSwitch } from 'features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch'; +import { entityIsBookmarkedForQuickSwitchChanged } from 'features/controlLayers/store/canvasSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiBookmarkSimpleBold, PiBookmarkSimpleFill } from 'react-icons/pi'; + +export const CanvasEntityIsBookmarkedForQuickSwitchToggle = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const isBookmarked = useEntityIsBookmarkedForQuickSwitch(entityIdentifier); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + if (isBookmarked) { + dispatch(entityIsBookmarkedForQuickSwitchChanged({ entityIdentifier: null })); + } else { + dispatch(entityIsBookmarkedForQuickSwitchChanged({ entityIdentifier })); + } + }, [dispatch, entityIdentifier, isBookmarked]); + + return ( + : } + onClick={onClick} + /> + ); +}); + +CanvasEntityIsBookmarkedForQuickSwitchToggle.displayName = 'CanvasEntityIsBookmarkedForQuickSwitchToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts index 20fc464465..9b48fcc51e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts @@ -6,6 +6,7 @@ import { entitySelected } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasSlice, selectEntity, + selectQuickSwitchEntityIdentifier, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -20,51 +21,63 @@ export const useCanvasEntityQuickSwitchHotkey = () => { const dispatch = useAppDispatch(); const selectedEntityBuffer = useStore($selectedEntityBuffer); - const selectedEntity = useAppSelector(selectSelectedEntityIdentifier); - const selectDoesBufferExist = useMemo( + const quickSwitchEntityIdentifier = useAppSelector(selectQuickSwitchEntityIdentifier); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const selectBufferEntityIdentifier = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { - if (!selectedEntityBuffer) { - return true; - } - const bufferEntity = selectEntity(canvas, selectedEntityBuffer); - if (bufferEntity) { - return true; - } - return false; - }), + createSelector(selectCanvasSlice, (canvas) => + selectedEntityBuffer ? (selectEntity(canvas, selectedEntityBuffer) ?? null) : null + ), [selectedEntityBuffer] ); - const doesBufferExist = useAppSelector(selectDoesBufferExist); + const bufferEntityIdentifier = useAppSelector(selectBufferEntityIdentifier); const quickSwitch = useCallback(() => { - // If there is no selected entity or buffer, we should not do anything - if (selectedEntity === null && selectedEntityBuffer === null) { - return; + if (quickSwitchEntityIdentifier !== null) { + // If there is a quick switch entity, we should switch between it and the buffer + if (quickSwitchEntityIdentifier.id !== selectedEntityIdentifier?.id) { + // The quick switch entity is not selected - select it + dispatch(entitySelected({ entityIdentifier: quickSwitchEntityIdentifier })); + $selectedEntityBuffer.set(selectedEntityIdentifier); + } else if (bufferEntityIdentifier !== null) { + // The quick switch entity is already selected - select the buffer + dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); + $selectedEntityBuffer.set(quickSwitchEntityIdentifier); + } + } else { + // No quick switch entity, so we should switch between buffer and selected entity + // If there is no selected entity or buffer, we should not do anything + if (selectedEntityIdentifier === null && bufferEntityIdentifier === null) { + return; + } + // If there is no selected entity but we do have a buffer, we should select the buffer + if (selectedEntityIdentifier === null && bufferEntityIdentifier !== null) { + dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); + return; + } + // If there is a selected entity but no buffer, we should buffer the selected entity + if (selectedEntityIdentifier !== null && bufferEntityIdentifier === null) { + $selectedEntityBuffer.set(selectedEntityIdentifier); + return; + } + // If there is a selected entity and a buffer, and they are different, we should swap the selected entity and the buffer + if ( + selectedEntityIdentifier !== null && + bufferEntityIdentifier !== null && + selectedEntityIdentifier.id !== bufferEntityIdentifier.id + ) { + $selectedEntityBuffer.set(selectedEntityIdentifier); + dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); + return; + } } - // If there is no selected entity but we do have a buffer, we should select the buffer - if (selectedEntity === null && selectedEntityBuffer !== null) { - dispatch(entitySelected({ entityIdentifier: selectedEntityBuffer })); - return; - } - // If there is a selected entity but no buffer, we should buffer the selected entity - if (selectedEntity !== null && selectedEntityBuffer === null) { - $selectedEntityBuffer.set(selectedEntity); - return; - } - // If there is a selected entity and a buffer, and they are different, we should swap the selected entity and the buffer - if (selectedEntity !== null && selectedEntityBuffer !== null && selectedEntity.id !== selectedEntityBuffer.id) { - $selectedEntityBuffer.set(selectedEntity); - dispatch(entitySelected({ entityIdentifier: selectedEntityBuffer })); - return; - } - }, [dispatch, selectedEntity, selectedEntityBuffer]); + }, [bufferEntityIdentifier, dispatch, quickSwitchEntityIdentifier, selectedEntityIdentifier]); useEffect(() => { - if (!doesBufferExist) { + if (!bufferEntityIdentifier) { $selectedEntityBuffer.set(null); } - }, [doesBufferExist]); + }, [bufferEntityIdentifier]); useHotkeys('q', quickSwitch, { enabled: true, preventDefault: true }, [quickSwitch]); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts new file mode 100644 index 0000000000..43040cafb3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts @@ -0,0 +1,18 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntityIsBookmarkedForQuickSwitch = (entityIdentifier: CanvasEntityIdentifier) => { + const selectIsBookmarkedForQuickSwitch = useMemo( + () => + createSelector(selectCanvasSlice, (canvas) => { + return canvas.quickSwitchEntityIdentifier?.id === entityIdentifier.id; + }), + [entityIdentifier] + ); + const isBookmarkedForQuickSwitch = useAppSelector(selectIsBookmarkedForQuickSwitch); + + return isBookmarkedForQuickSwitch; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 3d8f8557a6..a5916d32f9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -84,6 +84,7 @@ const getRGMaskFill = (state: CanvasState): RgbColor => { const initialState: CanvasState = { _version: 3, selectedEntityIdentifier: null, + quickSwitchEntityIdentifier: null, rasterLayers: { isHidden: false, entities: [], @@ -791,6 +792,22 @@ export const canvasSlice = createSlice({ } state.selectedEntityIdentifier = entityIdentifier; }, + entityIsBookmarkedForQuickSwitchChanged: ( + state, + action: PayloadAction<{ entityIdentifier: CanvasEntityIdentifier | null }> + ) => { + const { entityIdentifier } = action.payload; + if (!entityIdentifier) { + state.quickSwitchEntityIdentifier = null; + return; + } + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + // Cannot select a non-existent entity + return; + } + state.quickSwitchEntityIdentifier = entityIdentifier; + }, entityNameChanged: (state, action: PayloadAction>) => { const { entityIdentifier, name } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -1105,6 +1122,7 @@ export const { canvasClearHistory, // All entities entitySelected, + entityIsBookmarkedForQuickSwitchChanged, entityNameChanged, entityReset, entityIsEnabledToggled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 6246eef153..9691979a94 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -181,6 +181,11 @@ export const selectSelectedEntityIdentifier = createSelector( (canvas) => canvas.selectedEntityIdentifier ); +export const selectQuickSwitchEntityIdentifier = createSelector( + selectCanvasSlice, + (canvas) => canvas.quickSwitchEntityIdentifier +); + export const selectIsSelectedEntityDrawable = createSelector( selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3422d39fff..130abf791a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -688,6 +688,7 @@ export type StagingAreaImage = { export type CanvasState = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; + quickSwitchEntityIdentifier: CanvasEntityIdentifier | null; inpaintMasks: { isHidden: boolean; entities: CanvasInpaintMaskState[];