mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
84cf8bdc08
commit
9332ce639c
@ -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`}
|
||||
|
@ -21,7 +21,7 @@ const NodeCollapseButton = ({ nodeId, isOpen }: Props) => {
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
className="nopan"
|
||||
className="nodrag"
|
||||
onClick={handleClick}
|
||||
aria-label="Minimize"
|
||||
sx={{
|
||||
|
@ -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',
|
||||
|
@ -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);
|
@ -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,
|
||||
|
@ -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,
|
||||
}}
|
||||
>
|
||||
|
@ -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,
|
||||
|
@ -141,7 +141,6 @@ const InputFieldWrapper = memo(
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
className="nopan"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
|
@ -29,7 +29,11 @@ const BooleanInputFieldComponent = (
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch onChange={handleValueChanged} isChecked={field.value}></Switch>
|
||||
<Switch
|
||||
className="nodrag"
|
||||
onChange={handleValueChanged}
|
||||
isChecked={field.value}
|
||||
></Switch>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -85,7 +85,7 @@ const ControlNetModelInputFieldComponent = (
|
||||
|
||||
return (
|
||||
<IAIMantineSelect
|
||||
className="nowheel"
|
||||
className="nowheel nodrag"
|
||||
tooltip={selectedModel?.description}
|
||||
value={selectedModel?.id ?? null}
|
||||
placeholder="Pick one"
|
||||
|
@ -30,7 +30,7 @@ const EnumInputFieldComponent = (
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="nowheel"
|
||||
className="nowheel nodrag"
|
||||
onChange={handleValueChanged}
|
||||
value={field.value}
|
||||
>
|
||||
|
@ -68,6 +68,7 @@ const ImageInputFieldComponent = (
|
||||
|
||||
return (
|
||||
<Flex
|
||||
className="nodrag"
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -64,7 +64,7 @@ const NumberInputFieldComponent = (
|
||||
step={isIntegerField ? 1 : 0.1}
|
||||
precision={isIntegerField ? 0 : 3}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputField className="nodrag" />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -31,6 +31,7 @@ const StringInputFieldComponent = (
|
||||
if (fieldTemplate.ui_component === 'textarea') {
|
||||
return (
|
||||
<IAITextarea
|
||||
className="nodrag"
|
||||
onChange={handleValueChanged}
|
||||
value={field.value}
|
||||
rows={5}
|
||||
|
@ -85,6 +85,7 @@ const VaeModelInputFieldComponent = (
|
||||
|
||||
return (
|
||||
<IAIMantineSearchableSelect
|
||||
className="nowheel nodrag"
|
||||
itemComponent={IAIMantineSelectItemWithTooltip}
|
||||
tooltip={selectedVaeModel?.description}
|
||||
label={
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user