Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1616,6 +1616,7 @@
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
"enableModelDescriptions": "Enable Model Descriptions in Dropdowns",
"enableHighlightFocusedRegions": "Highlight Focused Regions",
"middleClickOpenInNewTab": "Use Middle Click to Open Images in New Tab",
"modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled",
"modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.",
"enableInvisibleWatermark": "Enable Invisible Watermark",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useAppSelector } from 'app/store/storeHooks';
import { openImageInNewTab } from 'common/util/openImageInNewTab';
import { selectSystemShouldUseMiddleClickToOpenInNewTab } from 'features/system/store/systemSlice';
import type { RefObject } from 'react';
import { useEffect } from 'react';

type Options = {
requireDirectTarget?: boolean;
};

const shouldHandleMiddleClick = <T extends HTMLElement>(
event: MouseEvent,
element: T,
requireDirectTarget: boolean
) => {
if (event.button !== 1) {
return false;
}

if (requireDirectTarget && event.target !== element) {
return false;
}

return true;
};

export const useMiddleClickOpenInNewTab = <T extends HTMLElement = HTMLElement>(
ref: RefObject<T>,
imageUrl: string,
{ requireDirectTarget = false }: Options = {}
) => {
const shouldUseMiddleClickToOpenInNewTab = useAppSelector(selectSystemShouldUseMiddleClickToOpenInNewTab);

useEffect(() => {
const element = ref.current;

if (!element || !shouldUseMiddleClickToOpenInNewTab) {
return;
}

// If auxclick is unsupported, leave the browser's default middle-click behavior intact.
if (!('onauxclick' in element)) {
return;
}

const onMouseDown = (event: MouseEvent) => {
if (!shouldHandleMiddleClick(event, element, requireDirectTarget)) {
return;
}

event.preventDefault();
};

const onAuxClick = (event: MouseEvent) => {
if (!shouldHandleMiddleClick(event, element, requireDirectTarget)) {
return;
}

event.preventDefault();
event.stopPropagation();
openImageInNewTab(imageUrl);
};

element.addEventListener('mousedown', onMouseDown);
element.addEventListener('auxclick', onAuxClick);

return () => {
element.removeEventListener('mousedown', onMouseDown);
element.removeEventListener('auxclick', onAuxClick);
};
}, [imageUrl, ref, requireDirectTarget, shouldUseMiddleClickToOpenInNewTab]);
};
3 changes: 3 additions & 0 deletions invokeai/frontend/web/src/common/util/openImageInNewTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const openImageInNewTab = (imageUrl: string) => {
window.open(imageUrl, '_blank', 'noopener,noreferrer');
};
9 changes: 4 additions & 5 deletions invokeai/frontend/web/src/features/dnd/DndImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import type { ImageProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Image } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/storeHooks';
import { useMiddleClickOpenInNewTab } from 'common/hooks/useMiddleClickOpenInNewTab';
import { singleImageDndSource } from 'features/dnd/dnd';
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
Expand All @@ -15,7 +15,6 @@ const sx = {
objectFit: 'contain',
maxW: 'full',
maxH: 'full',
cursor: 'grab',
'&[data-is-dragging=true]': {
opacity: 0.3,
},
Expand All @@ -28,13 +27,13 @@ type Props = {

export const DndImage = memo(
forwardRef(({ imageDTO, asThumbnail, ...rest }: Props, forwardedRef) => {
const store = useAppStore();

const [isDragging, setIsDragging] = useState(false);
const ref = useRef<HTMLImageElement>(null);
useImperativeHandle(forwardedRef, () => ref.current!, []);
const [dragPreviewState, setDragPreviewState] = useState<DndDragPreviewSingleImageState | null>(null);

useMiddleClickOpenInNewTab(ref, imageDTO.image_url);

useEffect(() => {
const element = ref.current;
if (!element) {
Expand Down Expand Up @@ -62,7 +61,7 @@ export const DndImage = memo(
},
})
);
}, [forwardedRef, imageDTO, store]);
}, [imageDTO]);

useImageContextMenu(imageDTO, ref);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IconMenuItem } from 'common/components/IconMenuItem';
import { openImageInNewTab } from 'common/util/openImageInNewTab';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -8,7 +9,7 @@ export const ContextMenuItemOpenInNewTab = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const onClick = useCallback(() => {
window.open(imageDTO.image_url, '_blank');
openImageInNewTab(imageDTO.image_url);
}, [imageDTO]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Flex, Icon, Image } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import type { AppDispatch, AppGetState } from 'app/store/store';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useMiddleClickOpenInNewTab } from 'common/hooks/useMiddleClickOpenInNewTab';
import { uniq } from 'es-toolkit';
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPreviewMultipleImage';
Expand Down Expand Up @@ -184,6 +185,9 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
}, []);

const onClick = useMemo(() => buildOnClick(imageDTO.image_name, store.dispatch, store.getState), [imageDTO, store]);
useMiddleClickOpenInNewTab(ref, imageDTO.image_url, {
requireDirectTarget: true,
});

const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => {
store.dispatch(imageToCompareChanged(null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
selectSystemShouldEnableInformationalPopovers,
selectSystemShouldEnableModelDescriptions,
selectSystemShouldShowInvocationProgressDetail,
selectSystemShouldUseMiddleClickToOpenInNewTab,
selectSystemShouldUseNSFWChecker,
selectSystemShouldUseWatermarker,
setPrefersNumericAttentionStyle,
Expand All @@ -43,6 +44,7 @@ import {
setShouldEnableModelDescriptions,
setShouldHighlightFocusedRegions,
setShouldShowInvocationProgressDetail,
setShouldUseMiddleClickToOpenInNewTab,
shouldAntialiasProgressImageChanged,
shouldConfirmOnNewSessionToggled,
shouldUseNSFWCheckerChanged,
Expand Down Expand Up @@ -83,6 +85,7 @@ const SettingsModal = (props: { children: ReactElement }) => {
const shouldEnableInformationalPopovers = useAppSelector(selectSystemShouldEnableInformationalPopovers);
const shouldEnableModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
const shouldHighlightFocusedRegions = useAppSelector(selectSystemShouldEnableHighlightFocusedRegions);
const shouldUseMiddleClickToOpenInNewTab = useAppSelector(selectSystemShouldUseMiddleClickToOpenInNewTab);
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
const shouldShowInvocationProgressDetail = useAppSelector(selectSystemShouldShowInvocationProgressDetail);
const onToggleConfirmOnNewSession = useCallback(() => {
Expand Down Expand Up @@ -165,6 +168,13 @@ const SettingsModal = (props: { children: ReactElement }) => {
[dispatch]
);

const handleChangeShouldUseMiddleClickToOpenInNewTab = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(setShouldUseMiddleClickToOpenInNewTab(e.target.checked));
},
[dispatch]
);

const handleChangePreferAttentionStyleNumeric = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(setPrefersNumericAttentionStyle(e.target.checked));
Expand Down Expand Up @@ -258,6 +268,13 @@ const SettingsModal = (props: { children: ReactElement }) => {
onChange={handleChangeShouldHighlightFocusedRegions}
/>
</FormControl>
<FormControl>
<FormLabel>{t('settings.middleClickOpenInNewTab')}</FormLabel>
<Switch
isChecked={shouldUseMiddleClickToOpenInNewTab}
onChange={handleChangeShouldUseMiddleClickToOpenInNewTab}
/>
</FormControl>
</StickyScrollable>

<StickyScrollable title={t('settings.prompt')}>
Expand Down
14 changes: 13 additions & 1 deletion invokeai/frontend/web/src/features/system/store/systemSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { assert } from 'tsafe';
import { type Language, type SystemState, zSystemState } from './types';

const getInitialState = (): SystemState => ({
_version: 2,
_version: 3,
shouldConfirmOnDelete: true,
shouldAntialiasProgressImage: false,
shouldConfirmOnNewSession: true,
Expand All @@ -26,6 +26,7 @@ const getInitialState = (): SystemState => ({
logNamespaces: [...zLogNamespace.options],
shouldShowInvocationProgressDetail: false,
shouldHighlightFocusedRegions: false,
shouldUseMiddleClickToOpenInNewTab: false,
prefersNumericAttentionWeights: false,
});

Expand Down Expand Up @@ -79,6 +80,9 @@ const slice = createSlice({
setShouldHighlightFocusedRegions(state, action: PayloadAction<boolean>) {
state.shouldHighlightFocusedRegions = action.payload;
},
setShouldUseMiddleClickToOpenInNewTab(state, action: PayloadAction<boolean>) {
state.shouldUseMiddleClickToOpenInNewTab = action.payload;
},
},
});

Expand All @@ -97,6 +101,7 @@ export const {
setShouldShowInvocationProgressDetail,
setPrefersNumericAttentionStyle,
setShouldHighlightFocusedRegions,
setShouldUseMiddleClickToOpenInNewTab,
} = slice.actions;

export const systemSliceConfig: SliceConfig<typeof slice> = {
Expand All @@ -113,6 +118,10 @@ export const systemSliceConfig: SliceConfig<typeof slice> = {
state.language = (state as SystemState).language.replace('_', '-');
state._version = 2;
}
if (state._version === 2) {
state.shouldUseMiddleClickToOpenInNewTab = false;
state._version = 3;
}
return zSystemState.parse(state);
},
},
Expand Down Expand Up @@ -141,6 +150,9 @@ export const selectSystemShouldEnableModelDescriptions = createSystemSelector(
export const selectSystemShouldEnableHighlightFocusedRegions = createSystemSelector(
(system) => system.shouldHighlightFocusedRegions
);
export const selectSystemShouldUseMiddleClickToOpenInNewTab = createSystemSelector(
(system) => system.shouldUseMiddleClickToOpenInNewTab
);
export const selectSystemPrefersNumericAttentionWeights = createSystemSelector(
(system) => system.prefersNumericAttentionWeights
);
Expand Down
3 changes: 2 additions & 1 deletion invokeai/frontend/web/src/features/system/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type Language = z.infer<typeof zLanguage>;
export const isLanguage = (v: unknown): v is Language => zLanguage.safeParse(v).success;

export const zSystemState = z.object({
_version: z.literal(2),
_version: z.literal(3),
shouldConfirmOnDelete: z.boolean(),
shouldAntialiasProgressImage: z.boolean(),
shouldConfirmOnNewSession: z.boolean(),
Expand All @@ -44,6 +44,7 @@ export const zSystemState = z.object({
logNamespaces: z.array(zLogNamespace),
shouldShowInvocationProgressDetail: z.boolean(),
shouldHighlightFocusedRegions: z.boolean(),
shouldUseMiddleClickToOpenInNewTab: z.boolean(),
prefersNumericAttentionWeights: z.boolean(),
});
export type SystemState = z.infer<typeof zSystemState>;
Loading