2025-11-20 21:24:59 +01:00
|
|
|
import { useEffect } from 'react';
|
|
|
|
|
import { useHistoryStore } from '@/store/history-store';
|
2025-11-21 16:08:24 +01:00
|
|
|
import { useToolStore, useLayerStore, useTextStore } from '@/store';
|
|
|
|
|
import { duplicateLayerWithHistory, deleteLayerWithHistory } from '@/lib/layer-operations';
|
|
|
|
|
import type { ToolType } from '@/types';
|
2025-11-20 21:24:59 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Keyboard shortcuts configuration
|
|
|
|
|
*/
|
|
|
|
|
interface KeyboardShortcut {
|
|
|
|
|
key: string;
|
|
|
|
|
ctrl?: boolean;
|
|
|
|
|
shift?: boolean;
|
|
|
|
|
alt?: boolean;
|
|
|
|
|
meta?: boolean;
|
|
|
|
|
handler: () => void;
|
|
|
|
|
description: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 16:08:24 +01:00
|
|
|
/**
|
|
|
|
|
* Tool shortcuts mapping
|
|
|
|
|
*/
|
|
|
|
|
const TOOL_SHORTCUTS: Record<string, ToolType> = {
|
|
|
|
|
'1': 'pencil',
|
|
|
|
|
'2': 'brush',
|
|
|
|
|
'3': 'eraser',
|
|
|
|
|
'4': 'fill',
|
|
|
|
|
'5': 'eyedropper',
|
|
|
|
|
'6': 'text',
|
|
|
|
|
'7': 'select',
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-20 21:24:59 +01:00
|
|
|
/**
|
|
|
|
|
* Hook to manage keyboard shortcuts
|
|
|
|
|
*/
|
|
|
|
|
export function useKeyboardShortcuts() {
|
|
|
|
|
const { undo, redo, canUndo, canRedo } = useHistoryStore();
|
2025-11-21 16:08:24 +01:00
|
|
|
const { setActiveTool } = useToolStore();
|
|
|
|
|
const { layers, activeLayerId, setActiveLayer } = useLayerStore();
|
|
|
|
|
const { isOnCanvasEditorActive } = useTextStore();
|
2025-11-20 21:24:59 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
2025-11-21 16:08:24 +01:00
|
|
|
// Check if we're in an input field or text editor is active
|
2025-11-20 21:24:59 +01:00
|
|
|
const target = e.target as HTMLElement;
|
|
|
|
|
if (
|
2025-11-21 16:08:24 +01:00
|
|
|
isOnCanvasEditorActive ||
|
2025-11-20 21:24:59 +01:00
|
|
|
target.tagName === 'INPUT' ||
|
|
|
|
|
target.tagName === 'TEXTAREA' ||
|
|
|
|
|
target.isContentEditable
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 16:08:24 +01:00
|
|
|
// Tool selection shortcuts: 1-7
|
|
|
|
|
if (TOOL_SHORTCUTS[e.key]) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setActiveTool(TOOL_SHORTCUTS[e.key]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Layer navigation: Arrow Up/Down
|
|
|
|
|
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// Sort layers by order (highest first, same as UI)
|
|
|
|
|
const sortedLayers = [...layers].sort((a, b) => b.order - a.order);
|
|
|
|
|
const currentIndex = sortedLayers.findIndex((l) => l.id === activeLayerId);
|
|
|
|
|
|
|
|
|
|
if (currentIndex === -1) return;
|
|
|
|
|
|
|
|
|
|
if (e.key === 'ArrowUp' && currentIndex > 0) {
|
|
|
|
|
setActiveLayer(sortedLayers[currentIndex - 1].id);
|
|
|
|
|
} else if (e.key === 'ArrowDown' && currentIndex < sortedLayers.length - 1) {
|
|
|
|
|
setActiveLayer(sortedLayers[currentIndex + 1].id);
|
2025-11-20 21:24:59 +01:00
|
|
|
}
|
2025-11-21 16:08:24 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete layer: Delete or Backspace (without modifier keys)
|
|
|
|
|
if ((e.key === 'Delete' || e.key === 'Backspace') && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (activeLayerId && layers.length > 1) {
|
|
|
|
|
if (confirm('Delete this layer?')) {
|
|
|
|
|
deleteLayerWithHistory(activeLayerId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Duplicate layer: Ctrl+D
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'd' && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (activeLayerId) {
|
|
|
|
|
duplicateLayerWithHistory(activeLayerId);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Undo: Ctrl+Z (but not Ctrl+Shift+Z)
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (canUndo()) {
|
|
|
|
|
undo();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Redo: Ctrl+Shift+Z or Ctrl+Y
|
|
|
|
|
if (
|
|
|
|
|
((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) ||
|
|
|
|
|
((e.ctrlKey || e.metaKey) && e.key === 'y')
|
|
|
|
|
) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (canRedo()) {
|
|
|
|
|
redo();
|
|
|
|
|
}
|
|
|
|
|
return;
|
2025-11-20 21:24:59 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
};
|
2025-11-21 16:08:24 +01:00
|
|
|
}, [
|
|
|
|
|
undo,
|
|
|
|
|
redo,
|
|
|
|
|
canUndo,
|
|
|
|
|
canRedo,
|
|
|
|
|
setActiveTool,
|
|
|
|
|
layers,
|
|
|
|
|
activeLayerId,
|
|
|
|
|
setActiveLayer,
|
|
|
|
|
isOnCanvasEditorActive,
|
|
|
|
|
]);
|
2025-11-20 21:24:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get keyboard shortcut display string
|
|
|
|
|
*/
|
|
|
|
|
export function getShortcutDisplay(shortcut: {
|
|
|
|
|
key: string;
|
|
|
|
|
ctrl?: boolean;
|
|
|
|
|
shift?: boolean;
|
|
|
|
|
alt?: boolean;
|
|
|
|
|
meta?: boolean;
|
|
|
|
|
}): string {
|
|
|
|
|
const parts: string[] = [];
|
|
|
|
|
|
|
|
|
|
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
|
|
|
|
|
|
|
|
if (shortcut.ctrl || shortcut.meta) {
|
|
|
|
|
parts.push(isMac ? '⌘' : 'Ctrl');
|
|
|
|
|
}
|
|
|
|
|
if (shortcut.shift) {
|
|
|
|
|
parts.push(isMac ? '⇧' : 'Shift');
|
|
|
|
|
}
|
|
|
|
|
if (shortcut.alt) {
|
|
|
|
|
parts.push(isMac ? '⌥' : 'Alt');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parts.push(shortcut.key.toUpperCase());
|
|
|
|
|
|
|
|
|
|
return parts.join(isMac ? '' : '+');
|
|
|
|
|
}
|