From ee0b03fed025f259d16e84f5c226c36a082d51fc Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Tue, 4 Jun 2024 16:07:53 +0200 Subject: [PATCH] Add UI for anchor/position dropdowns (#2940) * Add UI for anchor/position dropdowns * Fixed MenuIconRowGroup --- .../nodes/properties/inputs/generic_inputs.py | 47 +++++++- .../image/create_images/text_as_image.py | 95 ++++----------- .../image_utility/compositing/blend_images.py | 40 +++---- src/common/common-types.ts | 2 +- .../groups/FromToDropdownsGroup.tsx | 7 +- .../components/groups/MenuIconRowGroup.tsx | 29 +++-- .../components/inputs/DropDownInput.tsx | 21 +++- .../inputs/elements/AnchorSelector.tsx | 112 ++++++++++++++++++ .../components/inputs/elements/Checkbox.tsx | 14 +-- .../components/inputs/elements/Dropdown.tsx | 11 +- .../components/inputs/elements/IconList.tsx | 14 +-- .../components/inputs/elements/TabList.tsx | 14 +-- src/renderer/hooks/useValidDropDownValue.ts | 23 ++++ 13 files changed, 284 insertions(+), 145 deletions(-) create mode 100644 src/renderer/components/inputs/elements/AnchorSelector.tsx create mode 100644 src/renderer/hooks/useValidDropDownValue.ts diff --git a/backend/src/nodes/properties/inputs/generic_inputs.py b/backend/src/nodes/properties/inputs/generic_inputs.py index a42d0263e5..8231a507e1 100644 --- a/backend/src/nodes/properties/inputs/generic_inputs.py +++ b/backend/src/nodes/properties/inputs/generic_inputs.py @@ -44,7 +44,7 @@ class DropDownOption(TypedDict): condition: NotRequired[ConditionJson | None] -DropDownStyle = Literal["dropdown", "checkbox", "tabs", "icons"] +DropDownStyle = Literal["dropdown", "checkbox", "tabs", "icons", "anchor"] """ This specified the preferred style in which the frontend may display the dropdown. @@ -53,6 +53,7 @@ class DropDownOption(TypedDict): The first option will be interpreted as the yes/true option while the second option will be interpreted as the no/false option. - `tabs`: The options are displayed as tab list. The label of the input itself will *not* be displayed. - `icons`: The options are displayed as a list of icons. This is only available if all options have icons. Labels are still required for all options. +- `anchor`: The options are displayed as a 3x3 grid where the user is allowed to select one of 9 anchor positions. This only works for dropdowns with 9 options. """ @@ -613,3 +614,47 @@ def RowOrderDropdown() -> DropDownInput: label="Order", default=OrderEnum.ROW_MAJOR, ) + + +class Anchor(Enum): + TOP_LEFT = "top_left" + TOP = "top_centered" + TOP_RIGHT = "top_right" + LEFT = "centered_left" + CENTER = "centered" + RIGHT = "centered_right" + BOTTOM_LEFT = "bottom_left" + BOTTOM = "bottom_centered" + BOTTOM_RIGHT = "bottom_right" + + +def AnchorInput(label: str = "Anchor", icon: str = "BsFillImageFill") -> DropDownInput: + return EnumInput( + Anchor, + label=label, + label_style="inline", + option_labels={ + Anchor.TOP_LEFT: "Top Left", + Anchor.TOP: "Top", + Anchor.TOP_RIGHT: "Top Right", + Anchor.LEFT: "Left", + Anchor.CENTER: "Center", + Anchor.RIGHT: "Right", + Anchor.BOTTOM_LEFT: "Bottom Left", + Anchor.BOTTOM: "Bottom", + Anchor.BOTTOM_RIGHT: "Bottom Right", + }, + icons={ + Anchor.TOP_LEFT: icon, + Anchor.TOP: icon, + Anchor.TOP_RIGHT: icon, + Anchor.LEFT: icon, + Anchor.CENTER: icon, + Anchor.RIGHT: icon, + Anchor.BOTTOM_LEFT: icon, + Anchor.BOTTOM: icon, + Anchor.BOTTOM_RIGHT: icon, + }, + preferred_style="anchor", + default=Anchor.CENTER, + ) diff --git a/backend/src/packages/chaiNNer_standard/image/create_images/text_as_image.py b/backend/src/packages/chaiNNer_standard/image/create_images/text_as_image.py index b52cd25352..c53651a491 100644 --- a/backend/src/packages/chaiNNer_standard/image/create_images/text_as_image.py +++ b/backend/src/packages/chaiNNer_standard/image/create_images/text_as_image.py @@ -12,6 +12,8 @@ from nodes.impl.color.color import Color from nodes.impl.image_utils import normalize, to_uint8 from nodes.properties.inputs import ( + Anchor, + AnchorInput, BoolInput, ColorInput, EnumInput, @@ -31,64 +33,22 @@ ] -class TextAsImageAlignment(Enum): +class TextAlignment(Enum): LEFT = "left" CENTER = "center" RIGHT = "right" -class TextAsImagePosition(Enum): - TOP_LEFT = "top_left" - TOP_CENTERED = "top_centered" - TOP_RIGHT = "top_right" - CENTERED_LEFT = "centered_left" - CENTERED = "centered" - CENTERED_RIGHT = "centered_right" - BOTTOM_LEFT = "bottom_left" - BOTTOM_CENTERED = "bottom_centered" - BOTTOM_RIGHT = "bottom_right" - - -TEXT_AS_IMAGE_POSITION_LABELS = { - TextAsImagePosition.TOP_LEFT: "Top left", - TextAsImagePosition.TOP_CENTERED: "Top centered", - TextAsImagePosition.TOP_RIGHT: "Top right", - TextAsImagePosition.CENTERED_LEFT: "Centered left", - TextAsImagePosition.CENTERED: "Centered", - TextAsImagePosition.CENTERED_RIGHT: "Centered right", - TextAsImagePosition.BOTTOM_LEFT: "Bottom left", - TextAsImagePosition.BOTTOM_CENTERED: "Bottom centered", - TextAsImagePosition.BOTTOM_RIGHT: "Bottom right", -} - -TEXT_AS_IMAGE_X_Y_REF_FACTORS = { - TextAsImagePosition.TOP_LEFT: {"x": np.array([0, 0.5]), "y": np.array([0, 0.5])}, - TextAsImagePosition.TOP_CENTERED: { - "x": np.array([0.5, 0]), - "y": np.array([0, 0.5]), - }, - TextAsImagePosition.TOP_RIGHT: {"x": np.array([1, -0.5]), "y": np.array([0, 0.5])}, - TextAsImagePosition.CENTERED_LEFT: { - "x": np.array([0, 0.5]), - "y": np.array([0.5, 0]), - }, - TextAsImagePosition.CENTERED: {"x": np.array([0.5, 0]), "y": np.array([0.5, 0])}, - TextAsImagePosition.CENTERED_RIGHT: { - "x": np.array([1, -0.5]), - "y": np.array([0.5, 0]), - }, - TextAsImagePosition.BOTTOM_LEFT: { - "x": np.array([0, 0.5]), - "y": np.array([1, -0.5]), - }, - TextAsImagePosition.BOTTOM_CENTERED: { - "x": np.array([0.5, 0]), - "y": np.array([1, -0.5]), - }, - TextAsImagePosition.BOTTOM_RIGHT: { - "x": np.array([1, -0.5]), - "y": np.array([1, -0.5]), - }, +X_Y_REF_FACTORS = { + Anchor.TOP_LEFT: {"x": np.array([0, 0.5]), "y": np.array([0, 0.5])}, + Anchor.TOP: {"x": np.array([0.5, 0]), "y": np.array([0, 0.5])}, + Anchor.TOP_RIGHT: {"x": np.array([1, -0.5]), "y": np.array([0, 0.5])}, + Anchor.LEFT: {"x": np.array([0, 0.5]), "y": np.array([0.5, 0])}, + Anchor.CENTER: {"x": np.array([0.5, 0]), "y": np.array([0.5, 0])}, + Anchor.RIGHT: {"x": np.array([1, -0.5]), "y": np.array([0.5, 0])}, + Anchor.BOTTOM_LEFT: {"x": np.array([0, 0.5]), "y": np.array([1, -0.5])}, + Anchor.BOTTOM: {"x": np.array([0.5, 0]), "y": np.array([1, -0.5])}, + Anchor.BOTTOM_RIGHT: {"x": np.array([1, -0.5]), "y": np.array([1, -0.5])}, } @@ -105,26 +65,21 @@ class TextAsImagePosition(Enum): BoolInput("Italic", default=False, icon="FaItalic").with_id(2), ), EnumInput( - TextAsImageAlignment, + TextAlignment, label="Alignment", preferred_style="icons", icons={ - TextAsImageAlignment.LEFT: "FaAlignLeft", - TextAsImageAlignment.CENTER: "FaAlignCenter", - TextAsImageAlignment.RIGHT: "FaAlignRight", + TextAlignment.LEFT: "FaAlignLeft", + TextAlignment.CENTER: "FaAlignCenter", + TextAlignment.RIGHT: "FaAlignRight", }, - default=TextAsImageAlignment.CENTER, + default=TextAlignment.CENTER, ).with_id(4), ), ColorInput(channels=[3], default=Color.bgr((0, 0, 0))).with_id(3), - NumberInput("Width", min=1, max=None, default=500).with_id(5), - NumberInput("Height", min=1, max=None, default=100).with_id(6), - EnumInput( - TextAsImagePosition, - label="Position", - option_labels=TEXT_AS_IMAGE_POSITION_LABELS, - default=TextAsImagePosition.CENTERED, - ).with_id(7), + NumberInput("Width", min=1, max=None, default=500, unit="px").with_id(5), + NumberInput("Height", min=1, max=None, default=100, unit="px").with_id(6), + AnchorInput(label="Position", icon="MdTextFields").with_id(7), ], outputs=[ ImageOutput( @@ -143,11 +98,11 @@ def text_as_image_node( text: str, bold: bool, italic: bool, - alignment: TextAsImageAlignment, + alignment: TextAlignment, color: Color, width: int, height: int, - position: TextAsImagePosition, + position: Anchor, ) -> np.ndarray: path = TEXT_AS_IMAGE_FONT_PATH[int(bold)][int(italic)] font_path = os.path.join( @@ -178,11 +133,11 @@ def text_as_image_node( drawing = ImageDraw.Draw(pil_image) x_ref = round( - np.sum(np.array([width, w_text]) * TEXT_AS_IMAGE_X_Y_REF_FACTORS[position]["x"]) # type: ignore + np.sum(np.array([width, w_text]) * X_Y_REF_FACTORS[position]["x"]) # type: ignore ) y_ref = round( np.sum( - np.array([height, h_text]) * TEXT_AS_IMAGE_X_Y_REF_FACTORS[position]["y"] # type: ignore + np.array([height, h_text]) * X_Y_REF_FACTORS[position]["y"] # type: ignore ) ) diff --git a/backend/src/packages/chaiNNer_standard/image_utility/compositing/blend_images.py b/backend/src/packages/chaiNNer_standard/image_utility/compositing/blend_images.py index 0ae300961b..cad378f453 100644 --- a/backend/src/packages/chaiNNer_standard/image_utility/compositing/blend_images.py +++ b/backend/src/packages/chaiNNer_standard/image_utility/compositing/blend_images.py @@ -26,41 +26,41 @@ class BlendOverlayPosition(Enum): TOP_LEFT = "top_left" - TOP_CENTERED = "top_centered" + TOP = "top_centered" TOP_RIGHT = "top_right" - CENTERED_LEFT = "centered_left" - CENTERED = "centered" - CENTERED_RIGHT = "centered_right" + LEFT = "centered_left" + CENTER = "centered" + RIGHT = "centered_right" BOTTOM_LEFT = "bottom_left" - BOTTOM_CENTERED = "bottom_centered" + BOTTOM = "bottom_centered" BOTTOM_RIGHT = "bottom_right" PERCENT_OFFSET = "percent_offset" PIXEL_OFFSET = "pixel_offset" BLEND_OVERLAY_POSITION_LABELS = { - BlendOverlayPosition.TOP_LEFT: "Top left", - BlendOverlayPosition.TOP_CENTERED: "Top centered", - BlendOverlayPosition.TOP_RIGHT: "Top right", - BlendOverlayPosition.CENTERED_LEFT: "Centered left", - BlendOverlayPosition.CENTERED: "Centered", - BlendOverlayPosition.CENTERED_RIGHT: "Centered right", - BlendOverlayPosition.BOTTOM_LEFT: "Bottom left", - BlendOverlayPosition.BOTTOM_CENTERED: "Bottom centered", - BlendOverlayPosition.BOTTOM_RIGHT: "Bottom right", + BlendOverlayPosition.TOP_LEFT: "Top Left", + BlendOverlayPosition.TOP: "Top", + BlendOverlayPosition.TOP_RIGHT: "Top Right", + BlendOverlayPosition.LEFT: "Left", + BlendOverlayPosition.CENTER: "Center", + BlendOverlayPosition.RIGHT: "Right", + BlendOverlayPosition.BOTTOM_LEFT: "Bottom Left", + BlendOverlayPosition.BOTTOM: "Bottom", + BlendOverlayPosition.BOTTOM_RIGHT: "Bottom Right", BlendOverlayPosition.PERCENT_OFFSET: "Offset (%)", BlendOverlayPosition.PIXEL_OFFSET: "Offset (pixels)", } BLEND_OVERLAY_X0_Y0_FACTORS = { BlendOverlayPosition.TOP_LEFT: np.array([0, 0]), - BlendOverlayPosition.TOP_CENTERED: np.array([0.5, 0]), + BlendOverlayPosition.TOP: np.array([0.5, 0]), BlendOverlayPosition.TOP_RIGHT: np.array([1, 0]), - BlendOverlayPosition.CENTERED_LEFT: np.array([0, 0.5]), - BlendOverlayPosition.CENTERED: np.array([0.5, 0.5]), - BlendOverlayPosition.CENTERED_RIGHT: np.array([1, 0.5]), + BlendOverlayPosition.LEFT: np.array([0, 0.5]), + BlendOverlayPosition.CENTER: np.array([0.5, 0.5]), + BlendOverlayPosition.RIGHT: np.array([1, 0.5]), BlendOverlayPosition.BOTTOM_LEFT: np.array([0, 1]), - BlendOverlayPosition.BOTTOM_CENTERED: np.array([0.5, 1]), + BlendOverlayPosition.BOTTOM: np.array([0.5, 1]), BlendOverlayPosition.BOTTOM_RIGHT: np.array([1, 1]), BlendOverlayPosition.PERCENT_OFFSET: np.array([1, 1]), BlendOverlayPosition.PIXEL_OFFSET: np.array([0, 0]), @@ -81,7 +81,7 @@ class BlendOverlayPosition(Enum): BlendOverlayPosition, label="Overlay position", option_labels=BLEND_OVERLAY_POSITION_LABELS, - default=BlendOverlayPosition.CENTERED, + default=BlendOverlayPosition.CENTER, ), if_enum_group(3, (BlendOverlayPosition.PERCENT_OFFSET))( SliderInput("X offset", min=-200, max=200, default=0, unit="%"), diff --git a/src/common/common-types.ts b/src/common/common-types.ts index 613826523b..541dd2cc41 100644 --- a/src/common/common-types.ts +++ b/src/common/common-types.ts @@ -69,7 +69,7 @@ export interface InputOption { readonly condition?: Condition | null; } export type FileInputKind = 'image' | 'pth' | 'pt' | 'video' | 'bin' | 'param' | 'onnx'; -export type DropDownStyle = 'dropdown' | 'checkbox' | 'tabs' | 'icons'; +export type DropDownStyle = 'dropdown' | 'checkbox' | 'tabs' | 'icons' | 'anchor'; export interface DropdownGroup { readonly label?: string | null; readonly startAt: InputSchemaValue; diff --git a/src/renderer/components/groups/FromToDropdownsGroup.tsx b/src/renderer/components/groups/FromToDropdownsGroup.tsx index 3545b95e8e..737b63ac34 100644 --- a/src/renderer/components/groups/FromToDropdownsGroup.tsx +++ b/src/renderer/components/groups/FromToDropdownsGroup.tsx @@ -4,6 +4,7 @@ import { IoMdArrowForward } from 'react-icons/io'; import { Input, InputData, InputId, InputValue, OfKind } from '../../../common/common-types'; import { getInputValue } from '../../../common/util'; import { getPassthroughIgnored } from '../../helpers/nodeState'; +import { useValidDropDownValue } from '../../hooks/useValidDropDownValue'; import { DropDown } from '../inputs/elements/Dropdown'; import { InputContainer } from '../inputs/InputContainer'; import { GroupProps } from './props'; @@ -15,11 +16,15 @@ interface SmallDropDownProps { isLocked: boolean; } const SmallDropDown = memo(({ input, inputData, setInputValue, isLocked }: SmallDropDownProps) => { - const value = getInputValue(input.id, inputData); const setValue = useCallback( (data?: string | number) => setInputValue(input.id, data ?? input.def), [setInputValue, input] ); + const value = useValidDropDownValue( + getInputValue(input.id, inputData), + setValue, + input + ); return ( diff --git a/src/renderer/components/groups/MenuIconRowGroup.tsx b/src/renderer/components/groups/MenuIconRowGroup.tsx index 8bfb362d34..0601a0f1d6 100644 --- a/src/renderer/components/groups/MenuIconRowGroup.tsx +++ b/src/renderer/components/groups/MenuIconRowGroup.tsx @@ -1,12 +1,30 @@ import { Box } from '@chakra-ui/react'; import { memo } from 'react'; +import { DropDownInput, InputSchemaValue } from '../../../common/common-types'; import { getUniqueKey } from '../../../common/group-inputs'; -import { getPassthroughIgnored } from '../../helpers/nodeState'; +import { NodeState, getPassthroughIgnored } from '../../helpers/nodeState'; +import { useValidDropDownValue } from '../../hooks/useValidDropDownValue'; import { IconList } from '../inputs/elements/IconList'; import { InputContainer, WithoutLabel } from '../inputs/InputContainer'; import { IconSet } from './IconSetGroup'; import { GroupProps } from './props'; +const IconListWrapper = memo( + ({ input, nodeState }: { input: DropDownInput; nodeState: NodeState }) => { + const setValue = (value: InputSchemaValue) => nodeState.setInputValue(input.id, value); + const value = useValidDropDownValue(nodeState.inputData[input.id], setValue, input); + + return ( + + ); + } +); + export const MenuIconRowGroup = memo(({ inputs, nodeState }: GroupProps<'menu-icon-row'>) => { return ( @@ -30,13 +48,10 @@ export const MenuIconRowGroup = memo(({ inputs, nodeState }: GroupProps<'menu-ic if (item.kind === 'dropdown' && item.preferredStyle === 'icons') { return ( - nodeState.setInputValue(item.id, item.def)} - value={nodeState.inputData[item.id]} - onChange={(value) => nodeState.setInputValue(item.id, value)} + nodeState={nodeState} /> ); } diff --git a/src/renderer/components/inputs/DropDownInput.tsx b/src/renderer/components/inputs/DropDownInput.tsx index 77d53905ea..6ee48b081d 100644 --- a/src/renderer/components/inputs/DropDownInput.tsx +++ b/src/renderer/components/inputs/DropDownInput.tsx @@ -1,7 +1,9 @@ import { QuestionIcon } from '@chakra-ui/icons'; import { Tooltip } from '@chakra-ui/react'; import { memo, useCallback } from 'react'; +import { useValidDropDownValue } from '../../hooks/useValidDropDownValue'; import { Markdown } from '../Markdown'; +import { AnchorSelector } from './elements/AnchorSelector'; import { Checkbox } from './elements/Checkbox'; import { DropDown } from './elements/Dropdown'; import { IconList } from './elements/IconList'; @@ -15,6 +17,9 @@ export const DropDownInput = memo( ({ value, setValue, input, isLocked, testCondition }: DropDownInputProps) => { const { options, def, label, preferredStyle, groups, hint, description } = input; + // eslint-disable-next-line no-param-reassign + value = useValidDropDownValue(value, setValue, input); + const reset = useCallback(() => setValue(def), [setValue, def]); if (preferredStyle === 'checkbox' && options.length === 2) { @@ -46,7 +51,6 @@ export const DropDownInput = memo( isDisabled={isLocked} label={label} no={options[1]} - reset={reset} value={value} yes={options[0]} onChange={setValue} @@ -61,7 +65,6 @@ export const DropDownInput = memo( @@ -75,7 +78,19 @@ export const DropDownInput = memo( + + ); + } + + if (preferredStyle === 'anchor' && options.length === 9) { + return ( + + diff --git a/src/renderer/components/inputs/elements/AnchorSelector.tsx b/src/renderer/components/inputs/elements/AnchorSelector.tsx new file mode 100644 index 0000000000..09086c3274 --- /dev/null +++ b/src/renderer/components/inputs/elements/AnchorSelector.tsx @@ -0,0 +1,112 @@ +import { Button, HStack, Tooltip, VStack } from '@chakra-ui/react'; +import { memo } from 'react'; +import { + BsArrowDown, + BsArrowDownLeft, + BsArrowDownRight, + BsArrowLeft, + BsArrowRight, + BsArrowUp, + BsArrowUpLeft, + BsArrowUpRight, + BsDot, +} from 'react-icons/bs'; +import { DropDownInput, InputSchemaValue } from '../../../../common/common-types'; +import { IconFactory } from '../../CustomIcons'; + +const indexes = [0, 1, 2] as const; + +type OffsetKey = string & { __offsetKey: never }; +const getOffsetKey = (x: number, y: number) => `${y} ${x}` as OffsetKey; +const otherIcons: Partial> = { + [getOffsetKey(-1, 0)]: , + [getOffsetKey(+1, 0)]: , + [getOffsetKey(0, -1)]: , + [getOffsetKey(0, +1)]: , + [getOffsetKey(-1, -1)]: , + [getOffsetKey(+1, +1)]: , + [getOffsetKey(+1, -1)]: , + [getOffsetKey(-1, +1)]: , +}; + +export interface AnchorSelectorProps { + value: InputSchemaValue; + onChange: (value: InputSchemaValue) => void; + isDisabled?: boolean; + options: DropDownInput['options']; +} + +export const AnchorSelector = memo( + ({ value, onChange, isDisabled, options }: AnchorSelectorProps) => { + let selectedIndex = options.findIndex((o) => o.value === value); + if (selectedIndex === -1) selectedIndex = 0; + + const selectedY = Math.floor(selectedIndex / indexes.length); + const selectedX = selectedIndex % indexes.length; + + return ( + + {indexes.map((y) => ( + + {indexes.map((x) => { + const o = options[y * indexes.length + x]; + const selected = value === o.value; + + let icon; + if (selected) { + icon = ; + } else { + icon = otherIcons[getOffsetKey(x - selectedX, y - selectedY)] ?? ( + + ); + } + + return ( + + + + ); + })} + + ))} + + ); + } +); diff --git a/src/renderer/components/inputs/elements/Checkbox.tsx b/src/renderer/components/inputs/elements/Checkbox.tsx index 6f37a2d986..e4208e656b 100644 --- a/src/renderer/components/inputs/elements/Checkbox.tsx +++ b/src/renderer/components/inputs/elements/Checkbox.tsx @@ -1,5 +1,5 @@ import { Box, Checkbox as ChakraCheckbox } from '@chakra-ui/react'; -import { ReactNode, memo, useEffect } from 'react'; +import { ReactNode, memo } from 'react'; import { DropDownInput, InputSchemaValue } from '../../../../common/common-types'; import './Checkbox.scss'; @@ -7,9 +7,8 @@ type ArrayItem = T extends readonly (infer I)[] ? I : never; type Option = ArrayItem; export interface CheckboxProps { - value: InputSchemaValue | undefined; + value: InputSchemaValue; onChange: (value: InputSchemaValue) => void; - reset: () => void; isDisabled?: boolean; yes: Option; no: Option; @@ -18,14 +17,7 @@ export interface CheckboxProps { } export const Checkbox = memo( - ({ value, onChange, reset, isDisabled, yes, no, label, afterText }: CheckboxProps) => { - // reset invalid values to default - useEffect(() => { - if (value === undefined || (yes.value !== value && no.value !== value)) { - reset(); - } - }, [value, reset, yes, no]); - + ({ value, onChange, isDisabled, yes, no, label, afterText }: CheckboxProps) => { return ( void; reset: () => void; isDisabled?: boolean; @@ -20,13 +20,6 @@ export interface DropDownProps { export const DropDown = memo( ({ value, onChange, reset, isDisabled, options, groups, testCondition }: DropDownProps) => { - // reset invalid values to default - useEffect(() => { - if (value === undefined || options.every((o) => o.value !== value)) { - reset(); - } - }, [value, reset, options]); - let selection = options.findIndex((o) => o.value === value); if (selection === -1) selection = 0; @@ -49,7 +42,7 @@ export const DropDown = memo( }, [options, testCondition]); useEffect(() => { - if (value !== undefined && unavailableOptions.includes(value)) { + if (unavailableOptions.includes(value)) { // we can't use reset since the default value might be unavailable too // so we search for the first available option const firstAvailable = options.find((o) => !unavailableOptions.includes(o.value)); diff --git a/src/renderer/components/inputs/elements/IconList.tsx b/src/renderer/components/inputs/elements/IconList.tsx index 257a213f95..04382baf07 100644 --- a/src/renderer/components/inputs/elements/IconList.tsx +++ b/src/renderer/components/inputs/elements/IconList.tsx @@ -1,24 +1,16 @@ import { Button, ButtonGroup, Tooltip } from '@chakra-ui/react'; -import { memo, useEffect } from 'react'; +import { memo } from 'react'; import { DropDownInput, InputSchemaValue } from '../../../../common/common-types'; import { IconFactory } from '../../CustomIcons'; export interface IconListProps { - value: InputSchemaValue | undefined; + value: InputSchemaValue; onChange: (value: InputSchemaValue) => void; - reset: () => void; isDisabled?: boolean; options: DropDownInput['options']; } -export const IconList = memo(({ value, onChange, reset, isDisabled, options }: IconListProps) => { - // reset invalid values to default - useEffect(() => { - if (value === undefined || options.every((o) => o.value !== value)) { - reset(); - } - }, [value, reset, options]); - +export const IconList = memo(({ value, onChange, isDisabled, options }: IconListProps) => { let selection = options.findIndex((o) => o.value === value); if (selection === -1) selection = 0; diff --git a/src/renderer/components/inputs/elements/TabList.tsx b/src/renderer/components/inputs/elements/TabList.tsx index 2dc3955683..9b25388063 100644 --- a/src/renderer/components/inputs/elements/TabList.tsx +++ b/src/renderer/components/inputs/elements/TabList.tsx @@ -1,23 +1,15 @@ import { TabList as ChakraTabList, Tab, TabIndicator, Tabs } from '@chakra-ui/react'; -import { memo, useEffect } from 'react'; +import { memo } from 'react'; import { DropDownInput, InputSchemaValue } from '../../../../common/common-types'; export interface TabListProps { - value: InputSchemaValue | undefined; + value: InputSchemaValue; onChange: (value: InputSchemaValue) => void; - reset: () => void; isDisabled?: boolean; options: DropDownInput['options']; } -export const TabList = memo(({ value, onChange, reset, isDisabled, options }: TabListProps) => { - // reset invalid values to default - useEffect(() => { - if (value === undefined || options.every((o) => o.value !== value)) { - reset(); - } - }, [value, reset, options]); - +export const TabList = memo(({ value, onChange, isDisabled, options }: TabListProps) => { let selection = options.findIndex((o) => o.value === value); if (selection === -1) selection = 0; diff --git a/src/renderer/hooks/useValidDropDownValue.ts b/src/renderer/hooks/useValidDropDownValue.ts new file mode 100644 index 0000000000..b9bacb7157 --- /dev/null +++ b/src/renderer/hooks/useValidDropDownValue.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { DropDownInput, InputSchemaValue } from '../../common/common-types'; + +export const useValidDropDownValue = ( + value: InputSchemaValue | undefined, + setValue: (value: InputSchemaValue) => void, + input: Pick +) => { + let valid = value ?? input.def; + if (input.options.every((o) => o.value !== valid)) { + valid = input.def; + } + + // reset to valid value + const resetTo = valid !== value ? valid : undefined; + useEffect(() => { + if (resetTo !== undefined) { + setValue(resetTo); + } + }, [resetTo, setValue]); + + return valid; +};