Port shortcut definitions from Fabric.js to PixiJS SceneManager/SelectionManager

Replace all Fabric.js Canvas references with SceneManager, SelectionManager,
UndoManager, and Viewport. Remove withBreak/breakSelection helper (no longer
needed without ActiveSelection). Implement z-swap layer ordering for ] and [
keys, clone-based paste/duplicate via SceneManager._createItem, and direct
history.undo()/redo() calls instead of undoRef indirection.
This commit is contained in:
Hiren Kangad
2026-03-09 23:23:10 +05:30
parent 9c5056d11e
commit edcac21983
2 changed files with 206 additions and 152 deletions
+195 -143
View File
@@ -2,29 +2,20 @@
* All shortcut definitions — the single source of truth for keyboard shortcuts.
* Used by the keyboard handler AND the ShortcutsHelp overlay.
*
* BUGS FIXED in this revision:
* Ported to PixiJS: uses SceneManager/SelectionManager instead of Fabric.js Canvas.
*
* BUGS FIXED in previous revision (preserved):
* - Ctrl+Y conflict (was both redo AND overlay-compare) → overlay moved to Ctrl+Shift+Y
* - Ctrl+P (browser print) → changed to Ctrl+Shift+P for arrange optimal
* - '?' key (Shift+/) → e.key is '?' not '/', fixed matcher key
* - ArrowUp/Down with selection → layer ordering uses ] / [ again (arrows cycle/nudge)
* - ArrowLeft/Right bare → only cycle when nothing selected, otherwise no-op
* - ActiveSelection coordinates → use withBreak() for position-dependent ops
* - Added Ctrl+S (prevent browser save-as)
*/
import { ShortcutDef } from './shortcuts';
import { FabricObject } from 'fabric';
import * as ops from './operations';
/** Helper: break ActiveSelection, run operation on canvas-level coords, restore selection */
function withBreak(ctx: { canvas: any; onCanvasChange: () => void }, fn: (objs: FabricObject[]) => void) {
const { objects, restore } = ops.breakSelection(ctx.canvas);
fn(objects);
restore();
ctx.canvas.requestRenderAll();
ctx.onCanvasChange();
}
export const shortcuts: ShortcutDef[] = [
// ═══════════════════════════════════════
@@ -34,22 +25,34 @@ export const shortcuts: ShortcutDef[] = [
{
id: 'align-left', keys: { key: 'arrowleft', ctrl: true },
category: 'alignment', description: 'Align left', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.alignLeft(objs)),
handler: (ctx) => {
ops.alignLeft(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'align-right', keys: { key: 'arrowright', ctrl: true },
category: 'alignment', description: 'Align right', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.alignRight(objs)),
handler: (ctx) => {
ops.alignRight(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'align-top', keys: { key: 'arrowup', ctrl: true },
category: 'alignment', description: 'Align top', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.alignTop(objs)),
handler: (ctx) => {
ops.alignTop(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'align-bottom', keys: { key: 'arrowdown', ctrl: true },
category: 'alignment', description: 'Align bottom', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.alignBottom(objs)),
handler: (ctx) => {
ops.alignBottom(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
// ═══════════════════════════════════════
@@ -59,12 +62,18 @@ export const shortcuts: ShortcutDef[] = [
{
id: 'distribute-h', keys: { key: 'arrowup', ctrl: true, alt: true, shift: true },
category: 'alignment', description: 'Distribute horizontal', needsSelection: true, minSelection: 3,
handler: (ctx) => withBreak(ctx, (objs) => ops.distributeHorizontal(objs)),
handler: (ctx) => {
ops.distributeHorizontal(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'distribute-v', keys: { key: 'arrowdown', ctrl: true, alt: true, shift: true },
category: 'alignment', description: 'Distribute vertical', needsSelection: true, minSelection: 3,
handler: (ctx) => withBreak(ctx, (objs) => ops.distributeVertical(objs)),
handler: (ctx) => {
ops.distributeVertical(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
// ═══════════════════════════════════════
@@ -74,58 +83,79 @@ export const shortcuts: ShortcutDef[] = [
{
id: 'normalize-size', keys: { key: 'arrowup', ctrl: true, alt: true },
category: 'normalize', description: 'Normalize size (same area)', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeSize(objs)),
handler: (ctx) => {
ops.normalizeSize(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'normalize-scale', keys: { key: 'arrowdown', ctrl: true, alt: true },
category: 'normalize', description: 'Normalize scale', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeScale(objs)),
handler: (ctx) => {
ops.normalizeScale(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'normalize-height', keys: { key: 'arrowleft', ctrl: true, alt: true },
category: 'normalize', description: 'Normalize height', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeHeight(objs)),
handler: (ctx) => {
ops.normalizeHeight(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'normalize-width', keys: { key: 'arrowright', ctrl: true, alt: true },
category: 'normalize', description: 'Normalize width', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeWidth(objs)),
handler: (ctx) => {
ops.normalizeWidth(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
// ═══════════════════════════════════════
// ARRANGEMENT
// ═══════════════════════════════════════
// FIX: Changed from Ctrl+P (browser print) to Ctrl+Shift+P
{
id: 'arrange-optimal', keys: { key: 'p', ctrl: true, shift: true },
category: 'arrangement', description: 'Arrange optimal (pack)', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.arrangeOptimal(objs)),
handler: (ctx) => {
ops.arrangeOptimal(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'arrange-by-name', keys: { key: 'n', ctrl: true, alt: true },
category: 'arrangement', description: 'Arrange by name', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.arrangeByName(objs)),
handler: (ctx) => {
ops.arrangeByName(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'arrange-by-order', keys: { key: 'o', ctrl: true, alt: true },
category: 'arrangement', description: 'Arrange by z-order', needsSelection: true, minSelection: 2,
handler: (ctx) => {
const { objects, restore } = ops.breakSelection(ctx.canvas);
ops.arrangeByZOrder(objects, ctx.canvas.getObjects());
restore();
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
ops.arrangeByZOrder(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'arrange-random', keys: { key: 'r', ctrl: true, alt: true },
category: 'arrangement', description: 'Arrange randomly', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.arrangeRandomly(objs)),
handler: (ctx) => {
ops.arrangeRandomly(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'stack', keys: { key: 's', ctrl: true, alt: true },
category: 'arrangement', description: 'Stack (pile on top)', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.stackObjects(objs)),
handler: (ctx) => {
ops.stackObjects(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
// ═══════════════════════════════════════
@@ -136,47 +166,49 @@ export const shortcuts: ShortcutDef[] = [
id: 'flip-h', keys: { key: 'h', alt: true, shift: true },
category: 'image', description: 'Flip horizontal', needsSelection: true,
handler: (ctx) => {
ops.flipHorizontal(ctx.getActiveObjects());
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
ops.flipHorizontal(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'flip-v', keys: { key: 'v', alt: true, shift: true },
category: 'image', description: 'Flip vertical', needsSelection: true,
handler: (ctx) => {
ops.flipVertical(ctx.getActiveObjects());
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
ops.flipVertical(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'reset-transform', keys: { key: 't', ctrl: true, shift: true },
category: 'image', description: 'Reset transform', needsSelection: true,
handler: (ctx) => {
ops.resetTransform(ctx.getActiveObjects());
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
ops.resetTransform(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'toggle-grayscale', keys: { key: 'g', alt: true },
category: 'image', description: 'Toggle grayscale', needsSelection: true,
handler: (ctx) => {
ops.toggleGrayscale(ctx.getActiveObjects());
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
ops.toggleGrayscale(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
{
id: 'toggle-locked', keys: { key: 'l', alt: true },
category: 'image', description: 'Toggle locked', needsSelection: true,
handler: (ctx) => {
ops.toggleLocked(ctx.getActiveObjects());
ctx.canvas.requestRenderAll(); ctx.refreshLayers();
ops.toggleLocked(ctx.selection.getSelectedItems());
ctx.refreshLayers();
},
},
// FIX: Changed from Ctrl+Y (conflicts with redo) to Ctrl+Shift+Y
{
id: 'overlay-compare', keys: { key: 'y', ctrl: true, shift: true },
category: 'image', description: 'Overlay / compare', needsSelection: true, minSelection: 2,
handler: (ctx) => withBreak(ctx, (objs) => ops.overlayCompare(objs)),
handler: (ctx) => {
ops.overlayCompare(ctx.selection.getSelectedItems());
ctx.onChange();
},
},
// ═══════════════════════════════════════
@@ -187,8 +219,7 @@ export const shortcuts: ShortcutDef[] = [
id: 'clear-selection', keys: { key: 'escape' },
category: 'navigation', description: 'Clear selection',
handler: (ctx) => {
ctx.canvas.discardActiveObject();
ctx.canvas.requestRenderAll();
ctx.selection.clear();
},
},
{
@@ -196,40 +227,51 @@ export const shortcuts: ShortcutDef[] = [
category: 'navigation', description: 'Fit all in view',
handler: (ctx) => ctx.fitAll(),
},
// FIX: Bare arrow Left/Right only cycle when nothing is selected.
// Bare arrow Left/Right only cycle when nothing is selected.
// When something IS selected, they're no-ops (prevent accidental cycling).
// Layer ordering uses ] / [ (restored) to avoid arrow conflicts.
// Layer ordering uses ] / [ to avoid arrow conflicts.
{
id: 'cycle-next', keys: { key: 'arrowright' },
category: 'navigation', description: 'Select next object',
handler: (ctx) => {
// Only cycle when nothing selected — if selected, do nothing
if (ctx.getActiveObjects().length > 0) return;
const objects = ctx.canvas.getObjects();
if (objects.length === 0) return;
ctx.canvas.setActiveObject(objects[0]);
ctx.canvas.requestRenderAll();
if (ctx.selection.selectedIds.size > 0) return;
const all = ctx.scene.getAllItems();
if (all.length === 0) return;
// Sort by z to get consistent order
all.sort((a, b) => a.data.z - b.data.z);
ctx.selection.selectOnly(all[0].id);
},
},
{
id: 'cycle-prev', keys: { key: 'arrowleft' },
category: 'navigation', description: 'Select previous object',
handler: (ctx) => {
if (ctx.getActiveObjects().length > 0) return;
const objects = ctx.canvas.getObjects();
if (objects.length === 0) return;
ctx.canvas.setActiveObject(objects[objects.length - 1]);
ctx.canvas.requestRenderAll();
if (ctx.selection.selectedIds.size > 0) return;
const all = ctx.scene.getAllItems();
if (all.length === 0) return;
all.sort((a, b) => a.data.z - b.data.z);
ctx.selection.selectOnly(all[all.length - 1].id);
},
},
// FIX: Layer ordering restored to ] / [ (not arrow keys — those conflict with cycle/nudge)
// Layer ordering: ] brings forward, [ sends backward
{
id: 'send-to-front', keys: { key: ']' },
category: 'navigation', description: 'Bring forward',
needsSelection: true,
handler: (ctx) => {
ctx.getActiveObjects().forEach((obj) => (ctx.canvas as any).bringObjectForward(obj));
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
const selected = ctx.selection.getSelectedItems();
const all = ctx.scene.getAllItems().sort((a, b) => a.data.z - b.data.z);
// For each selected item, swap z with the next non-selected item above it
const selectedIds = new Set(selected.map((s) => s.id));
for (let i = all.length - 2; i >= 0; i--) {
if (selectedIds.has(all[i].id) && !selectedIds.has(all[i + 1].id)) {
const tmp = all[i].data.z;
all[i].data.z = all[i + 1].data.z;
all[i + 1].data.z = tmp;
}
}
ctx.scene._applyZOrder();
ctx.onChange();
},
},
{
@@ -237,8 +279,18 @@ export const shortcuts: ShortcutDef[] = [
category: 'navigation', description: 'Send backward',
needsSelection: true,
handler: (ctx) => {
ctx.getActiveObjects().forEach((obj) => (ctx.canvas as any).sendObjectBackwards(obj));
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
const selected = ctx.selection.getSelectedItems();
const all = ctx.scene.getAllItems().sort((a, b) => a.data.z - b.data.z);
const selectedIds = new Set(selected.map((s) => s.id));
for (let i = 1; i < all.length; i++) {
if (selectedIds.has(all[i].id) && !selectedIds.has(all[i - 1].id)) {
const tmp = all[i].data.z;
all[i].data.z = all[i - 1].data.z;
all[i - 1].data.z = tmp;
}
}
ctx.scene._applyZOrder();
ctx.onChange();
},
},
@@ -250,23 +302,17 @@ export const shortcuts: ShortcutDef[] = [
id: 'select-all', keys: { key: 'a', ctrl: true },
category: 'editing', description: 'Select all',
handler: (ctx) => {
ctx.canvas.discardActiveObject();
const fabricNs = (window as any).fabric;
if (fabricNs?.ActiveSelection) {
const sel = new fabricNs.ActiveSelection(ctx.canvas.getObjects(), { canvas: ctx.canvas });
ctx.canvas.setActiveObject(sel);
}
ctx.canvas.requestRenderAll();
ctx.selection.selectAll();
},
},
{
id: 'copy', keys: { key: 'c', ctrl: true },
category: 'editing', description: 'Copy',
handler: (ctx) => {
const active = ctx.getActiveObjects();
if (active.length > 0) {
ctx.clipboardRef.current = [...active];
ctx.writeCanvasToClipboard(active);
const selected = ctx.selection.getSelectedItems();
if (selected.length > 0) {
ctx.clipboardRef.current = [...selected];
ctx.writeCanvasToClipboard(selected);
ctx.showToast('Copied');
} else {
ctx.writeCanvasToClipboard();
@@ -278,8 +324,8 @@ export const shortcuts: ShortcutDef[] = [
id: 'copy-as-image', keys: { key: 'c', ctrl: true, shift: true },
category: 'editing', description: 'Copy as image to clipboard',
handler: (ctx) => {
const active = ctx.getActiveObjects();
ctx.writeCanvasToClipboard(active.length > 0 ? active : undefined);
const selected = ctx.selection.getSelectedItems();
ctx.writeCanvasToClipboard(selected.length > 0 ? selected : undefined);
},
},
{
@@ -287,33 +333,38 @@ export const shortcuts: ShortcutDef[] = [
category: 'editing', description: 'Paste',
handler: async (ctx) => {
if (ctx.clipboardRef.current.length === 0) return;
const newObjs: FabricObject[] = [];
const newItems: typeof ctx.clipboardRef.current = [];
for (const original of ctx.clipboardRef.current) {
try {
const obj = await ctx.cloneFabricObject(original, 20, 20);
ctx.canvas.add(obj);
newObjs.push(obj);
} catch (err) {
console.error('Paste object failed:', err);
// Clone: duplicate the item data with new ID and offset position
const newData = {
...original.data,
id: crypto.randomUUID(),
x: original.data.x + 20,
y: original.data.y + 20,
z: ctx.scene.nextZ(),
};
await ctx.scene._createItem(newData, true);
const newItem = ctx.scene.getById(newData.id);
if (newItem) newItems.push(newItem);
}
}
ctx.clipboardRef.current = newObjs;
ctx.canvas.requestRenderAll();
ctx.onCanvasChange();
ctx.clipboardRef.current = newItems;
ctx.scene._applyZOrder();
ctx.onChange();
},
},
{
id: 'cut', keys: { key: 'x', ctrl: true },
category: 'editing', description: 'Cut',
handler: (ctx) => {
const active = ctx.getActiveObjects();
if (active.length === 0) return;
ctx.clipboardRef.current = [...active];
ctx.writeCanvasToClipboard(active);
active.forEach((obj) => ctx.canvas.remove(obj));
ctx.canvas.discardActiveObject();
ctx.canvas.requestRenderAll();
ctx.onCanvasChange();
const selected = ctx.selection.getSelectedItems();
if (selected.length === 0) return;
ctx.clipboardRef.current = [...selected];
ctx.writeCanvasToClipboard(selected);
for (const item of selected) {
ctx.scene.removeItem(item.id, true);
}
ctx.selection.clear();
ctx.onChange();
ctx.showToast('Cut');
},
},
@@ -321,19 +372,21 @@ export const shortcuts: ShortcutDef[] = [
id: 'duplicate', keys: { key: 'd', ctrl: true },
category: 'editing', description: 'Duplicate',
handler: async (ctx) => {
const active = ctx.getActiveObjects();
if (active.length === 0) return;
for (const original of active) {
try {
const obj = await ctx.cloneFabricObject(original, 20, 20);
ctx.canvas.add(obj);
} catch (err) {
console.error('Duplicate failed:', err);
const selected = ctx.selection.getSelectedItems();
if (selected.length === 0) return;
for (const original of selected) {
const newData = {
...original.data,
id: crypto.randomUUID(),
x: original.data.x + 20,
y: original.data.y + 20,
z: ctx.scene.nextZ(),
};
await ctx.scene._createItem(newData, true);
}
}
ctx.canvas.discardActiveObject();
ctx.canvas.requestRenderAll();
ctx.onCanvasChange();
ctx.selection.clear();
ctx.scene._applyZOrder();
ctx.onChange();
},
},
{
@@ -341,11 +394,12 @@ export const shortcuts: ShortcutDef[] = [
category: 'editing', description: 'Delete selected',
needsSelection: true,
handler: (ctx) => {
const active = ctx.getActiveObjects();
active.forEach((obj) => ctx.canvas.remove(obj));
ctx.canvas.discardActiveObject();
ctx.canvas.requestRenderAll();
ctx.onCanvasChange();
const selected = ctx.selection.getSelectedItems();
for (const item of selected) {
ctx.scene.removeItem(item.id, true);
}
ctx.selection.clear();
ctx.onChange();
},
},
{
@@ -353,41 +407,42 @@ export const shortcuts: ShortcutDef[] = [
category: 'editing', description: 'Delete selected',
needsSelection: true,
handler: (ctx) => {
const active = ctx.getActiveObjects();
active.forEach((obj) => ctx.canvas.remove(obj));
ctx.canvas.discardActiveObject();
ctx.canvas.requestRenderAll();
ctx.onCanvasChange();
const selected = ctx.selection.getSelectedItems();
for (const item of selected) {
ctx.scene.removeItem(item.id, true);
}
ctx.selection.clear();
ctx.onChange();
},
},
{
id: 'undo', keys: { key: 'z', ctrl: true },
category: 'editing', description: 'Undo',
handler: (ctx) => {
ctx.undoRef.current?.undo();
ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false);
ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false);
ctx.onCanvasChange();
ctx.history.undo();
ctx.setCanUndo(ctx.history.canUndo());
ctx.setCanRedo(ctx.history.canRedo());
ctx.onChange();
},
},
{
id: 'redo', keys: { key: 'z', ctrl: true, shift: true },
category: 'editing', description: 'Redo',
handler: (ctx) => {
ctx.undoRef.current?.redo();
ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false);
ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false);
ctx.onCanvasChange();
ctx.history.redo();
ctx.setCanUndo(ctx.history.canUndo());
ctx.setCanRedo(ctx.history.canRedo());
ctx.onChange();
},
},
{
id: 'redo-y', keys: { key: 'y', ctrl: true },
category: 'editing', description: 'Redo (alt)',
handler: (ctx) => {
ctx.undoRef.current?.redo();
ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false);
ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false);
ctx.onCanvasChange();
ctx.history.redo();
ctx.setCanUndo(ctx.history.canUndo());
ctx.setCanRedo(ctx.history.canRedo());
ctx.onChange();
},
},
{
@@ -400,7 +455,7 @@ export const shortcuts: ShortcutDef[] = [
category: 'editing', description: 'Ungroup', needsSelection: true,
handler: (ctx) => ctx.handleUngroup(),
},
// FIX: Block Ctrl+S from opening browser save-as dialog
// Block Ctrl+S from opening browser save-as dialog
{
id: 'save', keys: { key: 's', ctrl: true },
category: 'editing', description: 'Save (auto-saves)',
@@ -417,30 +472,27 @@ export const shortcuts: ShortcutDef[] = [
id: 'zoom-in', keys: { key: '=', ctrl: true },
category: 'view', description: 'Zoom in',
handler: (ctx) => {
const z = Math.min(ctx.canvas.getZoom() * 1.2, 5);
const center = ctx.canvas.getCenterPoint();
ctx.canvas.zoomToPoint(center, z);
ctx.canvas.requestRenderAll();
const current = ctx.viewport.scale.x;
const z = Math.min(current * 1.2, 5);
ctx.viewport.setZoom(z, true);
},
},
{
id: 'zoom-in-plus', keys: { key: '+', ctrl: true },
category: 'view', description: 'Zoom in',
handler: (ctx) => {
const z = Math.min(ctx.canvas.getZoom() * 1.2, 5);
const center = ctx.canvas.getCenterPoint();
ctx.canvas.zoomToPoint(center, z);
ctx.canvas.requestRenderAll();
const current = ctx.viewport.scale.x;
const z = Math.min(current * 1.2, 5);
ctx.viewport.setZoom(z, true);
},
},
{
id: 'zoom-out', keys: { key: '-', ctrl: true },
category: 'view', description: 'Zoom out',
handler: (ctx) => {
const z = Math.max(ctx.canvas.getZoom() / 1.2, 0.1);
const center = ctx.canvas.getCenterPoint();
ctx.canvas.zoomToPoint(center, z);
ctx.canvas.requestRenderAll();
const current = ctx.viewport.scale.x;
const z = Math.max(current / 1.2, 0.1);
ctx.viewport.setZoom(z, true);
},
},
{
@@ -448,7 +500,7 @@ export const shortcuts: ShortcutDef[] = [
category: 'view', description: 'Toggle grid',
handler: (ctx) => ctx.toggleGrid(),
},
// FIX: '?' key — when Shift+/ is pressed, e.key is '?' not '/'
// '?' key — when Shift+/ is pressed, e.key is '?' not '/'
{
id: 'show-help', keys: { key: '?' },
category: 'view', description: 'Show shortcuts help',
+11 -9
View File
@@ -3,7 +3,10 @@
* Actual shortcut definitions live in shortcut-definitions.ts.
*/
import { Canvas, FabricObject } from 'fabric';
import type { SceneManager, SceneItem } from './SceneManager';
import type { SelectionManager } from './SelectionManager';
import type { UndoManager } from './history';
import type { Viewport } from 'pixi-viewport';
import { ToolType } from './tools';
export interface ShortcutKeys {
@@ -24,17 +27,15 @@ export interface ShortcutDef {
}
export interface ShortcutContext {
canvas: Canvas;
getActiveObjects: () => FabricObject[];
getActiveObject: () => FabricObject | null;
clipboardRef: React.MutableRefObject<FabricObject[]>;
cloneFabricObject: (obj: any, dx: number, dy: number) => Promise<any>;
writeCanvasToClipboard: (objects?: FabricObject[]) => Promise<void>;
onCanvasChange: () => void;
scene: SceneManager;
selection: SelectionManager;
history: UndoManager;
viewport: Viewport;
onChange: () => void;
clipboardRef: React.MutableRefObject<SceneItem[]>;
showToast: (msg: string) => void;
fitAll: () => void;
setActiveTool: (tool: ToolType) => void;
undoRef: React.MutableRefObject<any>;
setCanUndo: (v: boolean) => void;
setCanRedo: (v: boolean) => void;
refreshLayers: () => void;
@@ -42,6 +43,7 @@ export interface ShortcutContext {
handleUngroup: () => void;
toggleGrid: () => void;
toggleShowHelp: () => void;
writeCanvasToClipboard: (items?: SceneItem[]) => Promise<void>;
}
/**