mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): double click to zoom to node
Requires a bit of fanagling to ensure the double click doesn't interfer w/ other stuff
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControl, Spacer } from '@invoke-ai/ui-library';
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { InputFieldDescriptionPopover } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover';
|
||||
import { InputFieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle';
|
||||
import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton';
|
||||
@@ -57,12 +57,12 @@ type CommonProps = {
|
||||
fieldTemplate: FieldInputTemplate;
|
||||
};
|
||||
|
||||
const ConnectedOrConnectionField = memo(({ nodeId, fieldName, isInvalid, isConnected }: CommonProps) => {
|
||||
const ConnectedOrConnectionField = memo(({ nodeId, fieldName, isInvalid }: CommonProps) => {
|
||||
return (
|
||||
<InputFieldWrapper>
|
||||
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
|
||||
<Flex px={2}>
|
||||
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<InputFieldHandle nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
@@ -70,8 +70,10 @@ const ConnectedOrConnectionField = memo(({ nodeId, fieldName, isInvalid, isConne
|
||||
ConnectedOrConnectionField.displayName = 'ConnectedOrConnectionField';
|
||||
|
||||
const directFieldSx: SystemStyleObject = {
|
||||
orientation: 'vertical',
|
||||
w: 'full',
|
||||
px: 2,
|
||||
flexDir: 'column',
|
||||
gap: 1,
|
||||
'&[data-is-dragging="true"]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
@@ -100,31 +102,28 @@ const DirectField = memo(({ nodeId, fieldName, isInvalid, isConnected, fieldTemp
|
||||
|
||||
return (
|
||||
<InputFieldWrapper>
|
||||
<FormControl
|
||||
<Flex
|
||||
ref={draggableRef}
|
||||
isInvalid={isInvalid}
|
||||
isDisabled={isConnected}
|
||||
sx={directFieldSx}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
data-is-connected={isConnected}
|
||||
data-is-dragging={isDragging}
|
||||
>
|
||||
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Flex gap={1}>
|
||||
<Flex className="nodrag" ref={dragHandleRef}>
|
||||
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} isDragging={isDragging} />
|
||||
</Flex>
|
||||
<Spacer />
|
||||
{isHovered && (
|
||||
<>
|
||||
<InputFieldDescriptionPopover nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToDefaultValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
</>
|
||||
)}
|
||||
<Flex gap={1}>
|
||||
<Flex ref={dragHandleRef}>
|
||||
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} isDragging={isDragging} />
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
<Spacer />
|
||||
{isHovered && (
|
||||
<>
|
||||
<InputFieldDescriptionPopover nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToDefaultValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</FormControl>
|
||||
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
{fieldTemplate.input !== 'direct' && <InputFieldHandle nodeId={nodeId} fieldName={fieldName} />}
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,8 @@ import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsCo
|
||||
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
|
||||
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
|
||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY, NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -65,6 +66,19 @@ export const InputFieldTitle = memo((props: Props) => {
|
||||
inputRef,
|
||||
});
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() => (isConnectionInProgress && connectionError !== null && !isConnectionStartField) || isConnected,
|
||||
[isConnectionInProgress, connectionError, isConnectionStartField, isConnected]
|
||||
);
|
||||
|
||||
const onDoubleClick = useCallback(
|
||||
(e: MouseEvent<HTMLParagraphElement>) => {
|
||||
e.stopPropagation();
|
||||
editable.startEditing();
|
||||
},
|
||||
[editable]
|
||||
);
|
||||
|
||||
if (!editable.isEditing) {
|
||||
return (
|
||||
<Tooltip
|
||||
@@ -74,13 +88,12 @@ export const InputFieldTitle = memo((props: Props) => {
|
||||
isDisabled={isDragging}
|
||||
>
|
||||
<Text
|
||||
className={`nodrag ${NO_FIT_ON_DOUBLE_CLICK_CLASS}`}
|
||||
sx={labelSx}
|
||||
noOfLines={1}
|
||||
data-is-invalid={isInvalid}
|
||||
data-is-disabled={
|
||||
(isConnectionInProgress && connectionError !== null && !isConnectionStartField) || isConnected
|
||||
}
|
||||
onDoubleClick={editable.startEditing}
|
||||
data-is-disabled={isDisabled}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
{editable.value}
|
||||
</Text>
|
||||
@@ -88,7 +101,15 @@ export const InputFieldTitle = memo((props: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <Input ref={inputRef} variant="outline" {...editable.inputProps} />;
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className="nodrag"
|
||||
variant="outline"
|
||||
{...editable.inputProps}
|
||||
_focusVisible={{ borderRadius: 'base', h: 'unset', px: 2 }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldTitle.displayName = 'InputFieldTitle';
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { fieldSchedulerValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
|
||||
import type { SchedulerFieldInputInstance, SchedulerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants';
|
||||
import { isParameterScheduler } from 'features/parameters/types/parameterSchemas';
|
||||
@@ -34,7 +35,7 @@ const SchedulerFieldInputComponent = (props: Props) => {
|
||||
const value = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === field?.value), [field?.value]);
|
||||
|
||||
return (
|
||||
<FormControl className="nowheel nodrag">
|
||||
<FormControl className={`nowheel nodrag ${NO_FIT_ON_DOUBLE_CLICK_CLASS}`}>
|
||||
<Combobox value={value} options={SCHEDULER_OPTIONS} onChange={onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Icon, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useUpdateNodeInternals } from '@xyflow/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { nodeIsOpenChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiCaretUpBold } from 'react-icons/pi';
|
||||
|
||||
@@ -31,7 +32,7 @@ const NodeCollapseButton = ({ nodeId, isOpen }: Props) => {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
className="nodrag"
|
||||
className={`nodrag ${NO_FIT_ON_DOUBLE_CLICK_CLASS}`}
|
||||
onClick={handleClick}
|
||||
aria-label="Minimize"
|
||||
minW={8}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useBatchGroupId } from 'features/nodes/hooks/useBatchGroupId';
|
||||
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
|
||||
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
|
||||
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -50,7 +51,12 @@ const NodeTitle = ({ nodeId, title }: Props) => {
|
||||
return (
|
||||
<Flex overflow="hidden" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
{!editable.isEditing && (
|
||||
<Text fontWeight="semibold" color={batchGroupColorToken} onDoubleClick={editable.startEditing}>
|
||||
<Text
|
||||
className={NO_FIT_ON_DOUBLE_CLICK_CLASS}
|
||||
fontWeight="semibold"
|
||||
color={batchGroupColorToken}
|
||||
onDoubleClick={editable.startEditing}
|
||||
>
|
||||
{titleWithBatchGroupId}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
|
||||
import type { NodeChange } from '@xyflow/react';
|
||||
import { type NodeChange, useReactFlow } from '@xyflow/react';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { nodesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import { selectNodeOpacity } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from 'features/nodes/types/constants';
|
||||
import { DRAG_HANDLE_CLASSNAME, NO_FIT_ON_DOUBLE_CLICK_CLASS, NODE_WIDTH } from 'features/nodes/types/constants';
|
||||
import type { AnyNode } from 'features/nodes/types/invocation';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import type { MouseEvent, PropsWithChildren } from 'react';
|
||||
@@ -84,6 +84,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
const { nodeId, width, children, selected } = props;
|
||||
const store = useAppStore();
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
||||
const flow = useReactFlow();
|
||||
|
||||
const executionState = useNodeExecutionState(nodeId);
|
||||
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
|
||||
@@ -112,9 +113,35 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
[onCloseGlobal, store, dispatch, nodeId]
|
||||
);
|
||||
|
||||
const onDoubleClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!(e.target instanceof HTMLElement)) {
|
||||
// We have to manually narrow the type here thanks to a TS quirk
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement ||
|
||||
e.target instanceof HTMLButtonElement ||
|
||||
e.target instanceof HTMLAnchorElement
|
||||
) {
|
||||
// Don't fit the view if the user is editing a text field, select, button, or link
|
||||
return;
|
||||
}
|
||||
if (e.target.closest(`.${NO_FIT_ON_DOUBLE_CLICK_CLASS}`) !== null) {
|
||||
// This target is marked as not fitting the view on double click
|
||||
return;
|
||||
}
|
||||
flow.fitView({ duration: 300, maxZoom: 1.5, nodes: [{ id: nodeId }] });
|
||||
},
|
||||
[flow, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onMouseEnter={handleMouseOver}
|
||||
onMouseLeave={handleMouseOut}
|
||||
className={DRAG_HANDLE_CLASSNAME}
|
||||
|
||||
@@ -23,6 +23,8 @@ export const SHARED_NODE_PROPERTIES: Partial<AnyNode> = {
|
||||
dragHandle: `.${DRAG_HANDLE_CLASSNAME}`,
|
||||
};
|
||||
|
||||
export const NO_FIT_ON_DOUBLE_CLICK_CLASS = 'no-fit-on-double-click';
|
||||
|
||||
/**
|
||||
* Colors for each field type - applies to their handles and edges.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user