(Hierarchy View) Create Viewer for surface flinger traces.
Created the viewer component for SF traces, including rects view, properties view, hierarchy view (for re-use in other apps). This CL contains hierarchy view and associated reusable components. Bug: b/238089034 b/232081297 Test: npm run test:all. upload/run any SF trace to see what it looks like. Change-Id: I3dd7c3f73ac7d7dac9b65d5fc7853e9fc8b8e56e
This commit is contained in:
@@ -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]
|
||||
})
|
||||
|
||||
@@ -30,39 +30,42 @@ import { Viewer } from "viewers/viewer";
|
||||
template: `
|
||||
<div id="app-title">
|
||||
<span>Winscope Viewer 2.0</span>
|
||||
<button mat-raised-button *ngIf="dataLoaded" (click)="clearData()">Back to Home</button>
|
||||
<button mat-raised-button *ngIf="dataLoaded" (click)="toggleTimestamp()">Start/End Timestamp</button>
|
||||
<mat-slider
|
||||
*ngIf="dataLoaded"
|
||||
step="1"
|
||||
min="0"
|
||||
[max]="this.allTimestamps.length-1"
|
||||
aria-label="units"
|
||||
[value]="currentTimestampIndex"
|
||||
(input)="updateCurrentTimestamp($event)"
|
||||
class="time-slider"
|
||||
></mat-slider>
|
||||
<button mat-raised-button *ngIf="dataLoaded" (click)="toggleTimestamp()">Start/End Timestamp</button>
|
||||
<button class="upload-new-btn" mat-raised-button *ngIf="dataLoaded" (click)="clearData()">Upload New</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!dataLoaded" fxLayout="row wrap" fxLayoutGap="10px grid" class="card-grid">
|
||||
<mat-card class="homepage-card" id="collect-traces-card">
|
||||
<collect-traces [(traceCoordinator)]="traceCoordinator" (dataLoadedChange)="onDataLoadedChange($event)"[store]="store"></collect-traces>
|
||||
<collect-traces [traceCoordinator]="traceCoordinator" (dataLoadedChange)="onDataLoadedChange($event)"[store]="store"></collect-traces>
|
||||
</mat-card>
|
||||
<mat-card class="homepage-card" id="upload-traces-card">
|
||||
<upload-traces [(traceCoordinator)]="traceCoordinator" (dataLoadedChange)="onDataLoadedChange($event)"></upload-traces>
|
||||
<upload-traces [traceCoordinator]="traceCoordinator" (dataLoadedChange)="onDataLoadedChange($event)"></upload-traces>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div id="timescrub">
|
||||
</div>
|
||||
|
||||
<div id="timestamps">
|
||||
</div>
|
||||
|
||||
<div id="viewers" [class]="showViewers()">
|
||||
</div>
|
||||
|
||||
<div id="timescrub">
|
||||
<mat-slider
|
||||
*ngIf="dataLoaded"
|
||||
step="1"
|
||||
min="0"
|
||||
[max]="this.allTimestamps.length-1"
|
||||
aria-label="units"
|
||||
[value]="currentTimestampIndex"
|
||||
(input)="updateCurrentTimestamp($event)"
|
||||
class="time-slider"
|
||||
></mat-slider>
|
||||
</div>
|
||||
|
||||
<div id="timestamps">
|
||||
</div>
|
||||
`,
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
26
tools/winscope-ng/src/common/trace/timestamp_utils.ts
Normal file
26
tools/winscope-ng/src/common/trace/timestamp_utils.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
59
tools/winscope-ng/src/viewers/common/chip.ts
Normal file
59
tools/winscope-ng/src/viewers/common/chip.ts
Normal file
@@ -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",
|
||||
);
|
||||
252
tools/winscope-ng/src/viewers/common/tree_utils.spec.ts
Normal file
252
tools/winscope-ng/src/viewers/common/tree_utils.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
449
tools/winscope-ng/src/viewers/common/tree_utils.ts
Normal file
449
tools/winscope-ng/src/viewers/common/tree_utils.ts
Normal file
@@ -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<string>) {
|
||||
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<string>;
|
||||
private pinnedItems: Array<Tree> = [];
|
||||
private relZParentIds: Array<number> = [];
|
||||
private flattenedChildren: Array<Tree> = [];
|
||||
|
||||
constructor(tree: Tree, userOptions: UserOptions, filter: FilterType, pinnedIds?: Array<string>) {
|
||||
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>): 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<Tree | null>,
|
||||
oldTreeSiblings: Array<Tree | null>
|
||||
): Array<Tree | null> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
export interface UserOptions {
|
||||
[key: string]: {
|
||||
name: string,
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
@@ -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: `
|
||||
<mat-card-title class="trace-view-subtitle">Hierarchy</mat-card-title>
|
||||
`,
|
||||
styles: [
|
||||
".trace-view-subtitle { font-size: 18px}"
|
||||
]
|
||||
})
|
||||
|
||||
export class HierarchyComponent {
|
||||
}
|
||||
export const ViewerEvents = {
|
||||
HierarchyPinnedChange: "HierarchyPinnedChange",
|
||||
HighlightedChange: "HighlightedChange",
|
||||
HierarchyUserOptionsChange: "HierarchyUserOptionsChange",
|
||||
HierarchyFilterChange: "HierarchyFilterChange"
|
||||
};
|
||||
@@ -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<HierarchyComponent>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
218
tools/winscope-ng/src/viewers/components/hierarchy.component.ts
Normal file
218
tools/winscope-ng/src/viewers/components/hierarchy.component.ts
Normal file
@@ -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: `
|
||||
<mat-card-header class="view-header">
|
||||
<mat-card-title class="title-filter">
|
||||
<span class="hierarchy-title">Hierarchy</span>
|
||||
<mat-form-field class="filter-field">
|
||||
<mat-label>Filter...</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="filterString"
|
||||
(ngModelChange)="filterTree()"
|
||||
name="filter"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</mat-card-title>
|
||||
<div class="view-controls">
|
||||
<mat-checkbox
|
||||
*ngFor="let option of objectKeys(userOptions)"
|
||||
class="trace-box"
|
||||
[(ngModel)]="userOptions[option].enabled"
|
||||
(ngModelChange)="updateTree()"
|
||||
>{{userOptions[option].name}}</mat-checkbox>
|
||||
</div>
|
||||
<div class="pinned-items" *ngIf="pinnedItems.length > 0">
|
||||
<tree-node
|
||||
*ngFor="let pinnedItem of pinnedItems"
|
||||
class="node"
|
||||
[class]="diffClass(pinnedItem)"
|
||||
[class.selected]="isHighlighted(pinnedItem, highlightedItems)"
|
||||
[class.clickable]="true"
|
||||
[item]="pinnedItem"
|
||||
[isPinned]="true"
|
||||
[isInPinnedSection]="true"
|
||||
(pinNodeChange)="pinnedItemChange($event)"
|
||||
(click)="onPinnedNodeClick($event, pinnedItem.id)"
|
||||
></tree-node>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="hierarchy-content" [style]="maxHierarchyHeight()">
|
||||
<div class="tree-wrapper">
|
||||
<tree-view
|
||||
class="tree-view"
|
||||
*ngIf="tree"
|
||||
[isFlattened]="isFlattened()"
|
||||
[isShaded]="true"
|
||||
[item]="tree"
|
||||
[dependencies]="dependencies"
|
||||
[store]="store"
|
||||
[useGlobalCollapsedState]="true"
|
||||
[itemsClickable]="true"
|
||||
[highlightedItems]="highlightedItems"
|
||||
[pinnedItems]="pinnedItems"
|
||||
(highlightedItemChange)="highlightedItemChange($event)"
|
||||
(pinnedItemChange)="pinnedItemChange($event)"
|
||||
></tree-view>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
`,
|
||||
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<TraceType> = [];
|
||||
@Input() highlightedItems: Array<string> = [];
|
||||
@Input() pinnedItems: Array<Tree> = [];
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<string>) {
|
||||
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<string> = [];
|
||||
private camera: THREE.OrthographicCamera;
|
||||
private rects: Rectangle[] = [];
|
||||
private labelElements: HTMLElement[] = [];
|
||||
@@ -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();
|
||||
@@ -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: `
|
||||
<mat-card-header class="view-controls">
|
||||
<mat-radio-group (change)="onChangeView($event.value)">
|
||||
<mat-radio-button class="visible-radio" [value]="true" [checked]="visibleView()">Visible</mat-radio-button>
|
||||
<mat-radio-button class="xray-radio" [value]="false" [checked]="!visibleView()">X-ray</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-slider
|
||||
step="0.001"
|
||||
min="0.1"
|
||||
max="0.4"
|
||||
aria-label="units"
|
||||
[value]="getLayerSeparation()"
|
||||
(input)="canvasGraphics.updateLayerSeparation($event.value!)"
|
||||
></mat-slider>
|
||||
<mat-slider
|
||||
step="0.01"
|
||||
min="0.00"
|
||||
max="4"
|
||||
aria-label="units"
|
||||
[value]="xyCameraPos()"
|
||||
(input)="canvasGraphics.updateRotation($event.value!)"
|
||||
></mat-slider>
|
||||
<mat-card-title>Layers</mat-card-title>
|
||||
<div class="top-view-controls">
|
||||
<mat-checkbox
|
||||
[disabled]="visibleView()"
|
||||
class="rects-checkbox"
|
||||
[checked]="visibleView()"
|
||||
(change)="onChangeView($event.checked!)"
|
||||
>Only visible layers</mat-checkbox>
|
||||
<mat-checkbox
|
||||
[disabled]="!visibleView()"
|
||||
class="rects-checkbox"
|
||||
[checked]="showVirtualDisplays()"
|
||||
(change)="canvasGraphics.updateVirtualDisplays($event.checked!)"
|
||||
>Show virtual displays</mat-checkbox>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="rects-content">
|
||||
<div class="canvas-container">
|
||||
>Show virtual</mat-checkbox>
|
||||
<div class="zoom-container">
|
||||
<button id="zoom-btn" (click)="canvasGraphics.updateZoom(true)">
|
||||
<mat-icon aria-hidden="true">
|
||||
@@ -65,6 +50,35 @@ import * as THREE from "three";
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider-view-controls">
|
||||
<div class="slider" [class.rotation]="true">
|
||||
<span>Flat to isometric</span>
|
||||
<mat-slider
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="4"
|
||||
aria-label="units"
|
||||
[value]="xCameraPos()"
|
||||
(input)="canvasGraphics.updateRotation($event.value!)"
|
||||
></mat-slider>
|
||||
</div>
|
||||
<div class="slider" [class.spacing]="true">
|
||||
<span>Layer spacing</span>
|
||||
<mat-slider
|
||||
class="spacing-slider"
|
||||
step="0.001"
|
||||
min="0.1"
|
||||
max="0.4"
|
||||
aria-label="units"
|
||||
[value]="getLayerSeparation()"
|
||||
(input)="canvasGraphics.updateLayerSeparation($event.value!)"
|
||||
></mat-slider>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="rects-content">
|
||||
<div class="canvas-container">
|
||||
<canvas id="rects-canvas" (click)="onRectClick($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
@@ -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<number> = [];
|
||||
@Input() highlighted = "";
|
||||
@Input() highlightedItems: Array<string> = [];
|
||||
|
||||
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() {
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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<TreeComponent>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
242
tools/winscope-ng/src/viewers/components/tree.component.ts
Normal file
242
tools/winscope-ng/src/viewers/components/tree.component.ts
Normal file
@@ -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: `
|
||||
<div class="tree-view">
|
||||
<tree-node
|
||||
class="node"
|
||||
[class.leaf]="isLeaf()"
|
||||
[class.selected]="isHighlighted(item, highlightedItems)"
|
||||
[class.clickable]="isClickable()"
|
||||
[class.shaded]="isShaded"
|
||||
[class.hover]="nodeHover"
|
||||
[class.childHover]="childHover"
|
||||
[class]="diffClass(item)"
|
||||
[style]="nodeOffsetStyle()"
|
||||
[item]="item"
|
||||
[flattened]="isFlattened"
|
||||
[isLeaf]="isLeaf()"
|
||||
[isCollapsed]="isCollapsed()"
|
||||
[hasChildren]="hasChildren()"
|
||||
[isPinned]="isPinned()"
|
||||
(toggleTreeChange)="toggleTree()"
|
||||
(click)="onNodeClick($event)"
|
||||
(expandTreeChange)="expandTree()"
|
||||
(pinNodeChange)="sendNewPinnedItemToHierarchy($event)"
|
||||
></tree-node>
|
||||
|
||||
<div class="children" *ngIf="hasChildren()" [hidden]="isCollapsed()" [style]="childrenIndentation()">
|
||||
<ng-container *ngFor="let child of children()">
|
||||
<tree-view
|
||||
class="childrenTree"
|
||||
[item]="child"
|
||||
[store]="store"
|
||||
[dependencies]="dependencies"
|
||||
[isFlattened]="isFlattened"
|
||||
[isShaded]="!isShaded"
|
||||
[useGlobalCollapsedState]="useGlobalCollapsedState"
|
||||
[initialDepth]="initialDepth + 1"
|
||||
[highlightedItems]="highlightedItems"
|
||||
[pinnedItems]="pinnedItems"
|
||||
(highlightedItemChange)="sendNewHighlightedItemToHierarchy($event)"
|
||||
(pinnedItemChange)="sendNewPinnedItemToHierarchy($event)"
|
||||
[itemsClickable]="itemsClickable"
|
||||
(hoverStart)="childHover = true"
|
||||
(hoverEnd)="childHover = false"
|
||||
></tree-view>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [nodeStyles, treeNodeStyles]
|
||||
})
|
||||
|
||||
export class TreeComponent {
|
||||
diffClass = diffClass;
|
||||
isHighlighted = isHighlighted;
|
||||
|
||||
@Input() item!: Tree;
|
||||
@Input() dependencies: Array<TraceType> = [];
|
||||
@Input() store!: PersistentStore;
|
||||
@Input() isFlattened? = false;
|
||||
@Input() isShaded? = false;
|
||||
@Input() initialDepth = 0;
|
||||
@Input() highlightedItems: Array<string> = [];
|
||||
@Input() pinnedItems?: Array<Tree> = [];
|
||||
@Input() itemsClickable?: boolean;
|
||||
@Input() useGlobalCollapsedState?: boolean;
|
||||
|
||||
@Output() highlightedItemChange = new EventEmitter<string>();
|
||||
@Output() pinnedItemChange = new EventEmitter<Tree>();
|
||||
@Output() hoverStart = new EventEmitter<void>();
|
||||
@Output() hoverEnd = new EventEmitter<void>();
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
||||
@@ -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<TreeElementComponent>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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: `
|
||||
<span>
|
||||
<span class="kind">{{item.kind}}</span>
|
||||
<span *ngIf="item.kind && item.name">-</span>
|
||||
<span *ngIf="showShortName()" [matTooltip]="item.name">{{ item.shortName }}</span>
|
||||
<span *ngIf="!showShortName()">{{item.name}}</span>
|
||||
<div
|
||||
*ngFor="let chip of item.chips"
|
||||
[class]="chipClass(chip)"
|
||||
[matTooltip]="chip.long"
|
||||
>{{chip.short}}</div>
|
||||
</span>
|
||||
`,
|
||||
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"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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<TreeNodeComponent>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
114
tools/winscope-ng/src/viewers/components/tree_node.component.ts
Normal file
114
tools/winscope-ng/src/viewers/components/tree_node.component.ts
Normal file
@@ -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: `
|
||||
<button
|
||||
id="toggle-tree-btn"
|
||||
class="icon-button"
|
||||
(click)="toggleTree($event)"
|
||||
*ngIf="showChevron()"
|
||||
>
|
||||
<mat-icon class="icon-button">
|
||||
{{isCollapsed ? "chevron_right" : "expand_more"}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="leaf-node-icon-wrapper"
|
||||
*ngIf="showLeafNodeIcon()"
|
||||
>
|
||||
<mat-icon class="leaf-node-icon"></mat-icon>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="pin-node-btn"
|
||||
class="icon-button"
|
||||
(click)="pinNode($event)"
|
||||
*ngIf="!isEntryNode()"
|
||||
>
|
||||
<mat-icon class="icon-button">
|
||||
{{isPinned ? "star" : "star_border"}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="description">
|
||||
<tree-element
|
||||
[item]="item"
|
||||
></tree-element>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="expand-tree-btn"
|
||||
*ngIf="hasChildren && isCollapsed"
|
||||
(click)="expandTree($event)"
|
||||
class="icon-button"
|
||||
>
|
||||
<mat-icon
|
||||
aria-hidden="true"
|
||||
class="icon-button"
|
||||
>
|
||||
more_horiz
|
||||
</mat-icon>
|
||||
</button>
|
||||
`,
|
||||
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<void>();
|
||||
@Output() expandTreeChange = new EventEmitter<boolean>();
|
||||
@Output() pinNodeChange = new EventEmitter<Tree>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
68
tools/winscope-ng/src/viewers/styles/node.styles.ts
Normal file
68
tools/winscope-ng/src/viewers/styles/node.styles.ts
Normal file
@@ -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;}
|
||||
`;
|
||||
51
tools/winscope-ng/src/viewers/styles/tree_element.styles.ts
Normal file
51
tools/winscope-ng/src/viewers/styles/tree_element.styles.ts
Normal file
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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<TraceType, any>) {
|
||||
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<TraceType, any>) {
|
||||
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<number> = [];
|
||||
private hierarchyFilter: FilterType = getFilter("");
|
||||
private highlightedItems: Array<string> = [];
|
||||
private pinnedItems: Array<Tree> = [];
|
||||
private pinnedIds: Array<string> = [];
|
||||
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};
|
||||
|
||||
@@ -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> = [TraceType.SURFACE_FLINGER];
|
||||
rects?: Rectangle[] = [];
|
||||
highlighted?: string = "";
|
||||
displayIds?: number[] = [];
|
||||
highlightedItems?: Array<string> = [];
|
||||
pinnedItems?: Array<Tree> = [];
|
||||
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};
|
||||
|
||||
@@ -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<ViewerSurfaceFlingerComponent>;
|
||||
@@ -44,7 +44,7 @@ describe("ViewerSurfaceFlingerComponent", () => {
|
||||
PropertiesComponent,
|
||||
RectsComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
<rects-view
|
||||
[rects]="inputData?.rects ?? []"
|
||||
[displayIds]="inputData?.displayIds ?? []"
|
||||
[highlighted]="inputData?.highlighted ?? ''"
|
||||
class="rects-view"
|
||||
[highlightedItems]="inputData?.highlightedItems ?? []"
|
||||
></rects-view>
|
||||
</mat-card>
|
||||
<mat-card id="sf-hierarchy-view" class="hierarchy-view">
|
||||
<hierarchy-view></hierarchy-view>
|
||||
</mat-card>
|
||||
<mat-card id="sf-properties-view" class="properties-view">
|
||||
<properties-view></properties-view>
|
||||
</mat-card>
|
||||
<div fxLayout="row wrap" fxLayoutGap="10px grid" class="card-grid">
|
||||
<mat-card id="sf-hierarchy-view" class="hierarchy-view">
|
||||
<hierarchy-view
|
||||
[tree]="inputData?.tree"
|
||||
[dependencies]="inputData?.dependencies ?? []"
|
||||
[highlightedItems]="inputData?.highlightedItems ?? []"
|
||||
[pinnedItems]="inputData?.pinnedItems ?? []"
|
||||
[store]="store"
|
||||
[userOptions]="inputData?.hierarchyUserOptions ?? {}"
|
||||
></hierarchy-view>
|
||||
</mat-card>
|
||||
<mat-card id="sf-properties-view" class="properties-view">
|
||||
<properties-view></properties-view>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<TraceType, any>): void {
|
||||
|
||||
@@ -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<DummyEvent>(); // or EventEmitter<void>()
|
||||
@Output() outputEvent = new EventEmitter<DummyEvent>(); // or EventEmitter<void>()
|
||||
|
||||
public generateOutputEvent(event: MouseEvent) {
|
||||
this.outputEvent.emit(new DummyEvent());
|
||||
|
||||
Reference in New Issue
Block a user