拆分图片画布键盘快捷键
新增图片画布键盘快捷键 hook,承接撤销重做、删除、Escape 和临时抓手逻辑 保留主视图状态编排,只把 window 键盘监听移出巨型组件 补充键盘快捷键 hook 测试并更新拆分文档和 TRACKING 记录
This commit is contained in:
@@ -136,3 +136,4 @@
|
|||||||
- 2026-06-17 前端拆分第十八阶段:新增 `useImageCanvasStageInteractions`,把画布舞台 pointer 状态机、选择 / 框选、多选拖拽、生成占位拖拽、抓手 / Space 临时抓手 / 中键平移、小地图 click / drag 分流和吸附线状态从主视图抽出;主视图继续保留上传 drop、右键菜单、生成提交、项目持久化和工具栏动作分流。新增 hook 单测覆盖多选拖拽、框选、临时抓手、生成占位和小地图分流;主视图从 1802 行降至 1452 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新和未登录上传均弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏保持可见;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;普通滚轮不改变缩放,Ctrl 滚轮从 `146%` 到 `161%`;抓手 / 文字 / 选择工具可连续切换;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,关闭对话框后占位图保留,登录后控制台无前端 error。
|
- 2026-06-17 前端拆分第十八阶段:新增 `useImageCanvasStageInteractions`,把画布舞台 pointer 状态机、选择 / 框选、多选拖拽、生成占位拖拽、抓手 / Space 临时抓手 / 中键平移、小地图 click / drag 分流和吸附线状态从主视图抽出;主视图继续保留上传 drop、右键菜单、生成提交、项目持久化和工具栏动作分流。新增 hook 单测覆盖多选拖拽、框选、临时抓手、生成占位和小地图分流;主视图从 1802 行降至 1452 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新和未登录上传均弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏保持可见;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;普通滚轮不改变缩放,Ctrl 滚轮从 `146%` 到 `161%`;抓手 / 文字 / 选择工具可连续切换;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,关闭对话框后占位图保留,登录后控制台无前端 error。
|
||||||
- 2026-06-17 前端拆分第十九阶段:新增 `useImageCanvasCanvasDropWorkflow`,把画布区域 drag over / drag leave / drop 分流从主视图抽出,覆盖素材库图片拖入画布、本地文件拖入画布、无关拖拽不拦截、默认文件夹选择和画布遮罩清理;主视图继续注入素材建层、文件上传、drop 点换算和素材移动高亮清理。新增 hook 单测覆盖拖拽入画布细节,主视图从 1452 行降至 1405 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材 / 画布 / 小地图和底部工具栏可见,真实鼠标拖拽素材库图片到画布时出现 `添加到画布` 遮罩,松手后画布图层数量从 4 增至 5;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;抓手工具可切回选择工具,登录后控制台无前端 error。
|
- 2026-06-17 前端拆分第十九阶段:新增 `useImageCanvasCanvasDropWorkflow`,把画布区域 drag over / drag leave / drop 分流从主视图抽出,覆盖素材库图片拖入画布、本地文件拖入画布、无关拖拽不拦截、默认文件夹选择和画布遮罩清理;主视图继续注入素材建层、文件上传、drop 点换算和素材移动高亮清理。新增 hook 单测覆盖拖拽入画布细节,主视图从 1452 行降至 1405 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材 / 画布 / 小地图和底部工具栏可见,真实鼠标拖拽素材库图片到画布时出现 `添加到画布` 遮罩,松手后画布图层数量从 4 增至 5;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;抓手工具可切回选择工具,登录后控制台无前端 error。
|
||||||
- 2026-06-17 前端拆分第二十阶段:新增 `ImageCanvasMetadataModalView`,把图片信息弹窗从主视图抽出,承载图片类型、生成输入、参考图、模型、分辨率、Provider、Task 和 Object 信息渲染;主视图只保留 `metadataLayer` 状态和关闭回调。同步修复未登录进入编辑器时项目 / 素材接口抢跑 401、`重置画布视图` 点击事件误传给适合视图函数的问题。新增组件单测覆盖生成图 metadata、上传图 fallback 和关闭回调,新增 hook / 主视图测试覆盖未登录不请求受保护素材 / 工程数据和重置按钮回归;主视图从 1405 行降至 1337 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后直接弹出 `账号入口`,且未登录状态下没有发起 `/api/editor/*` 请求;登录临时开发账号后 `重置画布视图` 无控制台错误,`画布背景设置` 保持 Lovart 式白色浮层,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`,上传素材可加入画布,右上角图片信息按钮可打开不透明白底元数据弹窗,关闭后 `AI画布工具栏` 仍可见。
|
- 2026-06-17 前端拆分第二十阶段:新增 `ImageCanvasMetadataModalView`,把图片信息弹窗从主视图抽出,承载图片类型、生成输入、参考图、模型、分辨率、Provider、Task 和 Object 信息渲染;主视图只保留 `metadataLayer` 状态和关闭回调。同步修复未登录进入编辑器时项目 / 素材接口抢跑 401、`重置画布视图` 点击事件误传给适合视图函数的问题。新增组件单测覆盖生成图 metadata、上传图 fallback 和关闭回调,新增 hook / 主视图测试覆盖未登录不请求受保护素材 / 工程数据和重置按钮回归;主视图从 1405 行降至 1337 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后直接弹出 `账号入口`,且未登录状态下没有发起 `/api/editor/*` 请求;登录临时开发账号后 `重置画布视图` 无控制台错误,`画布背景设置` 保持 Lovart 式白色浮层,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`,上传素材可加入画布,右上角图片信息按钮可打开不透明白底元数据弹窗,关闭后 `AI画布工具栏` 仍可见。
|
||||||
|
- 2026-06-17 前端拆分第二十一阶段:新增 `useImageCanvasKeyboardShortcuts`,把 Ctrl / Cmd + Z 撤销、Ctrl / Cmd + Shift + Z 重做、Shift 状态、Backspace / Delete 删除、Escape 关闭临时面板和 Space 临时抓手从主视图抽出;主视图继续注入图层删除、生成对话框、快速编辑和 chrome 面板 setter。新增 hook 单测覆盖输入框忽略快捷键、删除选中图层、删除生成占位、Escape 保留生成中面板、Space 和 Shift;主视图从 1337 行降至 1250 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录直接弹出 `账号入口` 且未抢跑 `/api/editor/*`,登录后 `/api/editor/assets/library` 和 `/api/editor/projects/recent` 为 200,`AI画布工具栏` 与 `画布面板入口` 可见,viewport 背景为 `rgb(248, 250, 252)` 且 `background-image: none`;按住 Space 从 `文字工具` 临时切到 `抓手工具`,松开恢复 `文字工具`;`画布背景设置` 点击 `暖灰` 后背景变为 `rgb(243, 240, 234)`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 同时可见,登录后控制台无前端 error。
|
||||||
|
|||||||
@@ -176,6 +176,13 @@
|
|||||||
- 本阶段同步把项目 / 素材初始加载挂到 `AuthGate` 的受保护数据可访问状态之后;未登录进入编辑器只拉起 `账号入口`,不再抢跑 `/api/editor/*` 造成素材或工程读取 401 噪声。
|
- 本阶段同步把项目 / 素材初始加载挂到 `AuthGate` 的受保护数据可访问状态之后;未登录进入编辑器只拉起 `账号入口`,不再抢跑 `/api/editor/*` 造成素材或工程读取 401 噪声。
|
||||||
- `重置画布视图` 按钮必须显式调用 `onFitLayers()`,不能把 React click event 作为目标图层数组透传给适合视图逻辑。
|
- `重置画布视图` 按钮必须显式调用 `onFitLayers()`,不能把 React click event 作为目标图层数组透传给适合视图逻辑。
|
||||||
|
|
||||||
|
## 第二十一阶段模块
|
||||||
|
|
||||||
|
- `useImageCanvasKeyboardShortcuts.ts`
|
||||||
|
- 承载图片画布全局键盘快捷键:Ctrl / Cmd + Z 撤销、Ctrl / Cmd + Shift + Z 重做、Shift 按下态、Backspace / Delete 删除选中图层、删除可编辑生成占位、Escape 关闭临时面板,以及 Space 临时抓手。
|
||||||
|
- 主视图继续保留各工作流状态和具体副作用,例如图层删除、生成对话框、规格菜单、快速编辑面板和 chrome 面板状态;快捷键 hook 只接收 ref、setter 与回调,不直接读写素材库、路由或 API。
|
||||||
|
- 该 hook 用独立单测覆盖输入框忽略快捷键、撤销重做、选中图层删除、生成占位删除、Escape 保留生成中面板、Space 临时抓手和 Shift 状态;主视图 DOM 测试继续覆盖真实编辑器里的 Backspace、Escape、Space 和 undo / redo 集成路径。
|
||||||
|
|
||||||
## 后续阶段
|
## 后续阶段
|
||||||
|
|
||||||
- 后续可继续选择更高内聚的交互 workflow 或持久化边界,不再把生成链路继续拆成浅层 wrapper。
|
- 后续可继续选择更高内聚的交互 workflow 或持久化边界,不再把生成链路继续拆成浅层 wrapper。
|
||||||
@@ -183,8 +190,8 @@
|
|||||||
|
|
||||||
## 验证计划
|
## 验证计划
|
||||||
|
|
||||||
- `npm run test -- src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`
|
- `npm run test -- src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`
|
||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run check:encoding`
|
- `npm run check:encoding`
|
||||||
- `git diff --check`
|
- `git diff --check`
|
||||||
- 浏览器回归 `/editor/canvas`:确认登录弹窗、素材上传、背景设置面板、底部工具栏和画布基础渲染仍正常。
|
- 浏览器回归 `/editor/canvas`:确认登录弹窗、素材上传、背景设置面板、底部工具栏、Space 临时抓手、撤销 / 重做和画布基础渲染仍正常。
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWo
|
|||||||
import { useImageCanvasCanvasDropWorkflow } from './useImageCanvasCanvasDropWorkflow';
|
import { useImageCanvasCanvasDropWorkflow } from './useImageCanvasCanvasDropWorkflow';
|
||||||
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
|
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
|
||||||
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
||||||
|
import { useImageCanvasKeyboardShortcuts } from './useImageCanvasKeyboardShortcuts';
|
||||||
import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands';
|
import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands';
|
||||||
import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence';
|
import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence';
|
||||||
import { useImageCanvasStageInteractions } from './useImageCanvasStageInteractions';
|
import { useImageCanvasStageInteractions } from './useImageCanvasStageInteractions';
|
||||||
@@ -63,18 +64,6 @@ import {
|
|||||||
useImageCanvasViewportControls,
|
useImageCanvasViewportControls,
|
||||||
} from './useImageCanvasViewportControls';
|
} from './useImageCanvasViewportControls';
|
||||||
|
|
||||||
function isEditableTarget(event: KeyboardEvent) {
|
|
||||||
const target = event.target as HTMLElement | null;
|
|
||||||
if (!target) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
target.tagName === 'INPUT' ||
|
|
||||||
target.tagName === 'TEXTAREA' ||
|
|
||||||
target.isContentEditable
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImageCanvasEditorView() {
|
export function ImageCanvasEditorView() {
|
||||||
const authUi = useAuthUi();
|
const authUi = useAuthUi();
|
||||||
const editorRootRef = useRef<HTMLElement | null>(null);
|
const editorRootRef = useRef<HTMLElement | null>(null);
|
||||||
@@ -687,120 +676,31 @@ export function ImageCanvasEditorView() {
|
|||||||
onCloseImageContextMenu: () => setImageContextMenu(null),
|
onCloseImageContextMenu: () => setImageContextMenu(null),
|
||||||
});
|
});
|
||||||
resetCanvasInteractionStateRef.current = clearActiveInteraction;
|
resetCanvasInteractionStateRef.current = clearActiveInteraction;
|
||||||
|
const deleteLayerByIdFromShortcut = useCallback(
|
||||||
|
(layerId: string | null) => deleteLayerByIdRef.current(layerId),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useImageCanvasKeyboardShortcuts({
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
generateDialogRef,
|
||||||
if (
|
selectedLayerIdRef,
|
||||||
(event.ctrlKey || event.metaKey) &&
|
|
||||||
event.code === 'KeyZ' &&
|
|
||||||
!isEditableTarget(event)
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (event.shiftKey) {
|
|
||||||
redoCanvasChange();
|
|
||||||
} else {
|
|
||||||
undoCanvasChange();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Shift') {
|
|
||||||
setShiftPressed(true);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(event.key === 'Backspace' || event.key === 'Delete') &&
|
|
||||||
!event.repeat &&
|
|
||||||
!isEditableTarget(event)
|
|
||||||
) {
|
|
||||||
const currentDialog = generateDialogRef.current;
|
|
||||||
const currentSelectedLayerId = selectedLayerIdRef.current;
|
|
||||||
if (currentSelectedLayerId) {
|
|
||||||
event.preventDefault();
|
|
||||||
deleteLayerByIdRef.current(currentSelectedLayerId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
currentDialog?.placeholder &&
|
|
||||||
currentDialog.status !== 'generating' &&
|
|
||||||
(currentDialog.mode === 'generate' ||
|
|
||||||
currentDialog.mode === 'spec' ||
|
|
||||||
currentDialog.mode === 'character' ||
|
|
||||||
currentDialog.mode === 'icon')
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
setGenerateDialog(null);
|
|
||||||
setActiveTool('select');
|
|
||||||
setIsCharacterSpecMenuOpen(false);
|
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
|
||||||
setIsIconSpecMenuOpen(false);
|
|
||||||
setIsPickingIconSpecFromCanvas(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
closeEditorChromePanels();
|
|
||||||
setIsSpecMenuOpen(false);
|
|
||||||
setImageContextMenu(null);
|
|
||||||
setContextMenu(null);
|
|
||||||
setQuickEditPanel((currentPanel) =>
|
|
||||||
currentPanel?.status === 'generating' ? currentPanel : null,
|
|
||||||
);
|
|
||||||
setIsCharacterSpecMenuOpen(false);
|
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
|
||||||
setIsIconSpecMenuOpen(false);
|
|
||||||
setIsPickingIconSpecFromCanvas(false);
|
|
||||||
setGenerateDialog((currentDialog) => {
|
|
||||||
if (!currentDialog || currentDialog.status === 'generating') {
|
|
||||||
return currentDialog;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
currentDialog.mode === 'generate' ||
|
|
||||||
currentDialog.mode === 'spec'
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...currentDialog,
|
|
||||||
composerOpen: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (currentDialog.mode === 'character') {
|
|
||||||
return currentDialog;
|
|
||||||
}
|
|
||||||
if (currentDialog.mode === 'icon') {
|
|
||||||
return currentDialog;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
window.addEventListener('keyup', handleKeyUp);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
closeEditorChromePanels,
|
|
||||||
redoCanvasChange,
|
redoCanvasChange,
|
||||||
|
undoCanvasChange,
|
||||||
|
deleteLayerById: deleteLayerByIdFromShortcut,
|
||||||
|
setActiveTool,
|
||||||
|
setGenerateDialog,
|
||||||
|
setImageContextMenu,
|
||||||
|
setContextMenu,
|
||||||
|
setQuickEditPanel,
|
||||||
|
closeEditorChromePanels,
|
||||||
|
setIsSpecMenuOpen,
|
||||||
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsIconSpecMenuOpen,
|
||||||
|
setIsPickingIconSpecFromCanvas,
|
||||||
setIsSpacePanning,
|
setIsSpacePanning,
|
||||||
setShiftPressed,
|
setShiftPressed,
|
||||||
undoCanvasChange,
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const blockBrowserZoom = (event: WheelEvent) => {
|
const blockBrowserZoom = (event: WheelEvent) => {
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CanvasTool,
|
||||||
|
GenerateDialogState,
|
||||||
|
QuickEditPanelState,
|
||||||
|
} from './ImageCanvasEditorTypes';
|
||||||
|
import { useImageCanvasKeyboardShortcuts } from './useImageCanvasKeyboardShortcuts';
|
||||||
|
|
||||||
|
function createPlaceholderDialog(
|
||||||
|
mode: GenerateDialogState['mode'] = 'generate',
|
||||||
|
): GenerateDialogState {
|
||||||
|
return {
|
||||||
|
id: 'dialog-1',
|
||||||
|
mode,
|
||||||
|
prompt: '生成一张图片',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
placeholder: {
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
originalWidth: 320,
|
||||||
|
originalHeight: 240,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyGenerateDialogUpdater(
|
||||||
|
updater:
|
||||||
|
| GenerateDialogState
|
||||||
|
| null
|
||||||
|
| ((dialog: GenerateDialogState | null) => GenerateDialogState | null),
|
||||||
|
currentDialog: GenerateDialogState | null,
|
||||||
|
) {
|
||||||
|
return typeof updater === 'function' ? updater(currentDialog) : updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyQuickEditPanelUpdater(
|
||||||
|
updater:
|
||||||
|
| QuickEditPanelState
|
||||||
|
| null
|
||||||
|
| ((panel: QuickEditPanelState | null) => QuickEditPanelState | null),
|
||||||
|
currentPanel: QuickEditPanelState | null,
|
||||||
|
) {
|
||||||
|
return typeof updater === 'function' ? updater(currentPanel) : updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
function KeyboardShortcutsHarness({
|
||||||
|
selectedLayerId = null,
|
||||||
|
initialGenerateDialog = null,
|
||||||
|
initialQuickEditPanel = null,
|
||||||
|
initialTool = 'select',
|
||||||
|
undoCanvasChange = vi.fn(),
|
||||||
|
redoCanvasChange = vi.fn(),
|
||||||
|
deleteLayerById = vi.fn(),
|
||||||
|
closeEditorChromePanels = vi.fn(),
|
||||||
|
}: {
|
||||||
|
selectedLayerId?: string | null;
|
||||||
|
initialGenerateDialog?: GenerateDialogState | null;
|
||||||
|
initialQuickEditPanel?: QuickEditPanelState | null;
|
||||||
|
initialTool?: CanvasTool;
|
||||||
|
undoCanvasChange?: () => void;
|
||||||
|
redoCanvasChange?: () => void;
|
||||||
|
deleteLayerById?: (layerId: string | null) => void;
|
||||||
|
closeEditorChromePanels?: () => void;
|
||||||
|
}) {
|
||||||
|
const [activeTool, setActiveTool] = useState<CanvasTool>(initialTool);
|
||||||
|
const [generateDialog, setGenerateDialogState] =
|
||||||
|
useState<GenerateDialogState | null>(initialGenerateDialog);
|
||||||
|
const [quickEditPanel, setQuickEditPanelState] =
|
||||||
|
useState<QuickEditPanelState | null>(initialQuickEditPanel);
|
||||||
|
const [imageContextMenuOpen, setImageContextMenuOpen] = useState(true);
|
||||||
|
const [contextMenuOpen, setContextMenuOpen] = useState(true);
|
||||||
|
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(true);
|
||||||
|
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(true);
|
||||||
|
const [isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas] =
|
||||||
|
useState(true);
|
||||||
|
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(true);
|
||||||
|
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
||||||
|
useState(true);
|
||||||
|
const [isSpacePanning, setIsSpacePanning] = useState(false);
|
||||||
|
const [shiftPressed, setShiftPressed] = useState(false);
|
||||||
|
const generateDialogRef = useRef<GenerateDialogState | null>(generateDialog);
|
||||||
|
const selectedLayerIdRef = useRef<string | null>(selectedLayerId);
|
||||||
|
generateDialogRef.current = generateDialog;
|
||||||
|
selectedLayerIdRef.current = selectedLayerId;
|
||||||
|
|
||||||
|
useImageCanvasKeyboardShortcuts({
|
||||||
|
generateDialogRef,
|
||||||
|
selectedLayerIdRef,
|
||||||
|
redoCanvasChange,
|
||||||
|
undoCanvasChange,
|
||||||
|
deleteLayerById,
|
||||||
|
setActiveTool: (tool) => setActiveTool(tool),
|
||||||
|
setGenerateDialog: (updater) =>
|
||||||
|
setGenerateDialogState((currentDialog) =>
|
||||||
|
applyGenerateDialogUpdater(updater, currentDialog),
|
||||||
|
),
|
||||||
|
setImageContextMenu: () => setImageContextMenuOpen(false),
|
||||||
|
setContextMenu: () => setContextMenuOpen(false),
|
||||||
|
setQuickEditPanel: (updater) =>
|
||||||
|
setQuickEditPanelState((currentPanel) =>
|
||||||
|
applyQuickEditPanelUpdater(updater, currentPanel),
|
||||||
|
),
|
||||||
|
closeEditorChromePanels,
|
||||||
|
setIsSpecMenuOpen,
|
||||||
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsIconSpecMenuOpen,
|
||||||
|
setIsPickingIconSpecFromCanvas,
|
||||||
|
setIsSpacePanning,
|
||||||
|
setShiftPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input aria-label="快捷键输入框" />
|
||||||
|
<span data-testid="active-tool">{activeTool}</span>
|
||||||
|
<span data-testid="generate-dialog">
|
||||||
|
{generateDialog
|
||||||
|
? `${generateDialog.mode}:${generateDialog.status}:${String(
|
||||||
|
generateDialog.composerOpen,
|
||||||
|
)}`
|
||||||
|
: 'none'}
|
||||||
|
</span>
|
||||||
|
<span data-testid="quick-edit">
|
||||||
|
{quickEditPanel ? quickEditPanel.status : 'none'}
|
||||||
|
</span>
|
||||||
|
<span data-testid="image-context">{String(imageContextMenuOpen)}</span>
|
||||||
|
<span data-testid="context-menu">{String(contextMenuOpen)}</span>
|
||||||
|
<span data-testid="spec-menu">{String(isSpecMenuOpen)}</span>
|
||||||
|
<span data-testid="character-menu">
|
||||||
|
{String(isCharacterSpecMenuOpen)}
|
||||||
|
</span>
|
||||||
|
<span data-testid="character-picking">
|
||||||
|
{String(isPickingCharacterSpecFromCanvas)}
|
||||||
|
</span>
|
||||||
|
<span data-testid="icon-menu">{String(isIconSpecMenuOpen)}</span>
|
||||||
|
<span data-testid="icon-picking">
|
||||||
|
{String(isPickingIconSpecFromCanvas)}
|
||||||
|
</span>
|
||||||
|
<span data-testid="space-panning">{String(isSpacePanning)}</span>
|
||||||
|
<span data-testid="shift-pressed">{String(shiftPressed)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useImageCanvasKeyboardShortcuts', () => {
|
||||||
|
it('routes undo and redo shortcuts while ignoring editable inputs', () => {
|
||||||
|
const undoCanvasChange = vi.fn();
|
||||||
|
const redoCanvasChange = vi.fn();
|
||||||
|
render(
|
||||||
|
<KeyboardShortcutsHarness
|
||||||
|
undoCanvasChange={undoCanvasChange}
|
||||||
|
redoCanvasChange={redoCanvasChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, { key: 'z', code: 'KeyZ', ctrlKey: true });
|
||||||
|
});
|
||||||
|
expect(undoCanvasChange).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, {
|
||||||
|
key: 'Z',
|
||||||
|
code: 'KeyZ',
|
||||||
|
ctrlKey: true,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(redoCanvasChange).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(screen.getByLabelText('快捷键输入框'), {
|
||||||
|
key: 'z',
|
||||||
|
code: 'KeyZ',
|
||||||
|
ctrlKey: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(undoCanvasChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes the selected layer with Backspace outside editable inputs', () => {
|
||||||
|
const deleteLayerById = vi.fn();
|
||||||
|
render(
|
||||||
|
<KeyboardShortcutsHarness
|
||||||
|
selectedLayerId="layer-selected"
|
||||||
|
deleteLayerById={deleteLayerById}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, { key: 'Backspace', code: 'Backspace' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteLayerById).toHaveBeenCalledWith('layer-selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes editable generation placeholders with Backspace', () => {
|
||||||
|
render(
|
||||||
|
<KeyboardShortcutsHarness
|
||||||
|
initialTool="character"
|
||||||
|
initialGenerateDialog={createPlaceholderDialog('character')}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, { key: 'Backspace', code: 'Backspace' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('generate-dialog').textContent).toBe('none');
|
||||||
|
expect(screen.getByTestId('active-tool').textContent).toBe('select');
|
||||||
|
expect(screen.getByTestId('character-menu').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('character-picking').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('icon-menu').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('icon-picking').textContent).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes transient editor panels with Escape and collapses idle composers', () => {
|
||||||
|
const closeEditorChromePanels = vi.fn();
|
||||||
|
render(
|
||||||
|
<KeyboardShortcutsHarness
|
||||||
|
closeEditorChromePanels={closeEditorChromePanels}
|
||||||
|
initialGenerateDialog={createPlaceholderDialog('generate')}
|
||||||
|
initialQuickEditPanel={{
|
||||||
|
sourceLayerId: 'layer-source',
|
||||||
|
prompt: '局部修改',
|
||||||
|
size: '1024x1024',
|
||||||
|
model: 'mock',
|
||||||
|
status: 'idle',
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, { key: 'Escape', code: 'Escape' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(closeEditorChromePanels).toHaveBeenCalledTimes(1);
|
||||||
|
expect(screen.getByTestId('generate-dialog').textContent).toBe(
|
||||||
|
'generate:idle:false',
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('quick-edit').textContent).toBe('none');
|
||||||
|
expect(screen.getByTestId('image-context').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('context-menu').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('spec-menu').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('character-menu').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('icon-menu').textContent).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps generating panels open when Escape closes transient chrome', () => {
|
||||||
|
const generatingDialog = createPlaceholderDialog('generate');
|
||||||
|
generatingDialog.status = 'generating';
|
||||||
|
render(
|
||||||
|
<KeyboardShortcutsHarness
|
||||||
|
initialGenerateDialog={generatingDialog}
|
||||||
|
initialQuickEditPanel={{
|
||||||
|
sourceLayerId: 'layer-source',
|
||||||
|
prompt: '继续修改',
|
||||||
|
size: '1024x1024',
|
||||||
|
model: 'mock',
|
||||||
|
status: 'generating',
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, { key: 'Escape', code: 'Escape' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('generate-dialog').textContent).toBe(
|
||||||
|
'generate:generating:true',
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('quick-edit').textContent).toBe('generating');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles temporary panning with Space outside editable inputs', () => {
|
||||||
|
render(<KeyboardShortcutsHarness />);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, { key: ' ', code: 'Space' });
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('space-panning').textContent).toBe('true');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyUp(window, { key: ' ', code: 'Space' });
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('space-panning').textContent).toBe('false');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(screen.getByLabelText('快捷键输入框'), {
|
||||||
|
key: ' ',
|
||||||
|
code: 'Space',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('space-panning').textContent).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks Shift key state', () => {
|
||||||
|
render(<KeyboardShortcutsHarness />);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyDown(window, { key: 'Shift', code: 'ShiftLeft' });
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('shift-pressed').textContent).toBe('true');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.keyUp(window, { key: 'Shift', code: 'ShiftLeft' });
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('shift-pressed').textContent).toBe('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
205
src/components/image-editor/useImageCanvasKeyboardShortcuts.ts
Normal file
205
src/components/image-editor/useImageCanvasKeyboardShortcuts.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { type RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
GenerateDialogState,
|
||||||
|
QuickEditPanelState,
|
||||||
|
} from './ImageCanvasEditorTypes';
|
||||||
|
|
||||||
|
type UseImageCanvasKeyboardShortcutsOptions = {
|
||||||
|
generateDialogRef: RefObject<GenerateDialogState | null>;
|
||||||
|
selectedLayerIdRef: RefObject<string | null>;
|
||||||
|
redoCanvasChange: () => void;
|
||||||
|
undoCanvasChange: () => void;
|
||||||
|
deleteLayerById: (layerId: string | null) => void;
|
||||||
|
setActiveTool: (tool: 'select') => 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;
|
||||||
|
setIsCharacterSpecMenuOpen: (open: boolean) => void;
|
||||||
|
setIsPickingCharacterSpecFromCanvas: (picking: boolean) => void;
|
||||||
|
setIsIconSpecMenuOpen: (open: boolean) => void;
|
||||||
|
setIsPickingIconSpecFromCanvas: (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;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
target.tagName === 'INPUT' ||
|
||||||
|
target.tagName === 'TEXTAREA' ||
|
||||||
|
target.isContentEditable
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCanvasGenerationPlaceholderDialog(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
Boolean(dialog?.placeholder) &&
|
||||||
|
dialog?.status !== 'generating' &&
|
||||||
|
(dialog?.mode === 'generate' ||
|
||||||
|
dialog?.mode === 'spec' ||
|
||||||
|
dialog?.mode === 'character' ||
|
||||||
|
dialog?.mode === 'icon')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImageCanvasKeyboardShortcuts({
|
||||||
|
generateDialogRef,
|
||||||
|
selectedLayerIdRef,
|
||||||
|
redoCanvasChange,
|
||||||
|
undoCanvasChange,
|
||||||
|
deleteLayerById,
|
||||||
|
setActiveTool,
|
||||||
|
setGenerateDialog,
|
||||||
|
setImageContextMenu,
|
||||||
|
setContextMenu,
|
||||||
|
setQuickEditPanel,
|
||||||
|
closeEditorChromePanels,
|
||||||
|
setIsSpecMenuOpen,
|
||||||
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsIconSpecMenuOpen,
|
||||||
|
setIsPickingIconSpecFromCanvas,
|
||||||
|
setIsSpacePanning,
|
||||||
|
setShiftPressed,
|
||||||
|
}: UseImageCanvasKeyboardShortcutsOptions) {
|
||||||
|
useEffect(() => {
|
||||||
|
const closeTransientEditorPanels = () => {
|
||||||
|
closeEditorChromePanels();
|
||||||
|
setIsSpecMenuOpen(false);
|
||||||
|
setImageContextMenu(null);
|
||||||
|
setContextMenu(null);
|
||||||
|
setQuickEditPanel((currentPanel) =>
|
||||||
|
currentPanel?.status === 'generating' ? currentPanel : null,
|
||||||
|
);
|
||||||
|
setIsCharacterSpecMenuOpen(false);
|
||||||
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
|
setIsIconSpecMenuOpen(false);
|
||||||
|
setIsPickingIconSpecFromCanvas(false);
|
||||||
|
setGenerateDialog((currentDialog) => {
|
||||||
|
if (!currentDialog || currentDialog.status === 'generating') {
|
||||||
|
return currentDialog;
|
||||||
|
}
|
||||||
|
if (currentDialog.mode === 'generate' || currentDialog.mode === 'spec') {
|
||||||
|
return {
|
||||||
|
...currentDialog,
|
||||||
|
composerOpen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (currentDialog.mode === 'character') {
|
||||||
|
return currentDialog;
|
||||||
|
}
|
||||||
|
if (currentDialog.mode === 'icon') {
|
||||||
|
return currentDialog;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
event.code === 'KeyZ' &&
|
||||||
|
!isEditableTarget(event)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.shiftKey) {
|
||||||
|
redoCanvasChange();
|
||||||
|
} else {
|
||||||
|
undoCanvasChange();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
setShiftPressed(true);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(event.key === 'Backspace' || event.key === 'Delete') &&
|
||||||
|
!event.repeat &&
|
||||||
|
!isEditableTarget(event)
|
||||||
|
) {
|
||||||
|
const currentSelectedLayerId = selectedLayerIdRef.current;
|
||||||
|
if (currentSelectedLayerId) {
|
||||||
|
event.preventDefault();
|
||||||
|
deleteLayerById(currentSelectedLayerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isCanvasGenerationPlaceholderDialog(generateDialogRef.current)) {
|
||||||
|
event.preventDefault();
|
||||||
|
setGenerateDialog(null);
|
||||||
|
setActiveTool('select');
|
||||||
|
setIsCharacterSpecMenuOpen(false);
|
||||||
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
|
setIsIconSpecMenuOpen(false);
|
||||||
|
setIsPickingIconSpecFromCanvas(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeTransientEditorPanels();
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
window.addEventListener('keyup', handleKeyUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
closeEditorChromePanels,
|
||||||
|
deleteLayerById,
|
||||||
|
generateDialogRef,
|
||||||
|
redoCanvasChange,
|
||||||
|
selectedLayerIdRef,
|
||||||
|
setActiveTool,
|
||||||
|
setContextMenu,
|
||||||
|
setGenerateDialog,
|
||||||
|
setImageContextMenu,
|
||||||
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsIconSpecMenuOpen,
|
||||||
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsPickingIconSpecFromCanvas,
|
||||||
|
setIsSpacePanning,
|
||||||
|
setIsSpecMenuOpen,
|
||||||
|
setQuickEditPanel,
|
||||||
|
setShiftPressed,
|
||||||
|
undoCanvasChange,
|
||||||
|
]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user