Files
feuerwerk/src/editor/panels/GroupNameDialog.ts
T
adminandClaude Opus 5.5 2eeabf7c23
game-maker / gate (push) Successful in 3m26s
game-maker / suite (push) Failing after 9m12s
Drag a whole selection and join existing groups in the hierarchy (#241)
Three follow-ups to the hierarchy groups. Dragging a row that belonged
to a multi-selection moved only that one row. The group dialog could
only create a new group. The group row drew its folder in front of the
label, unlike every other row in the hierarchy.

- Dragging a selected row moves every selected object. This holds for
  reordering, dropping into a group or between its members, dragging
  out of a group and dropping onto an object as new parent.
- The moved objects land side by side in hierarchy order, whichever
  row was grabbed. The whole move is one undo step.
- A selected child of a selected object travels with its parent, not
  on its own. Dropping the selection onto its own rows does nothing.
- Dragging a row that is not selected still moves only that row.
- hierarchyDrop.moveObjects replaces the single-object rule. One object
  is the case with one element, so the old drop tests hold unchanged.
- The group dialog has a "Group" dropdown: New group or an existing
  group. Picking a group greys out the name and turns the button into
  "Add". A group that holds all chosen objects already is not offered.
- Rename keeps using the dialog without the dropdown.
- ListRow draws an end icon at the very right and the hint left of it.
  Only the hierarchy uses iconAtEnd, so other lists do not change.
- New test hook __gm.hierarchyHint() gives the centre of a row's hint.

Not clicked in the browser: dragging a selection in the container tab
and onto an object as parent. Only the unit tests cover these.

Tests:
- ./check.sh: 178 files, 2564 tests, CHECK EXIT 0
- e2e 1 + 52: ALL PASSED, 27 checks
- 7 new unit tests in hierarchy-drop.test.ts, 4 new e2e checks in
  52i and 52j with real clicks and a real drag
- 4 counter-probes bite: drop rule ignores the selection (5 unit tests
  fail), icon column not kept free, dialog ignores the picked group,
  panel ignores the selection (only o2 lands in the group)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:19:12 +02:00

88 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ObjectGroup } from '../../core/model/SceneObject';
import { type Button, Dropdown, Menu, TextField, ui } from '../../ui';
import { dialogSlot, FormDialog } from '../FormDialog';
const FIELD_WIDTH = 220;
/** The dropdown entry that makes a group instead of joining one. */
export const NEW_GROUP = 'New group';
const slot = dialogSlot<GroupNameDialog>();
/** The open group name dialog, for the check scripts. */
export const currentGroupDialog = slot.current;
/** Offered by the dialog that groups objects: an existing group to put them into instead. */
export interface GroupJoin {
groups: readonly ObjectGroup[];
onJoin: (groupId: string) => void;
/** Label of the confirming button while a group is chosen. */
confirm: string;
}
/**
* Names a hierarchy group (#241) – a new one before it is made, or an existing one. The name is
* picked up with the caret already in it, so typing replaces it straight away. Given `join`, a
* dropdown above the name offers the existing groups too; picking one puts the objects into it.
*/
export class GroupNameDialog extends FormDialog {
private readonly field: TextField;
private readonly choice: Dropdown | undefined;
private readonly confirmButton: Button;
/** The existing group chosen in the dropdown, '' for a new one. */
private joinId = '';
constructor(
title: string,
name: string,
confirm: string,
onName: (name: string) => void,
private readonly join?: GroupJoin,
) {
super(320, title);
const accept = () => {
if (this.joinId) join?.onJoin(this.joinId);
else onName(this.field.value.trim() || name);
};
this.field = new TextField(name, {
width: FIELD_WIDTH,
onSubmit: () => {
this.close();
accept();
},
});
if (join && join.groups.length > 0) {
const choice = new Dropdown(NEW_GROUP, { width: FIELD_WIDTH });
choice.onClick = () => new Menu(this.choiceItems(confirm)).openBelow(choice);
this.choice = choice;
this.content.add(this.formRow(this.fieldLabel('Group'), choice));
}
this.content.add(this.formRow(this.fieldLabel('Name'), this.field));
this.confirmButton = this.addButtons(confirm, 'primary', accept);
}
private choiceItems(confirm: string): { label: string; onSelect: () => void }[] {
const pick = (groupId: string, label: string) => () => {
this.joinId = groupId;
this.choice?.setText(label);
this.field.setEnabled(!groupId);
this.confirmButton.setText(groupId ? (this.join?.confirm ?? confirm) : confirm);
if (!groupId) ui().focus.focus(this.field);
};
return [
{ label: NEW_GROUP, onSelect: pick('', NEW_GROUP) },
...(this.join?.groups ?? []).map((g) => ({
label: g.name || 'Group',
onSelect: pick(g.id, g.name || 'Group'),
})),
];
}
/** Opens the dialog centred with the name selected. */
static open(dialog: GroupNameDialog): GroupNameDialog {
slot.open(dialog);
ui().focus.focus(dialog.field);
return dialog;
}
}