修复画布右键菜单与复制粘贴
统一右键图层命令后关闭画布菜单和图片菜单 补齐快速编辑入口的菜单关闭逻辑 让 Ctrl+V 优先粘贴画布内部剪贴板 补充右键菜单、图层命令和快捷键回归测试
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,7 +286,11 @@ export function ImageCanvasContextMenusView({
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => onOpenQuickEditPanel(imageContextMenuLayer)}
|
||||
onClick={() => {
|
||||
onOpenQuickEditPanel(imageContextMenuLayer);
|
||||
onCloseContextMenu();
|
||||
onCloseImageContextMenu();
|
||||
}}
|
||||
>
|
||||
快速编辑
|
||||
</button>
|
||||
@@ -342,7 +346,10 @@ export function ImageCanvasContextMenusView({
|
||||
<PlatformFloatingMenu label="图片功能面板" placement="bottom-start">
|
||||
<PlatformFloatingMenuItem
|
||||
className="image-canvas-editor__context-menu-item"
|
||||
onClick={() => onOpenQuickEditPanel(imageContextMenuLayer)}
|
||||
onClick={() => {
|
||||
onOpenQuickEditPanel(imageContextMenuLayer);
|
||||
onCloseImageContextMenu();
|
||||
}}
|
||||
>
|
||||
快速编辑
|
||||
</PlatformFloatingMenuItem>
|
||||
|
||||
@@ -1225,6 +1225,7 @@ export function ImageCanvasEditorView() {
|
||||
selectAllCanvasObjects,
|
||||
clearCanvasSelection,
|
||||
copySelectedCanvasLayers: copySelectedLayers,
|
||||
pasteCanvasClipboard,
|
||||
duplicateSelectedCanvasLayers: duplicateSelectedLayers,
|
||||
moveSelectedCanvasLayers: moveSelectedLayers,
|
||||
nudgeSelectedCanvasObjects,
|
||||
|
||||
@@ -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(
|
||||
<KeyboardShortcutsHarness
|
||||
selectAllCanvasObjects={selectAllCanvasObjects}
|
||||
copySelectedCanvasLayers={copySelectedCanvasLayers}
|
||||
pasteCanvasClipboard={pasteCanvasClipboard}
|
||||
duplicateSelectedCanvasLayers={duplicateSelectedCanvasLayers}
|
||||
exportCanvasAssets={exportCanvasAssets}
|
||||
/>,
|
||||
@@ -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(
|
||||
<KeyboardShortcutsHarness pasteCanvasClipboard={pasteCanvasClipboard} />,
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -85,6 +85,10 @@ export function useImageCanvasLayerCommands({
|
||||
const [canvasClipboard, setCanvasClipboard] =
|
||||
useState<CanvasClipboard | null>(null);
|
||||
const canvasClipboardRef = useRef<CanvasClipboard | null>(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,
|
||||
|
||||
Reference in New Issue
Block a user