import type { Command } from '../../core/commands/Command'; import { oneStep } from '../../core/commands/CompoundCommand'; import { CollapseNodesCommand, DisconnectCommand, FoldEdgeCommand } from '../../core/commands/graphCommands'; import { annotationNoun } from '../../core/commands/annotationCommands'; import type { AnnotationKind, GraphEdge, GraphNode } from '../../core/graph'; import { Menu, type MenuItem } from '../../ui'; import { removeGraphSelection, removeNodes } from '../actions'; import type { EditorState } from '../state'; /** For the selected nodes; a selection mixing both states offers both directions. */ export function openNodeMenu( state: EditorState, assetId: string, nodes: readonly GraphNode[], x: number, y: number, extras: readonly MenuItem[] = [], ): void { const ids = state.selectedNodeIds; const fold = (list: readonly GraphNode[], verb: string, to: boolean): MenuItem[] => { if (list.length === 0) return []; const listIds = list.map((n) => n.id); const label = list.length > 1 ? `${verb} ${list.length} nodes` : `${verb} node`; return [{ label, onSelect: () => state.execute(new CollapseNodesCommand(assetId, listIds, to)) }]; }; const expanded = nodes.filter((n) => !n.collapsed); const collapsed = nodes.filter((n) => n.collapsed); new Menu([ ...extras, ...fold(expanded, 'Collapse', true), ...fold(collapsed, 'Expand', false), { label: ids.length > 1 ? `Delete ${ids.length} nodes` : 'Delete node', danger: true, onSelect: () => removeNodes(state, assetId, ids), }, ]).open(x, y); } /** * For one group or comment. Only deleting lives here: the title, the text and the colour are * edited in the inspector, where every other selected thing is edited too. */ export function openAnnotationMenu( state: EditorState, assetId: string, kind: AnnotationKind, id: string, x: number, y: number, ): void { // Right-clicking a box keeps whatever else is selected, so the entry speaks about all of it – // a menu that silently deleted one of five marked boxes would be the worse surprise. const held = state.selectedAnnotationIds.includes(id); const boxes = held ? state.selectedAnnotationIds : [id]; const nodes = held ? state.selectedNodeIds : []; const count = boxes.length + nodes.length; new Menu([ { label: count > 1 ? `Delete ${count} items` : `Delete ${annotationNoun(kind)}`, danger: true, onSelect: () => removeGraphSelection(state, assetId, nodes, boxes), }, ]).open(x, y); } /** * The links at one port, or behind one fold mark – the mark is the only handle a hidden link has, * so it carries the way back. Every entry is one undo step, however many links it covers. */ export function openLinkMenu( state: EditorState, assetId: string, edges: readonly GraphEdge[], x: number, y: number, ): void { if (edges.length === 0) return; const item = ( verb: string, single: string, list: readonly GraphEdge[], make: (edgeId: string) => Command, danger = false, ): MenuItem[] => { if (list.length === 0) return []; const label = list.length === 1 ? single : `${verb} ${list.length} links`; const commands = () => list.map((e) => make(e.id)); return [{ label, danger, onSelect: () => state.execute(oneStep(`${verb} links`, commands())) }]; }; const shown = edges.filter((e) => !e.folded); const hidden = edges.filter((e) => e.folded); new Menu([ ...item('Hide', 'Hide link', shown, (id) => new FoldEdgeCommand(assetId, id, true)), ...item('Show', 'Show link', hidden, (id) => new FoldEdgeCommand(assetId, id, false)), ...item('Disconnect', 'Disconnect', edges, (id) => new DisconnectCommand(assetId, id), true), ]).open(x, y); }