diff --git a/tools/winscope-ng/src/app/app.module.ts b/tools/winscope-ng/src/app/app.module.ts index 92c3da9d8..f1e0e88a9 100644 --- a/tools/winscope-ng/src/app/app.module.ts +++ b/tools/winscope-ng/src/app/app.module.ts @@ -17,6 +17,7 @@ import { MatRadioModule } from "@angular/material/radio"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { HttpClientModule } from "@angular/common/http"; import { MatSliderModule } from "@angular/material/slider"; +import { MatTooltipModule } from "@angular/material/tooltip"; import { AppComponent } from "./components/app.component"; import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component"; @@ -26,11 +27,14 @@ import { AdbProxyComponent } from "./components/adb_proxy.component"; import { WebAdbComponent } from "./components/web_adb.component"; import { TraceConfigComponent } from "./components/trace_config.component"; import { UploadTracesComponent } from "./components/upload_traces.component"; -import { HierarchyComponent } from "viewers/hierarchy.component"; -import { PropertiesComponent } from "viewers/properties.component"; -import { RectsComponent } from "viewers/rects.component"; +import { HierarchyComponent } from "viewers/components/hierarchy.component"; +import { PropertiesComponent } from "viewers/components/properties.component"; +import { RectsComponent } from "viewers/components/rects/rects.component"; import { TraceViewHeaderComponent } from "./components/trace_view_header.component"; import { TraceViewComponent } from "./components/trace_view.component"; +import { TreeComponent } from "viewers/components/tree.component"; +import { TreeNodeComponent } from "viewers/components/tree_node.component"; +import { TreeElementComponent } from "viewers/components/tree_element.component"; @NgModule({ declarations: [ @@ -46,7 +50,10 @@ import { TraceViewComponent } from "./components/trace_view.component"; PropertiesComponent, RectsComponent, TraceViewHeaderComponent, - TraceViewComponent + TraceViewComponent, + TreeComponent, + TreeNodeComponent, + TreeElementComponent ], imports: [ BrowserModule, @@ -67,7 +74,8 @@ import { TraceViewComponent } from "./components/trace_view.component"; BrowserAnimationsModule, HttpClientModule, MatSliderModule, - MatRadioModule + MatRadioModule, + MatTooltipModule ], bootstrap: [AppComponent] }) diff --git a/tools/winscope-ng/src/app/components/app.component.ts b/tools/winscope-ng/src/app/components/app.component.ts index b854e370f..09f9e039a 100644 --- a/tools/winscope-ng/src/app/components/app.component.ts +++ b/tools/winscope-ng/src/app/components/app.component.ts @@ -30,39 +30,42 @@ import { Viewer } from "viewers/viewer"; template: `
Winscope Viewer 2.0 - - - + +
- + - +
-
-
- -
-
-
+ +
+ +
+ +
+
`, - styles: [".time-slider {width: 100%}"], + styles: [ + ".time-slider {width: 100%}", + ".upload-new-btn {float: right}" + ], encapsulation: ViewEncapsulation.None }) export class AppComponent { @@ -128,7 +131,7 @@ export class AppComponent { const traceCardContent = traceCard.querySelector(".trace-card-content")!; const view = viewer.getView(); - (view as any).showTrace = (traceView as any).showTrace; + (view as any).store = this.store; traceCardContent.appendChild(view); }); } diff --git a/tools/winscope-ng/src/app/components/collect_traces.component.ts b/tools/winscope-ng/src/app/components/collect_traces.component.ts index c9939bb4f..28f989023 100644 --- a/tools/winscope-ng/src/app/components/collect_traces.component.ts +++ b/tools/winscope-ng/src/app/components/collect_traces.component.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, Inject, Input, Output, EventEmitter, OnInit, OnDestroy } from "@angular/core"; +import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from "@angular/core"; import { ProxyConnection } from "trace_collection/proxy_connection"; import { Connection } from "trace_collection/connection"; import { setTraces } from "trace_collection/set_traces"; diff --git a/tools/winscope-ng/src/app/trace_coordinator.ts b/tools/winscope-ng/src/app/trace_coordinator.ts index bd889bee5..773fd817e 100644 --- a/tools/winscope-ng/src/app/trace_coordinator.ts +++ b/tools/winscope-ng/src/app/trace_coordinator.ts @@ -22,6 +22,7 @@ import { Viewer } from "viewers/viewer"; import { ViewerFactory } from "viewers/viewer_factory"; import { LoadedTrace } from "app/loaded_trace"; import { TRACE_INFO } from "./trace_info"; +import { TimestampUtils } from "common/trace/timestamp_utils"; class TraceCoordinator { private parsers: Parser[]; @@ -105,8 +106,17 @@ class TraceCoordinator { this.parsers.forEach(parser => { const targetTimestamp = timestamp; const entry = parser.getTraceEntry(targetTimestamp); + let prevEntry = null; + + const parserTimestamps = parser.getTimestamps(timestamp.getType()); + if (parserTimestamps) { + const closestIndex = TimestampUtils.getClosestIndex(targetTimestamp, parserTimestamps); + if (closestIndex) { + prevEntry = parser.getTraceEntry(parserTimestamps[closestIndex-1]) ?? null; + } + } if (entry !== undefined) { - traceEntries.set(parser.getTraceType(), entry); + traceEntries.set(parser.getTraceType(), [entry, prevEntry]); } }); diff --git a/tools/winscope-ng/src/common/trace/flickerlib/layers/Layer.ts b/tools/winscope-ng/src/common/trace/flickerlib/layers/Layer.ts index f27a35efe..082e052cb 100644 --- a/tools/winscope-ng/src/common/trace/flickerlib/layers/Layer.ts +++ b/tools/winscope-ng/src/common/trace/flickerlib/layers/Layer.ts @@ -17,7 +17,6 @@ import { Layer, LayerProperties, Rect, toActiveBuffer, toColor, toRect, toRectF, toRegion } from "../common" import { shortenName } from '../mixin' -import { RELATIVE_Z_CHIP, GPU_CHIP, HWC_CHIP } from '../treeview/Chips' import Transform from './Transform' Layer.fromProto = function (proto: any): Layer { @@ -96,18 +95,6 @@ function addAttributes(entry: Layer, proto: any) { entry.rect.ref = entry; entry.rect.label = entry.name; entry.chips = []; - updateChips(entry); -} - -function updateChips(entry: Layer) { - if ((entry.zOrderRelativeOf || -1) !== -1) { - entry.chips.push(RELATIVE_Z_CHIP); - } - if (entry.hwcCompositionType === 'CLIENT') { - entry.chips.push(GPU_CHIP); - } else if (entry.hwcCompositionType === 'DEVICE' || entry.hwcCompositionType === 'SOLID_COLOR') { - entry.chips.push(HWC_CHIP); - } } export default Layer; diff --git a/tools/winscope-ng/src/common/trace/flickerlib/layers/LayerTraceEntry.ts b/tools/winscope-ng/src/common/trace/flickerlib/layers/LayerTraceEntry.ts index 12a5e40a0..978454a07 100644 --- a/tools/winscope-ng/src/common/trace/flickerlib/layers/LayerTraceEntry.ts +++ b/tools/winscope-ng/src/common/trace/flickerlib/layers/LayerTraceEntry.ts @@ -16,7 +16,6 @@ import { Display, LayerTraceEntry, LayerTraceEntryBuilder, toRect, toSize, toTransform } from "../common" import Layer from './Layer' -import { VISIBLE_CHIP, RELATIVE_Z_PARENT_CHIP, MISSING_LAYER } from '../treeview/Chips' LayerTraceEntry.fromProto = function (protos: any[], displayProtos: any[], timestamp: number, hwcBlob: string, where: string = ''): LayerTraceEntry { @@ -25,7 +24,6 @@ LayerTraceEntry.fromProto = function (protos: any[], displayProtos: any[], const builder = new LayerTraceEntryBuilder(timestamp, layers, displays, hwcBlob, where); const entry: LayerTraceEntry = builder.build(); - updateChildren(entry); addAttributes(entry, protos); return entry; } @@ -51,20 +49,6 @@ function addAttributes(entry: LayerTraceEntry, protos: any) { entry.isVisible = true; } -function updateChildren(entry: LayerTraceEntry) { - entry.flattenedLayers.forEach((it: any) => { - if (it.isVisible) { - it.chips.push(VISIBLE_CHIP); - } - if (it.zOrderRelativeOf) { - it.chips.push(RELATIVE_Z_PARENT_CHIP); - } - if (it.isMissing) { - it.chips.push(MISSING_LAYER); - } - }); -} - function newDisplay(proto: any): Display { return new Display( proto.id, diff --git a/tools/winscope-ng/src/common/trace/flickerlib/treeview/Chip.ts b/tools/winscope-ng/src/common/trace/flickerlib/treeview/Chip.ts deleted file mode 100644 index 30f1c839b..000000000 --- a/tools/winscope-ng/src/common/trace/flickerlib/treeview/Chip.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ChipType from "./ChipType" - -export default class Chip { - short: String - long: String - type: ChipType - - constructor(short: String, long: String, type: ChipType) { - this.short = short - this.long = long - this.type = type - } -} \ No newline at end of file diff --git a/tools/winscope-ng/src/common/trace/flickerlib/treeview/Chips.ts b/tools/winscope-ng/src/common/trace/flickerlib/treeview/Chips.ts deleted file mode 100644 index 23b8e8b85..000000000 --- a/tools/winscope-ng/src/common/trace/flickerlib/treeview/Chips.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Chip from "./Chip" -import ChipType from "./ChipType" - -export const VISIBLE_CHIP = new Chip("V", "visible", ChipType.DEFAULT) - -export const RELATIVE_Z_CHIP = { - short: 'RelZ', - long: 'Is relative Z-ordered to another surface', - class: 'warn', -}; - -export const RELATIVE_Z_PARENT_CHIP = { - short: 'RelZParent', - long: 'Something is relative Z-ordered to this surface', - class: 'warn', -}; - -export const MISSING_LAYER = { - short: 'MissingLayer', - long: 'This layer was referenced from the parent, but not present in the trace', - class: 'error', -}; - -export const GPU_CHIP = { - short: 'GPU', - long: 'This layer was composed on the GPU', - class: 'gpu', -}; - -export const HWC_CHIP = { - short: 'HWC', - long: 'This layer was composed by Hardware Composer', - class: 'hwc', -}; \ No newline at end of file diff --git a/tools/winscope-ng/src/common/trace/flickerlib/windows/Activity.ts b/tools/winscope-ng/src/common/trace/flickerlib/windows/Activity.ts index 51e8139e6..5455959da 100644 --- a/tools/winscope-ng/src/common/trace/flickerlib/windows/Activity.ts +++ b/tools/winscope-ng/src/common/trace/flickerlib/windows/Activity.ts @@ -16,7 +16,6 @@ import { shortenName } from '../mixin' import { Activity } from "../common" -import { VISIBLE_CHIP } from '../treeview/Chips' import WindowContainer from "./WindowContainer" Activity.fromProto = function (proto: any): Activity { @@ -50,7 +49,7 @@ function addAttributes(entry: Activity, proto: any) { entry.proto = proto; entry.kind = entry.constructor.name; entry.shortName = shortenName(entry.name); - entry.chips = entry.isVisible ? [VISIBLE_CHIP] : []; + entry.chips = []; } export default Activity; diff --git a/tools/winscope-ng/src/common/trace/flickerlib/windows/WindowState.ts b/tools/winscope-ng/src/common/trace/flickerlib/windows/WindowState.ts index a9cdf0829..e058bf7d4 100644 --- a/tools/winscope-ng/src/common/trace/flickerlib/windows/WindowState.ts +++ b/tools/winscope-ng/src/common/trace/flickerlib/windows/WindowState.ts @@ -16,7 +16,6 @@ import { shortenName } from '../mixin' import { toRect, Size, WindowState, WindowLayoutParams } from "../common" -import { VISIBLE_CHIP } from '../treeview/Chips' import WindowContainer from "./WindowContainer" WindowState.fromProto = function (proto: any, isActivityInTree: Boolean): WindowState { @@ -131,7 +130,7 @@ function addAttributes(entry: WindowState, proto: any) { entry.rect.label = entry.name; entry.proto = proto; entry.shortName = shortenName(entry.name); - entry.chips = entry.isVisible ? [VISIBLE_CHIP] : []; + entry.chips = []; } export default WindowState diff --git a/tools/winscope-ng/src/common/trace/timestamp_utils.ts b/tools/winscope-ng/src/common/trace/timestamp_utils.ts new file mode 100644 index 000000000..3abd9b16b --- /dev/null +++ b/tools/winscope-ng/src/common/trace/timestamp_utils.ts @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ArrayUtils } from "common/utils/array_utils"; +import { Timestamp } from "common/trace/timestamp"; + +export class TimestampUtils { + static getClosestIndex(targetTimestamp: Timestamp, timestamps: Timestamp[]) { + if (timestamps === undefined) { + throw TypeError(`Timestamps with type "${targetTimestamp.getType()}" not available`); + } + return ArrayUtils.binarySearchLowerOrEqual(timestamps, targetTimestamp); + } +} diff --git a/tools/winscope-ng/src/test/e2e/viewer_surface_flinger.spec.ts b/tools/winscope-ng/src/test/e2e/viewer_surface_flinger.spec.ts index fb82ee53c..103bd386d 100644 --- a/tools/winscope-ng/src/test/e2e/viewer_surface_flinger.spec.ts +++ b/tools/winscope-ng/src/test/e2e/viewer_surface_flinger.spec.ts @@ -29,7 +29,7 @@ describe("Viewer SurfaceFlinger", () => { const loadData = element(by.css(".load-btn")); loadData.click(); - const surfaceFlingerCard: ElementFinder = element(by.css(".trace-card")); + const surfaceFlingerCard: ElementFinder = element(by.css(".trace-card-title-text")); expect(surfaceFlingerCard.getText()).toContain("Surface Flinger"); }); }); diff --git a/tools/winscope-ng/src/viewers/common/chip.ts b/tools/winscope-ng/src/viewers/common/chip.ts new file mode 100644 index 000000000..842d4c2af --- /dev/null +++ b/tools/winscope-ng/src/viewers/common/chip.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class Chip { + short: string; + long: string; + type: string; + + constructor(short: string, long: string, type: string) { + this.short = short; + this.long = long; + this.type = type; + } +} + +export const VISIBLE_CHIP = new Chip("V", "visible", "default"); + +export const RELATIVE_Z_CHIP = new Chip( + "RelZ", + "Is relative Z-ordered to another surface", + "warn", +); + +export const RELATIVE_Z_PARENT_CHIP = new Chip( + "RelZParent", + "Something is relative Z-ordered to this surface", + "warn", +); + +export const MISSING_LAYER = new Chip( + "MissingLayer", + "This layer was referenced from the parent, but not present in the trace", + "error", +); + +export const GPU_CHIP = new Chip( + "GPU", + "This layer was composed on the GPU", + "gpu", +); + +export const HWC_CHIP = new Chip( + "HWC", + "This layer was composed by Hardware Composer", + "hwc", +); \ No newline at end of file diff --git a/tools/winscope-ng/src/viewers/common/tree_utils.spec.ts b/tools/winscope-ng/src/viewers/common/tree_utils.spec.ts new file mode 100644 index 000000000..5579bb77f --- /dev/null +++ b/tools/winscope-ng/src/viewers/common/tree_utils.spec.ts @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RELATIVE_Z_CHIP } from "viewers/common/chip"; +import { getFilter, TreeGenerator } from "viewers/common/tree_utils"; + +describe("TreeGenerator", () => { + it("generates tree", () => { + const tree = { + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + id: 0, + chips: [], + children: [{ + kind: "3", + id: "3", + name: "Child1", + children: [ + { + kind: "2", + id: "2", + name: "Child2", + children: [] + } + ]}] + }; + const expected = { + simplifyNames: false, + name: "BaseLayerTraceEntry", + id: 0, + children: [ + { + id: "3", + name: "Child1", + children: [{ + kind: "2", + id: "2", + name: "Child2", + children: [], + simplifyNames: false, + showInFilteredView: true, + stableId: undefined, + shortName: undefined, + chips: [ RELATIVE_Z_CHIP ] + }], + kind: "3", + simplifyNames: false, + showInFilteredView: true, + stableId: undefined, + shortName: undefined, + chips: [ RELATIVE_Z_CHIP ], + } + ], + kind: "entry", + stableId: undefined, + shortName: "BLTE", + chips: [], + showInFilteredView: true, + }; + + const userOptions = {}; + const filter = getFilter(""); + const generator = new TreeGenerator(tree, userOptions, filter); + expect(generator.generateTree()).toEqual(expected); + }); + + it("generates diff tree with no diff", () => { + const tree = { + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + stableId: "0", + chips: [], + id: 0, + children: [{ + kind: "3", + id: "3", + stableId: "3", + name: "Child1", + children: [ + { + kind: "2", + id: "2", + stableId: "2", + name: "Child2", + } + ]}] + }; + const newTree = tree; + const expected = { + simplifyNames: false, + name: "BaseLayerTraceEntry", + id: 0, + stableId: "0", + children: [ + { + id: "3", + stableId: "3", + name: "Child1", + children: [{ + kind: "2", + id: "2", + name: "Child2", + children: [], + simplifyNames: false, + showInFilteredView: true, + stableId: "2", + shortName: undefined, + diff: "none", + chips: [ RELATIVE_Z_CHIP ] + }], + kind: "3", + shortName: undefined, + simplifyNames: false, + showInFilteredView: true, + diff: "none", + chips: [ RELATIVE_Z_CHIP ] + } + ], + kind: "entry", + shortName: "BLTE", + chips: [], + showInFilteredView: true, + diff: "none" + }; + + const userOptions = {}; + const filter = getFilter(""); + const generator = new TreeGenerator(tree, userOptions, filter); + expect(generator.withUniqueNodeId((node: any) => { + if (node) return node.stableId; + else return null; + }).compareWith(newTree).generateFinalDiffTree()).toEqual(expected); + }); + + it("generates diff tree with moved node", () => { + const tree = { + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + stableId: "0", + chips: [], + id: 0, + children: [{ + kind: "3", + id: "3", + stableId: "3", + name: "Child1", + children: [ + { + kind: "2", + id: "2", + stableId: "2", + name: "Child2", + } + ]}] + }; + const newTree = { + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + stableId: "0", + chips: [], + id: 0, + children: [ + { + kind: "3", + id: "3", + stableId: "3", + name: "Child1", + children: [] + }, + { + kind: "2", + id: "2", + stableId: "2", + name: "Child2", + } + ] + }; + const expected = { + simplifyNames: false, + name: "BaseLayerTraceEntry", + id: 0, + stableId: "0", + children: [ + { + id: "3", + stableId: "3", + name: "Child1", + diff: "none", + children: [ { + kind: "2", + id: "2", + name: "Child2", + diff: "addedMove", + children: [], + simplifyNames: false, + showInFilteredView: true, + stableId: "2", + shortName: undefined, + chips: [ RELATIVE_Z_CHIP ] + }], + kind: "3", + shortName: undefined, + simplifyNames: false, + showInFilteredView: true, + chips: [ RELATIVE_Z_CHIP ] + }, + { + kind: "2", + id: "2", + name: "Child2", + diff: "deletedMove", + children: [], + simplifyNames: false, + showInFilteredView: true, + stableId: "2", + shortName: undefined, + chips: [ RELATIVE_Z_CHIP ] + } + ], + kind: "entry", + shortName: "BLTE", + chips: [], + showInFilteredView: true, + diff: "none" + }; + + const userOptions = {}; + const filter = getFilter(""); + const generator = new TreeGenerator(tree, userOptions, filter); + const newDiffTree = generator.withUniqueNodeId((node: any) => { + if (node) return node.stableId; + else return null; + }).compareWith(newTree).generateFinalDiffTree(); + expect(newDiffTree).toEqual(expected); + }); +}); diff --git a/tools/winscope-ng/src/viewers/common/tree_utils.ts b/tools/winscope-ng/src/viewers/common/tree_utils.ts new file mode 100644 index 000000000..95e3af050 --- /dev/null +++ b/tools/winscope-ng/src/viewers/common/tree_utils.ts @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Layer, BaseLayerTraceEntry } from "common/trace/flickerlib/common"; +import ObjectFormatter from "common/trace/flickerlib/ObjectFormatter"; +import { UserOptions } from "viewers/common/user_options"; +import { HWC_CHIP, GPU_CHIP, MISSING_LAYER, VISIBLE_CHIP, RELATIVE_Z_CHIP, RELATIVE_Z_PARENT_CHIP } from "viewers/common/chip"; + +type GetNodeIdCallbackType = (node: Tree | null) => number | null; +type IsModifiedCallbackType = (newTree: Tree | null, oldTree: Tree | null) => boolean; +interface IdNodeMap { [key: string]: Tree } +const DiffType = { + NONE: "none", + ADDED: "added", + DELETED: "deleted", + ADDED_MOVE: "addedMove", + DELETED_MOVE: "deletedMove", + MODIFIED: "modified", +}; +const HwcCompositionType = { + CLIENT: 1, + DEVICE: 2, + SOLID_COLOR: 3, +}; + +export type FilterType = (item: Tree | null) => boolean; +export type Tree = Layer | BaseLayerTraceEntry; + +export function diffClass(item: Tree): string { + const diff = item!.diff; + return diff ?? ""; +} + +export function isHighlighted(item: Tree, highlightedItems: Array) { + return highlightedItems.includes(`${item.id}`); +} + +export function getFilter(filterString: string): FilterType { + const filterStrings = filterString.split(","); + const positive: Tree | null[] = []; + const negative: Tree | null[] = []; + filterStrings.forEach((f) => { + f = f.trim(); + if (f.startsWith("!")) { + const regex = new RegExp(f.substring(1), "i"); + negative.push((s:any) => !regex.test(s)); + } else { + const regex = new RegExp(f, "i"); + positive.push((s:any) => regex.test(s)); + } + }); + const filter = (item:Tree | null) => { + if (item) { + const apply = (f:any) => f(`${item.name}`); + return (positive.length === 0 || positive.some(apply)) && + (negative.length === 0 || negative.every(apply)); + } + return false; + }; + return filter; +} + +export class TreeGenerator { + private userOptions: UserOptions; + private filter: FilterType; + private tree: Tree; + private diffWithTree: Tree | null = null; + private getNodeId?: GetNodeIdCallbackType; + private isModified?: IsModifiedCallbackType; + private newMapping: IdNodeMap | null = null; + private oldMapping: IdNodeMap | null = null; + private readonly pinnedIds: Array; + private pinnedItems: Array = []; + private relZParentIds: Array = []; + private flattenedChildren: Array = []; + + constructor(tree: Tree, userOptions: UserOptions, filter: FilterType, pinnedIds?: Array) { + this.tree = tree; + this.userOptions = userOptions; + this.filter = filter; + this.pinnedIds = pinnedIds ?? []; + } + + public generateTree(): Tree { + return this.getCustomisedTree(this.tree); + } + + public compareWith(tree: Tree | null): TreeGenerator { + this.diffWithTree = tree; + return this; + } + + public withUniqueNodeId(getNodeId?: GetNodeIdCallbackType): TreeGenerator { + this.getNodeId = (node: Tree | null) => { + const id = getNodeId ? getNodeId(node) : this.defaultNodeIdCallback(node); + if (id === null || id === undefined) { + console.error("Null node ID for node", node); + throw new Error("Node ID can't be null or undefined"); + } + return id; + }; + return this; + } + + public withModifiedCheck(isModified?: IsModifiedCallbackType): TreeGenerator { + this.isModified = isModified ?? this.defaultModifiedCheck; + return this; + } + + public generateFinalDiffTree(): Tree { + this.newMapping = this.generateIdToNodeMapping(this.tree); + this.oldMapping = this.diffWithTree ? this.generateIdToNodeMapping(this.diffWithTree) : null; + + const diffTrees = this.generateDiffTree(this.tree, this.diffWithTree, [], []); + + let diffTree; + if (diffTrees.length > 1) { + diffTree = { + kind: "", + name: "DiffTree", + children: diffTrees, + stableId: "DiffTree", + }; + } else { + diffTree = diffTrees[0]; + } + return this.getCustomisedTree(diffTree); + } + + private getCustomisedTree(tree: Tree | null) { + if (!tree) return null; + tree = this.generateTreeWithUserOptions(tree, false); + tree = this.updateTreeWithRelZParentChips(tree); + + if (this.isFlatView() && tree.children) { + this.flattenChildren(tree.children); + tree.children = this.flattenedChildren; + } + return Object.freeze(tree); + } + + public getPinnedItems() { + return this.pinnedItems; + } + + private flattenChildren(children: Array): Tree { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const showInOnlyVisibleView = this.isOnlyVisibleView() && child.isVisible; + const passVisibleCheck = !this.isOnlyVisibleView() || showInOnlyVisibleView; + if (this.filterMatches(child) && passVisibleCheck) { + this.flattenedChildren.push(child); + } + if (child.children) { + this.flattenChildren(child.children); + } + } + } + + private isOnlyVisibleView(): boolean { + return this.userOptions["onlyVisible"]?.enabled ?? false; + } + + private isSimplifyNames(): boolean { + return this.userOptions["simplifyNames"]?.enabled ?? false; + } + + private isFlatView(): boolean { + return this.userOptions["flat"]?.enabled ?? false; + } + + private filterMatches(item: Tree | null): boolean { + return this.filter(item) ?? false; + } + + private generateTreeWithUserOptions(tree: Tree | null, parentFilterMatch: boolean): Tree | null { + return tree ? this.applyChecks(tree, this.cloneNode(tree), parentFilterMatch) : null; + } + + private updateTreeWithRelZParentChips(tree: Tree): Tree { + return this.applyRelZParentCheck(tree); + } + + private applyRelZParentCheck(tree: Tree) { + if (this.relZParentIds.includes(tree.id)) { + tree.chips.push(RELATIVE_Z_PARENT_CHIP); + } + + const numOfChildren = tree.children?.length ?? 0; + for (let i = 0; i < numOfChildren; i++) { + tree.children[i] = this.updateTreeWithRelZParentChips(tree.children[i]); + } + + return tree; + } + + private addChips(tree: Tree) { + tree.chips = []; + if (tree.hwcCompositionType == HwcCompositionType.CLIENT) { + tree.chips.push(GPU_CHIP); + } else if ((tree.hwcCompositionType == HwcCompositionType.DEVICE || tree.hwcCompositionType == HwcCompositionType.SOLID_COLOR)) { + tree.chips.push(HWC_CHIP); + } + if (tree.isVisible && tree.kind !== "entry") { + tree.chips.push(VISIBLE_CHIP); + } + if (tree.zOrderRelativeOfId !== -1 && tree.kind !== "entry" && !tree.isRootLayer) { + tree.chips.push(RELATIVE_Z_CHIP); + this.relZParentIds.push(tree.zOrderRelativeOfId); + } + if (tree.isMissing) { + tree.chips.push(MISSING_LAYER); + } + return tree; + } + + private applyChecks(tree: Tree | null, newTree: Tree | null, parentFilterMatch: boolean): Tree | null { + if (!tree || !newTree) { + return null; + } + + // simplify names check + newTree.simplifyNames = this.isSimplifyNames(); + + // check item either matches filter, or has parents/children matching filter + if (tree.kind === "entry" || parentFilterMatch) { + newTree.showInFilteredView = true; + } else { + newTree.showInFilteredView = this.filterMatches(tree); + parentFilterMatch = newTree.showInFilteredView; + } + + if (this.isOnlyVisibleView()) { + newTree.showInOnlyVisibleView = newTree.isVisible; + } + + newTree.children = []; + const numOfChildren = tree.children?.length ?? 0; + for (let i = 0; i < numOfChildren; i++) { + const child = tree.children[i]; + const newTreeChild = this.generateTreeWithUserOptions(child, parentFilterMatch); + + if (newTreeChild) { + if (newTreeChild.showInFilteredView) { + newTree.showInFilteredView = true; + } + + if (this.isOnlyVisibleView() && newTreeChild.showInOnlyVisibleView) { + newTree.showInOnlyVisibleView = true; + } + + newTree.children.push(newTreeChild); + } + } + + const doNotShowInOnlyVisibleView = this.isOnlyVisibleView() && !newTree.showInOnlyVisibleView; + if (!newTree.showInFilteredView || doNotShowInOnlyVisibleView) { + return null; + } + + newTree = this.addChips(newTree); + + if (this.pinnedIds.includes(`${newTree.id}`)) { + this.pinnedItems.push(newTree); + } + + return newTree; + } + + private generateIdToNodeMapping(node: Tree, acc?: IdNodeMap): IdNodeMap { + acc = acc || {}; + + const nodeId = this.getNodeId!(node)!; + + if (acc[nodeId]) { + throw new Error(`Duplicate node id '${nodeId}' detected...`); + } + acc[nodeId] = node; + + if (node.children) { + for (const child of node.children) { + this.generateIdToNodeMapping(child, acc); + } + } + return acc; + } + + private cloneNode(node: Tree | null): Tree | null { + const clone = ObjectFormatter.cloneObject(node); + if (node) { + clone.children = node.children; + clone.name = node.name; + clone.kind = node.kind; + clone.stableId = node.stableId; + clone.shortName = node.shortName; + if ("chips" in node) { + clone.chips = node.chips.slice(); + } + if ("diff" in node) { + clone.diff = node.diff; + } + } + return clone; + } + + private generateDiffTree( + newTree: Tree | null, + oldTree: Tree | null, + newTreeSiblings: Array, + oldTreeSiblings: Array + ): Array { + const diffTrees = []; + // NOTE: A null ID represents a non existent node. + if (!this.getNodeId) { + return []; + } + const newId = newTree ? this.getNodeId(newTree) : null; + const oldId = oldTree ? this.getNodeId(oldTree) : null; + + const newTreeSiblingIds = newTreeSiblings.map(this.getNodeId); + const oldTreeSiblingIds = oldTreeSiblings.map(this.getNodeId); + + if (newTree) { + // Clone is required because trees are frozen objects — we can't modify the original tree object. + const diffTree = this.cloneNode(newTree)!; + + // Default to no changes + diffTree.diff = DiffType.NONE; + + if (newTree.kind !== "entry" && newId !== oldId) { + // A move, addition, or deletion has occurred + let nextOldTree = null; + + // Check if newTree has been added or moved + if (newId && !oldTreeSiblingIds.includes(newId)) { + if (this.oldMapping && this.oldMapping[newId]) { + // Objected existed in old tree, so DELETED_MOVE will be/has been flagged and added to the + // diffTree when visiting it in the oldTree. + diffTree.diff = DiffType.ADDED_MOVE; + + // Switch out oldTree for new one to compare against + nextOldTree = this.oldMapping[newId]; + } else { + diffTree.diff = DiffType.ADDED; + + // Stop comparing against oldTree + nextOldTree = null; + } + } + + // Check if oldTree has been deleted of moved + if (oldId && oldTree && !newTreeSiblingIds.includes(oldId)) { + const deletedTreeDiff = this.cloneNode(oldTree)!; + + if (this.newMapping![oldId]) { + deletedTreeDiff.diff = DiffType.DELETED_MOVE; + + // Stop comparing against oldTree, will be/has been + // visited when object is seen in newTree + nextOldTree = null; + } else { + deletedTreeDiff.diff = DiffType.DELETED; + + // Visit all children to check if they have been deleted or moved + deletedTreeDiff.children = this.visitChildren(null, oldTree); + } + + diffTrees.push(deletedTreeDiff); + } + + oldTree = nextOldTree; + } else { + if (this.isModified && this.isModified(newTree, oldTree)) { + diffTree.diff = DiffType.MODIFIED; + } + } + + diffTree.children = this.visitChildren(newTree, oldTree); + diffTrees.push(diffTree); + } else if (oldTree) { + if (oldId && !newTreeSiblingIds.includes(oldId)) { + // Deep clone oldTree omitting children field + const diffTree = this.cloneNode(oldTree)!; + + // newTree doesn't exist, oldTree has either been moved or deleted. + if (this.newMapping![oldId]) { + diffTree.diff = DiffType.DELETED_MOVE; + } else { + diffTree.diff = DiffType.DELETED; + } + + diffTree.children = this.visitChildren(null, oldTree); + diffTrees.push(diffTree); + } + } else { + throw new Error("Both newTree and oldTree are undefined..."); + } + + return diffTrees; + } + + private visitChildren(newTree: Tree | null, oldTree: Tree | null) { + // Recursively traverse all children of new and old tree. + const diffChildren = []; + + if (!newTree) newTree = {}; + if (!oldTree) oldTree = {}; + const numOfChildren = Math.max(newTree.children?.length ?? 0, oldTree.children?.length ?? 0); + for (let i = 0; i < numOfChildren; i++) { + const newChild = newTree.children ? newTree.children[i] : null; + const oldChild = oldTree.children ? oldTree.children[i] : null; + + const childDiffTrees = this.generateDiffTree( + newChild, oldChild, + newTree.children ?? [], oldTree.children ?? [], + ); + diffChildren.push(...childDiffTrees); + } + + return diffChildren; + } + + private defaultNodeIdCallback(node: Tree | null): number | null { + return node ? node.stableId : null; + } + + private defaultModifiedCheck(newNode: Tree | null, oldNode: Tree | null): boolean { + if (!newNode && !oldNode) { + return false; + } else if (newNode && newNode.kind==="entry") { + return false; + } else if ((newNode && !oldNode) || (!newNode && oldNode)) { + return true; + } + return !newNode.equals(oldNode); + } +} diff --git a/tools/winscope-ng/src/common/trace/flickerlib/treeview/ChipType.ts b/tools/winscope-ng/src/viewers/common/user_options.ts similarity index 72% rename from tools/winscope-ng/src/common/trace/flickerlib/treeview/ChipType.ts rename to tools/winscope-ng/src/viewers/common/user_options.ts index 70c25d7f3..c1f2c1c42 100644 --- a/tools/winscope-ng/src/common/trace/flickerlib/treeview/ChipType.ts +++ b/tools/winscope-ng/src/viewers/common/user_options.ts @@ -1,11 +1,11 @@ /* - * Copyright 2020, The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -enum ChipType { - DEFAULT = 'default' -} - -export default ChipType \ No newline at end of file +export interface UserOptions { + [key: string]: { + name: string, + enabled: boolean + } +} \ No newline at end of file diff --git a/tools/winscope-ng/src/viewers/hierarchy.component.ts b/tools/winscope-ng/src/viewers/common/viewer_events.ts similarity index 68% rename from tools/winscope-ng/src/viewers/hierarchy.component.ts rename to tools/winscope-ng/src/viewers/common/viewer_events.ts index 386a1dad0..b9cda8fff 100644 --- a/tools/winscope-ng/src/viewers/hierarchy.component.ts +++ b/tools/winscope-ng/src/viewers/common/viewer_events.ts @@ -13,17 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from "@angular/core"; - -@Component({ - selector: "hierarchy-view", - template: ` - Hierarchy - `, - styles: [ - ".trace-view-subtitle { font-size: 18px}" - ] -}) - -export class HierarchyComponent { -} +export const ViewerEvents = { + HierarchyPinnedChange: "HierarchyPinnedChange", + HighlightedChange: "HighlightedChange", + HierarchyUserOptionsChange: "HierarchyUserOptionsChange", + HierarchyFilterChange: "HierarchyFilterChange" +}; \ No newline at end of file diff --git a/tools/winscope-ng/src/viewers/components/hierarchy.component.spec.ts b/tools/winscope-ng/src/viewers/components/hierarchy.component.spec.ts new file mode 100644 index 000000000..aae6c5dc7 --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/hierarchy.component.spec.ts @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed, ComponentFixtureAutoDetect} from "@angular/core/testing"; +import { HierarchyComponent } from "./hierarchy.component"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { PersistentStore } from "common/persistent_store"; +import { CommonModule } from "@angular/common"; +import { MatInputModule } from "@angular/material/input"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; + +describe("HierarchyComponent", () => { + let fixture: ComponentFixture; + let component: HierarchyComponent; + let htmlElement: HTMLElement; + + beforeAll(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ], + declarations: [ + HierarchyComponent + ], + imports: [ + CommonModule, + MatInputModule, + MatFormFieldModule, + MatCheckboxModule, + BrowserAnimationsModule + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HierarchyComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + component.tree = { + simplifyNames: false, + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + chips: [], + children: [{kind: "3", id: "3", name: "Child1"}] + }; + component.store = new PersistentStore(); + component.userOptions = { + onlyVisible: { + name: "Only visible", + enabled: false + }, + }; + component.pinnedItems = [{ + simplifyNames: false, + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + chips: [], + children: [{kind: "3", id: "3", name: "Child1"}] + }]; + component.diffClass = jasmine.createSpy().and.returnValue("none"); + }); + + it("can be created", () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it("creates title", () => { + fixture.detectChanges(); + const title = htmlElement.querySelector(".hierarchy-title"); + expect(title).toBeTruthy(); + }); + + it("creates view controls", () => { + fixture.detectChanges(); + const viewControls = htmlElement.querySelector(".view-controls"); + expect(viewControls).toBeTruthy(); + }); + + it("creates initial tree elements", () => { + fixture.detectChanges(); + const tree = htmlElement.querySelector(".tree-wrapper"); + expect(tree).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/components/hierarchy.component.ts b/tools/winscope-ng/src/viewers/components/hierarchy.component.ts new file mode 100644 index 000000000..f39c31c0c --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/hierarchy.component.ts @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input, Inject, ElementRef } from "@angular/core"; +import { UserOptions } from "viewers/common/user_options"; +import { PersistentStore } from "common/persistent_store"; +import { Tree, diffClass, isHighlighted } from "viewers/common/tree_utils"; +import { nodeStyles } from "viewers/styles/node.styles"; +import { ViewerEvents } from "viewers/common/viewer_events"; +import { TraceType } from "common/trace/trace_type"; + +@Component({ + selector: "hierarchy-view", + template: ` + + + Hierarchy + + Filter... + + + +
+ {{userOptions[option].name}} +
+
+ +
+
+ +
+ +
+
+ `, + styles: [ + ` + .view-header { + display: block; + width: 100%; + min-height: 3.75rem; + align-items: center; + border-bottom: 1px solid lightgrey; + } + + .title-filter { + position: relative; + display: flex; + align-items: center; + width: 100%; + } + + .hierarchy-title { + font-size: 16px; + } + + .filter-field { + font-size: 16px; + transform: scale(0.7); + right: 0px; + position: absolute + } + + .view-controls { + display: inline-block; + font-size: 12px; + font-weight: normal; + margin-left: 5px + } + + .hierarchy-content{ + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x:hidden + } + + .tree-view { + white-space: pre-line; + flex: 1 0 0; + height: 100%; + overflow-y: auto + } + + .pinned-items { + border: 2px solid yellow; + position: relative; + display: block; + width: 100%; + } + `, + nodeStyles + ], +}) + +export class HierarchyComponent { + objectKeys = Object.keys; + filterString = ""; + diffClass = diffClass; + isHighlighted = isHighlighted; + + @Input() tree!: Tree | null; + @Input() dependencies: Array = []; + @Input() highlightedItems: Array = []; + @Input() pinnedItems: Array = []; + @Input() store!: PersistentStore; + @Input() userOptions: UserOptions = {}; + + constructor( + @Inject(ElementRef) private elementRef: ElementRef, + ) {} + + isFlattened() { + return this.userOptions["flat"]?.enabled; + } + + maxHierarchyHeight() { + const headerHeight = this.elementRef.nativeElement.querySelector(".view-header").clientHeight; + return { + height: `${800 - headerHeight}px` + }; + } + + onPinnedNodeClick(event: MouseEvent, pinnedItemId: string) { + event.preventDefault(); + if (window.getSelection()?.type === "range") { + return; + } + this.highlightedItemChange(`${pinnedItemId}`); + } + + updateTree() { + const event: CustomEvent = new CustomEvent( + ViewerEvents.HierarchyUserOptionsChange, + { + bubbles: true, + detail: { userOptions: this.userOptions } + }); + this.elementRef.nativeElement.dispatchEvent(event); + } + + filterTree() { + const event: CustomEvent = new CustomEvent( + ViewerEvents.HierarchyFilterChange, + { + bubbles: true, + detail: { filterString: this.filterString } + }); + this.elementRef.nativeElement.dispatchEvent(event); + } + + highlightedItemChange(newId: string) { + const event: CustomEvent = new CustomEvent( + ViewerEvents.HighlightedChange, + { + bubbles: true, + detail: { id: newId } + }); + this.elementRef.nativeElement.dispatchEvent(event); + } + + pinnedItemChange(item: Tree) { + const event: CustomEvent = new CustomEvent( + ViewerEvents.HierarchyPinnedChange, + { + bubbles: true, + detail: { pinnedItem: item } + }); + this.elementRef.nativeElement.dispatchEvent(event); + } +} diff --git a/tools/winscope-ng/src/viewers/properties.component.ts b/tools/winscope-ng/src/viewers/components/properties.component.ts similarity index 100% rename from tools/winscope-ng/src/viewers/properties.component.ts rename to tools/winscope-ng/src/viewers/components/properties.component.ts diff --git a/tools/winscope-ng/src/viewers/canvas_graphics.ts b/tools/winscope-ng/src/viewers/components/rects/canvas_graphics.ts similarity index 90% rename from tools/winscope-ng/src/viewers/canvas_graphics.ts rename to tools/winscope-ng/src/viewers/components/rects/canvas_graphics.ts index 86d24d3e5..fbbc5bafd 100644 --- a/tools/winscope-ng/src/viewers/canvas_graphics.ts +++ b/tools/winscope-ng/src/viewers/components/rects/canvas_graphics.ts @@ -41,8 +41,7 @@ export class CanvasGraphics { this.canvas!.style.width = "100%"; this.canvas!.style.height = "40rem"; - // TODO: click and drag rotation control - this.camera.position.set(this.xyCameraPos, this.xyCameraPos, 6); + this.camera.position.set(this.xCameraPos, Math.abs(this.xCameraPos), 6); this.camera.lookAt(0, 0, 0); this.camera.zoom = this.camZoom; this.camera.updateProjectionMatrix(); @@ -142,17 +141,17 @@ export class CanvasGraphics { ) { this.targetObjects = []; this.rects.forEach(rect => { - const visibleViewInvisibleRect = this.visibleView && !rect.isVisible; - const xrayViewNoVirtualDisplaysVirtualRect = !this.visibleView && !this.showVirtualDisplays && rect.isDisplay && rect.isVirtual; - if (visibleViewInvisibleRect || xrayViewNoVirtualDisplaysVirtualRect) { + const mustNotDrawInVisibleView = this.visibleView && !rect.isVisible; + const mustNotDrawInXrayViewWithoutVirtualDisplays = !this.visibleView && !this.showVirtualDisplays && rect.isDisplay && rect.isVirtual; + if (mustNotDrawInVisibleView || mustNotDrawInXrayViewWithoutVirtualDisplays) { rectCounter++; return; } //set colour mapping let planeColor; - if (this.highlighted === `${rect.id}`) { - planeColor = this.colorMapping("highlight", numberOfRects, 0); + if (this.highlightedItems.includes(`${rect.id}`)) { + planeColor = this.colorMapping("highlighted", numberOfRects, 0); } else if (rect.isVisible) { planeColor = this.colorMapping("green", visibleRects, visibleDarkFactor); visibleDarkFactor++; @@ -178,7 +177,9 @@ export class CanvasGraphics { // label circular marker const circle = this.setCircleMaterial(planeRect, rect); scene.add(circle); - this.targetObjects.push(planeRect); + + // if not a display rect, should be clickable + if (!rect.isDisplay) this.targetObjects.push(planeRect); // label line const [line, rectLabel] = this.createLabel(rect, circle, lowestY, rectCounter); @@ -252,7 +253,7 @@ export class CanvasGraphics { const linePoints = [circle.position, cornerPos]; if (this.isLandscape && cornerPos.x > 0 || !this.isLandscape) { - endPos = new THREE.Vector3(cornerPos.x - 1, cornerPos.y - this.labelShift, cornerPos.z); + endPos = new THREE.Vector3(cornerPos.x - 0.75, cornerPos.y - 0.75*this.labelShift, cornerPos.z); } else { endPos = cornerPos; } @@ -313,8 +314,8 @@ export class CanvasGraphics { return this.visibleView; } - getXyCameraPos() { - return this.xyCameraPos; + getXCameraPos() { + return this.xCameraPos; } getShowVirtualDisplays() { @@ -326,14 +327,14 @@ export class CanvasGraphics { } updateRotation(userInput: number) { - this.xyCameraPos = userInput; + this.xCameraPos = userInput; this.camZoom = userInput/4 * 0.2 + 0.9; this.labelShift = userInput/4 * this.maxLabelShift; - this.lowestYShift = userInput/4 + 2; + this.lowestYShift = Math.abs(userInput)/4 + 2; } - updateHighlighted(highlighted: string) { - this.highlighted = highlighted; + updateHighlightedItems(newItems: Array) { + this.highlightedItems = newItems; } updateRects(rects: Rectangle[]) { @@ -358,14 +359,16 @@ export class CanvasGraphics { updateZoom(isZoomIn: boolean) { if (isZoomIn && this.camZoom < 2) { + this.labelXFactor -= 0.001; this.camZoom += this.camZoomFactor * 1.5; } else if (!isZoomIn && this.camZoom > 0.5) { + this.labelXFactor += 0.001; this.camZoom -= this.camZoomFactor * 1.5; } } colorMapping(scale: string, numberOfRects: number, darkFactor:number): THREE.Color { - if (scale === "highlight") { + if (scale === "highlighted") { return new THREE.Color(0xD2E3FC); } else if (scale === "grey") { // darkness of grey rect depends on z order - darkest 64, lightest 128 @@ -387,8 +390,8 @@ export class CanvasGraphics { } shortenText(text: string): string { - if (text.length > 40) { - text = text.slice(0, 40); + if (text.length > 35) { + text = text.slice(0, 35); } return text; } @@ -397,17 +400,17 @@ export class CanvasGraphics { readonly cameraHalfWidth = 2.8; readonly cameraHalfHeight = 3.2; private readonly maxLabelShift = 0.305; - private readonly labelXFactor = 0.008; + private labelXFactor = 0.009; private lowestYShift = 3; private camZoom = 1.1; private camZoomFactor = 0.1; private labelShift = this.maxLabelShift; - private highlighted = ""; private visibleView = false; private isLandscape = false; private showVirtualDisplays = false; private layerSeparation = 0.4; - private xyCameraPos = 4; + private xCameraPos = 4; + private highlightedItems: Array = []; private camera: THREE.OrthographicCamera; private rects: Rectangle[] = []; private labelElements: HTMLElement[] = []; diff --git a/tools/winscope-ng/src/viewers/rects.component.spec.ts b/tools/winscope-ng/src/viewers/components/rects/rects.component.spec.ts similarity index 92% rename from tools/winscope-ng/src/viewers/rects.component.spec.ts rename to tools/winscope-ng/src/viewers/components/rects/rects.component.spec.ts index b4778be36..b10e76a9d 100644 --- a/tools/winscope-ng/src/viewers/rects.component.spec.ts +++ b/tools/winscope-ng/src/viewers/components/rects/rects.component.spec.ts @@ -14,15 +14,15 @@ * limitations under the License. */ import { CommonModule } from "@angular/common"; -import { Component , ViewChild } from "@angular/core"; +import { Component, ViewChild } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { RectsComponent } from "./rects.component"; +import { RectsComponent } from "viewers/components/rects/rects.component"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { MatCardModule } from "@angular/material/card"; import { MatRadioModule } from "@angular/material/radio"; import { MatSliderModule } from "@angular/material/slider"; import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; -import { Rectangle } from "./viewer_surface_flinger/ui_data"; +import { Rectangle } from "viewers/viewer_surface_flinger/ui_data"; describe("RectsComponent", () => { let component: TestHostComponent; @@ -61,7 +61,7 @@ describe("RectsComponent", () => { }); it("check that layer separation slider causes view to change", () => { - const slider = htmlElement.querySelector("mat-slider"); + const slider = htmlElement.querySelector(".spacing-slider"); spyOn(component.rectsComponent.canvasGraphics, "updateLayerSeparation"); slider?.dispatchEvent(new MouseEvent("mousedown")); fixture.detectChanges(); @@ -97,6 +97,7 @@ describe("RectsComponent", () => { ref: null, id: 12345, displayId: 0, + isVirtual: false } ]); spyOn(component.rectsComponent, "drawRects").and.callThrough(); diff --git a/tools/winscope-ng/src/viewers/rects.component.ts b/tools/winscope-ng/src/viewers/components/rects/rects.component.ts similarity index 74% rename from tools/winscope-ng/src/viewers/rects.component.ts rename to tools/winscope-ng/src/viewers/components/rects/rects.component.ts index bf6e34ceb..08f32c652 100644 --- a/tools/winscope-ng/src/viewers/rects.component.ts +++ b/tools/winscope-ng/src/viewers/components/rects/rects.component.ts @@ -14,45 +14,30 @@ * limitations under the License. */ import { Component, Input, OnChanges, OnDestroy, Inject, ElementRef, SimpleChanges } from "@angular/core"; -import { RectsUtils } from "./rects_utils"; +import { RectsUtils } from "viewers/components/rects/rects_utils"; import { Point, Rectangle, RectMatrix, RectTransform } from "viewers/viewer_surface_flinger/ui_data"; import { interval, Subscription } from "rxjs"; -import { CanvasGraphics } from "./canvas_graphics"; +import { CanvasGraphics } from "viewers/components/rects/canvas_graphics"; import * as THREE from "three"; +import { ViewerEvents } from "viewers/common/viewer_events"; @Component({ selector: "rects-view", template: ` - - Visible - X-ray - - - + Layers +
Only visible layers + Show virtual displays - - -
+ >Show virtual
+
+
+
+ Flat to isometric + +
+
+ Layer spacing + +
+
+ + +
@@ -79,9 +93,16 @@ import * as THREE from "three"; ".canvas-container {height: 40rem; width: 100%; position: relative}", "#rects-canvas {height: 40rem; width: 100%; cursor: pointer; position: absolute; top: 0px}", "#labels-canvas {height: 40rem; width: 100%; position: absolute; top: 0px}", - ".view-controls {display: inline-block; position: relative; min-height: 72px}", - ".zoom-container {position: absolute; top: 0px; z-index: 10}", - "#zoom-btn {position:relative; display: block; background: none; border: none}", + ".view-controls, .slider-view-controls {display: inline-block; position: relative; min-height: 4.5rem; width: 100%}", + ".slider {display: inline-block}", + ".slider.spacing {float: right}", + ".slider span, .slider mat-slider { display: block; padding-left: 0px; padding-top: 0px; font-weight: bold}", + ".top-view-controls {min-height: 1.5rem; width: 100%; position: relative; display: inline-block; vertical-align: middle;}", + ".zoom-container {position: relative; vertical-align: middle; float: right}", + "#zoom-btn {position:relative; display: inline-flex; background: none; border: none}", + "mat-card-title {font-size: 16px !important}", + ":host /deep/ .mat-card-header-text {width: 100%; margin: 0;}", + "mat-radio-group {vertical-align: middle}", "mat-radio-button {font-size: 16px; font-weight: normal}", ".mat-radio-button, .mat-radio-button-frame {transform: scale(0.8);}", ".rects-checkbox {font-size: 14px; font-weight: normal}", @@ -96,7 +117,7 @@ import * as THREE from "three"; export class RectsComponent implements OnChanges, OnDestroy { @Input() rects!: Rectangle[]; @Input() displayIds: Array = []; - @Input() highlighted = ""; + @Input() highlightedItems: Array = []; constructor( @Inject(ElementRef) private elementRef: ElementRef, @@ -112,6 +133,9 @@ export class RectsComponent implements OnChanges, OnDestroy { } ngOnChanges(changes: SimpleChanges) { + if (changes["highlightedItems"]) { + this.canvasGraphics.updateHighlightedItems(this.highlightedItems); + } if (this.rects.length > 0) { //change in rects so they must undergo transformation and scaling before canvas refreshed this.canvasGraphics.clearLabelElements(); @@ -126,13 +150,15 @@ export class RectsComponent implements OnChanges, OnDestroy { } }); this.scaleRects(); - this.drawRects(); + if (changes["rects"]) { + this.drawRects(); + } } else if (this.canvasSubscription) { this.canvasSubscription.unsubscribe(); } } - onRectClick(event:PointerEvent) { + onRectClick(event:MouseEvent) { this.setNormalisedMousePos(event); const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(this.mouse, this.canvasGraphics.getCamera()); @@ -140,18 +166,12 @@ export class RectsComponent implements OnChanges, OnDestroy { const intersects = raycaster.intersectObjects(this.canvasGraphics.getTargetObjects()); // if there is one (or more) intersections if (intersects.length > 0){ - if (this.highlighted === intersects[0].object.name) { - this.highlighted = ""; - this.canvasGraphics.updateHighlighted(""); - } else { - this.highlighted = intersects[0].object.name; - this.canvasGraphics.updateHighlighted(intersects[0].object.name); - } - this.updateHighlightedRect(); + const id = intersects[0].object.name; + this.updateHighlightedItems(id); } } - setNormalisedMousePos(event:PointerEvent) { + setNormalisedMousePos(event:MouseEvent) { event.preventDefault(); const canvas = (event.target as Element); const canvasOffset = canvas.getBoundingClientRect(); @@ -160,11 +180,13 @@ export class RectsComponent implements OnChanges, OnDestroy { this.mouse.z = 0; } - updateHighlightedRect() { - const event: CustomEvent = new CustomEvent("highlightedChange", { - bubbles: true, - detail: { layerId: this.highlighted } - }); + updateHighlightedItems(newId: string) { + const event: CustomEvent = new CustomEvent( + ViewerEvents.HighlightedChange, + { + bubbles: true, + detail: { id: newId } + }); this.elementRef.nativeElement.dispatchEvent(event); } @@ -181,8 +203,8 @@ export class RectsComponent implements OnChanges, OnDestroy { } updateVariablesBeforeRefresh() { - this.rects = this.rects.filter(rect => rect.displayId === this.currentDisplayId); - this.canvasGraphics.updateRects(this.rects); + const rects = this.rects.filter(rect => rect.displayId === this.currentDisplayId); + this.canvasGraphics.updateRects(rects); const biggestX = Math.max(...this.rects.map(rect => rect.topLeft.x + rect.width/2)); this.canvasGraphics.updateIsLandscape(biggestX > this.s({x: this.boundsWidth, y:this.boundsHeight}).x/2); } @@ -272,8 +294,8 @@ export class RectsComponent implements OnChanges, OnDestroy { return this.canvasGraphics.getLayerSeparation(); } - xyCameraPos() { - return this.canvasGraphics.getXyCameraPos(); + xCameraPos() { + return this.canvasGraphics.getXCameraPos(); } showVirtualDisplays() { diff --git a/tools/winscope-ng/src/viewers/rects_utils.spec.ts b/tools/winscope-ng/src/viewers/components/rects/rects_utils.spec.ts similarity index 91% rename from tools/winscope-ng/src/viewers/rects_utils.spec.ts rename to tools/winscope-ng/src/viewers/components/rects/rects_utils.spec.ts index 605b1f9ca..931397728 100644 --- a/tools/winscope-ng/src/viewers/rects_utils.spec.ts +++ b/tools/winscope-ng/src/viewers/components/rects/rects_utils.spec.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RectsUtils } from "./rects_utils"; +import { RectsUtils } from "viewers/components/rects/rects_utils"; describe("RectsUtils", () => { it("transforms rect", () => { @@ -38,7 +38,8 @@ describe("RectsUtils", () => { width: 1, ref: null, id: 12345, - displayId: 0 + displayId: 0, + isVirtual: false }; const expected = { topLeft: {x: 1, y: 1}, @@ -52,7 +53,7 @@ describe("RectsUtils", () => { ref: null, id: 12345, displayId: 0, - isVirtual: undefined + isVirtual: false }; expect(RectsUtils.transformRect(rect.transform.matrix, rect)).toEqual(expected); }); diff --git a/tools/winscope-ng/src/viewers/rects_utils.ts b/tools/winscope-ng/src/viewers/components/rects/rects_utils.ts similarity index 100% rename from tools/winscope-ng/src/viewers/rects_utils.ts rename to tools/winscope-ng/src/viewers/components/rects/rects_utils.ts diff --git a/tools/winscope-ng/src/viewers/components/tree.component.spec.ts b/tools/winscope-ng/src/viewers/components/tree.component.spec.ts new file mode 100644 index 000000000..4732dc629 --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/tree.component.spec.ts @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from "@angular/core/testing"; +import { TreeComponent } from "./tree.component"; +import { ComponentFixtureAutoDetect } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; + +describe("TreeComponent", () => { + let fixture: ComponentFixture; + let component: TreeComponent; + let htmlElement: HTMLElement; + + beforeAll(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ], + declarations: [ + TreeComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TreeComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + component.isFlattened = true; + component.item = { + simplifyNames: false, + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + chips: [], + children: [{kind: "3", id: "3", name: "Child1"}] + }; + component.diffClass = jasmine.createSpy().and.returnValue("none"); + component.isHighlighted = jasmine.createSpy().and.returnValue(false); + component.hasChildren = jasmine.createSpy().and.returnValue(true); + }); + + it("can be created", () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it("creates node element", () => { + fixture.detectChanges(); + const nodeElement = htmlElement.querySelector(".node"); + expect(nodeElement).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/components/tree.component.ts b/tools/winscope-ng/src/viewers/components/tree.component.ts new file mode 100644 index 000000000..0c1e705b0 --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/tree.component.ts @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Inject, Input, Output, ElementRef, EventEmitter } from "@angular/core"; +import { PersistentStore } from "common/persistent_store"; +import { nodeStyles, treeNodeStyles } from "viewers/styles/node.styles"; +import { Tree, diffClass, isHighlighted } from "viewers/common/tree_utils"; +import { TraceType } from "common/trace/trace_type"; + +@Component({ + selector: "tree-view", + template: ` +
+ + +
+ + + +
+
+ `, + styles: [nodeStyles, treeNodeStyles] +}) + +export class TreeComponent { + diffClass = diffClass; + isHighlighted = isHighlighted; + + @Input() item!: Tree; + @Input() dependencies: Array = []; + @Input() store!: PersistentStore; + @Input() isFlattened? = false; + @Input() isShaded? = false; + @Input() initialDepth = 0; + @Input() highlightedItems: Array = []; + @Input() pinnedItems?: Array = []; + @Input() itemsClickable?: boolean; + @Input() useGlobalCollapsedState?: boolean; + + @Output() highlightedItemChange = new EventEmitter(); + @Output() pinnedItemChange = new EventEmitter(); + @Output() hoverStart = new EventEmitter(); + @Output() hoverEnd = new EventEmitter(); + + isCollapsedByDefault = true; + localCollapsedState = this.isCollapsedByDefault; + nodeHover = false; + childHover = false; + readonly levelOffset = 24; + nodeElement: HTMLElement; + + constructor( + @Inject(ElementRef) elementRef: ElementRef, + ) { + this.nodeElement = elementRef.nativeElement.querySelector(".node"); + this.nodeElement?.addEventListener("mousedown", this.nodeMouseDownEventListener); + this.nodeElement?.addEventListener("mouseenter", this.nodeMouseEnterEventListener); + this.nodeElement?.addEventListener("mouseleave", this.nodeMouseLeaveEventListener); + } + + ngOnDestroy() { + this.nodeElement?.removeEventListener("mousedown", this.nodeMouseDownEventListener); + this.nodeElement?.removeEventListener("mouseenter", this.nodeMouseEnterEventListener); + this.nodeElement?.removeEventListener("mouseleave", this.nodeMouseLeaveEventListener); + } + + onNodeClick(event: MouseEvent) { + event.preventDefault(); + if (window.getSelection()?.type === "range") { + return; + } + + if (!this.isLeaf() && event.detail % 2 === 0) { + // Double click collapsable node + event.preventDefault(); + this.toggleTree(); + } else { + this.updateHighlightedItems(); + } + } + + nodeOffsetStyle() { + const offset = this.levelOffset * (this.initialDepth) + "px"; + + return { + marginLeft: "-" + offset, + paddingLeft: offset, + }; + } + + updateHighlightedItems() { + if (this.item && this.item.id) { + this.highlightedItemChange.emit(`${this.item.id}`); + } + } + + isPinned() { + if (this.item) { + return this.pinnedItems?.map((item: Tree) => `${item.id}`).includes(`${this.item.id}`); + } + return false; + } + + sendNewHighlightedItemToHierarchy(newId: string) { + this.highlightedItemChange.emit(newId); + } + + sendNewPinnedItemToHierarchy(newPinnedItem: Tree) { + this.pinnedItemChange.emit(newPinnedItem); + } + + isLeaf() { + return !this.item.children || this.item.children.length === 0; + } + + isClickable() { + return !this.isLeaf() || this.itemsClickable; + } + + toggleTree() { + this.setCollapseValue(!this.isCollapsed()); + } + + expandTree() { + this.setCollapseValue(false); + } + + isCollapsed() { + if (this.isLeaf()) { + return false; + } + + if (this.useGlobalCollapsedState) { + return this.store.getFromStore(`collapsedState.item.${this.dependencies}.${this.item.id}`)==="true" + ?? this.isCollapsedByDefault; + } + + return this.localCollapsedState; + } + + children() { + return this.item.children; + } + + hasChildren() { + const isParentEntryInFlatView = this.item.kind === "entry" && this.isFlattened; + return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf(); + } + + setCollapseValue(isCollapsed:boolean) { + if (this.useGlobalCollapsedState) { + this.store.addToStore(`collapsedState.item.${this.dependencies}.${this.item.id}`, `${isCollapsed}`); + } else { + this.localCollapsedState = isCollapsed; + } + } + + childrenIndentation() { + if (this.isFlattened) { + return { + marginLeft: "0px", + paddingLeft: "0px", + marginTop: "0px", + }; + } else { + // Aligns border with collapse arrows + return { + marginLeft: "12px", + paddingLeft: "11px", + borderLeft: "1px solid rgb(238, 238, 238)", + marginTop: "0px", + }; + } + } + + nodeMouseDownEventListener = (event:MouseEvent) => { + if (event.detail > 1) { + event.preventDefault(); + return false; + } + return true; + }; + + nodeMouseEnterEventListener = () => { + this.nodeHover = true; + this.hoverStart.emit(); + }; + + nodeMouseLeaveEventListener = () => { + this.nodeHover = false; + this.hoverEnd.emit(); + }; +} diff --git a/tools/winscope-ng/src/viewers/components/tree_element.component.spec.ts b/tools/winscope-ng/src/viewers/components/tree_element.component.spec.ts new file mode 100644 index 000000000..8322e8929 --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/tree_element.component.spec.ts @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from "@angular/core/testing"; +import { TreeElementComponent } from "./tree_element.component"; +import { ComponentFixtureAutoDetect } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; + +describe("TreeElementComponent", () => { + let fixture: ComponentFixture; + let component: TreeElementComponent; + let htmlElement: HTMLElement; + + beforeAll(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ], + declarations: [ + TreeElementComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TreeElementComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + }); + + it("can be created", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/components/tree_element.component.ts b/tools/winscope-ng/src/viewers/components/tree_element.component.ts new file mode 100644 index 000000000..4ac8d7fe2 --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/tree_element.component.ts @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input } from "@angular/core"; +import { treeElementStyles } from "viewers/styles/tree_element.styles"; +import { Tree } from "viewers/common/tree_utils"; +import Chip from "viewers/common/chip"; + +@Component({ + selector: "tree-element", + template: ` + + {{item.kind}} + - + {{ item.shortName }} + {{item.name}} +
{{chip.short}}
+
+ `, + styles: [ treeElementStyles ] +}) + +export class TreeElementComponent { + @Input() item!: Tree; + + showShortName() { + return this.item.simplifyNames && this.item.shortName !== this.item.name; + } + + chipClass(chip: Chip) { + return [ + "tree-view-internal-chip", + "tree-view-chip", + "tree-view-chip" + "-" + + (chip.type.toString() || "default"), + ]; + } +} diff --git a/tools/winscope-ng/src/viewers/components/tree_node.component.spec.ts b/tools/winscope-ng/src/viewers/components/tree_node.component.spec.ts new file mode 100644 index 000000000..d8cd4bb1d --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/tree_node.component.spec.ts @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from "@angular/core/testing"; +import { TreeNodeComponent } from "./tree_node.component"; +import { ComponentFixtureAutoDetect } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; + +describe("TreeNodeComponent", () => { + let fixture: ComponentFixture; + let component: TreeNodeComponent; + let htmlElement: HTMLElement; + + beforeAll(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ], + declarations: [ + TreeNodeComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TreeNodeComponent); + component = fixture.componentInstance; + htmlElement = fixture.nativeElement; + component.item = { + simplifyNames: false, + kind: "entry", + name: "BaseLayerTraceEntry", + shortName: "BLTE", + chips: [], + }; + component.isCollapsed = true; + component.hasChildren = false; + component.isPinned = false; + component.isInPinnedSection = false; + }); + + it("can be created", () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it("creates tree element", () => { + fixture.detectChanges(); + const treeElement = htmlElement.querySelector("tree-element"); + expect(treeElement).toBeTruthy(); + }); +}); diff --git a/tools/winscope-ng/src/viewers/components/tree_node.component.ts b/tools/winscope-ng/src/viewers/components/tree_node.component.ts new file mode 100644 index 000000000..698ccdfdd --- /dev/null +++ b/tools/winscope-ng/src/viewers/components/tree_node.component.ts @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input, Output, EventEmitter } from "@angular/core"; +import { nodeInnerItemStyles } from "viewers/styles/node.styles"; +import { Tree } from "viewers/common/tree_utils"; + +@Component({ + selector: "tree-node", + template: ` + + +
+ +
+ + + +
+ +
+ + + `, + styles: [nodeInnerItemStyles] +}) + +export class TreeNodeComponent { + @Input() item!: Tree | null; + @Input() isLeaf?: boolean; + @Input() flattened?: boolean; + @Input() isCollapsed?: boolean; + @Input() hasChildren?: boolean = false; + @Input() isPinned?: boolean = false; + @Input() isInPinnedSection?: boolean = false; + + @Output() toggleTreeChange = new EventEmitter(); + @Output() expandTreeChange = new EventEmitter(); + @Output() pinNodeChange = new EventEmitter(); + + isEntryNode() { + return this.item.kind === "entry" ?? false; + } + + toggleTree(event: MouseEvent) { + event.stopPropagation(); + this.toggleTreeChange.emit(); + } + + showChevron() { + return !this.isLeaf && !this.flattened && !this.isInPinnedSection; + } + + showLeafNodeIcon() { + return !this.showChevron() && !this.isInPinnedSection; + } + + expandTree(event: MouseEvent) { + event.stopPropagation(); + this.expandTreeChange.emit(); + } + + pinNode(event: MouseEvent) { + event.stopPropagation(); + this.pinNodeChange.emit(this.item); + } +} diff --git a/tools/winscope-ng/src/viewers/styles/node.styles.ts b/tools/winscope-ng/src/viewers/styles/node.styles.ts new file mode 100644 index 000000000..303ebb167 --- /dev/null +++ b/tools/winscope-ng/src/viewers/styles/node.styles.ts @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const nodeStyles = ` + .node {position: relative;display: inline-block;padding: 2px; height: 100%; width: 100%;} + .node.clickable {cursor: pointer;} + .node:not(.selected).added, + .node:not(.selected).addedMove, + .expand-tree-btn.added, + .expand-tree-btn.addedMove { + background: #03ff35; + } + + .node:not(.selected).deleted, + .node:not(.selected).deletedMove, + .expand-tree-btn.deleted, + .expand-tree-btn.deletedMove { + background: #ff6b6b; + } + + .node:hover:not(.selected) {background: #f1f1f1;} + + .node:not(.selected).modified, + .expand-tree-btn.modified { + background: cyan; + } + + .node.addedMove:after, + .node.deletedMove:after { + content: 'moved'; + margin: 0 5px; + background: #448aff; + border-radius: 5px; + padding: 3px; + color: white; + } + + .selected {background-color: #365179;color: white;} +`; + +export const treeNodeStyles = ` + .node.shaded:not(:hover):not(.selected):not(.added):not(.addedMove):not(.deleted):not(.deletedMove):not(.modified) {background: #f8f9fa} + .node.selected + .children {border-left: 1px solid rgb(200, 200, 200);} + .node.child-hover + .children {border-left: 1px solid #b4b4b4;} + .node.hover + .children { border-left: 1px solid rgb(200, 200, 200);} +`; + +export const nodeInnerItemStyles = ` + .leaf-node-icon {content: ''; display: inline-block; margin-left: 40%; margin-top: 40%; height: 5px; width: 5px; border-radius: 50%;background-color: #9b9b9b;} + .leaf-node-icon-wrapper, .description, #toggle-tree-btn, #expand-tree-btn, #pin-node-btn { position: relative; display: inline-block;} + mat-icon {margin: 0} + #pin-node-btn {padding: 0; transform: scale(0.7)} + .description {position: relative; align-items: center; flex: 1 1 auto; vertical-align: middle; word-break: break-all;} + .leaf-node-icon-wrapper{padding-left: 6px; padding-right: 6px; min-height: 24px; width: 24px; position:relative; align-content: center; vertical-align: middle;} + .icon-button { background: none;border: none;display: inline-block;vertical-align: middle;} +`; \ No newline at end of file diff --git a/tools/winscope-ng/src/viewers/styles/tree_element.styles.ts b/tools/winscope-ng/src/viewers/styles/tree_element.styles.ts new file mode 100644 index 000000000..8b1cc61f8 --- /dev/null +++ b/tools/winscope-ng/src/viewers/styles/tree_element.styles.ts @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const treeElementStyles = ` + .kind {font-weight: bold} + + span {overflow-wrap: break-word; flex: 1 1 auto; width: 0; word-break: break-all} + + .tree-view-internal-chip { + display: inline-block; + } + + .tree-view-chip { + padding: 0 10px; + border-radius: 10px; + background-color: #aaa; + color: black; + } + + .tree-view-chip.tree-view-chip-warn { + background-color: #ffaa6b; + color: black; + } + + .tree-view-chip.tree-view-chip-error { + background-color: #ff6b6b; + color: black; + } + + .tree-view-chip.tree-view-chip-gpu { + background-color: #00c853; + color: black; + } + + .tree-view-chip.tree-view-chip-hwc { + background-color: #448aff; + color: black; + } +`; diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts index ca678e929..d4ccd4bbc 100644 --- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts @@ -15,36 +15,69 @@ */ import { Rectangle, RectMatrix, RectTransform, UiData } from "viewers/viewer_surface_flinger/ui_data"; import { TraceType } from "common/trace/trace_type"; +import { UserOptions } from "viewers/common/user_options"; +import { TreeGenerator, getFilter, FilterType, Tree } from "viewers/common/tree_utils"; type NotifyViewCallbackType = (uiData: UiData) => void; class Presenter { constructor(notifyViewCallback: NotifyViewCallbackType) { this.notifyViewCallback = notifyViewCallback; - this.uiData = new UiData("Initial UI data"); + this.uiData = new UiData(); this.notifyViewCallback(this.uiData); } - updateHighlightedRect(event: CustomEvent) { - this.highlighted = event.detail.layerId; - this.uiData.highlighted = this.highlighted; - console.log("changed highlighted rect: ", this.uiData.highlighted); + public updatePinnedItems(event: CustomEvent) { + const pinnedItem = event.detail.pinnedItem; + const pinnedId = `${pinnedItem.id}`; + if (this.pinnedItems.map(item => `${item.id}`).includes(pinnedId)) { + this.pinnedItems = this.pinnedItems.filter(pinned => `${pinned.id}` != pinnedId); + } else { + this.pinnedItems.push(pinnedItem); + } + this.updatePinnedIds(pinnedId); + this.uiData.pinnedItems = this.pinnedItems; this.notifyViewCallback(this.uiData); } - notifyCurrentTraceEntries(entries: Map) { - const entry = entries.get(TraceType.SURFACE_FLINGER); - this.uiData = new UiData("New surface flinger ui data"); + public updateHighlightedItems(event: CustomEvent) { + const id = `${event.detail.id}`; + if (this.highlightedItems.includes(id)) { + this.highlightedItems = this.highlightedItems.filter(hl => hl != id); + } else { + this.highlightedItems = []; //if multi-select implemented, remove this line + this.highlightedItems.push(id); + } + this.uiData.highlightedItems = this.highlightedItems; + this.notifyViewCallback(this.uiData); + } + + public updateHierarchyTree(event: CustomEvent) { + this.hierarchyUserOptions = event.detail.userOptions; + this.uiData.hierarchyUserOptions = this.hierarchyUserOptions; + this.uiData.tree = this.generateTree(); + this.notifyViewCallback(this.uiData); + } + + public filterHierarchyTree(event: CustomEvent) { + this.hierarchyFilter = getFilter(event.detail.filterString); + this.uiData.tree = this.generateTree(); + this.notifyViewCallback(this.uiData); + } + + public notifyCurrentTraceEntries(entries: Map) { + this.uiData = new UiData(); + const entry = entries.get(TraceType.SURFACE_FLINGER)[0]; + this.uiData.rects = []; const displayRects = entry.displays.map((display: any) => { const rect = display.layerStackSpace; rect.label = display.name; rect.id = display.id; rect.displayId = display.layerStackId; rect.isDisplay = true; - rect.isVirtual = display.isVirtual; + rect.isVirtual = display.isVirtual ?? false; return rect; }) ?? []; - this.uiData.highlighted = this.highlighted; this.displayIds = []; const rects = entry.visibleLayers @@ -59,10 +92,36 @@ class Presenter { }); this.uiData.rects = this.rectsToUiData(rects.concat(displayRects)); this.uiData.displayIds = this.displayIds; + this.uiData.highlightedItems = this.highlightedItems; + this.uiData.rects = this.rectsToUiData(entry.rects.concat(displayRects)); + this.uiData.hierarchyUserOptions = this.hierarchyUserOptions; + this.previousEntry = entries.get(TraceType.SURFACE_FLINGER)[1]; + this.entry = entry; + + this.uiData.tree = this.generateTree(); this.notifyViewCallback(this.uiData); } - rectsToUiData(rects: any[]): Rectangle[] { + private generateTree() { + if (!this.entry) { + return null; + } + const generator = new TreeGenerator(this.entry, this.hierarchyUserOptions, this.hierarchyFilter, this.pinnedIds) + .withUniqueNodeId(); + let tree: Tree; + if (!this.hierarchyUserOptions["showDiff"]?.enabled) { + tree = generator.generateTree(); + } else { + tree = generator.compareWith(this.previousEntry) + .withModifiedCheck() + .generateFinalDiffTree(); + } + this.pinnedItems = generator.getPinnedItems(); + this.uiData.pinnedItems = this.pinnedItems; + return tree; + } + + private rectsToUiData(rects: any[]): Rectangle[] { const uiRects: Rectangle[] = []; rects.forEach((rect: any) => { let t = null; @@ -86,14 +145,6 @@ class Presenter { }; } - let isVisible = false, isDisplay = false; - if (rect.ref && rect.ref.isVisible) { - isVisible = rect.ref.isVisible; - } - if (rect.isDisplay) { - isDisplay = rect.isDisplay; - } - const newRect: Rectangle = { topLeft: {x: rect.left, y: rect.top}, bottomRight: {x: rect.right, y: -rect.bottom}, @@ -101,22 +152,53 @@ class Presenter { width: rect.width, label: rect.label, transform: transform, - isVisible: isVisible, - isDisplay: isDisplay, + isVisible: rect.ref?.isVisible ?? false, + isDisplay: rect.isDisplay ?? false, ref: rect.ref, id: rect.id ?? rect.ref.id, displayId: rect.displayId ?? rect.ref.stackId, - isVirtual: rect.isVirtual + isVirtual: rect.isVirtual ?? false }; uiRects.push(newRect); }); return uiRects; } + private updatePinnedIds(newId: string) { + if (this.pinnedIds.includes(newId)) { + this.pinnedIds = this.pinnedIds.filter(pinned => pinned != newId); + } else { + this.pinnedIds.push(newId); + } + } + private readonly notifyViewCallback: NotifyViewCallbackType; private uiData: UiData; - private highlighted = ""; private displayIds: Array = []; + private hierarchyFilter: FilterType = getFilter(""); + private highlightedItems: Array = []; + private pinnedItems: Array = []; + private pinnedIds: Array = []; + private previousEntry: any = null; + private entry: any = null; + private hierarchyUserOptions: UserOptions = { + showDiff: { + name: "Show diff", + enabled: false + }, + simplifyNames: { + name: "Simplify names", + enabled: true + }, + onlyVisible: { + name: "Only visible", + enabled: false + }, + flat: { + name: "Flat", + enabled: false + } + }; } export {Presenter}; diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts index de90e5c42..b318c7f69 100644 --- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts @@ -13,13 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -class UiData { - constructor(public text: string) { - console.log(text); - } +import { TraceType } from "common/trace/trace_type"; +import { Tree } from "viewers/common/tree_utils"; +import { UserOptions } from "viewers/common/user_options"; + +export class UiData { + dependencies: Array = [TraceType.SURFACE_FLINGER]; rects?: Rectangle[] = []; - highlighted?: string = ""; displayIds?: number[] = []; + highlightedItems?: Array = []; + pinnedItems?: Array = []; + hierarchyUserOptions?: UserOptions = {}; + tree?: Tree | null = null; } export interface Rectangle { @@ -34,7 +39,7 @@ export interface Rectangle { ref: any; id: number; displayId: number; - isVirtual?: boolean; + isVirtual: boolean; } export interface Point { @@ -60,5 +65,3 @@ export interface RectMatrix { tx: number; ty: number; } - -export {UiData}; diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts index f4aa30284..9d13e6fcc 100644 --- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts @@ -16,13 +16,13 @@ import {ComponentFixture, TestBed} from "@angular/core/testing"; import {ViewerSurfaceFlingerComponent} from "./viewer_surface_flinger.component"; -import { HierarchyComponent } from "viewers/hierarchy.component"; -import { PropertiesComponent } from "viewers/properties.component"; -import { RectsComponent } from "viewers/rects.component"; +import { HierarchyComponent } from "viewers/components/hierarchy.component"; +import { PropertiesComponent } from "viewers/components/properties.component"; +import { RectsComponent } from "viewers/components/rects/rects.component"; import { MatIconModule } from "@angular/material/icon"; import { MatCardModule } from "@angular/material/card"; import { ComponentFixtureAutoDetect } from "@angular/core/testing"; -import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core"; describe("ViewerSurfaceFlingerComponent", () => { let fixture: ComponentFixture; @@ -44,7 +44,7 @@ describe("ViewerSurfaceFlingerComponent", () => { PropertiesComponent, RectsComponent ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA] }).compileComponents(); }); diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts index 644ae03c2..1b945be1e 100644 --- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts @@ -20,6 +20,7 @@ import { import { UiData } from "./ui_data"; import { TRACE_INFO } from "app/trace_info"; import { TraceType } from "common/trace/trace_type"; +import { PersistentStore } from "common/persistent_store"; @Component({ selector: "viewer-surface-flinger", @@ -29,33 +30,86 @@ import { TraceType } from "common/trace/trace_type"; - - - - - - +
+ + + + + + +
`, styles: [ - "@import 'https://fonts.googleapis.com/icon?family=Material+Icons';", - "mat-icon {margin: 5px}", - "viewer-surface-flinger {font-family: Arial, Helvetica, sans-serif;}", - ".trace-card-title {display: inline-block; vertical-align: middle;}", - ".header-button {background: none; border: none; display: inline-block; vertical-align: middle;}", - ".card-grid {width: 100%;height: 100%;display: flex;flex-direction: row;overflow: auto;}", - ".rects-view {font: inherit; flex: none !important;width: 400px;margin: 8px;}", - ".hierarchy-view, .properties-view {font: inherit; flex: 1;margin: 8px;min-width: 400px;min-height: 50rem;max-height: 50rem;}", + ` + @import 'https://fonts.googleapis.com/icon?family=Material+Icons'; + + mat-icon { + margin: 5px + } + + .icon-button { + background: none; + border: none; + display: inline-block; + vertical-align: middle; + } + + viewer-surface-flinger { + font-family: Arial, Helvetica, sans-serif; + } + + .header-button { + background: none; + border: none; + display: inline-block; + vertical-align: middle; + } + + .card-grid { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + overflow: auto; + } + + .rects-view { + font: inherit; + flex: none !important; + width: 350px; + height: 52.5rem; + margin: 0px; + border: 1px solid rgb(129, 129, 129); + border-radius: 0; + } + + .hierarchy-view, .properties-view { + font: inherit; + margin: 0px; + width: 50%; + height: 52.5rem; + border-radius: 0; + border-top: 1px solid rgb(129, 129, 129); + border-right: 1px solid rgb(129, 129, 129); + border-bottom: 1px solid rgb(129, 129, 129); + } + `, ] }) export class ViewerSurfaceFlingerComponent { - @Input() - inputData?: UiData; - + @Input() inputData?: UiData; + @Input() store: PersistentStore = new PersistentStore(); TRACE_INFO = TRACE_INFO; TraceType = TraceType; } diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts index 5d49f6edd..20fea48ac 100644 --- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts +++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts @@ -17,6 +17,7 @@ import {TraceType} from "common/trace/trace_type"; import {Viewer} from "viewers/viewer"; import {Presenter} from "./presenter"; import {UiData} from "./ui_data"; +import { ViewerEvents } from "viewers/common/viewer_events"; class ViewerSurfaceFlinger implements Viewer { constructor() { @@ -24,7 +25,10 @@ class ViewerSurfaceFlinger implements Viewer { this.presenter = new Presenter((uiData: UiData) => { (this.view as any).inputData = uiData; }); - this.view.addEventListener("highlightedChange", (event) => this.presenter.updateHighlightedRect((event as CustomEvent))); + this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems((event as CustomEvent))); + this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems((event as CustomEvent))); + this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent))); + this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent))); } public notifyCurrentTraceEntries(entries: Map): void { diff --git a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts index 00fe4467b..0296425f5 100644 --- a/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts +++ b/tools/winscope-ng/src/viewers/viewer_window_manager/viewer_window_manager.component.ts @@ -20,6 +20,7 @@ import { Output } from "@angular/core"; import {UiData} from "./ui_data"; +import { PersistentStore } from "common/persistent_store"; @Component({ selector: "viewer-window-manager", @@ -32,11 +33,10 @@ import {UiData} from "./ui_data"; ` }) export class ViewerWindowManagerComponent { - @Input() - inputData?: UiData; + @Input() inputData?: UiData; + @Input() store?: PersistentStore; - @Output() - outputEvent = new EventEmitter(); // or EventEmitter() + @Output() outputEvent = new EventEmitter(); // or EventEmitter() public generateOutputEvent(event: MouseEvent) { this.outputEvent.emit(new DummyEvent());