import { GroupObjectsCommand, RenameGroupCommand, SetObjectGroupCommand, UngroupCommand, } from '../core/commands/groupCommands'; import { nextFreeName } from '../core/commands/naming'; import { canGroup, findGroup, findObject, groupMemberIds } from '../core/model/hierarchy'; import type { ObjectHolder } from '../core/model/Project'; import type { MenuEntry, MenuItem } from '../ui'; import { GroupNameDialog } from './panels/GroupNameDialog'; import { type EditorState, objectSelection } from './state'; /** "Group", "Group 2", … – free among the groups that are listed. */ function nextGroupName(holder: ObjectHolder): string { const listed = (holder.groups ?? []).filter((g) => groupMemberIds(holder, g.id).length > 0); return nextFreeName( listed.map((g) => g.name), 'Group', ); } /** * The object menu's entries for groups (#241). `source` is the stored holder: content of a placed * container is a child, so it can never be grouped. */ export function groupMenuItems( state: EditorState, source: ObjectHolder, selected: readonly string[], ): MenuItem[] { const many = selected.length > 1; const grouped = selected.filter((id) => findGroup(source, findObject(source, id)?.groupId ?? '')); return [ { label: many ? `Group ${selected.length} objects…` : 'Group…', disabled: !canGroup(source, selected), onSelect: () => groupObjects(state, source, selected), }, { label: 'Remove from group', disabled: grouped.length === 0, onSelect: () => state.execute(new SetObjectGroupCommand(source.id, grouped, '')), }, ]; } /** * Asks for the name of a new group – or, from a dropdown, for an existing group to put the objects * into. A group all of them are in already is not offered. */ function groupObjects(state: EditorState, source: ObjectHolder, ids: readonly string[]): void { const title = ids.length > 1 ? `Group ${ids.length} objects` : 'Group the object'; const groups = (source.groups ?? []).filter((g) => { const members = groupMemberIds(source, g.id); return members.length > 0 && !ids.every((id) => members.includes(id)); }); GroupNameDialog.open( new GroupNameDialog( title, nextGroupName(source), 'Group', (name) => state.execute(new GroupObjectsCommand(source.id, ids, name)), { groups, confirm: 'Add', onJoin: (groupId) => state.execute(new SetObjectGroupCommand(source.id, ids, groupId)), }, ), ); } /** The menu of a group's row in the hierarchy; `extras` go below, where the panel adds folding. */ export function groupRowItems( state: EditorState, source: ObjectHolder, groupId: string, extras: readonly MenuItem[], ): MenuEntry[] { const group = findGroup(source, groupId); if (!group) return [...extras]; return [ { label: 'Select members', onSelect: () => selectGroup(state, source, groupId), }, { label: 'Rename…', onSelect: () => GroupNameDialog.open( new GroupNameDialog(`Rename group "${group.name}"`, group.name, 'Rename', (name) => { if (name !== group.name) state.execute(new RenameGroupCommand(source.id, groupId, name)); }), ), }, { label: 'Ungroup', onSelect: () => state.execute(new UngroupCommand(source.id, groupId)), }, { separator: true }, ...extras, ]; } export function selectGroup(state: EditorState, source: ObjectHolder, groupId: string): void { const members = groupMemberIds(source, groupId); state.select(objectSelection(source.id, members, members[0])); }