@@ -79,9 +93,16 @@ import * as THREE from "three";
".canvas-container {height: 40rem; width: 100%; position: relative}",
"#rects-canvas {height: 40rem; width: 100%; cursor: pointer; position: absolute; top: 0px}",
"#labels-canvas {height: 40rem; width: 100%; position: absolute; top: 0px}",
- ".view-controls {display: inline-block; position: relative; min-height: 72px}",
- ".zoom-container {position: absolute; top: 0px; z-index: 10}",
- "#zoom-btn {position:relative; display: block; background: none; border: none}",
+ ".view-controls, .slider-view-controls {display: inline-block; position: relative; min-height: 4.5rem; width: 100%}",
+ ".slider {display: inline-block}",
+ ".slider.spacing {float: right}",
+ ".slider span, .slider mat-slider { display: block; padding-left: 0px; padding-top: 0px; font-weight: bold}",
+ ".top-view-controls {min-height: 1.5rem; width: 100%; position: relative; display: inline-block; vertical-align: middle;}",
+ ".zoom-container {position: relative; vertical-align: middle; float: right}",
+ "#zoom-btn {position:relative; display: inline-flex; background: none; border: none}",
+ "mat-card-title {font-size: 16px !important}",
+ ":host /deep/ .mat-card-header-text {width: 100%; margin: 0;}",
+ "mat-radio-group {vertical-align: middle}",
"mat-radio-button {font-size: 16px; font-weight: normal}",
".mat-radio-button, .mat-radio-button-frame {transform: scale(0.8);}",
".rects-checkbox {font-size: 14px; font-weight: normal}",
@@ -96,7 +117,7 @@ import * as THREE from "three";
export class RectsComponent implements OnChanges, OnDestroy {
@Input() rects!: Rectangle[];
@Input() displayIds: Array = [];
- @Input() highlighted = "";
+ @Input() highlightedItems: Array = [];
constructor(
@Inject(ElementRef) private elementRef: ElementRef,
@@ -112,6 +133,9 @@ export class RectsComponent implements OnChanges, OnDestroy {
}
ngOnChanges(changes: SimpleChanges) {
+ if (changes["highlightedItems"]) {
+ this.canvasGraphics.updateHighlightedItems(this.highlightedItems);
+ }
if (this.rects.length > 0) {
//change in rects so they must undergo transformation and scaling before canvas refreshed
this.canvasGraphics.clearLabelElements();
@@ -126,13 +150,15 @@ export class RectsComponent implements OnChanges, OnDestroy {
}
});
this.scaleRects();
- this.drawRects();
+ if (changes["rects"]) {
+ this.drawRects();
+ }
} else if (this.canvasSubscription) {
this.canvasSubscription.unsubscribe();
}
}
- onRectClick(event:PointerEvent) {
+ onRectClick(event:MouseEvent) {
this.setNormalisedMousePos(event);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(this.mouse, this.canvasGraphics.getCamera());
@@ -140,18 +166,12 @@ export class RectsComponent implements OnChanges, OnDestroy {
const intersects = raycaster.intersectObjects(this.canvasGraphics.getTargetObjects());
// if there is one (or more) intersections
if (intersects.length > 0){
- if (this.highlighted === intersects[0].object.name) {
- this.highlighted = "";
- this.canvasGraphics.updateHighlighted("");
- } else {
- this.highlighted = intersects[0].object.name;
- this.canvasGraphics.updateHighlighted(intersects[0].object.name);
- }
- this.updateHighlightedRect();
+ const id = intersects[0].object.name;
+ this.updateHighlightedItems(id);
}
}
- setNormalisedMousePos(event:PointerEvent) {
+ setNormalisedMousePos(event:MouseEvent) {
event.preventDefault();
const canvas = (event.target as Element);
const canvasOffset = canvas.getBoundingClientRect();
@@ -160,11 +180,13 @@ export class RectsComponent implements OnChanges, OnDestroy {
this.mouse.z = 0;
}
- updateHighlightedRect() {
- const event: CustomEvent = new CustomEvent("highlightedChange", {
- bubbles: true,
- detail: { layerId: this.highlighted }
- });
+ updateHighlightedItems(newId: string) {
+ const event: CustomEvent = new CustomEvent(
+ ViewerEvents.HighlightedChange,
+ {
+ bubbles: true,
+ detail: { id: newId }
+ });
this.elementRef.nativeElement.dispatchEvent(event);
}
@@ -181,8 +203,8 @@ export class RectsComponent implements OnChanges, OnDestroy {
}
updateVariablesBeforeRefresh() {
- this.rects = this.rects.filter(rect => rect.displayId === this.currentDisplayId);
- this.canvasGraphics.updateRects(this.rects);
+ const rects = this.rects.filter(rect => rect.displayId === this.currentDisplayId);
+ this.canvasGraphics.updateRects(rects);
const biggestX = Math.max(...this.rects.map(rect => rect.topLeft.x + rect.width/2));
this.canvasGraphics.updateIsLandscape(biggestX > this.s({x: this.boundsWidth, y:this.boundsHeight}).x/2);
}
@@ -272,8 +294,8 @@ export class RectsComponent implements OnChanges, OnDestroy {
return this.canvasGraphics.getLayerSeparation();
}
- xyCameraPos() {
- return this.canvasGraphics.getXyCameraPos();
+ xCameraPos() {
+ return this.canvasGraphics.getXCameraPos();
}
showVirtualDisplays() {
diff --git a/tools/winscope-ng/src/viewers/rects_utils.spec.ts b/tools/winscope-ng/src/viewers/components/rects/rects_utils.spec.ts
similarity index 91%
rename from tools/winscope-ng/src/viewers/rects_utils.spec.ts
rename to tools/winscope-ng/src/viewers/components/rects/rects_utils.spec.ts
index 605b1f9ca..931397728 100644
--- a/tools/winscope-ng/src/viewers/rects_utils.spec.ts
+++ b/tools/winscope-ng/src/viewers/components/rects/rects_utils.spec.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { RectsUtils } from "./rects_utils";
+import { RectsUtils } from "viewers/components/rects/rects_utils";
describe("RectsUtils", () => {
it("transforms rect", () => {
@@ -38,7 +38,8 @@ describe("RectsUtils", () => {
width: 1,
ref: null,
id: 12345,
- displayId: 0
+ displayId: 0,
+ isVirtual: false
};
const expected = {
topLeft: {x: 1, y: 1},
@@ -52,7 +53,7 @@ describe("RectsUtils", () => {
ref: null,
id: 12345,
displayId: 0,
- isVirtual: undefined
+ isVirtual: false
};
expect(RectsUtils.transformRect(rect.transform.matrix, rect)).toEqual(expected);
});
diff --git a/tools/winscope-ng/src/viewers/rects_utils.ts b/tools/winscope-ng/src/viewers/components/rects/rects_utils.ts
similarity index 100%
rename from tools/winscope-ng/src/viewers/rects_utils.ts
rename to tools/winscope-ng/src/viewers/components/rects/rects_utils.ts
diff --git a/tools/winscope-ng/src/viewers/components/tree.component.spec.ts b/tools/winscope-ng/src/viewers/components/tree.component.spec.ts
new file mode 100644
index 000000000..4732dc629
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/components/tree.component.spec.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ComponentFixture, TestBed} from "@angular/core/testing";
+import { TreeComponent } from "./tree.component";
+import { ComponentFixtureAutoDetect } from "@angular/core/testing";
+import { NO_ERRORS_SCHEMA } from "@angular/core";
+
+describe("TreeComponent", () => {
+ let fixture: ComponentFixture;
+ let component: TreeComponent;
+ let htmlElement: HTMLElement;
+
+ beforeAll(async () => {
+ await TestBed.configureTestingModule({
+ providers: [
+ { provide: ComponentFixtureAutoDetect, useValue: true }
+ ],
+ declarations: [
+ TreeComponent
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TreeComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+ component.isFlattened = true;
+ component.item = {
+ simplifyNames: false,
+ kind: "entry",
+ name: "BaseLayerTraceEntry",
+ shortName: "BLTE",
+ chips: [],
+ children: [{kind: "3", id: "3", name: "Child1"}]
+ };
+ component.diffClass = jasmine.createSpy().and.returnValue("none");
+ component.isHighlighted = jasmine.createSpy().and.returnValue(false);
+ component.hasChildren = jasmine.createSpy().and.returnValue(true);
+ });
+
+ it("can be created", () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it("creates node element", () => {
+ fixture.detectChanges();
+ const nodeElement = htmlElement.querySelector(".node");
+ expect(nodeElement).toBeTruthy();
+ });
+});
diff --git a/tools/winscope-ng/src/viewers/components/tree.component.ts b/tools/winscope-ng/src/viewers/components/tree.component.ts
new file mode 100644
index 000000000..0c1e705b0
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/components/tree.component.ts
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Inject, Input, Output, ElementRef, EventEmitter } from "@angular/core";
+import { PersistentStore } from "common/persistent_store";
+import { nodeStyles, treeNodeStyles } from "viewers/styles/node.styles";
+import { Tree, diffClass, isHighlighted } from "viewers/common/tree_utils";
+import { TraceType } from "common/trace/trace_type";
+
+@Component({
+ selector: "tree-view",
+ template: `
+
+
+
+
+
+
+
+
+
+ `,
+ styles: [nodeStyles, treeNodeStyles]
+})
+
+export class TreeComponent {
+ diffClass = diffClass;
+ isHighlighted = isHighlighted;
+
+ @Input() item!: Tree;
+ @Input() dependencies: Array = [];
+ @Input() store!: PersistentStore;
+ @Input() isFlattened? = false;
+ @Input() isShaded? = false;
+ @Input() initialDepth = 0;
+ @Input() highlightedItems: Array = [];
+ @Input() pinnedItems?: Array = [];
+ @Input() itemsClickable?: boolean;
+ @Input() useGlobalCollapsedState?: boolean;
+
+ @Output() highlightedItemChange = new EventEmitter();
+ @Output() pinnedItemChange = new EventEmitter();
+ @Output() hoverStart = new EventEmitter();
+ @Output() hoverEnd = new EventEmitter();
+
+ isCollapsedByDefault = true;
+ localCollapsedState = this.isCollapsedByDefault;
+ nodeHover = false;
+ childHover = false;
+ readonly levelOffset = 24;
+ nodeElement: HTMLElement;
+
+ constructor(
+ @Inject(ElementRef) elementRef: ElementRef,
+ ) {
+ this.nodeElement = elementRef.nativeElement.querySelector(".node");
+ this.nodeElement?.addEventListener("mousedown", this.nodeMouseDownEventListener);
+ this.nodeElement?.addEventListener("mouseenter", this.nodeMouseEnterEventListener);
+ this.nodeElement?.addEventListener("mouseleave", this.nodeMouseLeaveEventListener);
+ }
+
+ ngOnDestroy() {
+ this.nodeElement?.removeEventListener("mousedown", this.nodeMouseDownEventListener);
+ this.nodeElement?.removeEventListener("mouseenter", this.nodeMouseEnterEventListener);
+ this.nodeElement?.removeEventListener("mouseleave", this.nodeMouseLeaveEventListener);
+ }
+
+ onNodeClick(event: MouseEvent) {
+ event.preventDefault();
+ if (window.getSelection()?.type === "range") {
+ return;
+ }
+
+ if (!this.isLeaf() && event.detail % 2 === 0) {
+ // Double click collapsable node
+ event.preventDefault();
+ this.toggleTree();
+ } else {
+ this.updateHighlightedItems();
+ }
+ }
+
+ nodeOffsetStyle() {
+ const offset = this.levelOffset * (this.initialDepth) + "px";
+
+ return {
+ marginLeft: "-" + offset,
+ paddingLeft: offset,
+ };
+ }
+
+ updateHighlightedItems() {
+ if (this.item && this.item.id) {
+ this.highlightedItemChange.emit(`${this.item.id}`);
+ }
+ }
+
+ isPinned() {
+ if (this.item) {
+ return this.pinnedItems?.map((item: Tree) => `${item.id}`).includes(`${this.item.id}`);
+ }
+ return false;
+ }
+
+ sendNewHighlightedItemToHierarchy(newId: string) {
+ this.highlightedItemChange.emit(newId);
+ }
+
+ sendNewPinnedItemToHierarchy(newPinnedItem: Tree) {
+ this.pinnedItemChange.emit(newPinnedItem);
+ }
+
+ isLeaf() {
+ return !this.item.children || this.item.children.length === 0;
+ }
+
+ isClickable() {
+ return !this.isLeaf() || this.itemsClickable;
+ }
+
+ toggleTree() {
+ this.setCollapseValue(!this.isCollapsed());
+ }
+
+ expandTree() {
+ this.setCollapseValue(false);
+ }
+
+ isCollapsed() {
+ if (this.isLeaf()) {
+ return false;
+ }
+
+ if (this.useGlobalCollapsedState) {
+ return this.store.getFromStore(`collapsedState.item.${this.dependencies}.${this.item.id}`)==="true"
+ ?? this.isCollapsedByDefault;
+ }
+
+ return this.localCollapsedState;
+ }
+
+ children() {
+ return this.item.children;
+ }
+
+ hasChildren() {
+ const isParentEntryInFlatView = this.item.kind === "entry" && this.isFlattened;
+ return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf();
+ }
+
+ setCollapseValue(isCollapsed:boolean) {
+ if (this.useGlobalCollapsedState) {
+ this.store.addToStore(`collapsedState.item.${this.dependencies}.${this.item.id}`, `${isCollapsed}`);
+ } else {
+ this.localCollapsedState = isCollapsed;
+ }
+ }
+
+ childrenIndentation() {
+ if (this.isFlattened) {
+ return {
+ marginLeft: "0px",
+ paddingLeft: "0px",
+ marginTop: "0px",
+ };
+ } else {
+ // Aligns border with collapse arrows
+ return {
+ marginLeft: "12px",
+ paddingLeft: "11px",
+ borderLeft: "1px solid rgb(238, 238, 238)",
+ marginTop: "0px",
+ };
+ }
+ }
+
+ nodeMouseDownEventListener = (event:MouseEvent) => {
+ if (event.detail > 1) {
+ event.preventDefault();
+ return false;
+ }
+ return true;
+ };
+
+ nodeMouseEnterEventListener = () => {
+ this.nodeHover = true;
+ this.hoverStart.emit();
+ };
+
+ nodeMouseLeaveEventListener = () => {
+ this.nodeHover = false;
+ this.hoverEnd.emit();
+ };
+}
diff --git a/tools/winscope-ng/src/viewers/components/tree_element.component.spec.ts b/tools/winscope-ng/src/viewers/components/tree_element.component.spec.ts
new file mode 100644
index 000000000..8322e8929
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/components/tree_element.component.spec.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ComponentFixture, TestBed} from "@angular/core/testing";
+import { TreeElementComponent } from "./tree_element.component";
+import { ComponentFixtureAutoDetect } from "@angular/core/testing";
+import { NO_ERRORS_SCHEMA } from "@angular/core";
+
+describe("TreeElementComponent", () => {
+ let fixture: ComponentFixture;
+ let component: TreeElementComponent;
+ let htmlElement: HTMLElement;
+
+ beforeAll(async () => {
+ await TestBed.configureTestingModule({
+ providers: [
+ { provide: ComponentFixtureAutoDetect, useValue: true }
+ ],
+ declarations: [
+ TreeElementComponent
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TreeElementComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+ });
+
+ it("can be created", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/tools/winscope-ng/src/viewers/components/tree_element.component.ts b/tools/winscope-ng/src/viewers/components/tree_element.component.ts
new file mode 100644
index 000000000..4ac8d7fe2
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/components/tree_element.component.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Input } from "@angular/core";
+import { treeElementStyles } from "viewers/styles/tree_element.styles";
+import { Tree } from "viewers/common/tree_utils";
+import Chip from "viewers/common/chip";
+
+@Component({
+ selector: "tree-element",
+ template: `
+
+ {{item.kind}}
+ -
+ {{ item.shortName }}
+ {{item.name}}
+
{{chip.short}}
+
+ `,
+ styles: [ treeElementStyles ]
+})
+
+export class TreeElementComponent {
+ @Input() item!: Tree;
+
+ showShortName() {
+ return this.item.simplifyNames && this.item.shortName !== this.item.name;
+ }
+
+ chipClass(chip: Chip) {
+ return [
+ "tree-view-internal-chip",
+ "tree-view-chip",
+ "tree-view-chip" + "-" +
+ (chip.type.toString() || "default"),
+ ];
+ }
+}
diff --git a/tools/winscope-ng/src/viewers/components/tree_node.component.spec.ts b/tools/winscope-ng/src/viewers/components/tree_node.component.spec.ts
new file mode 100644
index 000000000..d8cd4bb1d
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/components/tree_node.component.spec.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ComponentFixture, TestBed} from "@angular/core/testing";
+import { TreeNodeComponent } from "./tree_node.component";
+import { ComponentFixtureAutoDetect } from "@angular/core/testing";
+import { NO_ERRORS_SCHEMA } from "@angular/core";
+
+describe("TreeNodeComponent", () => {
+ let fixture: ComponentFixture;
+ let component: TreeNodeComponent;
+ let htmlElement: HTMLElement;
+
+ beforeAll(async () => {
+ await TestBed.configureTestingModule({
+ providers: [
+ { provide: ComponentFixtureAutoDetect, useValue: true }
+ ],
+ declarations: [
+ TreeNodeComponent
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TreeNodeComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+ component.item = {
+ simplifyNames: false,
+ kind: "entry",
+ name: "BaseLayerTraceEntry",
+ shortName: "BLTE",
+ chips: [],
+ };
+ component.isCollapsed = true;
+ component.hasChildren = false;
+ component.isPinned = false;
+ component.isInPinnedSection = false;
+ });
+
+ it("can be created", () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it("creates tree element", () => {
+ fixture.detectChanges();
+ const treeElement = htmlElement.querySelector("tree-element");
+ expect(treeElement).toBeTruthy();
+ });
+});
diff --git a/tools/winscope-ng/src/viewers/components/tree_node.component.ts b/tools/winscope-ng/src/viewers/components/tree_node.component.ts
new file mode 100644
index 000000000..698ccdfdd
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/components/tree_node.component.ts
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, Input, Output, EventEmitter } from "@angular/core";
+import { nodeInnerItemStyles } from "viewers/styles/node.styles";
+import { Tree } from "viewers/common/tree_utils";
+
+@Component({
+ selector: "tree-node",
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ styles: [nodeInnerItemStyles]
+})
+
+export class TreeNodeComponent {
+ @Input() item!: Tree | null;
+ @Input() isLeaf?: boolean;
+ @Input() flattened?: boolean;
+ @Input() isCollapsed?: boolean;
+ @Input() hasChildren?: boolean = false;
+ @Input() isPinned?: boolean = false;
+ @Input() isInPinnedSection?: boolean = false;
+
+ @Output() toggleTreeChange = new EventEmitter();
+ @Output() expandTreeChange = new EventEmitter();
+ @Output() pinNodeChange = new EventEmitter();
+
+ isEntryNode() {
+ return this.item.kind === "entry" ?? false;
+ }
+
+ toggleTree(event: MouseEvent) {
+ event.stopPropagation();
+ this.toggleTreeChange.emit();
+ }
+
+ showChevron() {
+ return !this.isLeaf && !this.flattened && !this.isInPinnedSection;
+ }
+
+ showLeafNodeIcon() {
+ return !this.showChevron() && !this.isInPinnedSection;
+ }
+
+ expandTree(event: MouseEvent) {
+ event.stopPropagation();
+ this.expandTreeChange.emit();
+ }
+
+ pinNode(event: MouseEvent) {
+ event.stopPropagation();
+ this.pinNodeChange.emit(this.item);
+ }
+}
diff --git a/tools/winscope-ng/src/viewers/styles/node.styles.ts b/tools/winscope-ng/src/viewers/styles/node.styles.ts
new file mode 100644
index 000000000..303ebb167
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/styles/node.styles.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const nodeStyles = `
+ .node {position: relative;display: inline-block;padding: 2px; height: 100%; width: 100%;}
+ .node.clickable {cursor: pointer;}
+ .node:not(.selected).added,
+ .node:not(.selected).addedMove,
+ .expand-tree-btn.added,
+ .expand-tree-btn.addedMove {
+ background: #03ff35;
+ }
+
+ .node:not(.selected).deleted,
+ .node:not(.selected).deletedMove,
+ .expand-tree-btn.deleted,
+ .expand-tree-btn.deletedMove {
+ background: #ff6b6b;
+ }
+
+ .node:hover:not(.selected) {background: #f1f1f1;}
+
+ .node:not(.selected).modified,
+ .expand-tree-btn.modified {
+ background: cyan;
+ }
+
+ .node.addedMove:after,
+ .node.deletedMove:after {
+ content: 'moved';
+ margin: 0 5px;
+ background: #448aff;
+ border-radius: 5px;
+ padding: 3px;
+ color: white;
+ }
+
+ .selected {background-color: #365179;color: white;}
+`;
+
+export const treeNodeStyles = `
+ .node.shaded:not(:hover):not(.selected):not(.added):not(.addedMove):not(.deleted):not(.deletedMove):not(.modified) {background: #f8f9fa}
+ .node.selected + .children {border-left: 1px solid rgb(200, 200, 200);}
+ .node.child-hover + .children {border-left: 1px solid #b4b4b4;}
+ .node.hover + .children { border-left: 1px solid rgb(200, 200, 200);}
+`;
+
+export const nodeInnerItemStyles = `
+ .leaf-node-icon {content: ''; display: inline-block; margin-left: 40%; margin-top: 40%; height: 5px; width: 5px; border-radius: 50%;background-color: #9b9b9b;}
+ .leaf-node-icon-wrapper, .description, #toggle-tree-btn, #expand-tree-btn, #pin-node-btn { position: relative; display: inline-block;}
+ mat-icon {margin: 0}
+ #pin-node-btn {padding: 0; transform: scale(0.7)}
+ .description {position: relative; align-items: center; flex: 1 1 auto; vertical-align: middle; word-break: break-all;}
+ .leaf-node-icon-wrapper{padding-left: 6px; padding-right: 6px; min-height: 24px; width: 24px; position:relative; align-content: center; vertical-align: middle;}
+ .icon-button { background: none;border: none;display: inline-block;vertical-align: middle;}
+`;
\ No newline at end of file
diff --git a/tools/winscope-ng/src/viewers/styles/tree_element.styles.ts b/tools/winscope-ng/src/viewers/styles/tree_element.styles.ts
new file mode 100644
index 000000000..8b1cc61f8
--- /dev/null
+++ b/tools/winscope-ng/src/viewers/styles/tree_element.styles.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const treeElementStyles = `
+ .kind {font-weight: bold}
+
+ span {overflow-wrap: break-word; flex: 1 1 auto; width: 0; word-break: break-all}
+
+ .tree-view-internal-chip {
+ display: inline-block;
+ }
+
+ .tree-view-chip {
+ padding: 0 10px;
+ border-radius: 10px;
+ background-color: #aaa;
+ color: black;
+ }
+
+ .tree-view-chip.tree-view-chip-warn {
+ background-color: #ffaa6b;
+ color: black;
+ }
+
+ .tree-view-chip.tree-view-chip-error {
+ background-color: #ff6b6b;
+ color: black;
+ }
+
+ .tree-view-chip.tree-view-chip-gpu {
+ background-color: #00c853;
+ color: black;
+ }
+
+ .tree-view-chip.tree-view-chip-hwc {
+ background-color: #448aff;
+ color: black;
+ }
+`;
diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts
index ca678e929..d4ccd4bbc 100644
--- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts
+++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/presenter.ts
@@ -15,36 +15,69 @@
*/
import { Rectangle, RectMatrix, RectTransform, UiData } from "viewers/viewer_surface_flinger/ui_data";
import { TraceType } from "common/trace/trace_type";
+import { UserOptions } from "viewers/common/user_options";
+import { TreeGenerator, getFilter, FilterType, Tree } from "viewers/common/tree_utils";
type NotifyViewCallbackType = (uiData: UiData) => void;
class Presenter {
constructor(notifyViewCallback: NotifyViewCallbackType) {
this.notifyViewCallback = notifyViewCallback;
- this.uiData = new UiData("Initial UI data");
+ this.uiData = new UiData();
this.notifyViewCallback(this.uiData);
}
- updateHighlightedRect(event: CustomEvent) {
- this.highlighted = event.detail.layerId;
- this.uiData.highlighted = this.highlighted;
- console.log("changed highlighted rect: ", this.uiData.highlighted);
+ public updatePinnedItems(event: CustomEvent) {
+ const pinnedItem = event.detail.pinnedItem;
+ const pinnedId = `${pinnedItem.id}`;
+ if (this.pinnedItems.map(item => `${item.id}`).includes(pinnedId)) {
+ this.pinnedItems = this.pinnedItems.filter(pinned => `${pinned.id}` != pinnedId);
+ } else {
+ this.pinnedItems.push(pinnedItem);
+ }
+ this.updatePinnedIds(pinnedId);
+ this.uiData.pinnedItems = this.pinnedItems;
this.notifyViewCallback(this.uiData);
}
- notifyCurrentTraceEntries(entries: Map) {
- const entry = entries.get(TraceType.SURFACE_FLINGER);
- this.uiData = new UiData("New surface flinger ui data");
+ public updateHighlightedItems(event: CustomEvent) {
+ const id = `${event.detail.id}`;
+ if (this.highlightedItems.includes(id)) {
+ this.highlightedItems = this.highlightedItems.filter(hl => hl != id);
+ } else {
+ this.highlightedItems = []; //if multi-select implemented, remove this line
+ this.highlightedItems.push(id);
+ }
+ this.uiData.highlightedItems = this.highlightedItems;
+ this.notifyViewCallback(this.uiData);
+ }
+
+ public updateHierarchyTree(event: CustomEvent) {
+ this.hierarchyUserOptions = event.detail.userOptions;
+ this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+ this.uiData.tree = this.generateTree();
+ this.notifyViewCallback(this.uiData);
+ }
+
+ public filterHierarchyTree(event: CustomEvent) {
+ this.hierarchyFilter = getFilter(event.detail.filterString);
+ this.uiData.tree = this.generateTree();
+ this.notifyViewCallback(this.uiData);
+ }
+
+ public notifyCurrentTraceEntries(entries: Map) {
+ this.uiData = new UiData();
+ const entry = entries.get(TraceType.SURFACE_FLINGER)[0];
+ this.uiData.rects = [];
const displayRects = entry.displays.map((display: any) => {
const rect = display.layerStackSpace;
rect.label = display.name;
rect.id = display.id;
rect.displayId = display.layerStackId;
rect.isDisplay = true;
- rect.isVirtual = display.isVirtual;
+ rect.isVirtual = display.isVirtual ?? false;
return rect;
}) ?? [];
- this.uiData.highlighted = this.highlighted;
this.displayIds = [];
const rects = entry.visibleLayers
@@ -59,10 +92,36 @@ class Presenter {
});
this.uiData.rects = this.rectsToUiData(rects.concat(displayRects));
this.uiData.displayIds = this.displayIds;
+ this.uiData.highlightedItems = this.highlightedItems;
+ this.uiData.rects = this.rectsToUiData(entry.rects.concat(displayRects));
+ this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+ this.previousEntry = entries.get(TraceType.SURFACE_FLINGER)[1];
+ this.entry = entry;
+
+ this.uiData.tree = this.generateTree();
this.notifyViewCallback(this.uiData);
}
- rectsToUiData(rects: any[]): Rectangle[] {
+ private generateTree() {
+ if (!this.entry) {
+ return null;
+ }
+ const generator = new TreeGenerator(this.entry, this.hierarchyUserOptions, this.hierarchyFilter, this.pinnedIds)
+ .withUniqueNodeId();
+ let tree: Tree;
+ if (!this.hierarchyUserOptions["showDiff"]?.enabled) {
+ tree = generator.generateTree();
+ } else {
+ tree = generator.compareWith(this.previousEntry)
+ .withModifiedCheck()
+ .generateFinalDiffTree();
+ }
+ this.pinnedItems = generator.getPinnedItems();
+ this.uiData.pinnedItems = this.pinnedItems;
+ return tree;
+ }
+
+ private rectsToUiData(rects: any[]): Rectangle[] {
const uiRects: Rectangle[] = [];
rects.forEach((rect: any) => {
let t = null;
@@ -86,14 +145,6 @@ class Presenter {
};
}
- let isVisible = false, isDisplay = false;
- if (rect.ref && rect.ref.isVisible) {
- isVisible = rect.ref.isVisible;
- }
- if (rect.isDisplay) {
- isDisplay = rect.isDisplay;
- }
-
const newRect: Rectangle = {
topLeft: {x: rect.left, y: rect.top},
bottomRight: {x: rect.right, y: -rect.bottom},
@@ -101,22 +152,53 @@ class Presenter {
width: rect.width,
label: rect.label,
transform: transform,
- isVisible: isVisible,
- isDisplay: isDisplay,
+ isVisible: rect.ref?.isVisible ?? false,
+ isDisplay: rect.isDisplay ?? false,
ref: rect.ref,
id: rect.id ?? rect.ref.id,
displayId: rect.displayId ?? rect.ref.stackId,
- isVirtual: rect.isVirtual
+ isVirtual: rect.isVirtual ?? false
};
uiRects.push(newRect);
});
return uiRects;
}
+ private updatePinnedIds(newId: string) {
+ if (this.pinnedIds.includes(newId)) {
+ this.pinnedIds = this.pinnedIds.filter(pinned => pinned != newId);
+ } else {
+ this.pinnedIds.push(newId);
+ }
+ }
+
private readonly notifyViewCallback: NotifyViewCallbackType;
private uiData: UiData;
- private highlighted = "";
private displayIds: Array = [];
+ private hierarchyFilter: FilterType = getFilter("");
+ private highlightedItems: Array = [];
+ private pinnedItems: Array = [];
+ private pinnedIds: Array = [];
+ private previousEntry: any = null;
+ private entry: any = null;
+ private hierarchyUserOptions: UserOptions = {
+ showDiff: {
+ name: "Show diff",
+ enabled: false
+ },
+ simplifyNames: {
+ name: "Simplify names",
+ enabled: true
+ },
+ onlyVisible: {
+ name: "Only visible",
+ enabled: false
+ },
+ flat: {
+ name: "Flat",
+ enabled: false
+ }
+ };
}
export {Presenter};
diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts
index de90e5c42..b318c7f69 100644
--- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts
+++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/ui_data.ts
@@ -13,13 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-class UiData {
- constructor(public text: string) {
- console.log(text);
- }
+import { TraceType } from "common/trace/trace_type";
+import { Tree } from "viewers/common/tree_utils";
+import { UserOptions } from "viewers/common/user_options";
+
+export class UiData {
+ dependencies: Array = [TraceType.SURFACE_FLINGER];
rects?: Rectangle[] = [];
- highlighted?: string = "";
displayIds?: number[] = [];
+ highlightedItems?: Array = [];
+ pinnedItems?: Array = [];
+ hierarchyUserOptions?: UserOptions = {};
+ tree?: Tree | null = null;
}
export interface Rectangle {
@@ -34,7 +39,7 @@ export interface Rectangle {
ref: any;
id: number;
displayId: number;
- isVirtual?: boolean;
+ isVirtual: boolean;
}
export interface Point {
@@ -60,5 +65,3 @@ export interface RectMatrix {
tx: number;
ty: number;
}
-
-export {UiData};
diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts
index f4aa30284..9d13e6fcc 100644
--- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts
+++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.spec.ts
@@ -16,13 +16,13 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ViewerSurfaceFlingerComponent} from "./viewer_surface_flinger.component";
-import { HierarchyComponent } from "viewers/hierarchy.component";
-import { PropertiesComponent } from "viewers/properties.component";
-import { RectsComponent } from "viewers/rects.component";
+import { HierarchyComponent } from "viewers/components/hierarchy.component";
+import { PropertiesComponent } from "viewers/components/properties.component";
+import { RectsComponent } from "viewers/components/rects/rects.component";
import { MatIconModule } from "@angular/material/icon";
import { MatCardModule } from "@angular/material/card";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
-import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
+import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core";
describe("ViewerSurfaceFlingerComponent", () => {
let fixture: ComponentFixture;
@@ -44,7 +44,7 @@ describe("ViewerSurfaceFlingerComponent", () => {
PropertiesComponent,
RectsComponent
],
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
}).compileComponents();
});
diff --git a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts
index 644ae03c2..1b945be1e 100644
--- a/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts
+++ b/tools/winscope-ng/src/viewers/viewer_surface_flinger/viewer_surface_flinger.component.ts
@@ -20,6 +20,7 @@ import {
import { UiData } from "./ui_data";
import { TRACE_INFO } from "app/trace_info";
import { TraceType } from "common/trace/trace_type";
+import { PersistentStore } from "common/persistent_store";
@Component({
selector: "viewer-surface-flinger",
@@ -29,33 +30,86 @@ import { TraceType } from "common/trace/trace_type";
-
-
-
-
-
-
+