diff --git a/src/components/image-editor/ImageCanvasContextMenusView.test.tsx b/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
index 3819db426..4b05224f9 100644
--- a/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
+++ b/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
@@ -120,6 +120,7 @@ describe('ImageCanvasContextMenusView', () => {
fireEvent.click(
screen.getByRole('menuitem', { name: 'Spine 导出(zip)' }),
);
+ fireEvent.click(screen.getByRole('menuitem', { name: '快速编辑' }));
fireEvent.click(screen.getByRole('menuitem', { name: '生成动画' }));
fireEvent.click(screen.getByRole('menuitem', { name: '删除' }));
@@ -138,9 +139,10 @@ describe('ImageCanvasContextMenusView', () => {
2,
expect.objectContaining({ mode: 'spine-json' }),
);
+ expect(props.onOpenQuickEditPanel).toHaveBeenCalledWith(layer);
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
- expect(props.onCloseContextMenu).toHaveBeenCalledTimes(1);
- expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(1);
+ expect(props.onCloseContextMenu).toHaveBeenCalledTimes(2);
+ expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(2);
expect(props.onDeleteContextLayers).toHaveBeenCalledTimes(1);
});
@@ -170,7 +172,7 @@ describe('ImageCanvasContextMenusView', () => {
expect(props.onOpenQuickEditPanel).toHaveBeenCalledWith(layer);
expect(props.onOpenLayerMetadata).toHaveBeenCalledWith(layer);
- expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(1);
+ expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(2);
expect(props.onDeleteLayerById).toHaveBeenCalledWith(layer.id);
});
});
diff --git a/src/components/image-editor/ImageCanvasContextMenusView.tsx b/src/components/image-editor/ImageCanvasContextMenusView.tsx
index 99c3c5182..57d70cdd1 100644
--- a/src/components/image-editor/ImageCanvasContextMenusView.tsx
+++ b/src/components/image-editor/ImageCanvasContextMenusView.tsx
@@ -286,7 +286,11 @@ export function ImageCanvasContextMenusView({
@@ -342,7 +346,10 @@ export function ImageCanvasContextMenusView({
onOpenQuickEditPanel(imageContextMenuLayer)}
+ onClick={() => {
+ onOpenQuickEditPanel(imageContextMenuLayer);
+ onCloseImageContextMenu();
+ }}
>
快速编辑
diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx
index 6854f3cd7..32e6fa65a 100644
--- a/src/components/image-editor/ImageCanvasEditorView.tsx
+++ b/src/components/image-editor/ImageCanvasEditorView.tsx
@@ -1225,6 +1225,7 @@ export function ImageCanvasEditorView() {
selectAllCanvasObjects,
clearCanvasSelection,
copySelectedCanvasLayers: copySelectedLayers,
+ pasteCanvasClipboard,
duplicateSelectedCanvasLayers: duplicateSelectedLayers,
moveSelectedCanvasLayers: moveSelectedLayers,
nudgeSelectedCanvasObjects,
diff --git a/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx b/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx
index e6a3fbc88..22e74f157 100644
--- a/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx
+++ b/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx
@@ -65,6 +65,7 @@ function KeyboardShortcutsHarness({
selectAllCanvasObjects = vi.fn(),
clearCanvasSelection = vi.fn(),
copySelectedCanvasLayers = vi.fn(),
+ pasteCanvasClipboard = vi.fn(),
duplicateSelectedCanvasLayers = vi.fn(),
moveSelectedCanvasLayers = vi.fn(),
nudgeSelectedCanvasObjects = vi.fn(),
@@ -91,6 +92,7 @@ function KeyboardShortcutsHarness({
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;
@@ -159,6 +161,7 @@ function KeyboardShortcutsHarness({
selectAllCanvasObjects,
clearCanvasSelection,
copySelectedCanvasLayers,
+ pasteCanvasClipboard,
duplicateSelectedCanvasLayers,
moveSelectedCanvasLayers,
nudgeSelectedCanvasObjects,
@@ -287,15 +290,17 @@ describe('useImageCanvasKeyboardShortcuts', () => {
expect(undoCanvasChange).toHaveBeenCalledTimes(1);
});
- it('routes Windows edit shortcuts to selected layer commands except paste', () => {
+ it('routes Windows edit shortcuts to selected layer commands and canvas paste', () => {
const selectAllCanvasObjects = vi.fn();
const copySelectedCanvasLayers = vi.fn();
+ const pasteCanvasClipboard = vi.fn(() => true);
const duplicateSelectedCanvasLayers = vi.fn();
const exportCanvasAssets = vi.fn();
render(
,
@@ -318,10 +323,30 @@ describe('useImageCanvasKeyboardShortcuts', () => {
expect(selectAllCanvasObjects).toHaveBeenCalledTimes(1);
expect(copySelectedCanvasLayers).toHaveBeenNthCalledWith(1);
expect(copySelectedCanvasLayers).toHaveBeenNthCalledWith(2, { cut: true });
+ expect(pasteCanvasClipboard).toHaveBeenCalledTimes(1);
expect(duplicateSelectedCanvasLayers).toHaveBeenCalledTimes(1);
expect(exportCanvasAssets).toHaveBeenCalledTimes(1);
});
+ it('leaves system paste available when the canvas clipboard is empty', () => {
+ const pasteCanvasClipboard = vi.fn(() => false);
+ render(
+ ,
+ );
+
+ const pasteEvent = new KeyboardEvent('keydown', {
+ key: 'v',
+ code: 'KeyV',
+ ctrlKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ window.dispatchEvent(pasteEvent);
+
+ expect(pasteCanvasClipboard).toHaveBeenCalledTimes(1);
+ expect(pasteEvent.defaultPrevented).toBe(false);
+ });
+
it('routes Windows view and layer ordering shortcuts', () => {
const fitLayers = vi.fn();
const updateScaleFromCenter = vi.fn();
diff --git a/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts b/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts
index ba06f08ea..1a728bc2e 100644
--- a/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts
+++ b/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts
@@ -19,6 +19,7 @@ type UseImageCanvasKeyboardShortcutsOptions = {
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;
@@ -160,6 +161,7 @@ export function useImageCanvasKeyboardShortcuts({
selectAllCanvasObjects,
clearCanvasSelection,
copySelectedCanvasLayers,
+ pasteCanvasClipboard,
duplicateSelectedCanvasLayers,
moveSelectedCanvasLayers,
nudgeSelectedCanvasObjects,
@@ -275,6 +277,12 @@ export function useImageCanvasKeyboardShortcuts({
copySelectedCanvasLayers?.({ cut: true });
return;
}
+ if (event.code === 'KeyV') {
+ if (pasteCanvasClipboard?.()) {
+ event.preventDefault();
+ }
+ return;
+ }
if (event.code === 'KeyD') {
event.preventDefault();
duplicateSelectedCanvasLayers?.();
@@ -465,6 +473,7 @@ export function useImageCanvasKeyboardShortcuts({
closeEditorChromePanels,
clearCanvasSelection,
copySelectedCanvasLayers,
+ pasteCanvasClipboard,
duplicateSelectedCanvasLayers,
deleteSelectedCanvasObjects,
deleteLayerById,
diff --git a/src/components/image-editor/useImageCanvasLayerCommands.test.tsx b/src/components/image-editor/useImageCanvasLayerCommands.test.tsx
index b78863f17..c54d722e0 100644
--- a/src/components/image-editor/useImageCanvasLayerCommands.test.tsx
+++ b/src/components/image-editor/useImageCanvasLayerCommands.test.tsx
@@ -251,6 +251,7 @@ describe('useImageCanvasLayerCommands', () => {
fireEvent.click(screen.getByRole('button', { name: '复制' }));
expect(screen.getByTestId('clipboard').textContent).toBe('copy:2');
expect(screen.getByTestId('context').textContent).toBe('closed');
+ expect(screen.getByTestId('image-context-closed').textContent).toBe('1');
fireEvent.click(screen.getByRole('button', { name: '粘贴' }));
expect(screen.getByTestId('layers').textContent).toContain(
@@ -263,6 +264,7 @@ describe('useImageCanvasLayerCommands', () => {
'layer-copy-',
);
expect(screen.getByTestId('tool').textContent).toBe('select');
+ expect(screen.getByTestId('image-context-closed').textContent).toBe('2');
fireEvent.click(screen.getByRole('button', { name: '右键第三层' }));
fireEvent.click(screen.getByRole('button', { name: '只选第三层' }));
@@ -271,6 +273,7 @@ describe('useImageCanvasLayerCommands', () => {
expect(screen.getByTestId('clipboard').textContent).toBe('cut:1');
expect(screen.getByTestId('layers').textContent).not.toContain('third');
expect(screen.getByTestId('selection').textContent).toBe('-:');
+ expect(screen.getByTestId('image-context-closed').textContent).toBe('3');
expect(Number(screen.getByTestId('history').textContent)).toBeGreaterThan(
1,
);
@@ -336,7 +339,9 @@ describe('useImageCanvasLayerCommands', () => {
expect.any(Object),
);
expect(screen.getByTestId('context').textContent).toBe('closed');
- expect(screen.getByTestId('image-context-closed').textContent).toBe('1');
+ expect(Number(screen.getByTestId('image-context-closed').textContent)).toBe(
+ 5,
+ );
});
it('deletes selected and direct layers while running delete side effects', () => {
diff --git a/src/components/image-editor/useImageCanvasLayerCommands.ts b/src/components/image-editor/useImageCanvasLayerCommands.ts
index f34f4c49b..fc97dcb67 100644
--- a/src/components/image-editor/useImageCanvasLayerCommands.ts
+++ b/src/components/image-editor/useImageCanvasLayerCommands.ts
@@ -85,6 +85,10 @@ export function useImageCanvasLayerCommands({
const [canvasClipboard, setCanvasClipboard] =
useState(null);
const canvasClipboardRef = useRef(null);
+ const closeContextMenus = useCallback(() => {
+ setContextMenu(null);
+ setImageContextMenu(null);
+ }, [setContextMenu, setImageContextMenu]);
const getContextTargetLayerIds = useCallback(
(menu: CanvasContextMenuState | null = contextMenu) =>
@@ -121,13 +125,9 @@ export function useImageCanvasLayerCommands({
if (!clipboard?.layers.length) {
return false;
}
- const nextLayers = duplicateLayersToPoint(
- clipboard.layers,
- canvasPoint,
- {
- renameCopies: clipboard.mode !== 'cut',
- },
- );
+ const nextLayers = duplicateLayersToPoint(clipboard.layers, canvasPoint, {
+ renameCopies: clipboard.mode !== 'cut',
+ });
if (!nextLayers.length) {
return false;
}
@@ -136,14 +136,14 @@ export function useImageCanvasLayerCommands({
setSelectedLayerIds(nextLayers.map((layer) => layer.id));
setSelectedLayerId(nextLayers[0]?.id ?? null);
setActiveTool('select');
- setContextMenu(null);
+ closeContextMenus();
return true;
},
[
captureCanvasHistory,
+ closeContextMenus,
duplicateLayersToPoint,
setActiveTool,
- setContextMenu,
setLayers,
setSelectedLayerId,
setSelectedLayerIds,
@@ -175,14 +175,14 @@ export function useImageCanvasLayerCommands({
: currentLayer,
);
}
- setContextMenu(null);
+ closeContextMenus();
},
[
captureCanvasHistory,
+ closeContextMenus,
getContextTargetLayerIds,
layers,
selectSingleLayer,
- setContextMenu,
setLayers,
setMetadataLayer,
],
@@ -241,13 +241,13 @@ export function useImageCanvasLayerCommands({
setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
setSelectedLayerIds(nextLayers.map((layer) => layer.id));
setSelectedLayerId(nextLayers[0]?.id ?? null);
- setContextMenu(null);
+ closeContextMenus();
}, [
captureCanvasHistory,
+ closeContextMenus,
duplicateLayersToPoint,
getContextTargetLayerIds,
layers,
- setContextMenu,
setLayers,
setSelectedLayerId,
setSelectedLayerIds,
@@ -288,9 +288,14 @@ export function useImageCanvasLayerCommands({
setLayers((currentLayers) =>
updateCanvasLayersByIds(currentLayers, targetIds, updater),
);
- setContextMenu(null);
+ closeContextMenus();
},
- [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers],
+ [
+ captureCanvasHistory,
+ closeContextMenus,
+ getContextTargetLayerIds,
+ setLayers,
+ ],
);
const moveContextLayers = useCallback(
@@ -303,9 +308,14 @@ export function useImageCanvasLayerCommands({
setLayers((currentLayers) =>
moveCanvasLayers(currentLayers, targetIds, mode),
);
- setContextMenu(null);
+ closeContextMenus();
},
- [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers],
+ [
+ captureCanvasHistory,
+ closeContextMenus,
+ getContextTargetLayerIds,
+ setLayers,
+ ],
);
const moveSelectedLayers = useCallback(
@@ -339,8 +349,13 @@ export function useImageCanvasLayerCommands({
setLayers((currentLayers) =>
groupCanvasLayers(currentLayers, targetIds, createGroupId()),
);
- setContextMenu(null);
- }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
+ closeContextMenus();
+ }, [
+ captureCanvasHistory,
+ closeContextMenus,
+ getContextTargetLayerIds,
+ setLayers,
+ ]);
const ungroupContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
@@ -349,8 +364,13 @@ export function useImageCanvasLayerCommands({
}
captureCanvasHistory();
setLayers((currentLayers) => ungroupCanvasLayers(currentLayers, targetIds));
- setContextMenu(null);
- }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
+ closeContextMenus();
+ }, [
+ captureCanvasHistory,
+ closeContextMenus,
+ getContextTargetLayerIds,
+ setLayers,
+ ]);
const toggleContextLayerVisibility = useCallback(() => {
const targetIds = getContextTargetLayerIds();
@@ -361,8 +381,13 @@ export function useImageCanvasLayerCommands({
setLayers((currentLayers) =>
toggleCanvasLayersVisibility(currentLayers, targetIds),
);
- setContextMenu(null);
- }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
+ closeContextMenus();
+ }, [
+ captureCanvasHistory,
+ closeContextMenus,
+ getContextTargetLayerIds,
+ setLayers,
+ ]);
const toggleContextLayerLock = useCallback(() => {
const targetIds = getContextTargetLayerIds();
@@ -373,8 +398,13 @@ export function useImageCanvasLayerCommands({
setLayers((currentLayers) =>
toggleCanvasLayersLock(currentLayers, targetIds),
);
- setContextMenu(null);
- }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
+ closeContextMenus();
+ }, [
+ captureCanvasHistory,
+ closeContextMenus,
+ getContextTargetLayerIds,
+ setLayers,
+ ]);
const flipContextLayers = useCallback(
(axis: CanvasLayerFlipAxis) => {
@@ -386,9 +416,14 @@ export function useImageCanvasLayerCommands({
setLayers((currentLayers) =>
flipCanvasLayers(currentLayers, targetIds, axis),
);
- setContextMenu(null);
+ closeContextMenus();
},
- [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers],
+ [
+ captureCanvasHistory,
+ closeContextMenus,
+ getContextTargetLayerIds,
+ setLayers,
+ ],
);
const deleteContextLayers = useCallback(() => {
@@ -403,12 +438,12 @@ export function useImageCanvasLayerCommands({
setMetadataLayer((currentLayer) =>
currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer,
);
- setContextMenu(null);
+ closeContextMenus();
}, [
captureCanvasHistory,
+ closeContextMenus,
getContextTargetLayerIds,
selectSingleLayer,
- setContextMenu,
setHoveredLayerId,
setLayers,
setMetadataLayer,