Skip to content

Commit 8f13e90

Browse files
authored
Inspector v2: Back compat for Inspector.OnSelectionChangeObservable and Inspector.OnPropertyChangedObservable (#17488)
This PR adds back compat imple for the following: - `Inspector.OnSelectionChangeObservable` - this one is simply observing the existing `ISelectionService.onSelectedEntityChanged`. - `Inspector.OnPropertyChangedObservable` - for this one, I introduced a React `PropertyContext` that is used in the Properties service. It exposes an Observable for when properties change. The Properties service exposes the observable, and `BoundProperty` consumes the context and fires the observable when a property is changed. Any properties handled outside of `BoundProperty` will not automatically fire the event, so we need to finish moving any remaining direct usages of *PropertyLine components to `BoundProperty`, and in any cases where we can't, we need to manually fire the observable from the context. I will work on this when I get back to finishing fleshing out the properties pane, but most properties should already be covered as we are using `BoundProperty` extensively already.
1 parent 80f2ab5 commit 8f13e90

File tree

6 files changed

+122
-15
lines changed

6 files changed

+122
-15
lines changed

packages/dev/inspector-v2/src/components/properties/boundProperty.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { ComponentProps, ComponentType } from "react";
2+
23
import { forwardRef, useMemo } from "react";
34

5+
import { usePropertyChangedNotifier } from "../../contexts/propertyContext";
46
import { MakePropertyHook, useProperty } from "../../hooks/compoundPropertyHooks";
57

68
/**
@@ -58,6 +60,8 @@ function BoundPropertyCoreImpl<TargetT extends object, PropertyKeyT extends keyo
5860
// Determine which specific property hook to use based on the value's type.
5961
const useSpecificProperty = useMemo(() => MakePropertyHook(value), [value]);
6062

63+
const notifyPropertyChanged = usePropertyChangedNotifier();
64+
6165
// Create an inline nested component that changes when the desired specific hook type changes (since hooks can't be conditional).
6266
// eslint-disable-next-line @typescript-eslint/naming-convention
6367
const SpecificComponent = useMemo(() => {
@@ -74,8 +78,10 @@ function BoundPropertyCoreImpl<TargetT extends object, PropertyKeyT extends keyo
7478
ref,
7579
value: convertedValue as TargetT[PropertyKeyT],
7680
onChange: (val: TargetT[PropertyKeyT]) => {
81+
const oldValue = target[propertyKey];
7782
const newValue = convertFrom ? convertFrom(val) : val;
7883
target[propertyKey] = newValue;
84+
notifyPropertyChanged(target, propertyKey, oldValue, newValue);
7985
},
8086
};
8187

packages/dev/inspector-v2/src/components/properties/commonGeneralProperties.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import type { IDisposable } from "core/index";
44

55
import { useMemo } from "react";
66

7+
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
78
import { TextInputPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/inputPropertyLine";
8-
import { TextPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/textPropertyLine";
99
import { StringifiedPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/stringifiedPropertyLine";
10+
import { TextPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/textPropertyLine";
1011
import { useProperty } from "../../hooks/compoundPropertyHooks";
1112
import { GetPropertyDescriptor, IsPropertyReadonly } from "../../instrumentation/propertyInstrumentation";
12-
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
13+
import { BoundProperty } from "./boundProperty";
14+
15+
function IsEntityWithProperty<ObjectT, PropertyT extends keyof ObjectT>(entity: ObjectT, property: PropertyT): entity is ObjectT & Required<Pick<ObjectT, PropertyT>> {
16+
return !!entity && typeof entity === "object" && property in entity && entity[property] !== undefined;
17+
}
1318

1419
type CommonEntity = {
1520
readonly id?: number;
@@ -18,6 +23,15 @@ type CommonEntity = {
1823
getClassName?: () => string;
1924
};
2025

26+
export function IsCommonEntity(entity: unknown): entity is CommonEntity {
27+
return (
28+
IsEntityWithProperty(entity as CommonEntity, "id") ||
29+
IsEntityWithProperty(entity as CommonEntity, "uniqueId") ||
30+
IsEntityWithProperty(entity as CommonEntity, "name") ||
31+
IsEntityWithProperty(entity as CommonEntity, "getClassName")
32+
);
33+
}
34+
2135
export const CommonGeneralProperties: FunctionComponent<{ commonEntity: CommonEntity }> = (props) => {
2236
const { commonEntity } = props;
2337

@@ -29,14 +43,14 @@ export const CommonGeneralProperties: FunctionComponent<{ commonEntity: CommonEn
2943

3044
return (
3145
<>
32-
{commonEntity.id !== undefined && <StringifiedPropertyLine key="EntityId" label="ID" description="The id of the node." value={commonEntity.id} />}
33-
{name !== undefined &&
46+
{IsEntityWithProperty(commonEntity, "id") && <StringifiedPropertyLine key="EntityId" label="ID" description="The id of the node." value={commonEntity.id} />}
47+
{IsEntityWithProperty(commonEntity, "name") &&
3448
(isNameReadonly ? (
35-
<TextPropertyLine key="EntityName" label="Name" description="The name of the node." value={name} />
49+
<TextPropertyLine key="EntityName" label="Name" description="The name of the node." value={name ?? ""} />
3650
) : (
37-
<TextInputPropertyLine key="EntityName" label="Name" description="The name of the node." value={name} onChange={(newName) => (commonEntity.name = newName)} />
51+
<BoundProperty key="EntityName" component={TextInputPropertyLine} label="Name" description="The name of the node." target={commonEntity} propertyKey="name" />
3852
))}
39-
{commonEntity.uniqueId !== undefined && (
53+
{IsEntityWithProperty(commonEntity, "uniqueId") && (
4054
<StringifiedPropertyLine key="EntityUniqueId" label="Unique ID" description="The unique id of the node." value={commonEntity.uniqueId} />
4155
)}
4256
{className !== undefined && <TextPropertyLine key="EntityClassName" label="Class" description="The class of the node." value={className} />}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Observable } from "core/index";
2+
3+
import { createContext, useContext } from "react";
4+
5+
export type PropertyChangeInfo = {
6+
readonly entity: unknown;
7+
readonly propertyKey: PropertyKey;
8+
readonly oldValue: unknown;
9+
readonly newValue: unknown;
10+
};
11+
12+
export type PropertyContext = {
13+
readonly onPropertyChanged: Observable<PropertyChangeInfo>;
14+
};
15+
16+
export const PropertyContext = createContext<PropertyContext | undefined>(undefined);
17+
18+
export function usePropertyChangedNotifier() {
19+
const propertyContext = useContext(PropertyContext);
20+
return <ObjectT, PropertyT extends keyof ObjectT>(entity: ObjectT, propertyKey: PropertyT, oldValue: ObjectT[PropertyT], newValue: ObjectT[PropertyT]) => {
21+
propertyContext?.onPropertyChanged.notifyObservers({ entity, propertyKey, oldValue, newValue });
22+
};
23+
}

packages/dev/inspector-v2/src/legacy/inspector.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import type { InspectorOptions as InspectorV2Options } from "../inspector";
1313
import type { WeaklyTypedServiceDefinition } from "../modularity/serviceContainer";
1414
import type { ServiceDefinition } from "../modularity/serviceDefinition";
1515
import type { IGizmoService } from "../services/gizmoService";
16+
import type { IPropertiesService } from "../services/panes/properties/propertiesService";
1617
import type { ISceneExplorerService } from "../services/panes/scene/sceneExplorerService";
18+
import type { ISelectionService } from "../services/selectionService";
1719
import type { IShellService } from "../services/shellService";
1820

1921
import { BranchRegular } from "@fluentui/react-icons";
@@ -25,7 +27,9 @@ import { UniqueIdGenerator } from "core/Misc/uniqueIdGenerator";
2527
import { ShowInspector } from "../inspector";
2628
import { InterceptProperty } from "../instrumentation/propertyInstrumentation";
2729
import { GizmoServiceIdentity } from "../services/gizmoService";
30+
import { PropertiesServiceIdentity } from "../services/panes/properties/propertiesService";
2831
import { SceneExplorerServiceIdentity } from "../services/panes/scene/sceneExplorerService";
32+
import { SelectionServiceIdentity } from "../services/selectionService";
2933
import { ShellServiceIdentity } from "../services/shellService";
3034

3135
type PropertyChangedEvent = {
@@ -322,6 +326,8 @@ export class Inspector {
322326
}
323327

324328
let options = ConvertOptions(userOptions);
329+
const serviceDefinitions: WeaklyTypedServiceDefinition[] = [];
330+
325331
const popupServiceDefinition: ServiceDefinition<[], [IShellService]> = {
326332
friendlyName: "Popup Service (Backward Compatibility)",
327333
consumes: [ShellServiceIdentity],
@@ -340,10 +346,55 @@ export class Inspector {
340346
};
341347
},
342348
};
349+
serviceDefinitions.push(popupServiceDefinition);
350+
351+
const selectionChangedServiceDefinition: ServiceDefinition<[], [ISelectionService]> = {
352+
friendlyName: "Selection Changed Service (Backward Compatibility)",
353+
consumes: [SelectionServiceIdentity],
354+
factory: (selectionService) => {
355+
const selectionServiceObserver = selectionService.onSelectedEntityChanged.add(() => {
356+
this.OnSelectionChangeObservable.notifyObservers(selectionService.selectedEntity);
357+
});
358+
359+
const legacyObserver = this.OnSelectionChangeObservable.add((entity) => {
360+
selectionService.selectedEntity = entity;
361+
});
362+
363+
return {
364+
dispose: () => {
365+
selectionServiceObserver.remove();
366+
legacyObserver.remove();
367+
},
368+
};
369+
},
370+
};
371+
serviceDefinitions.push(selectionChangedServiceDefinition);
372+
373+
const propertyChangedServiceDefinition: ServiceDefinition<[], [IPropertiesService]> = {
374+
friendlyName: "Property Changed Service (Backward Compatibility)",
375+
consumes: [PropertiesServiceIdentity],
376+
factory: (propertiesService) => {
377+
const observer = propertiesService.onPropertyChanged.add((changeInfo) => {
378+
this.OnPropertyChangedObservable.notifyObservers({
379+
object: changeInfo.entity,
380+
property: changeInfo.propertyKey.toString(),
381+
value: changeInfo.newValue,
382+
initialValue: changeInfo.oldValue,
383+
});
384+
});
385+
386+
return {
387+
dispose: () => {
388+
observer.remove();
389+
},
390+
};
391+
},
392+
};
393+
serviceDefinitions.push(propertyChangedServiceDefinition);
343394

344395
options = {
345396
...options,
346-
serviceDefinitions: [...(options.serviceDefinitions ?? []), popupServiceDefinition],
397+
serviceDefinitions: [...(options.serviceDefinitions ?? []), ...serviceDefinitions],
347398
};
348399

349400
this._CurrentInspectorToken = ShowInspector(scene, options);

packages/dev/inspector-v2/src/services/panes/properties/commonPropertiesService.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { IPropertiesService } from "./propertiesService";
44

55
import { Scene } from "core/scene";
66

7-
import { CommonGeneralProperties, DisposableGeneralProperties } from "../../../components/properties/commonGeneralProperties";
7+
import { CommonGeneralProperties, DisposableGeneralProperties, IsCommonEntity } from "../../../components/properties/commonGeneralProperties";
88
import { PropertiesServiceIdentity } from "./propertiesService";
99

1010
type CommonEntity = {
@@ -26,8 +26,7 @@ export const CommonPropertiesServiceDefinition: ServiceDefinition<[], [IProperti
2626
return false;
2727
}
2828

29-
const commonEntity = entity as CommonEntity;
30-
return commonEntity.id !== undefined || commonEntity.name !== undefined || commonEntity.uniqueId !== undefined || commonEntity.getClassName !== undefined;
29+
return IsCommonEntity(entity);
3130
},
3231
content: [
3332
{

packages/dev/inspector-v2/src/services/panes/properties/propertiesService.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import type { IDisposable } from "core/index";
2-
1+
import type { IDisposable, IReadonlyObservable } from "core/index";
32
import type { DynamicAccordionSection, DynamicAccordionSectionContent } from "../../../components/extensibleAccordion";
3+
import type { PropertyChangeInfo } from "../../../contexts/propertyContext";
44
import type { IService, ServiceDefinition } from "../../../modularity/serviceDefinition";
55
import type { ISelectionService } from "../../selectionService";
66
import type { IShellService } from "../../shellService";
77

88
import { DocumentTextRegular } from "@fluentui/react-icons";
9+
import { useMemo } from "react";
910

11+
import { Observable } from "core/Misc/observable";
1012
import { PropertiesPane } from "../../../components/properties/propertiesPane";
13+
import { PropertyContext } from "../../../contexts/propertyContext";
1114
import { useObservableCollection, useObservableState, useOrderedObservableCollection } from "../../../hooks/observableHooks";
1215
import { ObservableCollection } from "../../../misc/observableCollection";
1316
import { SelectionServiceIdentity } from "../../selectionService";
1417
import { ShellServiceIdentity } from "../../shellService";
15-
import { useMemo } from "react";
1618

1719
export const PropertiesServiceIdentity = Symbol("PropertiesService");
1820

@@ -45,6 +47,12 @@ export interface IPropertiesService extends IService<typeof PropertiesServiceIde
4547
* @param content A description of the content to add.
4648
*/
4749
addSectionContent<EntityT>(content: PropertiesSectionContent<EntityT>): IDisposable;
50+
51+
/**
52+
* An observable that notifies when a property has been changed by the user.
53+
* @remarks This observable only fires for changes made through the properties pane.
54+
*/
55+
readonly onPropertyChanged: IReadonlyObservable<PropertyChangeInfo>;
4856
}
4957

5058
/**
@@ -57,6 +65,7 @@ export const PropertiesServiceDefinition: ServiceDefinition<[IPropertiesService]
5765
factory: (shellService, selectionService) => {
5866
const sectionsCollection = new ObservableCollection<DynamicAccordionSection>();
5967
const sectionContentCollection = new ObservableCollection<PropertiesSectionContent<unknown>>();
68+
const onPropertyChanged = new Observable<PropertyChangeInfo>();
6069

6170
const registration = shellService.addSidePane({
6271
key: "Properties",
@@ -89,13 +98,18 @@ export const PropertiesServiceDefinition: ServiceDefinition<[IPropertiesService]
8998
[sectionContent, entity]
9099
);
91100

92-
return <PropertiesPane sections={sections} sectionContent={applicableContent} context={entity} />;
101+
return (
102+
<PropertyContext.Provider value={{ onPropertyChanged }}>
103+
<PropertiesPane sections={sections} sectionContent={applicableContent} context={entity} />
104+
</PropertyContext.Provider>
105+
);
93106
},
94107
});
95108

96109
return {
97110
addSection: (section) => sectionsCollection.add(section),
98111
addSectionContent: (content) => sectionContentCollection.add(content as PropertiesSectionContent<unknown>),
112+
onPropertyChanged,
99113
dispose: () => registration.dispose(),
100114
};
101115
},

0 commit comments

Comments
 (0)