Maintain property collapse/expand across frames. Collapse state for a particular property maintained across frames for a specific selected item. Bug: b/315444345 Test: npm run test:unit:ci Change-Id: I0c98b51e4f65484609092a1329fb336e38acd714
diff --git a/tools/winscope/src/viewers/components/hierarchy_component.ts b/tools/winscope/src/viewers/components/hierarchy_component.ts index d902200..f758039 100644 --- a/tools/winscope/src/viewers/components/hierarchy_component.ts +++ b/tools/winscope/src/viewers/components/hierarchy_component.ts
@@ -70,7 +70,7 @@ [item]="tree" [dependencies]="dependencies" [store]="store" - [useGlobalCollapsedState]="true" + [useStoredExpandedState]="true" [itemsClickable]="true" [highlightedItem]="highlightedItem" [pinnedItems]="pinnedItems"
diff --git a/tools/winscope/src/viewers/components/properties_component.ts b/tools/winscope/src/viewers/components/properties_component.ts index 569843f..e4c2b78 100644 --- a/tools/winscope/src/viewers/components/properties_component.ts +++ b/tools/winscope/src/viewers/components/properties_component.ts
@@ -14,6 +14,7 @@ * limitations under the License. */ import {Component, ElementRef, Inject, Input} from '@angular/core'; +import {PersistentStore} from 'common/persistent_store'; import {TraceTreeNode} from 'trace/trace_tree_node'; import {TraceType, ViewNode} from 'trace/trace_type'; import {PropertiesTreeNode, Terminal} from 'viewers/common/ui_tree_utils'; @@ -70,11 +71,13 @@ *ngIf="objectKeys(propertiesTree).length > 0 && !showViewCaptureFormat()" [item]="propertiesTree" [showNode]="showNode" + [store]="store" + [useStoredExpandedState]="true" + [isExpandedByDefault]="false" [itemsClickable]="true" [highlightedItem]="highlightedProperty" (highlightedChange)="onHighlightedPropertyChange($event)" - [isLeaf]="isLeaf" - [isAlwaysCollapsed]="true"></tree-view> + [isLeaf]="isLeaf"></tree-view> </div> </div> `, @@ -136,6 +139,7 @@ @Input() displayPropertyGroups = false; @Input() isProtoDump = false; @Input() traceType: TraceType | undefined; + @Input() store!: PersistentStore; constructor(@Inject(ElementRef) private elementRef: ElementRef) {}
diff --git a/tools/winscope/src/viewers/components/properties_component_test.ts b/tools/winscope/src/viewers/components/properties_component_test.ts index f118575..05c6a89 100644 --- a/tools/winscope/src/viewers/components/properties_component_test.ts +++ b/tools/winscope/src/viewers/components/properties_component_test.ts
@@ -22,6 +22,7 @@ import {MatFormFieldModule} from '@angular/material/form-field'; import {MatInputModule} from '@angular/material/input'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {PersistentStore} from 'common/persistent_store'; import {PropertiesComponent} from './properties_component'; import {SurfaceFlingerPropertyGroupsComponent} from './surface_flinger_property_groups_component'; import {TreeComponent} from './tree_component'; @@ -52,6 +53,7 @@ component = fixture.componentInstance; htmlElement = fixture.nativeElement; + component.store = new PersistentStore(); component.userOptions = { showDiff: { name: 'Show diff',
diff --git a/tools/winscope/src/viewers/components/tree_component.ts b/tools/winscope/src/viewers/components/tree_component.ts index d662ce7..09af453 100644 --- a/tools/winscope/src/viewers/components/tree_component.ts +++ b/tools/winscope/src/viewers/components/tree_component.ts
@@ -21,7 +21,9 @@ Inject, Input, Output, + SimpleChanges, } from '@angular/core'; +import {assertDefined} from 'common/assert_utils'; import {PersistentStore} from 'common/persistent_store'; import {TraceType} from 'trace/trace_type'; import {HierarchyTreeNode, UiTreeNode, UiTreeUtils} from 'viewers/common/ui_tree_utils'; @@ -41,13 +43,12 @@ [class.child-selected]="hasSelectedChild()" [class.hover]="nodeHover" [class.childHover]="childHover" - [isAlwaysCollapsed]="isAlwaysCollapsed" [class]="diffClass(item)" [style]="nodeOffsetStyle()" [item]="item" [flattened]="isFlattened" [isLeaf]="isLeaf(this.item)" - [isCollapsed]="isAlwaysCollapsed ?? isCollapsed()" + [isExpanded]="isExpanded()" [hasChildren]="hasChildren()" [isPinned]="isPinned()" [isSelected]="isHighlighted(item, highlightedItem)" @@ -60,7 +61,7 @@ *ngIf="hasChildren()" class="children" [class.flattened]="isFlattened" - [hidden]="!isCollapsed()"> + [hidden]="!isExpanded()"> <tree-view *ngFor="let child of children(); trackBy: childTrackById" class="childrenTree" @@ -70,7 +71,7 @@ [isLeaf]="isLeaf" [dependencies]="dependencies" [isFlattened]="isFlattened" - [useGlobalCollapsedState]="useGlobalCollapsedState" + [useStoredExpandedState]="useStoredExpandedState" [initialDepth]="initialDepth + 1" [highlightedItem]="highlightedItem" [pinnedItems]="pinnedItems" @@ -94,14 +95,16 @@ @Input() dependencies: TraceType[] = []; @Input() item?: UiTreeNode; - @Input() store!: PersistentStore; + @Input() store?: PersistentStore; @Input() isFlattened? = false; @Input() initialDepth = 0; @Input() highlightedItem: string = ''; - @Input() pinnedItems?: HierarchyTreeNode[] = []; + @Input() pinnedItems: HierarchyTreeNode[] = []; @Input() itemsClickable?: boolean; - @Input() useGlobalCollapsedState?: boolean; - @Input() isAlwaysCollapsed?: boolean; + + // Conditionally use stored states. Some traces (e.g. transactions) do not provide items with the "stable id" field needed to search values in the storage. + @Input() useStoredExpandedState = false; + @Input() showNode = (item: UiTreeNode) => true; @Input() isLeaf = (item?: UiTreeNode) => { return !item || !item.children || item.children.length === 0; @@ -113,13 +116,14 @@ @Output() hoverStart = new EventEmitter<void>(); @Output() hoverEnd = new EventEmitter<void>(); - isCollapsedByDefault = true; - localCollapsedState = this.isCollapsedByDefault; + localExpandedState = true; nodeHover = false; childHover = false; readonly levelOffset = 24; nodeElement: HTMLElement; + private storeKeyExpandedState = ''; + childTrackById(index: number, child: UiTreeNode): string { if (child.stableId !== undefined) { return child.stableId; @@ -138,13 +142,18 @@ this.nodeElement?.addEventListener('mouseleave', this.nodeMouseLeaveEventListener); } - ngOnInit() { - if (this.isCollapsedByDefault) { - this.setCollapseValue(this.isCollapsedByDefault); + ngOnChanges(changes: SimpleChanges) { + if (changes['item']) { + this.storeKeyExpandedState = `treeView.expandedState.item.${this.dependencies}.${this.item?.stableId}`; + if (this.store) { + this.setExpandedValue( + true, + assertDefined(this.store).get(this.storeKeyExpandedState) === null + ); + } else { + this.setExpandedValue(true); + } } - } - - ngOnChanges() { if ( this.item instanceof HierarchyTreeNode && UiTreeUtils.isHighlighted(this.item, this.highlightedItem) @@ -183,12 +192,6 @@ }; } - private updateHighlightedItem() { - if (this.item?.stableId) { - this.highlightedChange.emit(`${this.item.stableId}`); - } - } - isPinned() { if (this.item instanceof HierarchyTreeNode) { return this.pinnedItems?.map((item) => `${item.stableId}`).includes(`${this.item.stableId}`); @@ -213,25 +216,23 @@ } toggleTree() { - this.setCollapseValue(!this.isCollapsed()); + this.setExpandedValue(!this.isExpanded()); } expandTree() { - this.setCollapseValue(true); + this.setExpandedValue(true); } - isCollapsed() { - if (this.isAlwaysCollapsed || this.isLeaf(this.item)) { + isExpanded() { + if (this.isLeaf(this.item)) { return true; } - if (this.useGlobalCollapsedState) { - return ( - this.store.get(`collapsedState.item.${this.dependencies}.${this.item?.stableId}`) === - 'true' ?? this.isCollapsedByDefault - ); + if (this.useStoredExpandedState) { + return assertDefined(this.store).get(this.storeKeyExpandedState) === 'true' ?? false; } - return this.localCollapsedState; + + return this.localExpandedState; } children(): UiTreeNode[] { @@ -251,7 +252,7 @@ if (!this.hasChildren()) { return false; } - for (const child of this.item!.children!) { + for (const child of assertDefined(this.item?.children)) { if (child.stableId && this.highlightedItem === child.stableId) { return true; } @@ -259,14 +260,17 @@ return false; } - private setCollapseValue(isCollapsed: boolean) { - if (this.useGlobalCollapsedState) { - this.store.add( - `collapsedState.item.${this.dependencies}.${this.item?.stableId}`, - `${isCollapsed}` - ); + private updateHighlightedItem() { + if (this.item?.stableId) { + this.highlightedChange.emit(`${this.item.stableId}`); + } + } + + private setExpandedValue(isExpanded: boolean, shouldUpdateStoredState = true) { + if (this.useStoredExpandedState && shouldUpdateStoredState) { + assertDefined(this.store).add(this.storeKeyExpandedState, `${isExpanded}`); } else { - this.localCollapsedState = isCollapsed; + this.localExpandedState = isExpanded; } }
diff --git a/tools/winscope/src/viewers/components/tree_component_test.ts b/tools/winscope/src/viewers/components/tree_component_test.ts index 55a5b7c..63c1f2d 100644 --- a/tools/winscope/src/viewers/components/tree_component_test.ts +++ b/tools/winscope/src/viewers/components/tree_component_test.ts
@@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Component, NO_ERRORS_SCHEMA, ViewChild} from '@angular/core'; +import {Component, NO_ERRORS_SCHEMA, QueryList, ViewChildren} from '@angular/core'; import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing'; import {assertDefined} from 'common/assert_utils'; import {PersistentStore} from 'common/persistent_store'; @@ -25,6 +25,7 @@ let fixture: ComponentFixture<TestHostComponent>; let component: TestHostComponent; let htmlElement: HTMLElement; + const children = makeTreeNodeChildren(); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -48,17 +49,17 @@ }); it('can identify if a parent node has a selected child', () => { - expect(component.treeComponent.hasSelectedChild()).toBeFalse(); - component.highlightedItem = 'child3'; + expect(component.treeComponents.first.hasSelectedChild()).toBeFalse(); + component.highlightedItem = '1child3'; fixture.detectChanges(); - expect(component.treeComponent.hasSelectedChild()).toBeTrue(); + expect(component.treeComponents.first.hasSelectedChild()).toBeTrue(); }); it('highlights item upon node click', () => { const treeNode = htmlElement.querySelector('tree-node'); expect(treeNode).toBeTruthy(); - const spy = spyOn(component.treeComponent.highlightedChange, 'emit'); + const spy = spyOn(component.treeComponents.first.highlightedChange, 'emit'); (treeNode as HTMLButtonElement).dispatchEvent(new MouseEvent('click', {detail: 1})); fixture.detectChanges(); expect(spy).toHaveBeenCalled(); @@ -68,32 +69,68 @@ const treeNode = htmlElement.querySelector('tree-node'); expect(treeNode).toBeTruthy(); - const currCollapseValue = component.treeComponent.localCollapsedState; + const currCollapseValue = component.treeComponents.first.localExpandedState; (treeNode as HTMLButtonElement).dispatchEvent(new MouseEvent('click', {detail: 2})); fixture.detectChanges(); - expect(!currCollapseValue).toBe(component.treeComponent.localCollapsedState); + expect(!currCollapseValue).toBe(component.treeComponents.first.localExpandedState); }); it('scrolls selected node into view if out of view', async () => { - const treeNode = assertDefined(htmlElement.querySelector(`#nodechild50`)); + const treeNode = assertDefined(htmlElement.querySelector('#node1child50')); const spy = spyOn(treeNode, 'scrollIntoView'); - component.highlightedItem = 'child50'; + component.highlightedItem = '1child50'; fixture.detectChanges(); expect(spy).toHaveBeenCalled(); }); it('does not scroll selected element if already in view', () => { - const treeNode = assertDefined(htmlElement.querySelector(`#nodechild2`)); + const treeNode = assertDefined(htmlElement.querySelector('#node1child2')); const spy = spyOn(treeNode, 'scrollIntoView'); - component.highlightedItem = 'child2'; + component.highlightedItem = '1child2'; fixture.detectChanges(); expect(spy).not.toHaveBeenCalled(); }); + it('sets initial expanded state to true by default', () => { + const tree = assertDefined(component.treeComponents.get(1)); + fixture.detectChanges(); + expect(tree.isExpanded()).toBeTrue(); + }); + + it('does not initially set expanded state to true if already exists in store', () => { + // item1 expanded by default + const tree = assertDefined(component.treeComponents.get(1)); + fixture.detectChanges(); + expect(tree.isExpanded()).toBeTrue(); + + // item1 collapsed + tree.toggleTree(); + fixture.detectChanges(); + expect(tree.isExpanded()).toBeFalse(); + + // item0 expanded by default + component.itemWithStoredExpandedState = component.item0; + fixture.detectChanges(); + expect(tree.isExpanded()).toBeTrue(); + + // item0 collapsed state retained + component.itemWithStoredExpandedState = component.item1; + fixture.detectChanges(); + expect(tree.isExpanded()).toBeFalse(); + }); + function makeTreeNodeChildren(): UiTreeNode[] { const children = []; for (let i = 0; i < 60; i++) { - children.push({kind: `${i}`, stableId: `child${i}`, name: `Child${i}`}); + const child: UiTreeNode = { + kind: `${i}`, + stableId: `1child${i}`, + name: `Child${i}`, + children: [ + {kind: `${i}`, stableId: `1innerChild${i}`, name: `InnerChild${i}`, children: []}, + ], + }; + children.push(child); } return children; } @@ -102,26 +139,57 @@ selector: 'host-component', template: ` <tree-view - [item]="item" + [item]="item0" [store]="store" [isFlattened]="false" [isPinned]="false" [highlightedItem]="highlightedItem" [itemsClickable]="true"></tree-view> + + <tree-view + [item]="itemWithStoredExpandedState" + [store]="store" + [isFlattened]="false" + [isPinned]="false" + [highlightedItem]="highlightedItem" + [useStoredExpandedState]="true" + [itemsClickable]="true"></tree-view> `, }) class TestHostComponent { - item: UiTreeNode = { + item0: UiTreeNode = { + simplifyNames: false, + kind: 'entry', + name: 'LayerTraceEntry', + stableId: 'LayerTraceEntry 1', + children, + }; + + item1: UiTreeNode = { simplifyNames: false, kind: 'entry', name: 'LayerTraceEntry', stableId: 'LayerTraceEntry 2', - children: makeTreeNodeChildren(), + children: [ + { + kind: '1', + stableId: '2child1', + name: 'Child1', + children: [], + }, + ], }; + + itemWithStoredExpandedState = this.item1; + store = new PersistentStore(); highlightedItem = ''; - @ViewChild(TreeComponent) - treeComponent!: TreeComponent; + constructor() { + localStorage.clear(); + } + + @ViewChildren(TreeComponent) + treeComponents!: QueryList<TreeComponent>; } });
diff --git a/tools/winscope/src/viewers/components/tree_node_component.ts b/tools/winscope/src/viewers/components/tree_node_component.ts index a2e3e6b..520842d 100644 --- a/tools/winscope/src/viewers/components/tree_node_component.ts +++ b/tools/winscope/src/viewers/components/tree_node_component.ts
@@ -22,7 +22,7 @@ template: ` <button *ngIf="showChevron()" class="icon-button toggle-tree-btn" (click)="toggleTree($event)"> <mat-icon> - {{ isCollapsed ? 'arrow_drop_down' : 'chevron_right' }} + {{ isExpanded ? 'arrow_drop_down' : 'chevron_right' }} </mat-icon> </button> @@ -44,7 +44,7 @@ </div> <button - *ngIf="hasChildren && !isCollapsed" + *ngIf="hasChildren && !isExpanded" class="icon-button expand-tree-btn" [class]="collapseDiffClass" (click)="expandTree($event)"> @@ -57,11 +57,10 @@ @Input() item!: UiTreeNode; @Input() isLeaf?: boolean; @Input() flattened?: boolean; - @Input() isCollapsed?: boolean; + @Input() isExpanded?: boolean; @Input() hasChildren?: boolean = false; @Input() isPinned?: boolean = false; @Input() isInPinnedSection?: boolean = false; - @Input() isAlwaysCollapsed?: boolean; @Input() isSelected?: boolean = false; @Output() toggleTreeChange = new EventEmitter<void>(); @@ -103,10 +102,8 @@ } toggleTree(event: MouseEvent) { - if (!this.isAlwaysCollapsed) { - event.stopPropagation(); - this.toggleTreeChange.emit(); - } + event.stopPropagation(); + this.toggleTreeChange.emit(); } showChevron() { @@ -128,7 +125,7 @@ } updateCollapseDiffClass() { - if (this.isCollapsed) { + if (this.isExpanded) { return ''; }
diff --git a/tools/winscope/src/viewers/components/tree_node_component_test.ts b/tools/winscope/src/viewers/components/tree_node_component_test.ts index c80e697..b6f8ca7 100644 --- a/tools/winscope/src/viewers/components/tree_node_component_test.ts +++ b/tools/winscope/src/viewers/components/tree_node_component_test.ts
@@ -83,7 +83,7 @@ template: ` <tree-node [item]="item" - [isCollapsed]="false" + [isExpanded]="false" [isPinned]="false" [isInPinnedSection]="false" [hasChildren]="true"
diff --git a/tools/winscope/src/viewers/components/viewer_input_method_component.ts b/tools/winscope/src/viewers/components/viewer_input_method_component.ts index 574d791..3f20f50 100644 --- a/tools/winscope/src/viewers/components/viewer_input_method_component.ts +++ b/tools/winscope/src/viewers/components/viewer_input_method_component.ts
@@ -48,6 +48,7 @@ <properties-view class="properties-view" + [store]="store" [userOptions]="inputData?.propertiesUserOptions ?? {}" [propertiesTree]="inputData?.propertiesTree ?? {}"></properties-view> </div>
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts index 21858bc..4712bce 100644 --- a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts +++ b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts
@@ -47,6 +47,7 @@ [highlightedProperty]="inputData?.highlightedProperty ?? ''" [selectedItem]="inputData?.selectedLayer ?? {}" [traceType]="${TraceType.SURFACE_FLINGER}" + [store]="store" [displayPropertyGroups]="inputData?.displayPropertyGroups" [isProtoDump]="true"></properties-view> </div>
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts index fdf3b4c..58b7433 100644 --- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts +++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
@@ -104,8 +104,7 @@ <tree-view [item]="uiData.selectedTransitionPropertiesTree" [showNode]="showNode" - [isLeaf]="isLeaf" - [isAlwaysCollapsed]="true"> + [isLeaf]="isLeaf"> </tree-view> <div *ngIf="!uiData.selectedTransitionPropertiesTree">No selected transition.</div> </div>
diff --git a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts index 9e4935c..e00d690 100644 --- a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts +++ b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts
@@ -52,6 +52,7 @@ [propertiesTree]="inputData?.propertiesTree ?? {}" [selectedItem]="inputData?.selectedViewNode ?? null" [traceType]="${TraceType.VIEW_CAPTURE}" + [store]="store" [isProtoDump]="false"> </properties-view> </div>
diff --git a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts index 907f6aa..3d558f8 100644 --- a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts +++ b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts
@@ -44,6 +44,7 @@ [userOptions]="inputData?.propertiesUserOptions ?? {}" [propertiesTree]="inputData?.propertiesTree ?? {}" [highlightedProperty]="inputData?.highlightedProperty ?? ''" + [store]="store" [isProtoDump]="true"></properties-view> </div> `,