mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 20:45:17 -05:00
refactor(ui): implement global panel registry, replace context-based panel API
This commit is contained in:
@@ -17,6 +17,7 @@ import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/w
|
||||
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
import { usePanelRegistryInit } from 'features/ui/layouts/panel-registry/use-panel-registry-init';
|
||||
import i18n from 'i18n';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
@@ -43,6 +44,7 @@ export const GlobalHookIsolator = memo(
|
||||
useGetOpenAPISchemaQuery();
|
||||
useSyncLoggingConfig();
|
||||
useCloseChakraTooltipsOnDragFix();
|
||||
usePanelRegistryInit();
|
||||
|
||||
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
|
||||
// and/or in progress canvas sessions.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -12,10 +13,10 @@ import { LaunchpadUseALayoutImageButton } from './LaunchpadUseALayoutImageButton
|
||||
|
||||
export const CanvasLaunchpadPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const focusCanvas = useCallback(() => {
|
||||
ctx.focusPanel(WORKSPACE_PANEL_ID);
|
||||
}, [ctx]);
|
||||
panelRegistry.focusPanelInTab(tab, WORKSPACE_PANEL_ID);
|
||||
}, [tab]);
|
||||
return (
|
||||
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" w="full" gap={4} px={14} maxW={768} pt="20vh">
|
||||
|
||||
@@ -21,10 +21,10 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
export const BoardsPanel = memo(() => {
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const searchDisclosure = useDisclosure(!!boardSearchText);
|
||||
const { _$rightPanelApi } = useAutoLayoutContext();
|
||||
const gridviewPanelApi = useStore(_$rightPanelApi);
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const collapsibleApi = useCollapsibleGridviewPanel(
|
||||
gridviewPanelApi,
|
||||
tab,
|
||||
'right',
|
||||
BOARDS_PANEL_ID,
|
||||
'vertical',
|
||||
BOARD_PANEL_DEFAULT_HEIGHT_PX,
|
||||
|
||||
@@ -32,10 +32,10 @@ const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery
|
||||
export const GalleryPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { _$rightPanelApi } = useAutoLayoutContext();
|
||||
const gridviewPanelApi = useStore(_$rightPanelApi);
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const collapsibleApi = useCollapsibleGridviewPanel(
|
||||
gridviewPanelApi,
|
||||
tab,
|
||||
'right',
|
||||
GALLERY_PANEL_ID,
|
||||
'vertical',
|
||||
GALLERY_PANEL_DEFAULT_HEIGHT_PX,
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
selectSelection,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { imageToCompareChanged, selectGallerySlice, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import type { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@@ -139,7 +139,6 @@ const buildOnClick =
|
||||
|
||||
export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
const store = useAppStore();
|
||||
const autoLayoutContext = useAutoLayoutContext();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragPreviewState, setDragPreviewState] = useState<
|
||||
DndDragPreviewSingleImageState | DndDragPreviewMultipleImageState | null
|
||||
@@ -239,8 +238,8 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
|
||||
const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => {
|
||||
store.dispatch(imageToCompareChanged(null));
|
||||
autoLayoutContext.focusPanel(VIEWER_PANEL_ID);
|
||||
}, [autoLayoutContext, store]);
|
||||
panelRegistry.focusPanelInActiveTab(VIEWER_PANEL_ID);
|
||||
}, [store]);
|
||||
|
||||
const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -14,14 +14,13 @@ type Props = {
|
||||
|
||||
export const GalleryImageOpenInViewerIconButton = memo(({ imageDTO }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { focusPanel } = useAutoLayoutContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO.image_name));
|
||||
focusPanel(VIEWER_PANEL_ID);
|
||||
}, [dispatch, focusPanel, imageDTO]);
|
||||
panelRegistry.focusPanelInActiveTab(VIEWER_PANEL_ID);
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<DndImageIcon
|
||||
|
||||
@@ -11,10 +11,9 @@ import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -45,7 +44,6 @@ export const CurrentImageButtons = memo(() => {
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const autoLayoutContext = useAutoLayoutContext();
|
||||
|
||||
const handleEdit = useCallback(async () => {
|
||||
if (!imageDTO) {
|
||||
@@ -59,14 +57,13 @@ export const CurrentImageButtons = memo(() => {
|
||||
getState,
|
||||
dispatch,
|
||||
});
|
||||
dispatch(setActiveTab('canvas'));
|
||||
autoLayoutContext?.focusPanel(WORKSPACE_PANEL_ID);
|
||||
panelRegistry.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [imageDTO, getState, dispatch, t, autoLayoutContext]);
|
||||
}, [imageDTO, getState, dispatch, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import type { MutableRefObject, RefObject } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -446,15 +445,13 @@ export const NewGallery = memo(() => {
|
||||
const rangeRef = useRef<ListRange>({ startIndex: 0, endIndex: 0 });
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isActiveTab } = useAutoLayoutContext();
|
||||
|
||||
// Get the ordered list of image names - this is our primary data source for virtualization
|
||||
const { queryArgs, imageNames, isLoading } = useGalleryImageNames();
|
||||
|
||||
// Use range-based fetching for bulk loading image DTOs into cache based on the visible range
|
||||
const { onRangeChanged } = useRangeBasedImageFetching({
|
||||
imageNames,
|
||||
enabled: !isLoading && isActiveTab,
|
||||
enabled: !isLoading,
|
||||
});
|
||||
|
||||
useKeepSelectedImageInView(imageNames, virtuosoRef, rootRef, rangeRef);
|
||||
@@ -482,7 +479,7 @@ export const NewGallery = memo(() => {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Flex height="100%" alignItems="center" justifyContent="center" gap={4}>
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center" gap={4}>
|
||||
<Spinner size="lg" opacity={0.3} />
|
||||
<Text color="base.300">Loading gallery...</Text>
|
||||
</Flex>
|
||||
@@ -491,7 +488,7 @@ export const NewGallery = memo(() => {
|
||||
|
||||
if (imageNames.length === 0) {
|
||||
return (
|
||||
<Flex height="100%" alignItems="center" justifyContent="center">
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text color="base.300">No images found</Text>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowMode, workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -13,7 +13,6 @@ export const WorkflowViewEditToggleButton = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
const { t } = useTranslation();
|
||||
const { focusPanel } = useAutoLayoutContext();
|
||||
|
||||
const onClickEdit = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
||||
(e) => {
|
||||
@@ -22,9 +21,9 @@ export const WorkflowViewEditToggleButton = memo(() => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
dispatch(workflowModeChanged('edit'));
|
||||
// Focus the Workflow Editor panel
|
||||
focusPanel(WORKSPACE_PANEL_ID);
|
||||
panelRegistry.focusPanelInTab('workflows', WORKSPACE_PANEL_ID);
|
||||
},
|
||||
[dispatch, focusPanel]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onClickView = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
||||
@@ -34,9 +33,9 @@ export const WorkflowViewEditToggleButton = memo(() => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
dispatch(workflowModeChanged('view'));
|
||||
// Focus the Image Viewer panel
|
||||
focusPanel(VIEWER_PANEL_ID);
|
||||
panelRegistry.focusPanelInTab('workflows', WORKSPACE_PANEL_ID);
|
||||
},
|
||||
[dispatch, focusPanel]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
if (mode === 'view') {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { withResultAsync } from 'common/util/result';
|
||||
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
|
||||
import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows';
|
||||
import { $isReadyToEnqueue } from 'features/queue/store/readiness';
|
||||
import { useAutoLayoutContextSafe } from 'features/ui/layouts/auto-layout-context';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback } from 'react';
|
||||
@@ -19,7 +19,6 @@ import { useEnqueueUpscaling } from './useEnqueueUpscaling';
|
||||
const log = logger('generation');
|
||||
|
||||
export const useInvoke = () => {
|
||||
const ctx = useAutoLayoutContextSafe();
|
||||
const tabName = useAppSelector(selectActiveTab);
|
||||
const isReady = useStore($isReadyToEnqueue);
|
||||
const isLocked = useIsWorkflowEditorLocked();
|
||||
@@ -64,20 +63,20 @@ export const useInvoke = () => {
|
||||
const enqueueBack = useCallback(() => {
|
||||
enqueue(false, false);
|
||||
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
|
||||
ctx?.focusPanel(VIEWER_PANEL_ID);
|
||||
panelRegistry.focusPanelInTab(tabName, VIEWER_PANEL_ID);
|
||||
} else if (tabName === 'canvas') {
|
||||
ctx?.focusPanel(WORKSPACE_PANEL_ID);
|
||||
panelRegistry.focusPanelInTab(tabName, WORKSPACE_PANEL_ID);
|
||||
}
|
||||
}, [ctx, enqueue, tabName]);
|
||||
}, [enqueue, tabName]);
|
||||
|
||||
const enqueueFront = useCallback(() => {
|
||||
enqueue(true, false);
|
||||
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
|
||||
ctx?.focusPanel(VIEWER_PANEL_ID);
|
||||
panelRegistry.focusPanelInTab(tabName, VIEWER_PANEL_ID);
|
||||
} else if (tabName === 'canvas') {
|
||||
ctx?.focusPanel(WORKSPACE_PANEL_ID);
|
||||
panelRegistry.focusPanelInTab(tabName, WORKSPACE_PANEL_ID);
|
||||
}
|
||||
}, [ctx, enqueue, tabName]);
|
||||
}, [enqueue, tabName]);
|
||||
|
||||
return { enqueueBack, enqueueFront, isLoading, isDisabled: !isReady || isLocked, enqueue };
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@ import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonToolt
|
||||
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { memo } from 'react';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiCircleNotchBold,
|
||||
@@ -52,13 +53,21 @@ export const FloatingCanvasLeftPanelButtons = memo(() => {
|
||||
FloatingCanvasLeftPanelButtons.displayName = 'FloatingCanvasLeftPanelButtons';
|
||||
|
||||
const ToggleLeftPanelButton = memo(() => {
|
||||
const { toggleLeftPanel } = useAutoLayoutContext();
|
||||
const { t } = useTranslation();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (panelRegistry.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
panelRegistry.toggleLeftPanelInTab(tab);
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleLeftPanel')}
|
||||
onClick={toggleLeftPanel}
|
||||
onClick={onClick}
|
||||
icon={<PiSlidersHorizontalBold />}
|
||||
flexGrow={1}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { memo } from 'react';
|
||||
import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesSquareBold } from 'react-icons/pi';
|
||||
|
||||
@@ -15,13 +16,20 @@ FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons';
|
||||
|
||||
const ToggleRightPanelButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { toggleRightPanel } = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (panelRegistry.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
panelRegistry.toggleLeftPanelInTab(tab);
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleRightPanel')}
|
||||
onClick={toggleRightPanel}
|
||||
onClick={onClick}
|
||||
icon={<PiImagesSquareBold />}
|
||||
h={48}
|
||||
/>
|
||||
|
||||
@@ -1,171 +1,248 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { FocusRegionName } from 'common/hooks/focus';
|
||||
import type { DockviewApi, GridviewApi, IDockviewPanelProps, IGridviewPanelProps } from 'dockview';
|
||||
import type { IDockviewPanelProps, IGridviewPanelProps } from 'dockview';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import type { WritableAtom } from 'nanostores';
|
||||
import { atom } from 'nanostores';
|
||||
import type { FunctionComponent, PropsWithChildren, RefObject } from 'react';
|
||||
import { createContext, memo, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { createContext, memo, useContext, useMemo } from 'react';
|
||||
|
||||
import { AutoLayoutPanelContainer } from './AutoLayoutPanelContainer';
|
||||
import { LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX } from './shared';
|
||||
import { panelRegistry } from './panel-registry/panelApiRegistry';
|
||||
|
||||
type AutoLayoutContextValue = {
|
||||
tab: TabName;
|
||||
isActiveTab: boolean;
|
||||
toggleLeftPanel: () => void;
|
||||
toggleRightPanel: () => void;
|
||||
toggleBothPanels: () => void;
|
||||
resetPanels: () => void;
|
||||
focusPanel: (id: string) => void;
|
||||
// isActiveTab: boolean;
|
||||
// toggleLeftPanel: () => void;
|
||||
// toggleRightPanel: () => void;
|
||||
// toggleBothPanels: () => void;
|
||||
// resetPanels: () => void;
|
||||
// focusPanel: (id: string) => void;
|
||||
rootRef: RefObject<HTMLDivElement>;
|
||||
_$rootPanelApi: WritableAtom<GridviewApi | null>;
|
||||
_$leftPanelApi: WritableAtom<GridviewApi | null>;
|
||||
_$centerPanelApi: WritableAtom<DockviewApi | null>;
|
||||
_$rightPanelApi: WritableAtom<GridviewApi | null>;
|
||||
// _$rootPanelApi: WritableAtom<GridviewApi | null>;
|
||||
// _$leftPanelApi: WritableAtom<GridviewApi | null>;
|
||||
// _$centerPanelApi: WritableAtom<DockviewApi | null>;
|
||||
// _$rightPanelApi: WritableAtom<GridviewApi | null>;
|
||||
// // Global registry access for cross-tab operations
|
||||
// registry: typeof panelApiRegistry;
|
||||
};
|
||||
|
||||
const AutoLayoutContext = createContext<AutoLayoutContextValue | null>(null);
|
||||
|
||||
const expandPanel = (api: GridviewApi, panelId: string, width: number) => {
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width });
|
||||
panel.api.setSize({ width: width });
|
||||
};
|
||||
// const expandPanel = (api: GridviewApi, panelId: string, width: number) => {
|
||||
// const panel = api.getPanel(panelId);
|
||||
// if (!panel) {
|
||||
// return;
|
||||
// }
|
||||
// panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width });
|
||||
// panel.api.setSize({ width: width });
|
||||
// };
|
||||
|
||||
const collapsePanel = (api: GridviewApi, panelId: string) => {
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
|
||||
panel.api.setSize({ width: 0 });
|
||||
};
|
||||
// const collapsePanel = (api: GridviewApi, panelId: string) => {
|
||||
// const panel = api.getPanel(panelId);
|
||||
// if (!panel) {
|
||||
// return;
|
||||
// }
|
||||
// panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
|
||||
// panel.api.setSize({ width: 0 });
|
||||
// };
|
||||
|
||||
const getIsCollapsed = (api: GridviewApi, panelId: string) => {
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
return true; // ??
|
||||
}
|
||||
return panel.maximumWidth === 0;
|
||||
};
|
||||
// const getIsCollapsed = (api: GridviewApi, panelId: string) => {
|
||||
// const panel = api.getPanel(panelId);
|
||||
// if (!panel) {
|
||||
// return true; // ??
|
||||
// }
|
||||
// return panel.maximumWidth === 0;
|
||||
// };
|
||||
|
||||
const activatePanel = (api: GridviewApi | DockviewApi, panelId: string) => {
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
panel.api.setActive();
|
||||
};
|
||||
// const activatePanel = (api: GridviewApi | DockviewApi, panelId: string) => {
|
||||
// const panel = api.getPanel(panelId);
|
||||
// if (!panel) {
|
||||
// return;
|
||||
// }
|
||||
// panel.api.setActive();
|
||||
// };
|
||||
|
||||
export const AutoLayoutProvider = (
|
||||
props: PropsWithChildren<{
|
||||
$rootApi: WritableAtom<GridviewApi | null>;
|
||||
// $rootApi: WritableAtom<GridviewApi | null>;
|
||||
rootRef: RefObject<HTMLDivElement>;
|
||||
tab: TabName;
|
||||
}>
|
||||
) => {
|
||||
const { $rootApi, rootRef, tab, children } = props;
|
||||
const selectIsActiveTab = useMemo(() => createSelector(selectActiveTab, (activeTab) => activeTab === tab), [tab]);
|
||||
const isActiveTab = useAppSelector(selectIsActiveTab);
|
||||
const $leftApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
const $centerApi = useState(() => atom<DockviewApi | null>(null))[0];
|
||||
const $rightApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
const { tab, rootRef, children } = props;
|
||||
// const { $rootApi, rootRef, tab, children } = props;
|
||||
// const selectIsActiveTab = useMemo(() => createSelector(selectActiveTab, (activeTab) => activeTab === tab), [tab]);
|
||||
// const isActiveTab = useAppSelector(selectIsActiveTab);
|
||||
// const $leftApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
// const $centerApi = useState(() => atom<DockviewApi | null>(null))[0];
|
||||
// const $rightApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
|
||||
const toggleLeftPanel = useCallback(() => {
|
||||
const api = $rootApi.get();
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
if (getIsCollapsed(api, LEFT_PANEL_ID)) {
|
||||
expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
collapsePanel(api, LEFT_PANEL_ID);
|
||||
}
|
||||
}, [$rootApi]);
|
||||
// // Register this tab with the global panel registry when APIs are available
|
||||
// useEffect(() => {
|
||||
// const rootApi = $rootApi.get();
|
||||
// const leftApi = $leftApi.get();
|
||||
// const centerApi = $centerApi.get();
|
||||
// const rightApi = $rightApi.get();
|
||||
|
||||
const toggleRightPanel = useCallback(() => {
|
||||
const api = $rootApi.get();
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
if (getIsCollapsed(api, RIGHT_PANEL_ID)) {
|
||||
expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
collapsePanel(api, RIGHT_PANEL_ID);
|
||||
}
|
||||
}, [$rootApi]);
|
||||
// if (rootApi) {
|
||||
// panelApiRegistry.registerTab(tab, {
|
||||
// root: rootApi,
|
||||
// left: leftApi,
|
||||
// center: centerApi,
|
||||
// right: rightApi,
|
||||
// });
|
||||
|
||||
const toggleBothPanels = useCallback(() => {
|
||||
const api = $rootApi.get();
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
if (getIsCollapsed(api, RIGHT_PANEL_ID) || getIsCollapsed(api, LEFT_PANEL_ID)) {
|
||||
expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
|
||||
expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
collapsePanel(api, LEFT_PANEL_ID);
|
||||
collapsePanel(api, RIGHT_PANEL_ID);
|
||||
}
|
||||
});
|
||||
}, [$rootApi]);
|
||||
// return () => {
|
||||
// panelApiRegistry.unregisterTab(tab);
|
||||
// };
|
||||
// }
|
||||
// }, [tab, $rootApi, $leftApi, $centerApi, $rightApi]);
|
||||
|
||||
const resetPanels = useCallback(() => {
|
||||
const api = $rootApi.get();
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
|
||||
expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
}, [$rootApi]);
|
||||
// // Subscribe to API changes and update registry
|
||||
// useEffect(() => {
|
||||
// const unsubscribeRoot = $rootApi.subscribe((rootApi) => {
|
||||
// if (rootApi) {
|
||||
// panelApiRegistry.registerTab(tab, {
|
||||
// root: rootApi,
|
||||
// left: $leftApi.get(),
|
||||
// center: $centerApi.get(),
|
||||
// right: $rightApi.get(),
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
const focusPanel = useCallback(
|
||||
(id: string) => {
|
||||
const api = $centerApi.get();
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
activatePanel(api, id);
|
||||
},
|
||||
[$centerApi]
|
||||
);
|
||||
// const unsubscribeLeft = $leftApi.subscribe((leftApi) => {
|
||||
// const rootApi = $rootApi.get();
|
||||
// if (rootApi) {
|
||||
// panelApiRegistry.registerTab(tab, {
|
||||
// root: rootApi,
|
||||
// left: leftApi,
|
||||
// center: $centerApi.get(),
|
||||
// right: $rightApi.get(),
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// const unsubscribeCenter = $centerApi.subscribe((centerApi) => {
|
||||
// const rootApi = $rootApi.get();
|
||||
// if (rootApi) {
|
||||
// panelApiRegistry.registerTab(tab, {
|
||||
// root: rootApi,
|
||||
// left: $leftApi.get(),
|
||||
// center: centerApi,
|
||||
// right: $rightApi.get(),
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// const unsubscribeRight = $rightApi.subscribe((rightApi) => {
|
||||
// const rootApi = $rootApi.get();
|
||||
// if (rootApi) {
|
||||
// panelApiRegistry.registerTab(tab, {
|
||||
// root: rootApi,
|
||||
// left: $leftApi.get(),
|
||||
// center: $centerApi.get(),
|
||||
// right: rightApi,
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// return () => {
|
||||
// unsubscribeRoot();
|
||||
// unsubscribeLeft();
|
||||
// unsubscribeCenter();
|
||||
// unsubscribeRight();
|
||||
// };
|
||||
// }, [tab, $rootApi, $leftApi, $centerApi, $rightApi]);
|
||||
|
||||
// const toggleLeftPanel = useCallback(() => {
|
||||
// const api = $rootApi.get();
|
||||
// if (!api) {
|
||||
// return;
|
||||
// }
|
||||
// if (getIsCollapsed(api, LEFT_PANEL_ID)) {
|
||||
// expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
|
||||
// } else {
|
||||
// collapsePanel(api, LEFT_PANEL_ID);
|
||||
// }
|
||||
// }, [$rootApi]);
|
||||
|
||||
// const toggleRightPanel = useCallback(() => {
|
||||
// const api = $rootApi.get();
|
||||
// if (!api) {
|
||||
// return;
|
||||
// }
|
||||
// if (getIsCollapsed(api, RIGHT_PANEL_ID)) {
|
||||
// expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
// } else {
|
||||
// collapsePanel(api, RIGHT_PANEL_ID);
|
||||
// }
|
||||
// }, [$rootApi]);
|
||||
|
||||
// const toggleBothPanels = useCallback(() => {
|
||||
// const api = $rootApi.get();
|
||||
// if (!api) {
|
||||
// return;
|
||||
// }
|
||||
// requestAnimationFrame(() => {
|
||||
// if (getIsCollapsed(api, RIGHT_PANEL_ID) || getIsCollapsed(api, LEFT_PANEL_ID)) {
|
||||
// expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
|
||||
// expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
// } else {
|
||||
// collapsePanel(api, LEFT_PANEL_ID);
|
||||
// collapsePanel(api, RIGHT_PANEL_ID);
|
||||
// }
|
||||
// });
|
||||
// }, [$rootApi]);
|
||||
|
||||
// const resetPanels = useCallback(() => {
|
||||
// const api = $rootApi.get();
|
||||
// if (!api) {
|
||||
// return;
|
||||
// }
|
||||
// expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX);
|
||||
// expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
// }, [$rootApi]);
|
||||
|
||||
// const focusPanel = useCallback(
|
||||
// (id: string) => {
|
||||
// const api = $centerApi.get();
|
||||
// if (!api) {
|
||||
// return;
|
||||
// }
|
||||
// activatePanel(api, id);
|
||||
// },
|
||||
// [$centerApi]
|
||||
// );
|
||||
|
||||
const value = useMemo<AutoLayoutContextValue>(
|
||||
() => ({
|
||||
tab,
|
||||
isActiveTab,
|
||||
toggleLeftPanel,
|
||||
toggleRightPanel,
|
||||
toggleBothPanels,
|
||||
resetPanels,
|
||||
focusPanel,
|
||||
// isActiveTab,
|
||||
// toggleLeftPanel,
|
||||
// toggleRightPanel,
|
||||
// toggleBothPanels,
|
||||
// resetPanels,
|
||||
// focusPanel,
|
||||
rootRef,
|
||||
_$rootPanelApi: $rootApi,
|
||||
_$leftPanelApi: $leftApi,
|
||||
_$centerPanelApi: $centerApi,
|
||||
_$rightPanelApi: $rightApi,
|
||||
// _$rootPanelApi: $rootApi,
|
||||
// _$leftPanelApi: $leftApi,
|
||||
// _$centerPanelApi: $centerApi,
|
||||
// _$rightPanelApi: $rightApi,
|
||||
// registry: panelApiRegistry,
|
||||
}),
|
||||
[
|
||||
tab,
|
||||
isActiveTab,
|
||||
$centerApi,
|
||||
$leftApi,
|
||||
$rightApi,
|
||||
$rootApi,
|
||||
focusPanel,
|
||||
resetPanels,
|
||||
// isActiveTab,
|
||||
// $centerApi,
|
||||
// $leftApi,
|
||||
// $rightApi,
|
||||
// $rootApi,
|
||||
// focusPanel,
|
||||
// resetPanels,
|
||||
rootRef,
|
||||
toggleBothPanels,
|
||||
toggleLeftPanel,
|
||||
toggleRightPanel,
|
||||
// toggleBothPanels,
|
||||
// toggleLeftPanel,
|
||||
// toggleRightPanel,
|
||||
]
|
||||
);
|
||||
return <AutoLayoutContext.Provider value={value}>{children}</AutoLayoutContext.Provider>;
|
||||
@@ -185,26 +262,51 @@ export const useAutoLayoutContextSafe = () => {
|
||||
};
|
||||
|
||||
export const PanelHotkeysLogical = memo(() => {
|
||||
const { toggleBothPanels, resetPanels, toggleLeftPanel, toggleRightPanel } = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'toggleLeftPanel',
|
||||
callback: toggleLeftPanel,
|
||||
callback: () => {
|
||||
if (panelRegistry.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
panelRegistry.toggleLeftPanelInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'toggleRightPanel',
|
||||
callback: toggleRightPanel,
|
||||
callback: () => {
|
||||
if (panelRegistry.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
panelRegistry.toggleRightPanelInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'resetPanelLayout',
|
||||
callback: resetPanels,
|
||||
callback: () => {
|
||||
if (panelRegistry.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
panelRegistry.resetPanelsInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'togglePanels',
|
||||
callback: toggleBothPanels,
|
||||
callback: () => {
|
||||
if (panelRegistry.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
panelRegistry.toggleBothPanelsInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
});
|
||||
|
||||
return null;
|
||||
@@ -212,6 +314,7 @@ export const PanelHotkeysLogical = memo(() => {
|
||||
PanelHotkeysLogical.displayName = 'PanelHotkeysLogical';
|
||||
|
||||
export type PanelParameters = {
|
||||
tab: TabName;
|
||||
focusRegion: FocusRegionName;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,12 +28,13 @@ import {
|
||||
withPanelContainer,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { dockviewTheme } from 'features/ui/styles/theme';
|
||||
import { atom } from 'nanostores';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { CanvasTabLeftPanel } from './CanvasTabLeftPanel';
|
||||
import { CanvasWorkspacePanel } from './CanvasWorkspacePanel';
|
||||
import { panelRegistry } from './panel-registry/panelApiRegistry';
|
||||
import {
|
||||
BOARD_PANEL_DEFAULT_HEIGHT_PX,
|
||||
BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -66,66 +67,69 @@ const tabComponents = {
|
||||
[TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
|
||||
};
|
||||
|
||||
const centerPanelComponents: AutoLayoutDockviewComponents = {
|
||||
const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
[LAUNCHPAD_PANEL_ID]: withPanelContainer(CanvasLaunchpadPanel),
|
||||
[WORKSPACE_PANEL_ID]: withPanelContainer(CanvasWorkspacePanel),
|
||||
[VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel),
|
||||
[PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel),
|
||||
};
|
||||
|
||||
const initializeCenterPanelLayout = (api: DockviewApi) => {
|
||||
const launchpadPanel = api.addPanel<PanelParameters>({
|
||||
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const workspacePanel = api.addPanel<PanelParameters>({
|
||||
const workspace = api.addPanel<PanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: 'Canvas',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'canvas',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpadPanel.id,
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
const viewerPanel = api.addPanel<PanelParameters>({
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpadPanel.id,
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
return { launchpadPanel, workspacePanel, viewerPanel } satisfies Record<string, IDockviewPanel>;
|
||||
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const CenterPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
const panels = initializeCenterPanelLayout(event.api);
|
||||
ctx._$centerPanelApi.set(event.api);
|
||||
const MainPanel = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
panels.launchpadPanel.api.setActive();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
const panels = initializeCenterPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'main', api);
|
||||
panels.launchpad.api.setActive();
|
||||
|
||||
const disposables = [
|
||||
event.api.onWillShowOverlay((e) => {
|
||||
api.onWillShowOverlay((e) => {
|
||||
if (e.kind === 'header_space' || e.kind === 'tab') {
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +143,7 @@ const CenterPanel = memo(() => {
|
||||
});
|
||||
};
|
||||
},
|
||||
[ctx._$centerPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -148,7 +152,7 @@ const CenterPanel = memo(() => {
|
||||
locked={true}
|
||||
disableFloatingGroups={true}
|
||||
dndEdges={false}
|
||||
components={centerPanelComponents}
|
||||
components={mainPanelComponents}
|
||||
onReady={onReady}
|
||||
theme={dockviewTheme}
|
||||
tabComponents={tabComponents}
|
||||
@@ -159,7 +163,7 @@ const CenterPanel = memo(() => {
|
||||
</>
|
||||
);
|
||||
});
|
||||
CenterPanel.displayName = 'CenterPanel';
|
||||
MainPanel.displayName = 'MainPanel';
|
||||
|
||||
const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel),
|
||||
@@ -167,55 +171,59 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[LAYERS_PANEL_ID]: withPanelContainer(CanvasLayersPanel),
|
||||
};
|
||||
|
||||
export const initializeRightPanelLayout = (api: GridviewApi) => {
|
||||
const galleryPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const layersPanel = api.addPanel<PanelParameters>({
|
||||
const layers = api.addPanel<PanelParameters>({
|
||||
id: LAYERS_PANEL_ID,
|
||||
component: LAYERS_PANEL_ID,
|
||||
minimumHeight: LAYERS_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'layers',
|
||||
},
|
||||
position: {
|
||||
direction: 'below',
|
||||
referencePanel: galleryPanel.id,
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
const boardsPanel = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: galleryPanel.id,
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
return { galleryPanel, layersPanel, boardsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
return { gallery, layers, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeRightPanelLayout(event.api);
|
||||
ctx._$rightPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[ctx._$rightPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<GridviewReact
|
||||
@@ -232,26 +240,28 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[SETTINGS_PANEL_ID]: withPanelContainer(CanvasTabLeftPanel),
|
||||
};
|
||||
|
||||
export const initializeLeftPanelLayout = (api: GridviewApi) => {
|
||||
const settingsPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeLeftPanelLayout(event.api);
|
||||
ctx._$leftPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[ctx._$leftPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<GridviewReact
|
||||
@@ -266,58 +276,57 @@ LeftPanel.displayName = 'LeftPanel';
|
||||
|
||||
export const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[LEFT_PANEL_ID]: LeftPanel,
|
||||
[MAIN_PANEL_ID]: CenterPanel,
|
||||
[MAIN_PANEL_ID]: MainPanel,
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
const mainPanel = api.addPanel({
|
||||
const main = api.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const leftPanel = api.addPanel({
|
||||
const left = api.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: mainPanel.id,
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const rightPanel = api.addPanel({
|
||||
const right = api.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: mainPanel.id,
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return { mainPanel, leftPanel, rightPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const CanvasTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
$rootPanelApi.set(event.api);
|
||||
const { mainPanel } = initializeRootPanelLayout(event.api);
|
||||
mainPanel.api.setActive();
|
||||
},
|
||||
[$rootPanelApi]
|
||||
);
|
||||
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
const { main } = initializeRootPanelLayout(api);
|
||||
panelRegistry.registerPanel('canvas', 'root', api);
|
||||
main.api.setActive();
|
||||
setRootApi(api);
|
||||
}, []);
|
||||
|
||||
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="canvas">
|
||||
<AutoLayoutProvider tab="canvas" rootRef={rootRef}>
|
||||
<GridviewReact
|
||||
ref={rootRef}
|
||||
className="dockview-theme-invoke"
|
||||
|
||||
@@ -27,11 +27,12 @@ import {
|
||||
withPanelContainer,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { dockviewTheme } from 'features/ui/styles/theme';
|
||||
import { atom } from 'nanostores';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { GenerateTabLeftPanel } from './GenerateTabLeftPanel';
|
||||
import { panelRegistry } from './panel-registry/panelApiRegistry';
|
||||
import {
|
||||
BOARD_PANEL_DEFAULT_HEIGHT_PX,
|
||||
BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -61,51 +62,53 @@ const tabComponents = {
|
||||
[TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
|
||||
};
|
||||
|
||||
const centerPanelComponents: AutoLayoutDockviewComponents = {
|
||||
const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
[LAUNCHPAD_PANEL_ID]: withPanelContainer(GenerateLaunchpadPanel),
|
||||
[VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel),
|
||||
[PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel),
|
||||
};
|
||||
|
||||
const initializeCenterPanelLayout = (api: DockviewApi) => {
|
||||
const launchpadPanel = api.addPanel<PanelParameters>({
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const viewerPanel = api.addPanel<PanelParameters>({
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpadPanel.id,
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
return { launchpadPanel, viewerPanel } satisfies Record<string, IDockviewPanel>;
|
||||
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const CenterPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
const panels = initializeCenterPanelLayout(event.api);
|
||||
ctx._$centerPanelApi.set(event.api);
|
||||
const MainPanel = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
panels.launchpadPanel.api.setActive();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
const { launchpad } = initializeMainPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'main', api);
|
||||
launchpad.api.setActive();
|
||||
|
||||
const disposables = [
|
||||
event.api.onWillShowOverlay((e) => {
|
||||
api.onWillShowOverlay((e) => {
|
||||
if (e.kind === 'header_space' || e.kind === 'tab') {
|
||||
return;
|
||||
}
|
||||
@@ -119,7 +122,7 @@ const CenterPanel = memo(() => {
|
||||
});
|
||||
};
|
||||
},
|
||||
[ctx._$centerPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -129,7 +132,7 @@ const CenterPanel = memo(() => {
|
||||
disableFloatingGroups={true}
|
||||
dndEdges={false}
|
||||
tabComponents={tabComponents}
|
||||
components={centerPanelComponents}
|
||||
components={mainPanelComponents}
|
||||
onReady={onReady}
|
||||
theme={dockviewTheme}
|
||||
/>
|
||||
@@ -139,50 +142,53 @@ const CenterPanel = memo(() => {
|
||||
</>
|
||||
);
|
||||
});
|
||||
CenterPanel.displayName = 'CenterPanel';
|
||||
MainPanel.displayName = 'MainPanel';
|
||||
|
||||
const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel),
|
||||
[GALLERY_PANEL_ID]: withPanelContainer(GalleryPanel),
|
||||
};
|
||||
|
||||
export const initializeRightPanelLayout = (api: GridviewApi) => {
|
||||
const galleryPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boardsPanel = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: galleryPanel.id,
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return { galleryPanel, boardsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeRightPanelLayout(event.api);
|
||||
ctx._$rightPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[ctx._$rightPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<GridviewReact
|
||||
@@ -199,26 +205,28 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[SETTINGS_PANEL_ID]: withPanelContainer(GenerateTabLeftPanel),
|
||||
};
|
||||
|
||||
export const initializeLeftPanelLayout = (api: GridviewApi) => {
|
||||
const settingsPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeLeftPanelLayout(event.api);
|
||||
ctx._$leftPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[ctx._$leftPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<GridviewReact
|
||||
@@ -233,58 +241,56 @@ LeftPanel.displayName = 'LeftPanel';
|
||||
|
||||
export const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[LEFT_PANEL_ID]: LeftPanel,
|
||||
[MAIN_PANEL_ID]: CenterPanel,
|
||||
[MAIN_PANEL_ID]: MainPanel,
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
const mainPanel = layoutApi.addPanel<PanelParameters>({
|
||||
const main = layoutApi.addPanel<PanelParameters>({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const leftPanel = layoutApi.addPanel<PanelParameters>({
|
||||
const left = layoutApi.addPanel<PanelParameters>({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: mainPanel.id,
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const rightPanel = layoutApi.addPanel<PanelParameters>({
|
||||
const right = layoutApi.addPanel<PanelParameters>({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: mainPanel.id,
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return { mainPanel, leftPanel, rightPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const GenerateTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
$rootPanelApi.set(event.api);
|
||||
const { mainPanel } = initializeRootPanelLayout(event.api);
|
||||
mainPanel.api.setActive();
|
||||
},
|
||||
[$rootPanelApi]
|
||||
);
|
||||
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
const { main } = initializeRootPanelLayout(api);
|
||||
panelRegistry.registerPanel('generate', 'root', api);
|
||||
main.api.setActive();
|
||||
setRootApi(api);
|
||||
}, []);
|
||||
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="generate">
|
||||
<AutoLayoutProvider tab="generate" rootRef={rootRef}>
|
||||
<GridviewReact
|
||||
ref={rootRef}
|
||||
className="dockview-theme-invoke"
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { DockviewApi, GridviewApi, IGridviewPanel } from 'dockview';
|
||||
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/layouts/shared';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
export type TabPanelApis = {
|
||||
root: Readonly<GridviewApi> | null;
|
||||
left: Readonly<GridviewApi> | null;
|
||||
main: Readonly<DockviewApi> | null;
|
||||
right: Readonly<GridviewApi> | null;
|
||||
};
|
||||
|
||||
const getInitialTabPanelApis = (): TabPanelApis => ({
|
||||
root: null,
|
||||
left: null,
|
||||
main: null,
|
||||
right: null,
|
||||
});
|
||||
|
||||
type AppTabApi = {
|
||||
setTab: (tabName: TabName) => void;
|
||||
getTab: () => TabName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global registry for all panel APIs across all tabs
|
||||
*/
|
||||
export class PanelRegistry {
|
||||
private tabPanelApis = new Map<TabName, TabPanelApis>();
|
||||
|
||||
tabApi: AppTabApi | null = null;
|
||||
|
||||
/**
|
||||
* Set the Redux store reference for tab switching
|
||||
*/
|
||||
setTabApi(tabApi: AppTabApi): void {
|
||||
this.tabApi = tabApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register panel APIs for a specific tab
|
||||
*/
|
||||
registerPanel<T extends keyof TabPanelApis>(tab: TabName, panel: T, api: NonNullable<TabPanelApis[T]>): void {
|
||||
const current = this.tabPanelApis.get(tab) ?? getInitialTabPanelApis();
|
||||
const apis: TabPanelApis = {
|
||||
...current,
|
||||
[panel]: api,
|
||||
};
|
||||
this.tabPanelApis.set(tab, apis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister panel APIs for a specific tab
|
||||
*/
|
||||
unregisterPanel<T extends keyof TabPanelApis>(tab: TabName, panel: T): void {
|
||||
const current = this.tabPanelApis.get(tab);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
const apis: TabPanelApis = {
|
||||
...current,
|
||||
[panel]: null,
|
||||
};
|
||||
this.tabPanelApis.set(tab, apis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister panel APIs for a specific tab
|
||||
*/
|
||||
unregisterTab(tabName: TabName): void {
|
||||
this.tabPanelApis.delete(tabName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get panel APIs for a specific tab
|
||||
*/
|
||||
getTabPanelApis(tabName: TabName): TabPanelApis | null {
|
||||
return this.tabPanelApis.get(tabName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get panel APIs for the currently active tab
|
||||
*/
|
||||
getActiveTabPanelApis(): TabPanelApis | null {
|
||||
if (!this.tabApi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.getTabPanelApis(activeTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tab names
|
||||
*/
|
||||
getRegisteredTabs(): TabName[] {
|
||||
return Array.from(this.tabPanelApis.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tab is registered
|
||||
*/
|
||||
isTabRegistered(tabName: TabName): boolean {
|
||||
return this.tabPanelApis.has(tabName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific tab
|
||||
*/
|
||||
private switchToTab(tabName: TabName): boolean {
|
||||
if (!this.tabApi) {
|
||||
log.warn(`Cannot switch to tab "${tabName}": no store reference`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.tabApi.setTab(tabName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus a specific panel in a specific tab
|
||||
* Automatically switches to the target tab if specified
|
||||
*/
|
||||
focusPanelInTab(tabName: TabName, panelId: string): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis) {
|
||||
log.warn(`Tab "${tabName}" not registered`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Switch to target tab first
|
||||
if (!this.switchToTab(tabName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to focus in main panel (dockview) first
|
||||
if (apis.main) {
|
||||
const panel = apis.main.getPanel(panelId);
|
||||
if (panel) {
|
||||
panel.api.setActive();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Try left panel
|
||||
if (apis.left) {
|
||||
const panel = apis.left.getPanel(panelId);
|
||||
if (panel) {
|
||||
panel.api.setActive();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Try right panel
|
||||
if (apis.right) {
|
||||
const panel = apis.right.getPanel(panelId);
|
||||
if (panel) {
|
||||
panel.api.setActive();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn(`Panel "${panelId}" not found in tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus a panel in the currently active tab
|
||||
*/
|
||||
focusPanelInActiveTab(panelId: string): boolean {
|
||||
if (!this.tabApi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.focusPanelInTab(activeTab, panelId);
|
||||
}
|
||||
|
||||
expandPanel(panel: IGridviewPanel, width: number) {
|
||||
panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width });
|
||||
panel.api.setSize({ width: width });
|
||||
}
|
||||
|
||||
collapsePanel(panel: IGridviewPanel) {
|
||||
panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
|
||||
panel.api.setSize({ width: 0 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the left panel in a specific tab
|
||||
*/
|
||||
toggleLeftPanelInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switchToTab(tabName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const leftPanel = apis.root.getPanel('left');
|
||||
if (!leftPanel) {
|
||||
log.warn(`Left panel not found in tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isCollapsed = leftPanel.maximumWidth === 0;
|
||||
if (isCollapsed) {
|
||||
this.expandPanel(leftPanel, LEFT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
this.collapsePanel(leftPanel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the right panel in a specific tab
|
||||
*/
|
||||
toggleRightPanelInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switchToTab(tabName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rightPanel = apis.root.getPanel('right');
|
||||
if (!rightPanel) {
|
||||
log.warn(`Right panel not found in tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isCollapsed = rightPanel.maximumWidth === 0;
|
||||
if (isCollapsed) {
|
||||
this.expandPanel(rightPanel, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
this.collapsePanel(rightPanel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toggleBothPanelsInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const rightPanel = apis.root.getPanel('right');
|
||||
const leftPanel = apis.root.getPanel('left');
|
||||
if (!rightPanel || !leftPanel) {
|
||||
log.warn(`Right and/or left panel not found in tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLeftCollapsed = leftPanel.maximumWidth === 0;
|
||||
const isRightCollapsed = rightPanel.maximumWidth === 0;
|
||||
|
||||
if (isLeftCollapsed || isRightCollapsed) {
|
||||
this.expandPanel(leftPanel, LEFT_PANEL_MIN_SIZE_PX);
|
||||
this.expandPanel(rightPanel, RIGHT_PANEL_MIN_SIZE_PX);
|
||||
} else {
|
||||
this.collapsePanel(leftPanel);
|
||||
this.collapsePanel(rightPanel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the left panel in the currently active tab
|
||||
*/
|
||||
toggleLeftPanelInActiveTab(): boolean {
|
||||
if (!this.tabApi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.toggleLeftPanelInTab(activeTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the right panel in the currently active tab
|
||||
*/
|
||||
toggleRightPanelInActiveTab(): boolean {
|
||||
if (!this.tabApi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.toggleRightPanelInTab(activeTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset panels in a specific tab (expand both left and right)
|
||||
*/
|
||||
resetPanelsInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switchToTab(tabName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootApi = apis.root as GridviewApi;
|
||||
const leftPanel = rootApi.getPanel('left');
|
||||
const rightPanel = rootApi.getPanel('right');
|
||||
|
||||
if (leftPanel) {
|
||||
leftPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX });
|
||||
leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
}
|
||||
|
||||
if (rightPanel) {
|
||||
rightPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset panels in the currently active tab
|
||||
*/
|
||||
resetPanelsInActiveTab(): boolean {
|
||||
if (!this.tabApi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.resetPanelsInTab(activeTab);
|
||||
}
|
||||
}
|
||||
|
||||
// Global singleton instance
|
||||
export const panelRegistry = new PanelRegistry();
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { panelRegistry } from './panelApiRegistry';
|
||||
|
||||
/**
|
||||
* Hook that initializes the global panel registry with the Redux store.
|
||||
*/
|
||||
export const usePanelRegistryInit = () => {
|
||||
useAssertSingleton('usePanelRegistryInit');
|
||||
const store = useAppStore();
|
||||
const tabApi = useMemo(
|
||||
() => ({
|
||||
getTab: () => {
|
||||
return selectActiveTab(store.getState());
|
||||
},
|
||||
setTab: (tab: TabName) => {
|
||||
store.dispatch(setActiveTab(tab));
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
panelRegistry.setTabApi(tabApi);
|
||||
}, [store, tabApi]);
|
||||
};
|
||||
@@ -27,10 +27,11 @@ import {
|
||||
withPanelContainer,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { dockviewTheme } from 'features/ui/styles/theme';
|
||||
import { atom } from 'nanostores';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { panelRegistry } from './panel-registry/panelApiRegistry';
|
||||
import {
|
||||
BOARD_PANEL_DEFAULT_HEIGHT_PX,
|
||||
BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -61,51 +62,52 @@ const tabComponents = {
|
||||
[TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
|
||||
};
|
||||
|
||||
const centerComponents: AutoLayoutDockviewComponents = {
|
||||
const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
[LAUNCHPAD_PANEL_ID]: withPanelContainer(UpscalingLaunchpadPanel),
|
||||
[VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel),
|
||||
[PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel),
|
||||
};
|
||||
|
||||
const initializeCenterPanelLayout = (api: DockviewApi) => {
|
||||
const launchpadPanel = api.addPanel<PanelParameters>({
|
||||
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const viewerPanel = api.addPanel<PanelParameters>({
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpadPanel.id,
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
return { launchpadPanel, viewerPanel } satisfies Record<string, IDockviewPanel>;
|
||||
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const CenterPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const MainPanel = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
const panels = initializeCenterPanelLayout(event.api);
|
||||
panels.launchpadPanel.api.setActive();
|
||||
|
||||
ctx._$centerPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
const panels = initializeCenterPanelLayout(tab, api);
|
||||
panels.launchpad.api.setActive();
|
||||
panelRegistry.registerPanel(tab, 'main', api);
|
||||
|
||||
const disposables = [
|
||||
event.api.onWillShowOverlay((e) => {
|
||||
api.onWillShowOverlay((e) => {
|
||||
if (e.kind === 'header_space' || e.kind === 'tab') {
|
||||
return;
|
||||
}
|
||||
@@ -119,7 +121,7 @@ const CenterPanel = memo(() => {
|
||||
});
|
||||
};
|
||||
},
|
||||
[ctx._$centerPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -129,7 +131,7 @@ const CenterPanel = memo(() => {
|
||||
disableFloatingGroups={true}
|
||||
dndEdges={false}
|
||||
tabComponents={tabComponents}
|
||||
components={centerComponents}
|
||||
components={mainPanelComponents}
|
||||
onReady={onReady}
|
||||
theme={dockviewTheme}
|
||||
/>
|
||||
@@ -139,50 +141,53 @@ const CenterPanel = memo(() => {
|
||||
</>
|
||||
);
|
||||
});
|
||||
CenterPanel.displayName = 'CenterPanel';
|
||||
MainPanel.displayName = 'MainPanel';
|
||||
|
||||
const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel),
|
||||
[GALLERY_PANEL_ID]: withPanelContainer(GalleryPanel),
|
||||
};
|
||||
|
||||
export const initializeRightPanelLayout = (api: GridviewApi) => {
|
||||
const galleryPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boardsPanel = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: galleryPanel.id,
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return { galleryPanel, boardsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeRightPanelLayout(event.api);
|
||||
ctx._$rightPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[ctx._$rightPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<GridviewReact
|
||||
@@ -199,27 +204,30 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[SETTINGS_PANEL_ID]: withPanelContainer(UpscalingTabLeftPanel),
|
||||
};
|
||||
|
||||
export const initializeLeftPanelLayout = (api: GridviewApi) => {
|
||||
const settingsPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeLeftPanelLayout(event.api);
|
||||
ctx._$leftPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[ctx._$leftPanelApi]
|
||||
[tab]
|
||||
);
|
||||
|
||||
return (
|
||||
<GridviewReact
|
||||
className="dockview-theme-invoke"
|
||||
@@ -233,58 +241,56 @@ LeftPanel.displayName = 'LeftPanel';
|
||||
|
||||
export const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[LEFT_PANEL_ID]: LeftPanel,
|
||||
[MAIN_PANEL_ID]: CenterPanel,
|
||||
[MAIN_PANEL_ID]: MainPanel,
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
const mainPanel = layoutApi.addPanel({
|
||||
const main = layoutApi.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const leftPanel = layoutApi.addPanel({
|
||||
const left = layoutApi.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'left',
|
||||
referencePanel: mainPanel.id,
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
const rightPanel = layoutApi.addPanel({
|
||||
const right = layoutApi.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
position: {
|
||||
direction: 'right',
|
||||
referencePanel: mainPanel.id,
|
||||
referencePanel: main.id,
|
||||
},
|
||||
});
|
||||
|
||||
leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return { mainPanel, leftPanel, rightPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const UpscalingTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
$rootPanelApi.set(event.api);
|
||||
const { mainPanel } = initializeRootPanelLayout(event.api);
|
||||
mainPanel.api.setActive();
|
||||
},
|
||||
[$rootPanelApi]
|
||||
);
|
||||
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
const { main } = initializeRootPanelLayout(api);
|
||||
panelRegistry.registerPanel('upscaling', 'root', api);
|
||||
main.api.setActive();
|
||||
setRootApi(api);
|
||||
}, []);
|
||||
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="upscaling">
|
||||
<AutoLayoutProvider tab="upscaling" rootRef={rootRef}>
|
||||
<GridviewReact
|
||||
ref={rootRef}
|
||||
className="dockview-theme-invoke"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { GridviewApi, GridviewPanelApi, IGridviewPanel } from 'dockview';
|
||||
import type { GridviewPanelApi, IGridviewPanel } from 'dockview';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { TabPanelApis } from './panel-registry/panelApiRegistry';
|
||||
import { panelRegistry } from './panel-registry/panelApiRegistry';
|
||||
|
||||
const getIsCollapsed = (
|
||||
panel: IGridviewPanel<GridviewPanelApi>,
|
||||
orientation: 'vertical' | 'horizontal',
|
||||
@@ -14,7 +18,8 @@ const getIsCollapsed = (
|
||||
};
|
||||
|
||||
export const useCollapsibleGridviewPanel = (
|
||||
api: GridviewApi | null,
|
||||
tab: TabName,
|
||||
rootPanelId: Exclude<keyof TabPanelApis, 'main'>,
|
||||
panelId: string,
|
||||
orientation: 'horizontal' | 'vertical',
|
||||
defaultSize: number,
|
||||
@@ -23,6 +28,7 @@ export const useCollapsibleGridviewPanel = (
|
||||
const $isCollapsed = useState(() => atom(false))[0];
|
||||
const lastExpandedSizeRef = useRef<number>(0);
|
||||
const collapse = useCallback(() => {
|
||||
const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId];
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
@@ -38,9 +44,11 @@ export const useCollapsibleGridviewPanel = (
|
||||
} else {
|
||||
panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth });
|
||||
}
|
||||
}, [api, collapsedSize, orientation, panelId]);
|
||||
}, [collapsedSize, orientation, panelId, rootPanelId, tab]);
|
||||
|
||||
const expand = useCallback(() => {
|
||||
const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId];
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
@@ -53,9 +61,11 @@ export const useCollapsibleGridviewPanel = (
|
||||
} else {
|
||||
panel.api.setSize({ width: lastExpandedSizeRef.current || defaultSize });
|
||||
}
|
||||
}, [api, defaultSize, orientation, panelId]);
|
||||
}, [defaultSize, orientation, panelId, rootPanelId, tab]);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId];
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
@@ -69,9 +79,11 @@ export const useCollapsibleGridviewPanel = (
|
||||
} else {
|
||||
collapse();
|
||||
}
|
||||
}, [api, panelId, orientation, collapsedSize, expand, collapse]);
|
||||
}, [tab, rootPanelId, panelId, orientation, collapsedSize, expand, collapse]);
|
||||
|
||||
useEffect(() => {
|
||||
const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId];
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
@@ -90,7 +102,7 @@ export const useCollapsibleGridviewPanel = (
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [$isCollapsed, api, collapsedSize, orientation, panelId]);
|
||||
}, [$isCollapsed, collapsedSize, orientation, panelId, rootPanelId, tab]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import type { GridviewApi } from 'dockview';
|
||||
import type { Atom } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { MAIN_PANEL_ID } from './shared';
|
||||
|
||||
// Find the parent element that has display: none
|
||||
const findParentWithDisplayNone = (el: HTMLElement): HTMLElement | null => {
|
||||
let parent = el.parentElement;
|
||||
while (parent) {
|
||||
const computedStyle = window.getComputedStyle(parent);
|
||||
if (computedStyle.display === 'none') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useOnFirstVisible = (elementRef: RefObject<HTMLElement>, callback: () => void): void => {
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
@@ -12,20 +24,7 @@ export const useOnFirstVisible = (elementRef: RefObject<HTMLElement>, callback:
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the parent element that has display: none
|
||||
const findParentWithDisplay = (el: HTMLElement): HTMLElement | null => {
|
||||
let parent = el.parentElement;
|
||||
while (parent) {
|
||||
const computedStyle = window.getComputedStyle(parent);
|
||||
if (computedStyle.display === 'none') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const targetParent = findParentWithDisplay(element);
|
||||
const targetParent = findParentWithDisplayNone(element);
|
||||
if (!targetParent) {
|
||||
return;
|
||||
}
|
||||
@@ -51,9 +50,8 @@ export const useOnFirstVisible = (elementRef: RefObject<HTMLElement>, callback:
|
||||
}, [elementRef, callback]);
|
||||
};
|
||||
|
||||
export const useResizeMainPanelOnFirstVisit = ($api: Atom<GridviewApi | null>, ref: RefObject<HTMLElement>) => {
|
||||
export const useResizeMainPanelOnFirstVisit = (api: GridviewApi | null, ref: RefObject<HTMLElement>) => {
|
||||
const resizeMainPanelOnFirstVisible = useCallback(() => {
|
||||
const api = $api.get();
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
@@ -76,6 +74,6 @@ export const useResizeMainPanelOnFirstVisit = ($api: Atom<GridviewApi | null>, r
|
||||
}
|
||||
};
|
||||
setSize();
|
||||
}, [$api]);
|
||||
}, [api]);
|
||||
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
|
||||
};
|
||||
|
||||
@@ -29,10 +29,11 @@ import {
|
||||
withPanelContainer,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { dockviewTheme } from 'features/ui/styles/theme';
|
||||
import { atom } from 'nanostores';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { panelRegistry } from './panel-registry/panelApiRegistry';
|
||||
import {
|
||||
BOARD_PANEL_DEFAULT_HEIGHT_PX,
|
||||
BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -63,66 +64,69 @@ const tabComponents = {
|
||||
[TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
|
||||
};
|
||||
|
||||
const centerPanelComponents: AutoLayoutDockviewComponents = {
|
||||
const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
[LAUNCHPAD_PANEL_ID]: withPanelContainer(WorkflowsLaunchpadPanel),
|
||||
[WORKSPACE_PANEL_ID]: withPanelContainer(NodeEditor),
|
||||
[VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel),
|
||||
[PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel),
|
||||
};
|
||||
|
||||
const initializeCenterPanelLayout = (api: DockviewApi) => {
|
||||
const launchpadPanel = api.addPanel<PanelParameters>({
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: 'Launchpad',
|
||||
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
const workspacePanel = api.addPanel<PanelParameters>({
|
||||
const workspace = api.addPanel<PanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: 'Workflow Editor',
|
||||
tabComponent: DEFAULT_TAB_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'workflows',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpadPanel.id,
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
const viewerPanel = api.addPanel<PanelParameters>({
|
||||
const viewer = api.addPanel<PanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: 'Image Viewer',
|
||||
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
referencePanel: launchpadPanel.id,
|
||||
referencePanel: launchpad.id,
|
||||
},
|
||||
});
|
||||
|
||||
return { launchpadPanel, workspacePanel, viewerPanel } satisfies Record<string, IDockviewPanel>;
|
||||
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
const CenterPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
const panels = initializeCenterPanelLayout(event.api);
|
||||
ctx._$centerPanelApi.set(event.api);
|
||||
const MainPanel = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
panels.launchpadPanel.api.setActive();
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
const panels = initializeMainPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'main', api);
|
||||
panels.launchpad.api.setActive();
|
||||
|
||||
const disposables = [
|
||||
event.api.onWillShowOverlay((e) => {
|
||||
api.onWillShowOverlay((e) => {
|
||||
if (e.kind === 'header_space' || e.kind === 'tab') {
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +140,7 @@ const CenterPanel = memo(() => {
|
||||
});
|
||||
};
|
||||
},
|
||||
[ctx._$centerPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -146,7 +150,7 @@ const CenterPanel = memo(() => {
|
||||
disableFloatingGroups={true}
|
||||
dndEdges={false}
|
||||
tabComponents={tabComponents}
|
||||
components={centerPanelComponents}
|
||||
components={mainPanelComponents}
|
||||
onReady={onReady}
|
||||
theme={dockviewTheme}
|
||||
/>
|
||||
@@ -156,50 +160,53 @@ const CenterPanel = memo(() => {
|
||||
</>
|
||||
);
|
||||
});
|
||||
CenterPanel.displayName = 'CenterPanel';
|
||||
MainPanel.displayName = 'MainPanel';
|
||||
|
||||
const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel),
|
||||
[GALLERY_PANEL_ID]: withPanelContainer(GalleryPanel),
|
||||
};
|
||||
|
||||
export const initializeRightPanelLayout = (api: GridviewApi) => {
|
||||
const galleryPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'gallery',
|
||||
},
|
||||
});
|
||||
|
||||
const boardsPanel = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'boards',
|
||||
},
|
||||
position: {
|
||||
direction: 'above',
|
||||
referencePanel: galleryPanel.id,
|
||||
referencePanel: gallery.id,
|
||||
},
|
||||
});
|
||||
|
||||
boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return { galleryPanel, boardsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const RightPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeRightPanelLayout(event.api);
|
||||
ctx._$rightPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[ctx._$rightPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<GridviewReact
|
||||
@@ -216,26 +223,28 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
[SETTINGS_PANEL_ID]: withPanelContainer(WorkflowsTabLeftPanel),
|
||||
};
|
||||
|
||||
export const initializeLeftPanelLayout = (api: GridviewApi) => {
|
||||
const settingsPanel = api.addPanel<PanelParameters>({
|
||||
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
const settings = api.addPanel<PanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
const LeftPanel = memo(() => {
|
||||
const ctx = useAutoLayoutContext();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
initializeLeftPanelLayout(event.api);
|
||||
ctx._$leftPanelApi.set(event.api);
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
panelRegistry.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[ctx._$leftPanelApi]
|
||||
[tab]
|
||||
);
|
||||
return (
|
||||
<GridviewReact
|
||||
@@ -250,17 +259,17 @@ LeftPanel.displayName = 'LeftPanel';
|
||||
|
||||
export const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
[LEFT_PANEL_ID]: LeftPanel,
|
||||
[MAIN_PANEL_ID]: CenterPanel,
|
||||
[MAIN_PANEL_ID]: MainPanel,
|
||||
[RIGHT_PANEL_ID]: RightPanel,
|
||||
};
|
||||
|
||||
export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
api.addPanel({
|
||||
const main = api.addPanel({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
api.addPanel({
|
||||
const left = api.addPanel({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
@@ -269,7 +278,7 @@ export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
referencePanel: MAIN_PANEL_ID,
|
||||
},
|
||||
});
|
||||
api.addPanel({
|
||||
const right = api.addPanel({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
@@ -278,25 +287,25 @@ export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
referencePanel: MAIN_PANEL_ID,
|
||||
},
|
||||
});
|
||||
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
api.getPanel(RIGHT_PANEL_ID)?.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const WorkflowsTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
(event) => {
|
||||
$rootPanelApi.set(event.api);
|
||||
initializeRootPanelLayout(event.api);
|
||||
},
|
||||
[$rootPanelApi]
|
||||
);
|
||||
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
const panels = initializeRootPanelLayout(api);
|
||||
panelRegistry.registerPanel('workflows', 'root', api);
|
||||
panels.main.api.setActive();
|
||||
setRootApi(api);
|
||||
}, []);
|
||||
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="workflows">
|
||||
<AutoLayoutProvider tab="workflows" rootRef={rootRef}>
|
||||
<GridviewReact
|
||||
ref={rootRef}
|
||||
className="dockview-theme-invoke"
|
||||
|
||||
Reference in New Issue
Block a user