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>
This commit is contained in:
1 parent
893b81f798
commit
2eeabf7c23
10 files changed
+327
-64
No files matched your search
@@ -1,23 +1,49 @@
|
||||
import { TextField, ui } from '../../ui';
|
||||
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.
|
||||
* 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) {
|
||||
constructor(
|
||||
title: string,
|
||||
name: string,
|
||||
confirm: string,
|
||||
onName: (name: string) => void,
|
||||
private readonly join?: GroupJoin,
|
||||
) {
|
||||
super(320, title);
|
||||
const accept = () => onName(this.field.value.trim() || name);
|
||||
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: () => {
|
||||
@@ -25,8 +51,31 @@ export class GroupNameDialog extends FormDialog {
|
||||
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.addButtons(confirm, 'primary', accept);
|
||||
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. */
|
||||
|
||||
Reference in new issue
Block a user