Ping or open an object's asset from its hierarchy row (#353)
The asset link in the inspector already pings an asset on a click and opens it on a double-click. The rows of the Scene objects panel only selected and folded. Now they lead to the asset the same way. - A click on the row that is selected already pings the object's asset in the asset panel. The selection stays. The ping comes on release, so picking a row up to drag it pings nothing. - A double-click or Enter on an object's row opens its asset, selected or not. - ListView has a new onReselect for a plain click on the row that is selected alone. onSelect stays quiet then, as nothing changes. Decided without asking: - A double-click on an object's row no longer folds it. The chevron and the arrow keys still do. A group's row has no asset and keeps folding on a double-click. - A click with Ctrl or Shift, and one that narrows a multi-selection to its row, does not ping. Both change the selection. - A row whose asset is missing pings and opens nothing. Findings: - The double-click window goes by time (400 ms), not by the event's detail. Two quick click() calls from the harness are a double-click. The comment in e2e 01 said otherwise and is gone. Tests: - e2e 01 (melektaus-atx): ALL PASSED, 13 checks. The two checks for folding on a double-click became one for the ping and two for opening. Counter-check without the ping: that check red. - Gate (melektaus-atx): CHECK EXIT 0, 268 files, 4142 tests. - Not checked: Enter on a row, a group's row on a double-click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d53b9b3d0
commit
c95fbc167e
3 files changed
+75
-13
No files matched your search
@@ -14,6 +14,7 @@ import { findAsset, type ObjectHolder } from '../../core/model/Project';
|
|||||||
import { groupName, objectName } from '../../core/model/SceneObject';
|
import { groupName, objectName } from '../../core/model/SceneObject';
|
||||||
import { deviationMarks } from '../../core/properties/deviation';
|
import { deviationMarks } from '../../core/properties/deviation';
|
||||||
import { type DropSpot, ListView, Menu, Panel, ScrollContainer } from '../../ui';
|
import { type DropSpot, ListView, Menu, Panel, ScrollContainer } from '../../ui';
|
||||||
|
import { activateAsset } from '../actions';
|
||||||
import { groupRowItems, selectGroup } from '../objectGroups';
|
import { groupRowItems, selectGroup } from '../objectGroups';
|
||||||
import { openObjectMenu } from '../objectMenu';
|
import { openObjectMenu } from '../objectMenu';
|
||||||
import { ASSET_ICON_SVG, GROUP_ICON_SVG } from './assetIcons';
|
import { ASSET_ICON_SVG, GROUP_ICON_SVG } from './assetIcons';
|
||||||
@@ -59,7 +60,11 @@ export class HierarchyPanel extends Panel {
|
|||||||
};
|
};
|
||||||
this.list.onDrop = (draggedId, spot) => this.drop(draggedId, spot);
|
this.list.onDrop = (draggedId, spot) => this.drop(draggedId, spot);
|
||||||
this.list.onToggle = (id, expand) => this.setExpanded(id, expand);
|
this.list.onToggle = (id, expand) => this.setExpanded(id, expand);
|
||||||
this.list.onActivate = (id) => this.setExpanded(id, !this.expanded.has(id));
|
this.list.onActivate = (id) => this.activate(id);
|
||||||
|
this.list.onReselect = (id) => {
|
||||||
|
const assetId = this.assetOfRow(id);
|
||||||
|
if (assetId) state.pingAsset(assetId);
|
||||||
|
};
|
||||||
this.list.onContextMenu = (id, x, y) => this.openContextMenu(id, x, y);
|
this.list.onContextMenu = (id, x, y) => this.openContextMenu(id, x, y);
|
||||||
this.scroll.setContent(this.list);
|
this.scroll.setContent(this.list);
|
||||||
this.content.add(this.scroll, 1);
|
this.content.add(this.scroll, 1);
|
||||||
@@ -98,6 +103,23 @@ export class HierarchyPanel extends Panel {
|
|||||||
this.state.execute(oneStep(many ? 'Move objects' : 'Move object', commands));
|
this.state.execute(oneStep(many ? 'Move objects' : 'Move object', commands));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A double-click or Enter (#353): an object's row opens its asset, as a double-click on the asset
|
||||||
|
* link in the inspector does. A group's row has no asset and folds instead.
|
||||||
|
*/
|
||||||
|
private activate(rowId: string): void {
|
||||||
|
const assetId = this.assetOfRow(rowId);
|
||||||
|
if (assetId) activateAsset(this.state, assetId);
|
||||||
|
else if (groupIdOfRow(rowId)) this.setExpanded(rowId, !this.expanded.has(rowId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The asset an object's row was placed from – null for a group's row or a missing asset. */
|
||||||
|
private assetOfRow(rowId: string): string | null {
|
||||||
|
const holder = this.holder();
|
||||||
|
const object = holder && !groupIdOfRow(rowId) ? findObject(holder, rowId) : undefined;
|
||||||
|
return object && findAsset(this.state.project, object.assetId) ? object.assetId : null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Ctrl-click on a group row: all members join the selection, or leave it when all were in. */
|
/** Ctrl-click on a group row: all members join the selection, or leave it when all were in. */
|
||||||
private toggleGroup(holderId: string, groupId: string): void {
|
private toggleGroup(holderId: string, groupId: string): void {
|
||||||
const holder = this.holder();
|
const holder = this.holder();
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ interface DragState {
|
|||||||
spot: DropSpot | null;
|
spot: DropSpot | null;
|
||||||
/** Pressed inside a multi-selection: a release that never travelled narrows it to this row. */
|
/** Pressed inside a multi-selection: a release that never travelled narrows it to this row. */
|
||||||
narrows: boolean;
|
narrows: boolean;
|
||||||
|
/** Pressed on the row that was selected alone already: a release that never travelled reselects it. */
|
||||||
|
reselects: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +111,11 @@ export class ListView extends Widget {
|
|||||||
private contextId: string | null = null;
|
private contextId: string | null = null;
|
||||||
onSelect?: (id: string | null, modifiers?: PickModifiers) => void;
|
onSelect?: (id: string | null, modifiers?: PickModifiers) => void;
|
||||||
onActivate?: (id: string) => void;
|
onActivate?: (id: string) => void;
|
||||||
|
/**
|
||||||
|
* A plain click on the row that is selected already – `onSelect` stays quiet then, as nothing
|
||||||
|
* changes. In a list whose rows can be dragged it comes on release, so picking a row up is not it.
|
||||||
|
*/
|
||||||
|
onReselect?: (id: string) => void;
|
||||||
/** Returning the opened menu keeps its row marked for as long as it is up. */
|
/** Returning the opened menu keeps its row marked for as long as it is up. */
|
||||||
onContextMenu?: (id: string, globalX: number, globalY: number) => Popup | void;
|
onContextMenu?: (id: string, globalX: number, globalY: number) => Popup | void;
|
||||||
/** A right-click on the empty space below the rows or on a heading, which have no menu of their own. */
|
/** A right-click on the empty space below the rows or on a heading, which have no menu of their own. */
|
||||||
@@ -349,6 +356,7 @@ export class ListView extends Widget {
|
|||||||
// A press inside a multi-selection keeps it, so the selection is still there while dragging;
|
// A press inside a multi-selection keeps it, so the selection is still there while dragging;
|
||||||
// only a plain click (see `onDragEnd`) narrows it down to this row.
|
// only a plain click (see `onDragEnd`) narrows it down to this row.
|
||||||
const narrows = this.highlighted.size > 1 && this.highlighted.has(item.id);
|
const narrows = this.highlighted.size > 1 && this.highlighted.has(item.id);
|
||||||
|
const reselects = !narrows && item.id === this.selectedId;
|
||||||
const dragged = Boolean(this.onDrop || this.onDragOut);
|
const dragged = Boolean(this.onDrop || this.onDragOut);
|
||||||
if (narrows && !dragged) this.pick(item.id);
|
if (narrows && !dragged) this.pick(item.id);
|
||||||
else if (!narrows && !this.onDragOut && (!this.highlighted.has(item.id) || item.id === this.selectedId))
|
else if (!narrows && !this.onDragOut && (!this.highlighted.has(item.id) || item.id === this.selectedId))
|
||||||
@@ -362,8 +370,9 @@ export class ListView extends Widget {
|
|||||||
active: false,
|
active: false,
|
||||||
spot: null,
|
spot: null,
|
||||||
narrows,
|
narrows,
|
||||||
|
reselects,
|
||||||
};
|
};
|
||||||
}
|
} else if (reselects) this.onReselect?.(item.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +420,7 @@ export class ListView extends Widget {
|
|||||||
if (!drag?.active) {
|
if (!drag?.active) {
|
||||||
// The press never travelled, so it was a click after all – now it may select.
|
// The press never travelled, so it was a click after all – now it may select.
|
||||||
if (drag?.narrows) this.pick(drag.id);
|
if (drag?.narrows) this.pick(drag.id);
|
||||||
|
else if (drag?.reselects) this.onReselect?.(drag.id);
|
||||||
else if (drag && this.onDragOut) this.select(drag.id);
|
else if (drag && this.onDragOut) this.select(drag.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-11
@@ -4,9 +4,10 @@ import { same } from '../lib/harness.mjs';
|
|||||||
* Hierarchy folding. A freshly opened scene is fully collapsed, so only its top-level objects
|
* Hierarchy folding. A freshly opened scene is fully collapsed, so only its top-level objects
|
||||||
* are listed (o4/oc1 hang under o1, oc2 under o2). The chevron folds a row open and shut
|
* are listed (o4/oc1 hang under o1, oc2 under o2). The chevron folds a row open and shut
|
||||||
* without touching the selection, and selecting a nested object unfolds the way down to it.
|
* without touching the selection, and selecting a nested object unfolds the way down to it.
|
||||||
|
* A click on the selected row pings the object's asset, a double-click opens it (#353).
|
||||||
*/
|
*/
|
||||||
export async function hierarchy(t) {
|
export async function hierarchy(t) {
|
||||||
const { check, evaluate, json, click, clickText, doubleClick, press, select, state, shot } = t;
|
const { check, evaluate, json, click, clickText, doubleClick, press, select, sleep, state, shot } = t;
|
||||||
const rowIds = () => json('__gm.hierarchyOrder()');
|
const rowIds = () => json('__gm.hierarchyOrder()');
|
||||||
const rows = async () => JSON.stringify(await rowIds());
|
const rows = async () => JSON.stringify(await rowIds());
|
||||||
const selectionBefore = (await state()).selection;
|
const selectionBefore = (await state()).selection;
|
||||||
@@ -31,21 +32,50 @@ export async function hierarchy(t) {
|
|||||||
same(await rowIds(), ['o1', 'o2', 'o3', 'o5']),
|
same(await rowIds(), ['o1', 'o2', 'o3', 'o5']),
|
||||||
await rows(),
|
await rows(),
|
||||||
);
|
);
|
||||||
// The row itself folds on a double-click, the same as its chevron. Pixi leaves `detail` at 0 on
|
// 01b (#353). A click on the row that is selected already pings the object's asset in the asset
|
||||||
// a press, so this needs a real double-click from the harness, not two `click()` calls.
|
// panel, as one on the asset link in the inspector does; the first click only selects.
|
||||||
|
const assetOf = (id) =>
|
||||||
|
json(`__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(id)}).assetId`);
|
||||||
|
const o3Asset = await assetOf('o3');
|
||||||
|
await evaluate('__gm.state.pingedAssetId = null');
|
||||||
|
const o3Row = await json(`__gm.hierarchyRow('o3')`);
|
||||||
|
await click(o3Row.x, o3Row.y);
|
||||||
|
const afterFirst = await json('__gm.state.pingedAssetId');
|
||||||
|
// Past the double-click window, which goes by time: two quick clicks would open the asset.
|
||||||
|
await sleep(500);
|
||||||
|
await click(o3Row.x, o3Row.y);
|
||||||
|
const ping = await json('__gm.assetPing()');
|
||||||
|
const pingSelection = (await state()).selection;
|
||||||
|
check(
|
||||||
|
'a click on the selected row pings its asset and keeps the selection, the first click only selects',
|
||||||
|
afterFirst === null &&
|
||||||
|
ping?.id === o3Asset &&
|
||||||
|
pingSelection?.kind === 'object' &&
|
||||||
|
same(pingSelection.objectIds, ['o3']),
|
||||||
|
JSON.stringify({ afterFirst, ping, o3Asset, pingSelection }),
|
||||||
|
);
|
||||||
|
// A double-click opens the asset, selected or not – it no longer folds the row.
|
||||||
|
const o2Asset = await assetOf('o2');
|
||||||
const row = await json(`__gm.hierarchyRow('o2')`);
|
const row = await json(`__gm.hierarchyRow('o2')`);
|
||||||
await doubleClick(row.x, row.y);
|
await doubleClick(row.x, row.y);
|
||||||
|
const o2Selection = (await state()).selection;
|
||||||
check(
|
check(
|
||||||
'double-clicking the row unfolds it as well',
|
'double-clicking a row selects its asset and leaves the row folded',
|
||||||
same(await rowIds(), ['o1', 'o2', 'oc2', 'o3', 'o5']),
|
o2Selection?.kind === 'asset' &&
|
||||||
await rows(),
|
o2Selection.assetId === o2Asset &&
|
||||||
);
|
|
||||||
await doubleClick(row.x, row.y);
|
|
||||||
check(
|
|
||||||
'double-clicking it again folds the subtree away',
|
|
||||||
same(await rowIds(), ['o1', 'o2', 'o3', 'o5']),
|
same(await rowIds(), ['o1', 'o2', 'o3', 'o5']),
|
||||||
await rows(),
|
JSON.stringify({ o2Selection, o2Asset, rows: await rowIds() }),
|
||||||
);
|
);
|
||||||
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'o5' });
|
||||||
|
const o5Row = await json(`__gm.hierarchyRow('o5')`);
|
||||||
|
await doubleClick(o5Row.x, o5Row.y);
|
||||||
|
const openedByRow = await json('__gm.documents()');
|
||||||
|
check(
|
||||||
|
'double-clicking a selected row opens the tab of its asset',
|
||||||
|
openedByRow.active === 'asset:spawner',
|
||||||
|
JSON.stringify(openedByRow),
|
||||||
|
);
|
||||||
|
await evaluate(`__gm.closeDocument('asset:spawner'); __gm.state.select(null)`);
|
||||||
|
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'oc1' });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'oc1' });
|
||||||
check(
|
check(
|
||||||
|
|||||||
Reference in new issue
Block a user