import { ui } from '../ui'; import { createContainer, removeSelection } from './actions'; import type { EditorState } from './state'; import { groupShapes, ungroupShapes } from './vectorart/grouping'; export interface ShortcutActions { play(): void; stop(): void; togglePause(): void; /** * Copy and paste of what the visible tab edits: keys (#102), objects and graph pieces (#240). * False when there is nothing to do, so the key falls through. */ copy(): boolean; paste(): boolean; /** * Delete on a point the visible tab has picked inside the selection (#288) – a corner of a polygon * collider or of an outline, a point of a shape or a curve. False while none is, so the key takes * the selection itself. */ removePoint(): boolean; } /** * Global keyboard shortcuts (see the wiki page "Keyboard"). Text fields keep their own key handling; * these only run when no focused widget consumed the key. Returns the call that takes them away * again – signing out tears the editor down while the page stays up (#177). */ export function installShortcuts(state: EditorState, actions: ShortcutActions): () => void { return ui().focus.onGlobalKey((event) => { const mod = event.ctrlKey || event.metaKey; const key = event.key.toLowerCase(); // The eyedropper's two keys come first (#276): Enter writes its candidate, Escape leaves without. if (state.objectPick && !mod && (event.key === 'Enter' || event.key === 'Escape')) { state.endObjectPick(event.key === 'Enter' ? 'take' : 'cancel'); return true; } if (event.key === 'Escape' && state.mode === 'play') { actions.stop(); return true; } if (mod && key === 'z') { if (event.shiftKey) state.redo(); else state.undo(); return true; } if (mod && key === 'y') { state.redo(); return true; } if (mod && key === 'c') return actions.copy(); if (mod && key === 'v') return actions.paste(); if (mod && key === 'g') { const s = state.selection; if (s?.kind === 'object') createContainer(state, s.holderId, s.objectIds); // On the shapes of a vector drawing it groups them, and with Shift takes the group away (#190). else if (s?.kind === 'shape') (event.shiftKey ? ungroupShapes : groupShapes)(state, s.assetId); return true; } if (event.key === 'Delete' || event.key === 'Backspace') return actions.removePoint() || removeSelection(state); // Ctrl+Enter starts play and then holds/resumes it. A plain letter would be taken away from // the game, which owns the keyboard while playing. if (mod && event.key === 'Enter') { if (state.mode === 'edit') actions.play(); else actions.togglePause(); return true; } return false; }); }