diff --git a/package.json b/package.json index 748b471..7b09e9d 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,20 @@ "icon": "$(refresh)", "category": "Bevy Inspector" }, + { + "command": "bevyInspector.expandAllComponents", + "title": "Expand", + "icon": "$(expand-all)", + "category": "Bevy Inspector", + "enablement": "view == bevyInspector.components" + }, + { + "command": "bevyInspector.collapseAllComponents", + "title": "Collapse", + "icon": "$(collapse-all)", + "category": "Bevy Inspector", + "enablement": "view == bevyInspector.components" + }, { "command": "bevyInspector.enableComponentsPolling", "title": "Enable Polling Components", @@ -154,6 +168,20 @@ "category": "Bevy Inspector", "enablement": "view == bevyInspector.resources" }, + { + "command": "bevyInspector.expandAllResources", + "title": "Expand", + "icon": "$(expand-all)", + "category": "Bevy Inspector", + "enablement": "view == bevyInspector.resources" + }, + { + "command": "bevyInspector.collapseAllResources", + "title": "Collapse", + "icon": "$(collapse-all)", + "category": "Bevy Inspector", + "enablement": "view == bevyInspector.resources" + }, { "command": "bevyInspector.enableResourcesPolling", "title": "Enable Polling Resources", @@ -250,6 +278,16 @@ "when": "view == bevyInspector.components", "group": "navigation" }, + { + "command": "bevyInspector.expandAllComponents", + "when": "view == bevyInspector.components && !bevyInspector.anyComponentExpanded", + "group": "navigation@99" + }, + { + "command": "bevyInspector.collapseAllComponents", + "when": "view == bevyInspector.components && bevyInspector.anyComponentExpanded", + "group": "navigation@99" + }, { "command": "bevyInspector.enableComponentsPolling", "when": "view == bevyInspector.components && !bevyInspector.componentsPollingEnabled", @@ -265,6 +303,16 @@ "when": "view == bevyInspector.resources", "group": "navigation" }, + { + "command": "bevyInspector.expandAllResources", + "when": "view == bevyInspector.resources && !bevyInspector.anyResourceExpanded", + "group": "navigation@99" + }, + { + "command": "bevyInspector.collapseAllResources", + "when": "view == bevyInspector.resources && bevyInspector.anyResourceExpanded", + "group": "navigation@99" + }, { "command": "bevyInspector.enableResourcesPolling", "when": "view == bevyInspector.resources && !bevyInspector.resourcesPollingEnabled", diff --git a/src/components-view/components/ComponentsView.tsx b/src/components-view/components/ComponentsView.tsx index cc5abed..e7e88e8 100644 --- a/src/components-view/components/ComponentsView.tsx +++ b/src/components-view/components/ComponentsView.tsx @@ -1,5 +1,5 @@ -import type { ValuesUpdatedData } from '../../inspector-data/messages'; -import { ValuesUpdated, ViewReady } from '../../inspector-data/messages'; +import type { CollapsibleStateData, ValuesUpdatedData } from '../../inspector-data/messages'; +import { SetCollapsibleState, ValuesUpdated, ViewReady } from '../../inspector-data/messages'; import { usePublisher } from '../../messenger/usePublisher'; import { useSubscriber } from '../../messenger/useSubscriber'; import { EmptyDetails } from './EmptyDetails'; @@ -7,6 +7,7 @@ import { TypedValueDetails } from '../../schema-components/TypedValueDetails'; export function ComponentsView() { const components = useSubscriber(ValuesUpdated); + const collapsibleState = useSubscriber(SetCollapsibleState); usePublisher(ViewReady, null); if (!components) { @@ -15,7 +16,7 @@ export function ComponentsView() { return ( <> {components.map((component) => ( - + ))} ); diff --git a/src/extension/components/componentsController.ts b/src/extension/components/componentsController.ts index 4e99f98..d34a74f 100644 --- a/src/extension/components/componentsController.ts +++ b/src/extension/components/componentsController.ts @@ -1,6 +1,12 @@ import * as vscode from 'vscode'; import type { UpdateRequestedEvent, ValuesUpdatedEvent, ViewEvent } from '../../inspector-data/messages'; -import { UpdateRequested, ValuesUpdated, ViewReady } from '../../inspector-data/messages'; +import { + CollapsibleStateChanged, + SetCollapsibleState, + UpdateRequested, + ValuesUpdated, + ViewReady, +} from '../../inspector-data/messages'; import type { TypePath } from '../../inspector-data/types'; import { isEventMessage } from '../../inspector-data/types'; import type { EntityNode } from '../entities/entityTree'; @@ -31,6 +37,8 @@ export class ComponentsController implements vscode.Disposable { vscode.commands.registerCommand('bevyInspector.refreshComponents', this.refresh.bind(this)), vscode.commands.registerCommand('bevyInspector.enableComponentsPolling', this.enablePolling.bind(this)), vscode.commands.registerCommand('bevyInspector.disableComponentsPolling', this.disablePolling.bind(this)), + vscode.commands.registerCommand('bevyInspector.expandAllComponents', this.expandAll.bind(this)), + vscode.commands.registerCommand('bevyInspector.collapseAllComponents', this.collapseAll.bind(this)), ); this.pollingService.onRefresh(this.refresh.bind(this)); vscode.workspace.onDidChangeConfiguration((e) => { @@ -42,6 +50,8 @@ export class ComponentsController implements vscode.Disposable { // Enable polling by default. this.enablePolling(); + vscode.commands.executeCommand('setContext', 'bevyInspector.anyComponentExpanded', false); + this.componentsViewProvider.onVisibilityChanged((visible) => { if (visible) { this.enablePolling(); @@ -127,6 +137,10 @@ export class ComponentsController implements vscode.Disposable { case ViewReady: { break; } + case CollapsibleStateChanged: { + vscode.commands.executeCommand('setContext', 'bevyInspector.anyComponentExpanded', event.data.anyExpanded); + break; + } case UpdateRequested: { const updated = await this.setComponentValue(event); if (updated) { @@ -139,6 +153,20 @@ export class ComponentsController implements vscode.Disposable { } } + private expandAll(): void { + this.componentsViewProvider.postMessage({ + type: SetCollapsibleState, + data: { anyExpanded: true }, + }); + } + + private collapseAll(): void { + this.componentsViewProvider.postMessage({ + type: SetCollapsibleState, + data: { anyExpanded: false }, + }); + } + private async setComponentValue(event: UpdateRequestedEvent): Promise { try { if (this.selectedEntity === undefined) { diff --git a/src/extension/resources/resourcesController.ts b/src/extension/resources/resourcesController.ts index f56cd92..23b0404 100644 --- a/src/extension/resources/resourcesController.ts +++ b/src/extension/resources/resourcesController.ts @@ -1,6 +1,12 @@ import * as vscode from 'vscode'; import type { UpdateRequestedEvent, ValuesUpdatedEvent, ViewEvent } from '../../inspector-data/messages'; -import { UpdateRequested, ValuesUpdated, ViewReady } from '../../inspector-data/messages'; +import { + CollapsibleStateChanged, + SetCollapsibleState, + UpdateRequested, + ValuesUpdated, + ViewReady, +} from '../../inspector-data/messages'; import type { TypePath } from '../../inspector-data/types'; import { isEventMessage } from '../../inspector-data/types'; import { logger } from '../vscode/logger'; @@ -24,6 +30,8 @@ export class ResourcesController implements vscode.Disposable { vscode.commands.registerCommand('bevyInspector.refreshResources', this.refresh.bind(this)), vscode.commands.registerCommand('bevyInspector.enableResourcesPolling', this.enablePolling.bind(this)), vscode.commands.registerCommand('bevyInspector.disableResourcesPolling', this.disablePolling.bind(this)), + vscode.commands.registerCommand('bevyInspector.expandAllResources', this.expandAll.bind(this)), + vscode.commands.registerCommand('bevyInspector.collapseAllResources', this.collapseAll.bind(this)), ); this.pollingService.onRefresh(this.refresh.bind(this)); vscode.workspace.onDidChangeConfiguration((e) => { @@ -36,6 +44,8 @@ export class ResourcesController implements vscode.Disposable { context.subscriptions.push( vscode.commands.registerCommand('bevyInspector.insertResource', this.insertResource.bind(this)), ); + + vscode.commands.executeCommand('setContext', 'bevyInspector.anyResourceExpanded', false); } dispose() { @@ -69,6 +79,10 @@ export class ResourcesController implements vscode.Disposable { await this.refresh(); break; } + case CollapsibleStateChanged: { + vscode.commands.executeCommand('setContext', 'bevyInspector.anyResourceExpanded', event.data.anyExpanded); + break; + } case UpdateRequested: { const updated = await this.setResourceValue(event); if (updated) { @@ -81,6 +95,20 @@ export class ResourcesController implements vscode.Disposable { } } + private expandAll(): void { + this.resourcesViewProvider.postMessage({ + type: SetCollapsibleState, + data: { anyExpanded: true }, + }); + } + + private collapseAll(): void { + this.resourcesViewProvider.postMessage({ + type: SetCollapsibleState, + data: { anyExpanded: false }, + }); + } + private async setResourceValue(event: UpdateRequestedEvent): Promise { try { await this.repository.setResourceValue(event.data.typePath, event.data.path, event.data.newValue); diff --git a/src/inspector-data/messages.ts b/src/inspector-data/messages.ts index 84e7f75..8890c78 100644 --- a/src/inspector-data/messages.ts +++ b/src/inspector-data/messages.ts @@ -3,24 +3,35 @@ import type { EventMessage, TypedValue, TypePath } from './types'; // Events passing from the views to the extension. export const ViewReady = 'ViewReady'; export const UpdateRequested = 'UpdateRequested'; +export const CollapsibleStateChanged = 'CollapsibleStateChanged'; export type ViewReadyData = null; export interface UpdateRequestedData { typePath: TypePath; path: string; newValue: unknown; } +export interface CollapsibleStateData { + anyExpanded: boolean; +} export type ViewReadyEvent = EventMessage & { type: typeof ViewReady; }; export type UpdateRequestedEvent = EventMessage & { type: typeof UpdateRequested; }; -export type ViewEvent = ViewReadyEvent | UpdateRequestedEvent; +export type CollapsibleStateChangedEvent = EventMessage & { + type: typeof CollapsibleStateChanged; +}; +export type ViewEvent = ViewReadyEvent | UpdateRequestedEvent | CollapsibleStateChangedEvent; // Events passing from the extension to the views. export const ValuesUpdated = 'ValuesUpdated'; +export const SetCollapsibleState = 'SetCollapsibleState'; export type ValuesUpdatedData = TypedValue[]; export type ValuesUpdatedEvent = EventMessage & { type: typeof ValuesUpdated; }; -export type ExtensionEvent = ValuesUpdatedEvent; +export type SetCollapsibleStateEvent = EventMessage & { + type: typeof SetCollapsibleState; +}; +export type ExtensionEvent = ValuesUpdatedEvent | SetCollapsibleStateEvent; diff --git a/src/resources-view/components/ResourcesView.tsx b/src/resources-view/components/ResourcesView.tsx index 7946bb0..7ea17c2 100644 --- a/src/resources-view/components/ResourcesView.tsx +++ b/src/resources-view/components/ResourcesView.tsx @@ -1,11 +1,12 @@ -import type { ValuesUpdatedData } from '../../inspector-data/messages'; -import { ValuesUpdated, ViewReady } from '../../inspector-data/messages'; +import type { CollapsibleStateData, ValuesUpdatedData } from '../../inspector-data/messages'; +import { SetCollapsibleState, ValuesUpdated, ViewReady } from '../../inspector-data/messages'; import { usePublisher } from '../../messenger/usePublisher'; import { useSubscriber } from '../../messenger/useSubscriber'; import { TypedValueDetails } from '../../schema-components/TypedValueDetails'; export function ResourcesView() { const resources = useSubscriber(ValuesUpdated); + const collapsibleState = useSubscriber(SetCollapsibleState); usePublisher(ViewReady, null); if (!resources) { @@ -14,7 +15,7 @@ export function ResourcesView() { return ( <> {resources.map((resource) => ( - + ))} ); diff --git a/src/schema-components/TypedValueDetails.tsx b/src/schema-components/TypedValueDetails.tsx index 16b5e50..435b0a2 100644 --- a/src/schema-components/TypedValueDetails.tsx +++ b/src/schema-components/TypedValueDetails.tsx @@ -1,21 +1,51 @@ import '@vscode-elements/elements/dist/vscode-collapsible'; +import type { VscCollapsibleToggleEvent } from '@vscode-elements/elements/dist/vscode-collapsible/vscode-collapsible'; import '@vscode-elements/elements/dist/vscode-form-container'; -import { useCallback, useState } from 'react'; -import type { UpdateRequestedData } from '../inspector-data/messages'; +import { useCallback, useEffect, useState } from 'react'; +import type { CollapsibleStateData, UpdateRequestedData } from '../inspector-data/messages'; import { UpdateRequested } from '../inspector-data/messages'; import type { TypedValue } from '../inspector-data/types'; import { usePublisher } from '../messenger/usePublisher'; +import { reportCollapsibleState } from './collapsibleReporter'; import { DynamicValue } from './DynamicValue'; import { ErrorCard } from './ErrorCard'; import type { ValueUpdated } from './valueProps'; interface TypedValueDetailsProps { typedValue: TypedValue; + collapsibleState?: CollapsibleStateData; } -export function TypedValueDetails({ typedValue }: TypedValueDetailsProps) { +export function TypedValueDetails({ typedValue, collapsibleState }: TypedValueDetailsProps) { const [requestData, setRequestData] = useState(); usePublisher(UpdateRequested, requestData); + const [open, setOpen] = useState(false); + + const collapsibleKey = typedValue.schema.typePath; + + useEffect(() => { + reportCollapsibleState(collapsibleKey, open); + }, [collapsibleKey, open]); + + useEffect(() => { + return () => { + reportCollapsibleState(collapsibleKey, false); + }; + }, [collapsibleKey]); + + useEffect(() => { + if (collapsibleState !== undefined) { + setOpen(collapsibleState.anyExpanded); + } + }, [collapsibleState, collapsibleKey]); + + const onToggle = useCallback( + (e: VscCollapsibleToggleEvent) => { + const expanded = e.detail.open; + setOpen(expanded); + }, + [collapsibleKey], + ); if (typedValue.error || !typedValue.schema) { const errorMessage = typedValue.error || 'No schema found.'; @@ -43,7 +73,12 @@ export function TypedValueDetails({ typedValue }: TypedValueDetailsProps) { return ( - + diff --git a/src/schema-components/collapsibleReporter.ts b/src/schema-components/collapsibleReporter.ts new file mode 100644 index 0000000..35c6bd7 --- /dev/null +++ b/src/schema-components/collapsibleReporter.ts @@ -0,0 +1,21 @@ +import { CollapsibleStateChanged } from '../inspector-data/messages'; +import { vscodeMessenger } from '../messenger/vscodeMessenger'; + +const expandedKeys = new Set(); +let previouslyAnyExpanded: boolean | undefined; + +export function reportCollapsibleState(key: string, expanded: boolean): void { + if (expanded) { + expandedKeys.add(key); + } else { + expandedKeys.delete(key); + } + const anyExpanded = expandedKeys.size > 0; + if (previouslyAnyExpanded !== anyExpanded) { + previouslyAnyExpanded = anyExpanded; + vscodeMessenger.publishEvent({ + type: CollapsibleStateChanged, + data: { anyExpanded }, + }); + } +}