diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 7ae5fb648d..ff512cb48e 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1684,27 +1684,34 @@
"addPositivePrompt": "Add $t(common.positivePrompt)",
"addNegativePrompt": "Add $t(common.negativePrompt)",
"addIPAdapter": "Add $t(common.ipAdapter)",
+ "addRasterLayer": "Add $t(controlLayers.rasterLayer)",
+ "addControlLayer": "Add $t(controlLayers.controlLayer)",
+ "addInpaintMask": "Add $t(controlLayers.inpaintMask)",
+ "addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)",
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
"raster": "Raster",
- "rasterLayer_one": "Raster Layer",
- "controlLayer_one": "Control Layer",
- "inpaintMask_one": "Inpaint Mask",
- "regionalGuidance_one": "Regional Guidance",
- "ipAdapter_one": "IP Adapter",
- "rasterLayer_other": "Raster Layers",
- "controlLayer_other": "Control Layers",
- "inpaintMask_other": "Inpaint Masks",
- "regionalGuidance_other": "Regional Guidance",
- "ipAdapter_other": "IP Adapters",
+ "rasterLayer": "Raster Layer",
+ "controlLayer": "Control Layer",
+ "inpaintMask": "Inpaint Mask",
+ "regionalGuidance": "Regional Guidance",
+ "ipAdapter": "IP Adapter",
+ "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
+ "controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
+ "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
+ "regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
+ "ipAdapter_withCount_one": "$t(controlLayers.ipAdapter)",
+ "rasterLayer_withCount_other": "Raster Layers",
+ "controlLayer_withCount_other": "Control Layers",
+ "inpaintMask_withCount_other": "Inpaint Masks",
+ "regionalGuidance_withCount_other": "Regional Guidance",
+ "ipAdapter_withCount_other": "IP Adapters",
"opacity": "Opacity",
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
- "controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)",
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
"ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
- "controlAdapters_withCount_visible": "Control Adapters ({{count}})",
"controlLayers_withCount_visible": "Control Layers ({{count}})",
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
index 7b50721185..0eea95edc6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx
@@ -34,19 +34,19 @@ export const CanvasAddEntityButtons = memo(() => {
} onClick={addInpaintMask}>
- {t('controlLayers.inpaintMask', { count: 1 })}
+ {t('controlLayers.inpaintMask')}
} onClick={addRegionalGuidance}>
- {t('controlLayers.regionalGuidance', { count: 1 })}
+ {t('controlLayers.regionalGuidance')}
} onClick={addRasterLayer}>
- {t('controlLayers.rasterLayer', { count: 1 })}
+ {t('controlLayers.rasterLayer')}
} onClick={addControlLayer}>
- {t('controlLayers.controlLayer', { count: 1 })}
+ {t('controlLayers.controlLayer')}
} onClick={addIPAdapter}>
- {t('controlLayers.ipAdapter', { count: 1 })}
+ {t('controlLayers.ipAdapter')}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx
index 810422ad8a..492b28cbdd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx
@@ -33,19 +33,19 @@ export const CanvasEntityListMenuItems = memo(() => {
return (
<>
} onClick={addInpaintMask}>
- {t('controlLayers.inpaintMask', { count: 1 })}
+ {t('controlLayers.inpaintMask')}
} onClick={addRegionalGuidance}>
- {t('controlLayers.regionalGuidance', { count: 1 })}
+ {t('controlLayers.regionalGuidance')}
} onClick={addRasterLayer}>
- {t('controlLayers.rasterLayer', { count: 1 })}
+ {t('controlLayers.rasterLayer')}
} onClick={addControlLayer}>
- {t('controlLayers.controlLayer', { count: 1 })}
+ {t('controlLayers.controlLayer')}
} onClick={addIPAdapter}>
- {t('controlLayers.ipAdapter', { count: 1 })}
+ {t('controlLayers.ipAdapter')}
>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx
new file mode 100644
index 0000000000..f3dd8a2c1a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx
@@ -0,0 +1,70 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import {
+ controlLayerAdded,
+ inpaintMaskAdded,
+ ipaAdded,
+ rasterLayerAdded,
+ rgAdded,
+} from 'features/controlLayers/store/canvasSlice';
+import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiPlusBold } from 'react-icons/pi';
+
+type Props = {
+ type: CanvasEntityIdentifier['type'];
+};
+
+export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const onClick = useCallback(() => {
+ switch (type) {
+ case 'inpaint_mask':
+ dispatch(inpaintMaskAdded({ isSelected: true }));
+ break;
+ case 'regional_guidance':
+ dispatch(rgAdded({ isSelected: true }));
+ break;
+ case 'raster_layer':
+ dispatch(rasterLayerAdded({ isSelected: true }));
+ break;
+ case 'control_layer':
+ dispatch(controlLayerAdded({ isSelected: true }));
+ break;
+ case 'ip_adapter':
+ dispatch(ipaAdded({ isSelected: true }));
+ break;
+ }
+ }, [dispatch, type]);
+
+ const label = useMemo(() => {
+ switch (type) {
+ case 'inpaint_mask':
+ return t('controlLayers.addInpaintMask');
+ case 'regional_guidance':
+ return t('controlLayers.addRegionalGuidance');
+ case 'raster_layer':
+ return t('controlLayers.addRasterLayer');
+ case 'control_layer':
+ return t('controlLayers.addControlLayer');
+ case 'ip_adapter':
+ return t('controlLayers.addIPAdapter');
+ }
+ }, [type, t]);
+
+ return (
+ }
+ onClick={onClick}
+ alignSelf="stretch"
+ />
+ );
+});
+
+CanvasEntityAddOfTypeButton.displayName = 'CanvasEntityAddOfTypeButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx
index c665040905..61b42bd4c4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx
@@ -1,6 +1,7 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
import { useBoolean } from 'common/hooks/useBoolean';
+import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
@@ -53,6 +54,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
+
{type !== 'ip_adapter' && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
index d054083e16..6fd2f50799 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
@@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
const parts: string[] = [];
if (entityIdentifier.type === 'inpaint_mask') {
- parts.push(t('controlLayers.inpaintMask', { count: 1 }));
+ parts.push(t('controlLayers.inpaintMask'));
} else if (entityIdentifier.type === 'control_layer') {
- parts.push(t('controlLayers.controlLayer', { count: 1 }));
+ parts.push(t('controlLayers.controlLayer'));
} else if (entityIdentifier.type === 'raster_layer') {
- parts.push(t('controlLayers.rasterLayer', { count: 1 }));
+ parts.push(t('controlLayers.rasterLayer'));
} else if (entityIdentifier.type === 'ip_adapter') {
- parts.push(t('common.ipAdapter', { count: 1 }));
+ parts.push(t('common.ipAdapter'));
} else if (entityIdentifier.type === 'regional_guidance') {
- parts.push(t('controlLayers.regionalGuidance', { count: 1 }));
+ parts.push(t('controlLayers.regionalGuidance'));
} else {
assert(false, 'Unexpected entity type');
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts
index 5c1682cfe5..042f3ba21f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts
@@ -8,15 +8,15 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): strin
const typeString = useMemo(() => {
switch (type) {
case 'control_layer':
- return t('controlLayers.controlLayer', { count: 0 });
+ return t('controlLayers.controlLayer');
case 'raster_layer':
- return t('controlLayers.rasterLayer', { count: 0 });
+ return t('controlLayers.rasterLayer');
case 'inpaint_mask':
- return t('controlLayers.inpaintMask', { count: 0 });
+ return t('controlLayers.inpaintMask');
case 'regional_guidance':
- return t('controlLayers.regionalGuidance', { count: 0 });
+ return t('controlLayers.regionalGuidance');
case 'ip_adapter':
- return t('controlLayers.ipAdapter', { count: 0 });
+ return t('controlLayers.ipAdapter');
default:
return '';
}