import { type RefObject, useEffect } from 'react'; import type { CanvasGenerationDialogState, CanvasTool, GenerateDialogState, QuickEditPanelState, SidebarPanel, } from './ImageCanvasEditorTypes'; type UseImageCanvasKeyboardShortcutsOptions = { generateDialogRef: RefObject; selectedLayerIdRef: RefObject; selectedLayerIdsRef?: RefObject; 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, ]); }