9b49085869
同步生成器 active/inactive ref 与 state 激活已有生成器时清理选参考图和菜单临时状态 窗口失焦时释放 Shift 与空格按住态 补充生成器状态与快捷键回归测试 记录图片画布生成器排障经验
541 lines
16 KiB
TypeScript
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,
|
|
]);
|
|
}
|