From 0175d91565639e09762a2c5aaaf7f735e4bbb44c Mon Sep 17 00:00:00 2001 From: DustyShoe Date: Fri, 4 Sep 2026 17:05:46 +0300 Subject: [PATCH 1/3] fix(ui): prevent disabled hotkeys from blocking shared keys --- .../registeredHotkeyOptions.test.ts | 33 +++++++++++++++++++ .../HotkeysModal/registeredHotkeyOptions.ts | 32 ++++++++++++++++++ .../components/HotkeysModal/useHotkeyData.ts | 20 +++-------- 3 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts create mode 100644 invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.ts diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts new file mode 100644 index 00000000000..e20832580d7 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts @@ -0,0 +1,33 @@ +import type { Options } from 'react-hotkeys-hook'; +import { describe, expect, it, vi } from 'vitest'; + +import { getRegisteredHotkeyOptions } from './registeredHotkeyOptions'; + +type EnabledPredicate = Exclude; + +const event = {} as Parameters[0]; +const hotkey = {} as Parameters[1]; + +describe('getRegisteredHotkeyOptions', () => { + it('keeps a statically disabled hotkey disabled so it does not intercept a shared key', () => { + const options: Options = { enabled: false, preventDefault: true }; + const isTextSessionActive = vi.fn(() => false); + + expect(getRegisteredHotkeyOptions(options, isTextSessionActive)).toBe(options); + }); + + it('suppresses an enabled hotkey during an uncommitted canvas text session', () => { + const result = getRegisteredHotkeyOptions({ enabled: true }, () => true); + + expect(typeof result.enabled).toBe('function'); + expect((result.enabled as EnabledPredicate)(event, hotkey)).toBe(false); + }); + + it('preserves a configured enabled predicate outside a canvas text session', () => { + const configuredEnabled = vi.fn(() => false); + const result = getRegisteredHotkeyOptions({ enabled: configuredEnabled }, () => false); + + expect((result.enabled as EnabledPredicate)(event, hotkey)).toBe(false); + expect(configuredEnabled).toHaveBeenCalledWith(event, hotkey); + }); +}); diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.ts new file mode 100644 index 00000000000..10a7a29ca82 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.ts @@ -0,0 +1,32 @@ +import type { Options } from 'react-hotkeys-hook'; + +/** + * Adds the canvas text-session guard without converting a statically disabled hotkey into a mounted listener. + * + * react-hotkeys-hook does not attach its listener when `enabled` is exactly `false`. If `false` is wrapped in a + * function instead, the listener remains attached and stops immediate propagation when the key matches. That can + * prevent another, enabled handler for the same key from running. + */ +export const getRegisteredHotkeyOptions = ( + options: Options, + isUncommittedCanvasTextSessionActive: () => boolean +): Options => { + if (options.enabled === false) { + return options; + } + + const configuredEnabled = options.enabled; + + return { + ...options, + enabled: (event, hotkeysEvent) => { + if (isUncommittedCanvasTextSessionActive()) { + return false; + } + if (typeof configuredEnabled === 'function') { + return configuredEnabled(event, hotkeysEvent); + } + return configuredEnabled ?? true; + }, + }; +}; diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts index c31bf418eba..9ede67fc056 100644 --- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts @@ -7,6 +7,7 @@ import { type HotkeyKeyboardLayoutMap, IS_MAC_OS, } from 'features/system/components/HotkeysModal/hotkeyStrings'; +import { getRegisteredHotkeyOptions } from 'features/system/components/HotkeysModal/registeredHotkeyOptions'; import { useKeyboardLayoutMap } from 'features/system/components/HotkeysModal/useKeyboardLayoutMap'; import { selectCustomHotkeys } from 'features/system/store/hotkeysSlice'; import { useMemo } from 'react'; @@ -276,21 +277,10 @@ export const useRegisteredHotkeys = ({ id, category, callback, options, dependen } satisfies Options; }, [data.isEnabled, options]); - const _optionsWithCanvasTextGuard = useMemo(() => { - return { - ..._options, - enabled: (event, hotkeysEvent) => { - // Suppress all registered hotkeys while text editing is still uncommitted. - if (isUncommittedCanvasTextSessionActive()) { - return false; - } - if (typeof _options.enabled === 'function') { - return _options.enabled(event, hotkeysEvent); - } - return _options.enabled ?? true; - }, - } satisfies Options; - }, [_options, isUncommittedCanvasTextSessionActive]); + const _optionsWithCanvasTextGuard = useMemo( + () => getRegisteredHotkeyOptions(_options, isUncommittedCanvasTextSessionActive), + [_options, isUncommittedCanvasTextSessionActive] + ); return useHotkeys(data.hotkeys, callback, _optionsWithCanvasTextGuard, dependencies); }; From 9eb484ce3e439ccb53bc9b6a5ebfd17f37afa738 Mon Sep 17 00:00:00 2001 From: DustyShoe Date: Sat, 5 Sep 2026 20:51:26 +0300 Subject: [PATCH 2/3] fix(ui): let I toggle metadata from focused gallery --- .../controlLayers/components/Tool/ToolColorPickerButton.tsx | 6 +++++- .../components/ImageViewer/ToggleMetadataViewerButton.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx index 9a2b01d0f74..118383293e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx @@ -1,4 +1,5 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { memo } from 'react'; @@ -9,12 +10,15 @@ export const ToolColorPickerButton = memo(() => { const { t } = useTranslation(); const isSelected = useToolIsSelected('colorPicker'); const selectColorPicker = useSelectTool('colorPicker'); + const isGalleryFocused = useIsRegionFocused('gallery'); + const isViewerFocused = useIsRegionFocused('viewer'); useRegisteredHotkeys({ id: 'selectColorPickerTool', category: 'canvas', callback: selectColorPicker, - dependencies: [selectColorPicker], + options: { enabled: !isGalleryFocused && !isViewerFocused }, + dependencies: [selectColorPicker, isGalleryFocused, isViewerFocused], }); return ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx index 021d69b0176..dbf4c684e16 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx @@ -24,7 +24,9 @@ export const ToggleMetadataViewerButton = memo(() => { const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails); const imageDTO = useAppSelector(selectLastSelectedItem); const { t } = useTranslation(); + const isGalleryFocused = useIsRegionFocused('gallery'); const isViewerFocused = useIsRegionFocused('viewer'); + const isMetadataHotkeyFocused = isGalleryFocused || isViewerFocused; const toggleMetadataViewer = useCallback(() => { dispatch(setShouldShowItemDetails(!shouldShowItemDetails)); @@ -34,8 +36,8 @@ export const ToggleMetadataViewerButton = memo(() => { id: 'toggleMetadata', category: 'viewer', callback: toggleMetadataViewer, - options: { enabled: isViewerFocused && !isDisabledOverride, preventDefault: true }, - dependencies: [imageDTO, shouldShowItemDetails, isViewerFocused, isDisabledOverride], + options: { enabled: isMetadataHotkeyFocused && !isDisabledOverride, preventDefault: true }, + dependencies: [imageDTO, shouldShowItemDetails, isMetadataHotkeyFocused, isDisabledOverride], }); return ( From c6b92a5e550677c118bccf1b4efe78619946d4bb Mon Sep 17 00:00:00 2001 From: DustyShoe Date: Mon, 7 Sep 2026 01:51:09 +0300 Subject: [PATCH 3/3] fix(ui): address I hotkey review feedback Route the shared I hotkey based on the active central panel. Register the metadata hotkey independently of the toolbar button. Add routing regression coverage and memoize hotkey options. --- .../components/Tool/ToolColorPickerButton.tsx | 12 ++--- .../ImageViewer/ImageViewerPanel.tsx | 17 ++++-- .../ToggleMetadataViewerButton.tsx | 15 ------ .../ImageViewer/useToggleMetadataHotkey.ts | 36 +++++++++++++ .../gallery/store/gallerySelectors.ts | 4 ++ .../registeredHotkeyOptions.test.ts | 9 +++- .../HotkeysModal/sharedHotkeyRouting.test.ts | 53 +++++++++++++++++++ .../HotkeysModal/sharedHotkeyRouting.ts | 19 +++++++ .../HotkeysModal/useSharedInfoHotkeyTarget.ts | 22 ++++++++ 9 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/useToggleMetadataHotkey.ts create mode 100644 invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.test.ts create mode 100644 invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.ts create mode 100644 invokeai/frontend/web/src/features/system/components/HotkeysModal/useSharedInfoHotkeyTarget.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx index 118383293e2..908a42c64d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx @@ -1,8 +1,8 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { useIsRegionFocused } from 'common/hooks/focus'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; -import { memo } from 'react'; +import { useSharedInfoHotkeyTarget } from 'features/system/components/HotkeysModal/useSharedInfoHotkeyTarget'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiEyedropperBold } from 'react-icons/pi'; @@ -10,15 +10,15 @@ export const ToolColorPickerButton = memo(() => { const { t } = useTranslation(); const isSelected = useToolIsSelected('colorPicker'); const selectColorPicker = useSelectTool('colorPicker'); - const isGalleryFocused = useIsRegionFocused('gallery'); - const isViewerFocused = useIsRegionFocused('viewer'); + const hotkeyTarget = useSharedInfoHotkeyTarget(); + const hotkeyOptions = useMemo(() => ({ enabled: hotkeyTarget === 'colorPicker' }), [hotkeyTarget]); useRegisteredHotkeys({ id: 'selectColorPickerTool', category: 'canvas', callback: selectColorPicker, - options: { enabled: !isGalleryFocused && !isViewerFocused }, - dependencies: [selectColorPicker, isGalleryFocused, isViewerFocused], + options: hotkeyOptions, + dependencies: [selectColorPicker, hotkeyTarget], }); return ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx index 9e829ea8dcf..11bafe9554d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx @@ -6,6 +6,7 @@ import { memo } from 'react'; import { ImageViewerContextProvider } from './context'; import { ImageComparison } from './ImageComparison'; import { ImageViewer } from './ImageViewer'; +import { useToggleMetadataHotkey } from './useToggleMetadataHotkey'; const selectIsComparing = createSelector( [selectLastSelectedItem, selectImageToCompare], @@ -13,18 +14,28 @@ const selectIsComparing = createSelector( ); export const ImageViewerPanel = memo(() => { + return ( + + + + ); +}); +ImageViewerPanel.displayName = 'ImageViewerPanel'; + +const ImageViewerPanelContent = memo(() => { const isComparing = useAppSelector(selectIsComparing); const lastSelectedItem = useAppSelector(selectLastSelectedItem); + useToggleMetadataHotkey(); return ( - + <> { // The image viewer renders progress images - if no image is selected, show the image viewer anyway !isComparing && !lastSelectedItem && } {!isComparing && } {isComparing && } - + ); }); -ImageViewerPanel.displayName = 'ImageViewerPanel'; +ImageViewerPanelContent.displayName = 'ImageViewerPanelContent'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx index dbf4c684e16..33b7bccc9cb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx @@ -1,9 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/focus'; -import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors'; -import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; import { setShouldShowItemDetails } from 'features/ui/store/uiSlice'; import { memo, useCallback } from 'react'; @@ -22,24 +19,12 @@ export const ToggleMetadataViewerButton = memo(() => { const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer && !isTemporarilyShowingSelectedImage; const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails); - const imageDTO = useAppSelector(selectLastSelectedItem); const { t } = useTranslation(); - const isGalleryFocused = useIsRegionFocused('gallery'); - const isViewerFocused = useIsRegionFocused('viewer'); - const isMetadataHotkeyFocused = isGalleryFocused || isViewerFocused; const toggleMetadataViewer = useCallback(() => { dispatch(setShouldShowItemDetails(!shouldShowItemDetails)); }, [dispatch, shouldShowItemDetails]); - useRegisteredHotkeys({ - id: 'toggleMetadata', - category: 'viewer', - callback: toggleMetadataViewer, - options: { enabled: isMetadataHotkeyFocused && !isDisabledOverride, preventDefault: true }, - dependencies: [imageDTO, shouldShowItemDetails, isMetadataHotkeyFocused, isDisabledOverride], - }); - return ( } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useToggleMetadataHotkey.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useToggleMetadataHotkey.ts new file mode 100644 index 00000000000..77fd972bf80 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useToggleMetadataHotkey.ts @@ -0,0 +1,36 @@ +import { useStore } from '@nanostores/react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useImageViewerContext } from 'features/gallery/components/ImageViewer/context'; +import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; +import { useSharedInfoHotkeyTarget } from 'features/system/components/HotkeysModal/useSharedInfoHotkeyTarget'; +import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; +import { setShouldShowItemDetails } from 'features/ui/store/uiSlice'; +import { useCallback, useMemo } from 'react'; + +export const useToggleMetadataHotkey = () => { + const dispatch = useAppDispatch(); + const ctx = useImageViewerContext(); + const hasProgressImage = useStore(ctx.$hasProgressImage); + const isTemporarilyShowingSelectedImage = useStore(ctx.$isTemporarilyShowingSelectedImage); + const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); + const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails); + + const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer && !isTemporarilyShowingSelectedImage; + const hotkeyTarget = useSharedInfoHotkeyTarget(); + + const toggleMetadataViewer = useCallback(() => { + dispatch(setShouldShowItemDetails(!shouldShowItemDetails)); + }, [dispatch, shouldShowItemDetails]); + const hotkeyOptions = useMemo( + () => ({ enabled: hotkeyTarget === 'metadata' && !isDisabledOverride, preventDefault: true }), + [hotkeyTarget, isDisabledOverride] + ); + + useRegisteredHotkeys({ + id: 'toggleMetadata', + category: 'viewer', + callback: toggleMetadataViewer, + options: hotkeyOptions, + dependencies: [toggleMetadataViewer, hotkeyTarget, isDisabledOverride], + }); +}; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 54c65c541ea..f42d88f947d 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -91,6 +91,10 @@ export const selectGalleryImageMinimumWidth = createSelector( export const selectComparisonMode = createSelector(selectGallerySlice, (gallery) => gallery.comparisonMode); export const selectComparisonFit = createSelector(selectGallerySlice, (gallery) => gallery.comparisonFit); export const selectImageToCompare = createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare); +export const selectHasMetadataViewerItem = createSelector( + [selectLastSelectedItem, selectImageToCompare], + (lastSelectedItem, imageToCompare) => Boolean(lastSelectedItem && !imageToCompare) +); export const selectAlwaysShouldImageSizeBadge = createSelector( selectGallerySlice, (gallery) => gallery.alwaysShowImageSizeBadge diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts index e20832580d7..9902737ad26 100644 --- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/registeredHotkeyOptions.test.ts @@ -13,7 +13,7 @@ describe('getRegisteredHotkeyOptions', () => { const options: Options = { enabled: false, preventDefault: true }; const isTextSessionActive = vi.fn(() => false); - expect(getRegisteredHotkeyOptions(options, isTextSessionActive)).toBe(options); + expect(getRegisteredHotkeyOptions(options, isTextSessionActive)).toEqual(options); }); it('suppresses an enabled hotkey during an uncommitted canvas text session', () => { @@ -23,6 +23,13 @@ describe('getRegisteredHotkeyOptions', () => { expect((result.enabled as EnabledPredicate)(event, hotkey)).toBe(false); }); + it('enables a hotkey with no configured enabled option outside a canvas text session', () => { + const result = getRegisteredHotkeyOptions({}, () => false); + + expect(typeof result.enabled).toBe('function'); + expect((result.enabled as EnabledPredicate)(event, hotkey)).toBe(true); + }); + it('preserves a configured enabled predicate outside a canvas text session', () => { const configuredEnabled = vi.fn(() => false); const result = getRegisteredHotkeyOptions({ enabled: configuredEnabled }, () => false); diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.test.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.test.ts new file mode 100644 index 00000000000..03884fd3670 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; + +import { getSharedInfoHotkeyTarget } from './sharedHotkeyRouting'; + +describe('getSharedInfoHotkeyTarget', () => { + it.each([ + { isGalleryFocused: true, isViewerFocused: false }, + { isGalleryFocused: false, isViewerFocused: true }, + ])('routes the shared hotkey to metadata when Gallery or Viewer owns focus', (focusedRegion) => { + expect( + getSharedInfoHotkeyTarget({ + hasMetadataViewerItem: true, + isViewerPanelActive: true, + ...focusedRegion, + }) + ).toBe('metadata'); + }); + + it('keeps the shared hotkey on the color picker when Gallery is next to the active Canvas', () => { + expect( + getSharedInfoHotkeyTarget({ + hasMetadataViewerItem: true, + isGalleryFocused: true, + isViewerPanelActive: false, + isViewerFocused: false, + }) + ).toBe('colorPicker'); + }); + + it('routes the shared hotkey to the color picker outside Gallery and Viewer', () => { + expect( + getSharedInfoHotkeyTarget({ + hasMetadataViewerItem: true, + isGalleryFocused: false, + isViewerPanelActive: true, + isViewerFocused: false, + }) + ).toBe('colorPicker'); + }); + + it.each([ + { isGalleryFocused: true, isViewerFocused: false }, + { isGalleryFocused: false, isViewerFocused: true }, + ])('falls back to the color picker when the focused media panel has no selected item', (focusedRegion) => { + expect( + getSharedInfoHotkeyTarget({ + hasMetadataViewerItem: false, + isViewerPanelActive: true, + ...focusedRegion, + }) + ).toBe('colorPicker'); + }); +}); diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.ts new file mode 100644 index 00000000000..454a7a8d4e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/sharedHotkeyRouting.ts @@ -0,0 +1,19 @@ +type SharedInfoHotkeyTarget = 'colorPicker' | 'metadata'; + +export const getSharedInfoHotkeyTarget = ({ + hasMetadataViewerItem, + isGalleryFocused, + isViewerPanelActive, + isViewerFocused, +}: { + hasMetadataViewerItem: boolean; + isGalleryFocused: boolean; + isViewerPanelActive: boolean; + isViewerFocused: boolean; +}): SharedInfoHotkeyTarget => { + if (hasMetadataViewerItem && isViewerPanelActive && (isGalleryFocused || isViewerFocused)) { + return 'metadata'; + } + + return 'colorPicker'; +}; diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useSharedInfoHotkeyTarget.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useSharedInfoHotkeyTarget.ts new file mode 100644 index 00000000000..499c161aa33 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useSharedInfoHotkeyTarget.ts @@ -0,0 +1,22 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { useIsRegionFocused } from 'common/hooks/focus'; +import { selectHasMetadataViewerItem } from 'features/gallery/store/gallerySelectors'; +import { getSharedInfoHotkeyTarget } from 'features/system/components/HotkeysModal/sharedHotkeyRouting'; +import { navigationApi } from 'features/ui/layouts/navigation-api'; +import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; + +export const useSharedInfoHotkeyTarget = () => { + const activeTab = useAppSelector(selectActiveTab); + const hasMetadataViewerItem = useAppSelector(selectHasMetadataViewerItem); + const isGalleryFocused = useIsRegionFocused('gallery'); + const isViewerFocused = useIsRegionFocused('viewer'); + const isViewerPanelActive = navigationApi.isDockviewPanelActive(activeTab, VIEWER_PANEL_ID); + + return getSharedInfoHotkeyTarget({ + hasMetadataViewerItem, + isGalleryFocused, + isViewerPanelActive, + isViewerFocused, + }); +};