Files
Genarrative/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts
T
kdletters 9b49085869 修复画布生成器偶发无法打开
同步生成器 active/inactive ref 与 state

激活已有生成器时清理选参考图和菜单临时状态

窗口失焦时释放 Shift 与空格按住态

补充生成器状态与快捷键回归测试

记录图片画布生成器排障经验
2026-07-07 16:22:09 +08:00

541 lines
16 KiB
TypeScript

import { type RefObject, useEffect } from 'react';
import type {
CanvasGenerationDialogState,
CanvasTool,
GenerateDialogState,
QuickEditPanelState,
SidebarPanel,
} from './ImageCanvasEditorTypes';
type UseImageCanvasKeyboardShortcutsOptions = {
generateDialogRef: RefObject<GenerateDialogState | null>;
selectedLayerIdRef: RefObject<string | null>;
selectedLayerIdsRef?: RefObject<string[]>;
redoCanvasChange: () => void;
undoCanvasChange: () => void;
deleteLayerById: (layerId: string | null) => void;
deleteSelectedCanvasObjects?: () => void;
selectAllCanvasObjects?: () => void;
clearCanvasSelection?: () => void;
copySelectedCanvasLayers?: (options?: { cut?: boolean }) => void;
pasteCanvasClipboard?: () => boolean;
duplicateSelectedCanvasLayers?: () => void;
moveSelectedCanvasLayers?: (mode: 'up' | 'down' | 'top' | 'bottom') => void;
nudgeSelectedCanvasObjects?: (delta: { x: number; y: number }) => void;
requestRemoveCanvasGenerationDialog?: (
dialog: CanvasGenerationDialogState,
) => void;
setActiveTool: (tool: 'select') => void;
switchTool?: (tool: CanvasTool) => void;
updateScaleFromCenter?: (nextScale: number) => void;
zoomViewportBy?: (scaleFactor: number) => void;
fitLayers?: () => void;
toggleSidebarPanel?: (panel: SidebarPanel) => void;
toggleMinimap?: () => void;
startProjectRename?: () => void;
exportCanvasAssets?: () => void;
setGenerateDialog: (
updater:
| GenerateDialogState
| null
| ((dialog: GenerateDialogState | null) => GenerateDialogState | null),
) => void;
setImageContextMenu: (menu: null) => void;
setContextMenu: (menu: null) => void;
setQuickEditPanel: (
updater:
| QuickEditPanelState
| null
| ((panel: QuickEditPanelState | null) => QuickEditPanelState | null),
) => void;
closeEditorChromePanels: () => void;
setIsSpecMenuOpen: (open: boolean) => void;
setIsGenerationReferenceMenuOpen: (open: boolean) => void;
setIsPublicationReferenceMenuOpen: (open: boolean) => void;
setIsPickingGenerationReferenceFromCanvas: (picking: boolean) => void;
setIsPickingQuickEditReferenceFromCanvas: (picking: boolean) => void;
setIsPickingPublicationReferenceFromCanvas: (picking: boolean) => void;
setIsCharacterSpecMenuOpen: (open: boolean) => void;
setIsCharacterReferenceMenuOpen: (open: boolean) => void;
setIsPickingCharacterSpecFromCanvas: (picking: boolean) => void;
setIsPickingCharacterReferenceFromCanvas: (picking: boolean) => void;
setIsIconSpecMenuOpen: (open: boolean) => void;
setIsPickingIconSpecFromCanvas: (picking: boolean) => void;
setIsUiDesignSpecMenuOpen: (open: boolean) => void;
setIsPickingUiDesignSpecFromCanvas: (picking: boolean) => void;
setIsSpacePanning: (panning: boolean) => void;
setShiftPressed: (pressed: boolean) => void;
};
function isEditableTarget(event: KeyboardEvent) {
const target = event.target as HTMLElement | null;
if (!target) {
return false;
}
if (target.tagName === 'AUDIO' || target.tagName === 'VIDEO') {
return false;
}
return (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable
);
}
function isCanvasGenerationPlaceholderDialog(
dialog: GenerateDialogState | null,
) {
return (
Boolean(dialog?.placeholder) &&
(dialog?.mode === 'generate' ||
dialog?.mode === 'spec' ||
dialog?.mode === 'character' ||
dialog?.mode === 'icon' ||
dialog?.mode === 'ui-design' ||
dialog?.mode === 'quick-edit' ||
dialog?.mode === 'publication' ||
dialog?.mode === 'character-animation' ||
dialog?.mode === 'video' ||
dialog?.mode === 'audio-sound-effect' ||
dialog?.mode === 'audio-background-music')
);
}
function isCanvasGenerationDialogState(
dialog: GenerateDialogState | null,
): dialog is CanvasGenerationDialogState {
return Boolean(dialog?.id && isCanvasGenerationPlaceholderDialog(dialog));
}
function isCtrlShortcut(event: KeyboardEvent) {
return event.ctrlKey && !event.altKey && !event.metaKey;
}
function hasSelectedText() {
const selection = window.getSelection();
return Boolean(selection?.toString().trim());
}
function isKey(event: KeyboardEvent, key: string, code?: string) {
return event.key.toLowerCase() === key || event.code === code;
}
function isPlusKey(event: KeyboardEvent) {
return (
event.key === '+' ||
event.key === '=' ||
event.code === 'Equal' ||
event.code === 'NumpadAdd'
);
}
function isMinusKey(event: KeyboardEvent) {
return (
event.key === '-' ||
event.code === 'Minus' ||
event.code === 'NumpadSubtract'
);
}
function getArrowDelta(event: KeyboardEvent) {
const step = event.shiftKey ? 10 : 1;
if (event.key === 'ArrowUp') {
return { x: 0, y: -step };
}
if (event.key === 'ArrowDown') {
return { x: 0, y: step };
}
if (event.key === 'ArrowLeft') {
return { x: -step, y: 0 };
}
if (event.key === 'ArrowRight') {
return { x: step, y: 0 };
}
return null;
}
export function useImageCanvasKeyboardShortcuts({
generateDialogRef,
selectedLayerIdRef,
selectedLayerIdsRef,
redoCanvasChange,
undoCanvasChange,
deleteLayerById,
deleteSelectedCanvasObjects,
selectAllCanvasObjects,
clearCanvasSelection,
copySelectedCanvasLayers,
pasteCanvasClipboard,
duplicateSelectedCanvasLayers,
moveSelectedCanvasLayers,
nudgeSelectedCanvasObjects,
requestRemoveCanvasGenerationDialog,
setActiveTool,
switchTool,
updateScaleFromCenter,
zoomViewportBy,
fitLayers,
toggleSidebarPanel,
toggleMinimap,
startProjectRename,
exportCanvasAssets,
setGenerateDialog,
setImageContextMenu,
setContextMenu,
setQuickEditPanel,
closeEditorChromePanels,
setIsSpecMenuOpen,
setIsGenerationReferenceMenuOpen,
setIsPublicationReferenceMenuOpen,
setIsPickingGenerationReferenceFromCanvas,
setIsPickingQuickEditReferenceFromCanvas,
setIsPickingPublicationReferenceFromCanvas,
setIsCharacterSpecMenuOpen,
setIsCharacterReferenceMenuOpen,
setIsPickingCharacterSpecFromCanvas,
setIsPickingCharacterReferenceFromCanvas,
setIsIconSpecMenuOpen,
setIsPickingIconSpecFromCanvas,
setIsUiDesignSpecMenuOpen,
setIsPickingUiDesignSpecFromCanvas,
setIsSpacePanning,
setShiftPressed,
}: UseImageCanvasKeyboardShortcutsOptions) {
useEffect(() => {
const closeTransientEditorPanels = () => {
closeEditorChromePanels();
setIsSpecMenuOpen(false);
setIsGenerationReferenceMenuOpen(false);
setIsPublicationReferenceMenuOpen(false);
setIsPickingGenerationReferenceFromCanvas(false);
setIsPickingQuickEditReferenceFromCanvas(false);
setIsPickingPublicationReferenceFromCanvas(false);
setImageContextMenu(null);
setContextMenu(null);
setQuickEditPanel((currentPanel) =>
currentPanel?.status === 'generating' ? currentPanel : null,
);
setIsCharacterSpecMenuOpen(false);
setIsCharacterReferenceMenuOpen(false);
setIsPickingCharacterSpecFromCanvas(false);
setIsPickingCharacterReferenceFromCanvas(false);
setIsIconSpecMenuOpen(false);
setIsPickingIconSpecFromCanvas(false);
setIsUiDesignSpecMenuOpen(false);
setIsPickingUiDesignSpecFromCanvas(false);
setGenerateDialog((currentDialog) => {
if (!currentDialog || currentDialog.status === 'generating') {
return currentDialog;
}
if (
currentDialog.mode === 'generate' ||
currentDialog.mode === 'spec' ||
currentDialog.mode === 'ui-design' ||
currentDialog.mode === 'quick-edit' ||
currentDialog.mode === 'publication' ||
currentDialog.mode === 'character-animation' ||
currentDialog.mode === 'video'
) {
return {
...currentDialog,
composerOpen: false,
};
}
if (currentDialog.mode === 'character') {
return currentDialog;
}
if (currentDialog.mode === 'icon') {
return currentDialog;
}
return null;
});
};
const handleKeyDown = (event: KeyboardEvent) => {
if (
isCtrlShortcut(event) &&
event.code === 'KeyZ' &&
!isEditableTarget(event)
) {
event.preventDefault();
if (event.shiftKey) {
redoCanvasChange();
} else {
undoCanvasChange();
}
return;
}
if (isCtrlShortcut(event) && !isEditableTarget(event)) {
if (event.code === 'KeyA') {
event.preventDefault();
selectAllCanvasObjects?.();
return;
}
if (event.code === 'KeyC') {
if (hasSelectedText()) {
return;
}
event.preventDefault();
copySelectedCanvasLayers?.();
return;
}
if (event.code === 'KeyX') {
event.preventDefault();
copySelectedCanvasLayers?.({ cut: true });
return;
}
if (event.code === 'KeyV') {
if (pasteCanvasClipboard?.()) {
event.preventDefault();
}
return;
}
if (event.code === 'KeyD') {
event.preventDefault();
duplicateSelectedCanvasLayers?.();
return;
}
if (event.code === 'KeyS' && event.shiftKey) {
event.preventDefault();
exportCanvasAssets?.();
return;
}
if (event.key === '0' || event.code === 'Digit0') {
event.preventDefault();
fitLayers?.();
return;
}
if (event.key === '1' || event.code === 'Digit1') {
event.preventDefault();
updateScaleFromCenter?.(1);
return;
}
if (isPlusKey(event)) {
event.preventDefault();
zoomViewportBy?.(1.16);
return;
}
if (isMinusKey(event)) {
event.preventDefault();
zoomViewportBy?.(0.86);
return;
}
if (event.code === 'BracketRight') {
event.preventDefault();
moveSelectedCanvasLayers?.(event.shiftKey ? 'top' : 'up');
return;
}
if (event.code === 'BracketLeft') {
event.preventDefault();
moveSelectedCanvasLayers?.(event.shiftKey ? 'bottom' : 'down');
return;
}
}
if (
event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
!isEditableTarget(event)
) {
if (event.code === 'Digit1') {
event.preventDefault();
toggleSidebarPanel?.('assets');
return;
}
if (event.code === 'Digit2') {
event.preventDefault();
toggleSidebarPanel?.('layers');
return;
}
if (event.code === 'KeyM') {
event.preventDefault();
toggleMinimap?.();
return;
}
}
if (
!event.ctrlKey &&
!event.metaKey &&
!event.altKey &&
!isEditableTarget(event)
) {
const arrowDelta = getArrowDelta(event);
if (arrowDelta) {
event.preventDefault();
nudgeSelectedCanvasObjects?.(arrowDelta);
return;
}
if (event.key === 'F2') {
event.preventDefault();
startProjectRename?.();
return;
}
if (!event.repeat && event.code === 'KeyV' && event.shiftKey) {
event.preventDefault();
switchTool?.('video');
return;
}
if (!event.repeat && !event.shiftKey && isKey(event, 'v', 'KeyV')) {
event.preventDefault();
switchTool?.('select');
return;
}
if (!event.repeat && !event.shiftKey && isKey(event, 'h', 'KeyH')) {
event.preventDefault();
switchTool?.('hand');
return;
}
if (!event.repeat && !event.shiftKey && isKey(event, 'u', 'KeyU')) {
event.preventDefault();
switchTool?.('upload');
return;
}
if (!event.repeat && !event.shiftKey && isKey(event, 'g', 'KeyG')) {
event.preventDefault();
switchTool?.('generate');
return;
}
if (!event.repeat && !event.shiftKey && isKey(event, 'm', 'KeyM')) {
event.preventDefault();
switchTool?.('music');
return;
}
}
if (event.key === 'Shift') {
setShiftPressed(true);
}
if (
(event.key === 'Backspace' || event.key === 'Delete') &&
!event.repeat &&
!isEditableTarget(event)
) {
if (selectedLayerIdsRef?.current.length) {
event.preventDefault();
deleteSelectedCanvasObjects?.();
return;
}
const currentSelectedLayerId = selectedLayerIdRef.current;
if (currentSelectedLayerId) {
event.preventDefault();
deleteLayerById(currentSelectedLayerId);
return;
}
if (isCanvasGenerationPlaceholderDialog(generateDialogRef.current)) {
event.preventDefault();
const currentDialog = generateDialogRef.current;
if (
isCanvasGenerationDialogState(currentDialog) &&
requestRemoveCanvasGenerationDialog
) {
requestRemoveCanvasGenerationDialog(currentDialog);
} else {
setGenerateDialog(null);
}
setActiveTool('select');
setIsGenerationReferenceMenuOpen(false);
setIsPublicationReferenceMenuOpen(false);
setIsPickingGenerationReferenceFromCanvas(false);
setIsPickingQuickEditReferenceFromCanvas(false);
setIsPickingPublicationReferenceFromCanvas(false);
setIsCharacterSpecMenuOpen(false);
setIsCharacterReferenceMenuOpen(false);
setIsPickingCharacterSpecFromCanvas(false);
setIsPickingCharacterReferenceFromCanvas(false);
setIsIconSpecMenuOpen(false);
setIsPickingIconSpecFromCanvas(false);
setIsUiDesignSpecMenuOpen(false);
setIsPickingUiDesignSpecFromCanvas(false);
return;
}
}
if (event.key === 'Escape') {
closeTransientEditorPanels();
clearCanvasSelection?.();
return;
}
if (event.code !== 'Space' || event.repeat || isEditableTarget(event)) {
return;
}
event.preventDefault();
setIsSpacePanning(true);
};
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Shift') {
setShiftPressed(false);
}
if (event.code !== 'Space') {
return;
}
event.preventDefault();
setIsSpacePanning(false);
};
const resetPressedInteractionState = () => {
setShiftPressed(false);
setIsSpacePanning(false);
};
const handleVisibilityChange = () => {
if (document.visibilityState !== 'visible') {
resetPressedInteractionState();
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('blur', resetPressedInteractionState);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
window.removeEventListener('blur', resetPressedInteractionState);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [
closeEditorChromePanels,
clearCanvasSelection,
copySelectedCanvasLayers,
pasteCanvasClipboard,
duplicateSelectedCanvasLayers,
deleteSelectedCanvasObjects,
deleteLayerById,
exportCanvasAssets,
fitLayers,
generateDialogRef,
moveSelectedCanvasLayers,
nudgeSelectedCanvasObjects,
requestRemoveCanvasGenerationDialog,
redoCanvasChange,
selectAllCanvasObjects,
selectedLayerIdRef,
selectedLayerIdsRef,
startProjectRename,
setActiveTool,
setContextMenu,
setGenerateDialog,
setImageContextMenu,
setIsGenerationReferenceMenuOpen,
setIsPublicationReferenceMenuOpen,
setIsPickingGenerationReferenceFromCanvas,
setIsPickingQuickEditReferenceFromCanvas,
setIsPickingPublicationReferenceFromCanvas,
setIsCharacterSpecMenuOpen,
setIsCharacterReferenceMenuOpen,
setIsIconSpecMenuOpen,
setIsPickingCharacterReferenceFromCanvas,
setIsPickingCharacterSpecFromCanvas,
setIsPickingIconSpecFromCanvas,
setIsPickingUiDesignSpecFromCanvas,
setIsSpacePanning,
setIsSpecMenuOpen,
setIsUiDesignSpecMenuOpen,
setQuickEditPanel,
setShiftPressed,
switchTool,
toggleMinimap,
toggleSidebarPanel,
undoCanvasChange,
updateScaleFromCenter,
zoomViewportBy,
]);
}