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>
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
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;
|
||
}
|
||
}
|