fix(ui): fix node mouse interactions

Add "nodrag", "nowheel" and "nopan" class names in interactable elements, as neeeded. This fixes the mouse interactions and also makes the node draggable from anywhere without needing shift.

Also fixes ctrl/cmd multi-select to support deselecting.
This commit is contained in:
psychedelicious 2023-08-17 18:15:45 +10:00
parent 84cf8bdc08
commit 9332ce639c
21 changed files with 40 additions and 139 deletions

View File

@ -33,9 +33,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
<>
<Flex
layerStyle="nodeBody"
className={'nopan'}
sx={{
cursor: 'auto',
flexDirection: 'column',
w: 'full',
h: 'full',
@ -44,10 +42,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
borderBottomRadius: withFooter ? 0 : 'base',
}}
>
<Flex
className="nopan"
sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }}
>
<Flex sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }}>
{outputFieldNames.map((fieldName) => (
<OutputField
key={`${nodeId}.${fieldName}.output-field`}

View File

@ -21,7 +21,7 @@ const NodeCollapseButton = ({ nodeId, isOpen }: Props) => {
return (
<IAIIconButton
className="nopan"
className="nodrag"
onClick={handleClick}
aria-label="Minimize"
sx={{

View File

@ -23,7 +23,6 @@ import {
useNodeTemplateTitle,
} from 'features/nodes/hooks/useNodeData';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { isInvocationNodeData } from 'features/nodes/types/types';
import { ChangeEvent, memo, useCallback } from 'react';
import { FaInfoCircle } from 'react-icons/fa';
@ -45,7 +44,7 @@ const NodeNotesEdit = ({ nodeId }: Props) => {
shouldWrapChildren
>
<Flex
className={DRAG_HANDLE_CLASSNAME}
className="nodrag"
onClick={onOpen}
sx={{
alignItems: 'center',

View File

@ -1,69 +0,0 @@
import { Flex } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import IAISwitch from 'common/components/IAISwitch';
import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice';
import { InvocationNodeData } from 'features/nodes/types/types';
import { ChangeEvent, memo, useCallback } from 'react';
import { FaBars } from 'react-icons/fa';
interface Props {
data: InvocationNodeData;
}
const NodeSettings = (props: Props) => {
const { data } = props;
const dispatch = useAppDispatch();
const handleChangeIsIntermediate = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(
fieldBooleanValueChanged({
nodeId: data.id,
fieldName: 'is_intermediate',
value: e.target.checked,
})
);
},
[data.id, dispatch]
);
return (
<IAIPopover
isLazy={false}
triggerComponent={
<IAIIconButton
className="nopan"
aria-label="Node Settings"
variant="link"
sx={{
minW: 8,
color: 'base.500',
_dark: {
color: 'base.500',
},
_hover: {
color: 'base.700',
_dark: {
color: 'base.300',
},
},
}}
icon={<FaBars />}
/>
}
>
<Flex sx={{ flexDir: 'column', gap: 4, w: 64 }}>
<IAISwitch
label="Intermediate"
isChecked={Boolean(data.inputs['is_intermediate']?.value)}
onChange={handleChangeIsIntermediate}
helperText="The outputs of intermediate nodes are considered temporary objects. Intermediate images are not added to the gallery."
/>
</Flex>
</IAIPopover>
);
};
export default memo(NodeSettings);

View File

@ -45,7 +45,6 @@ const NodeTitle = ({ nodeId, title }: Props) => {
return (
<Flex
className="nopan"
sx={{
overflow: 'hidden',
w: 'full',
@ -76,6 +75,7 @@ const NodeTitle = ({ nodeId, title }: Props) => {
noOfLines={1}
/>
<EditableInput
className="nodrag"
fontSize="sm"
sx={{
p: 0,

View File

@ -5,29 +5,10 @@ import {
useToken,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { nodeClicked } from 'features/nodes/store/nodesSlice';
import {
MouseEvent,
PropsWithChildren,
memo,
useCallback,
useMemo,
} from 'react';
import { contextMenusClosed } from 'features/ui/store/uiSlice';
import { PropsWithChildren, memo, useCallback } from 'react';
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../../types/constants';
const useNodeSelect = (nodeId: string) => {
const dispatch = useAppDispatch();
const selectNode = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
dispatch(nodeClicked({ nodeId, ctrlOrMeta: e.ctrlKey || e.metaKey }));
},
[dispatch, nodeId]
);
return selectNode;
};
type NodeWrapperProps = PropsWithChildren & {
nodeId: string;
selected: boolean;
@ -35,7 +16,7 @@ type NodeWrapperProps = PropsWithChildren & {
};
const NodeWrapper = (props: NodeWrapperProps) => {
const { width, children, nodeId, selected } = props;
const { width, children, selected } = props;
const [
nodeSelectedOutlineLight,
@ -49,24 +30,23 @@ const NodeWrapper = (props: NodeWrapperProps) => {
'shadows.base',
]);
const selectNode = useNodeSelect(nodeId);
const dispatch = useAppDispatch();
const shadow = useColorModeValue(
nodeSelectedOutlineLight,
nodeSelectedOutlineDark
);
const shift = useAppSelector((state) => state.hotkeys.shift);
const opacity = useAppSelector((state) => state.nodes.nodeOpacity);
const className = useMemo(
() => (shift ? DRAG_HANDLE_CLASSNAME : 'nopan'),
[shift]
);
const handleClick = useCallback(() => {
dispatch(contextMenusClosed());
}, [dispatch]);
return (
<Box
onClickCapture={selectNode}
className={className}
onClick={handleClick}
className={DRAG_HANDLE_CLASSNAME}
sx={{
h: 'full',
position: 'relative',
@ -75,6 +55,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
transitionProperty: 'common',
transitionDuration: '0.1s',
shadow: selected ? shadow : undefined,
cursor: 'grab',
opacity,
}}
>

View File

@ -53,7 +53,6 @@ const FieldTitle = forwardRef((props: Props, ref) => {
return (
<Flex
ref={ref}
className="nopan"
sx={{
position: 'relative',
overflow: 'hidden',
@ -84,6 +83,7 @@ const FieldTitle = forwardRef((props: Props, ref) => {
noOfLines={1}
/>
<EditableInput
className="nodrag"
sx={{
p: 0,
fontWeight: 600,

View File

@ -141,7 +141,6 @@ const InputFieldWrapper = memo(
<Flex
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
className="nopan"
sx={{
position: 'relative',
minH: 8,

View File

@ -29,7 +29,11 @@ const BooleanInputFieldComponent = (
);
return (
<Switch onChange={handleValueChanged} isChecked={field.value}></Switch>
<Switch
className="nodrag"
onChange={handleValueChanged}
isChecked={field.value}
></Switch>
);
};

View File

@ -85,7 +85,7 @@ const ControlNetModelInputFieldComponent = (
return (
<IAIMantineSelect
className="nowheel"
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id ?? null}
placeholder="Pick one"

View File

@ -30,7 +30,7 @@ const EnumInputFieldComponent = (
return (
<Select
className="nowheel"
className="nowheel nodrag"
onChange={handleValueChanged}
value={field.value}
>

View File

@ -68,6 +68,7 @@ const ImageInputFieldComponent = (
return (
<Flex
className="nodrag"
sx={{
w: 'full',
h: 'full',

View File

@ -90,7 +90,7 @@ const LoRAModelInputFieldComponent = (
return (
<IAIMantineSearchableSelect
className="nowheel"
className="nowheel nodrag"
value={selectedLoRAModel?.id ?? null}
placeholder={data.length > 0 ? 'Select a LoRA' : 'No LoRAs available'}
data={data}

View File

@ -123,7 +123,7 @@ const MainModelInputFieldComponent = (
<Text variant="subtext">Loading...</Text>
) : (
<IAIMantineSearchableSelect
className="nowheel"
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id}
placeholder={
@ -135,7 +135,7 @@ const MainModelInputFieldComponent = (
onChange={handleChangeModel}
/>
)}
{isSyncModelEnabled && <SyncModelsButton iconMode />}
{isSyncModelEnabled && <SyncModelsButton className="nodrag" iconMode />}
</Flex>
);
};

View File

@ -64,7 +64,7 @@ const NumberInputFieldComponent = (
step={isIntegerField ? 1 : 0.1}
precision={isIntegerField ? 0 : 3}
>
<NumberInputField />
<NumberInputField className="nodrag" />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />

View File

@ -96,7 +96,7 @@ const RefinerModelInputFieldComponent = (
) : (
<Flex w="100%" alignItems="center" gap={2}>
<IAIMantineSearchableSelect
className="nowheel"
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id}
placeholder={data.length > 0 ? 'Select a model' : 'No models available'}
@ -107,7 +107,7 @@ const RefinerModelInputFieldComponent = (
/>
{isSyncModelEnabled && (
<Box mt={7}>
<SyncModelsButton iconMode />
<SyncModelsButton className="nodrag" iconMode />
</Box>
)}
</Flex>

View File

@ -123,7 +123,7 @@ const ModelInputFieldComponent = (
) : (
<Flex w="100%" alignItems="center" gap={2}>
<IAIMantineSearchableSelect
className="nowheel"
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id}
placeholder={data.length > 0 ? 'Select a model' : 'No models available'}
@ -132,7 +132,7 @@ const ModelInputFieldComponent = (
disabled={data.length === 0}
onChange={handleChangeModel}
/>
{isSyncModelEnabled && <SyncModelsButton iconMode />}
{isSyncModelEnabled && <SyncModelsButton className="nodrag" iconMode />}
</Flex>
);
};

View File

@ -31,6 +31,7 @@ const StringInputFieldComponent = (
if (fieldTemplate.ui_component === 'textarea') {
return (
<IAITextarea
className="nodrag"
onChange={handleValueChanged}
value={field.value}
rows={5}

View File

@ -85,6 +85,7 @@ const VaeModelInputFieldComponent = (
return (
<IAIMantineSearchableSelect
className="nowheel nodrag"
itemComponent={IAIMantineSelectItemWithTooltip}
tooltip={selectedVaeModel?.description}
label={

View File

@ -485,19 +485,6 @@ const nodesSlice = createSlice({
'image_name'
);
},
nodeClicked: (
state,
action: PayloadAction<{ nodeId: string; ctrlOrMeta?: boolean }>
) => {
const { nodeId, ctrlOrMeta } = action.payload;
state.nodes.forEach((node) => {
if (node.id === nodeId) {
node.selected = true;
} else if (!ctrlOrMeta) {
node.selected = false;
}
});
},
notesNodeValueChanged: (
state,
action: PayloadAction<{ nodeId: string; value: string }>
@ -665,7 +652,6 @@ export const {
connectionMade,
connectionStarted,
connectionEnded,
nodeClicked,
shouldShowFieldTypeLegendChanged,
shouldShowMinimapPanelChanged,
nodeTemplatesBuilt,

View File

@ -1,18 +1,19 @@
import { makeToast } from 'features/system/util/makeToast';
import { ButtonProps } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { useTranslation } from 'react-i18next';
import { FaSync } from 'react-icons/fa';
import { useSyncModelsMutation } from 'services/api/endpoints/models';
type SyncModelsButtonProps = {
type SyncModelsButtonProps = ButtonProps & {
iconMode?: boolean;
};
export default function SyncModelsButton(props: SyncModelsButtonProps) {
const { iconMode = false } = props;
const { iconMode = false, ...rest } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation();
@ -50,6 +51,7 @@ export default function SyncModelsButton(props: SyncModelsButtonProps) {
isLoading={isLoading}
onClick={syncModelsHandler}
minW="max-content"
{...rest}
>
Sync Models
</IAIButton>
@ -61,6 +63,7 @@ export default function SyncModelsButton(props: SyncModelsButtonProps) {
isLoading={isLoading}
onClick={syncModelsHandler}
size="sm"
{...rest}
/>
);
}