(Properties View) Create viewer for SF traces.

Created the viewer component for SF traces, including rects view,
properties view, hierarchy view (for re-use in other apps). This CL
contains properties view and changes to associated reusable components.
Currently follows same design as go/winscope-beta.

Bug: b/238089034 b/232081297
Test: npm run test:all. upload/run any SF trace to see what it looks
like.

Change-Id: I6f9f0b1002ab9901f1aee113cbb0fef4cbd078a6
This commit is contained in:
Priyanka Patel
2022-08-22 17:08:22 +00:00
parent f392dc6d0d
commit 363fadfae3
34 changed files with 2093 additions and 578 deletions

View File

@@ -35,7 +35,10 @@ import { TraceViewHeaderComponent } from "./components/trace_view_header.compone
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";
import { TreeNodeDataViewComponent } from "viewers/components/tree_node_data_view.component";
import { TreeNodePropertiesDataViewComponent } from "viewers/components/tree_node_properties_data_view.component";
import { PropertyGroupsComponent } from "viewers/components/property_groups.component";
import { TransformMatrixComponent } from "viewers/components/transform_matrix.component";
@NgModule({
declarations: [
@@ -54,7 +57,10 @@ import { TreeElementComponent } from "viewers/components/tree_element.component"
TraceViewComponent,
TreeComponent,
TreeNodeComponent,
TreeElementComponent
TreeNodeDataViewComponent,
TreeNodePropertiesDataViewComponent,
PropertyGroupsComponent,
TransformMatrixComponent
],
imports: [
BrowserModule,

View File

@@ -49,7 +49,7 @@ import { LoadedTrace } from "app/loaded_trace";
<mat-icon class= "listed-file-item">{{TRACE_INFO[trace.type].icon}}</mat-icon>
<span class="listed-file-item">{{trace.name}} ({{TRACE_INFO[trace.type].name}})</span>
<button
(click)="onRemoveTrace(trace)"
(click)="onRemoveTrace($event, trace)"
class="icon-button close-btn listed-file-item"
><mat-icon>close</mat-icon>
</button>
@@ -180,7 +180,9 @@ export class UploadTracesComponent {
await this.processFiles(Array.from(droppedFiles));
}
public onRemoveTrace(trace: LoadedTrace) {
public onRemoveTrace(event: MouseEvent, trace: LoadedTrace) {
event.preventDefault();
event.stopPropagation();
this.traceCoordinator.removeTrace(trace.type);
this.loadedTraces = this.loadedTraces.filter(loaded => loaded.type !== trace.type);
}

View File

@@ -21,6 +21,7 @@
--default-border: #DADCE0;
--default-blue: #1A73E8;
}
#app-title {
font-family: 'Google Sans', sans-serif;
font-size: 30;
@@ -31,6 +32,10 @@
font-size: 18px;
}
.labels-canvas div {
font-family: 'Google Sans', sans-serif;
}
h1, p, span {
font-family: 'Google Sans Text', sans-serif;
font-weight: 400;

View File

@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { RELATIVE_Z_CHIP } from "viewers/common/chip";
import { getFilter, TreeGenerator } from "viewers/common/tree_utils";
import { DiffType, getFilter } from "viewers/common/tree_utils";
import { TreeGenerator } from "viewers/common/tree_generator";
describe("TreeGenerator", () => {
it("generates tree", () => {
@@ -71,9 +72,8 @@ describe("TreeGenerator", () => {
showInFilteredView: true,
};
const userOptions = {};
const filter = getFilter("");
const generator = new TreeGenerator(tree, userOptions, filter);
const generator = new TreeGenerator(tree, filter);
expect(generator.generateTree()).toEqual(expected);
});
@@ -119,27 +119,26 @@ describe("TreeGenerator", () => {
showInFilteredView: true,
stableId: "2",
shortName: undefined,
diff: "none",
diffType: DiffType.NONE,
chips: [ RELATIVE_Z_CHIP ]
}],
kind: "3",
shortName: undefined,
simplifyNames: false,
showInFilteredView: true,
diff: "none",
chips: [ RELATIVE_Z_CHIP ]
chips: [ RELATIVE_Z_CHIP ],
diffType: DiffType.NONE
}
],
kind: "entry",
shortName: "BLTE",
chips: [],
diffType: DiffType.NONE,
showInFilteredView: true,
diff: "none"
};
const userOptions = {};
const filter = getFilter("");
const generator = new TreeGenerator(tree, userOptions, filter);
const generator = new TreeGenerator(tree, filter);
expect(generator.withUniqueNodeId((node: any) => {
if (node) return node.stableId;
else return null;
@@ -201,48 +200,47 @@ describe("TreeGenerator", () => {
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,
diffType: DiffType.ADDED_MOVE,
chips: [ RELATIVE_Z_CHIP ]
}],
kind: "3",
shortName: undefined,
simplifyNames: false,
showInFilteredView: true,
chips: [ RELATIVE_Z_CHIP ]
chips: [ RELATIVE_Z_CHIP ],
diffType: DiffType.NONE
},
{
kind: "2",
id: "2",
name: "Child2",
diff: "deletedMove",
children: [],
simplifyNames: false,
showInFilteredView: true,
stableId: "2",
shortName: undefined,
chips: [ RELATIVE_Z_CHIP ]
chips: [ RELATIVE_Z_CHIP ],
diffType: DiffType.DELETED_MOVE
}
],
kind: "entry",
shortName: "BLTE",
chips: [],
showInFilteredView: true,
diff: "none"
diffType: DiffType.NONE,
showInFilteredView: true
};
const userOptions = {};
const filter = getFilter("");
const generator = new TreeGenerator(tree, userOptions, filter);
const generator = new TreeGenerator(tree, filter);
const newDiffTree = generator.withUniqueNodeId((node: any) => {
if (node) return node.stableId;
else return null;

View File

@@ -0,0 +1,432 @@
/*
* 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 {
FilterType,
Tree,
DiffType
} from "./tree_utils";
import ObjectFormatter from "common/trace/flickerlib/ObjectFormatter";
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 HwcCompositionType = {
CLIENT: 1,
DEVICE: 2,
SOLID_COLOR: 3,
};
export class TreeGenerator {
private isOnlyVisibleView = false;
private isSimplifyNames = false;
private isFlatView = false;
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, filter: FilterType, pinnedIds?: Array<string>) {
this.tree = tree;
this.filter = filter;
this.pinnedIds = pinnedIds ?? [];
}
public setIsOnlyVisibleView(enabled: boolean) {
this.isOnlyVisibleView = enabled;
return this;
}
public setIsSimplifyNames(enabled: boolean) {
this.isSimplifyNames = enabled;
return this;
}
public setIsFlatView(enabled: boolean) {
this.isFlatView = enabled;
return this;
}
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 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, true),
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, postDiff = false): 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 (postDiff && "diffType" in node) {
clone.diffType = node.diffType;
}
}
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.diffType = 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.diffType = DiffType.ADDED_MOVE;
// Switch out oldTree for new one to compare against
nextOldTree = this.oldMapping[newId];
} else {
diffTree.diffType = 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.diffType = DiffType.DELETED_MOVE;
// Stop comparing against oldTree, will be/has been
// visited when object is seen in newTree
nextOldTree = null;
} else {
deletedTreeDiff.diffType = 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.diffType = 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.diffType = DiffType.DELETED_MOVE;
} else {
diffTree.diffType = 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);
}
}

View File

@@ -0,0 +1,383 @@
/*
* 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 ObjectFormatter from "common/trace/flickerlib/ObjectFormatter";
import {
FilterType,
PropertiesTree,
Tree,
DiffType,
Terminal
} from "./tree_utils";
interface TransformOptions {
freeze: boolean;
keepOriginal: boolean;
metadataKey: string | null;
}
interface TreeTransformerOptions {
skip?: any;
formatter?: any;
}
interface TransformedPropertiesObject {
properties: any;
diffType?: string;
}
export class TreeTransformer {
private stableId: string;
private rootName: string;
private isShowDefaults = false;
private isShowDiff = false;
private filter: FilterType;
private properties: PropertiesTree;
private compareWithProperties: PropertiesTree | null = null;
private options?: TreeTransformerOptions;
private transformOptions: TransformOptions = {
keepOriginal: false, freeze: true, metadataKey: null,
};
constructor(tree: Tree, filter: FilterType) {
this.stableId = this.compatibleStableId(tree);
this.rootName = tree.name;
this.filter = filter;
this.setProperties(tree);
}
public setIsShowDefaults(enabled: boolean) {
this.isShowDefaults = enabled;
return this;
}
public setIsShowDiff(enabled: boolean) {
this.isShowDiff = enabled;
return this;
}
public setTransformerOptions(options: TreeTransformerOptions) {
this.options = options;
if (!this.options.formatter) {
this.options.formatter = this.formatProto;
}
return this;
}
public setProperties(tree: Tree) {
const target = tree.obj ?? tree;
ObjectFormatter.displayDefaults = this.isShowDefaults;
this.properties = this.getPropertiesForDisplay(target);
}
public setDiffProperties(previousEntry: any) {
if (this.isShowDiff) {
const tree = this.findTree(previousEntry, this.stableId);
const target = tree ? tree.obj ?? tree : null;
this.compareWithProperties = this.getPropertiesForDisplay(target);
}
return this;
}
public getOriginalLayer(entry: any, stableId: string) {
return this.findTree(entry, stableId);
}
private getPropertiesForDisplay(entry: any): any {
if (!entry) {
return;
}
let obj: any = {};
obj.proto = Object.assign({}, entry.proto);
if (obj.proto.children) delete obj.proto.children;
if (obj.proto.childWindows) delete obj.proto.childWindows;
if (obj.proto.childrenWindows) delete obj.proto.childrenWindows;
if (obj.proto.childContainers) delete obj.proto.childContainers;
if (obj.proto.windowToken) delete obj.proto.windowToken;
if (obj.proto.rootDisplayArea) delete obj.proto.rootDisplayArea;
if (obj.proto.rootWindowContainer) delete obj.proto.rootWindowContainer;
if (obj.proto.windowContainer?.children) delete obj.proto.windowContainer.children;
obj = ObjectFormatter.format(obj);
Object.keys(obj.proto).forEach((prop: string) => {
if (Object.keys(obj.proto[prop]).length === 0) {
obj.proto[prop] = "empty";
}
});
return obj;
}
private findTree(tree: any, stableId: string) {
if (!tree) {
return null;
}
if (tree.stableId && tree.stableId === stableId) {
return tree;
}
if (!tree.children) {
return null;
}
for (const child of tree.children) {
const foundEntry: any = this.findTree(child, stableId);
if (foundEntry) {
return foundEntry;
}
}
return null;
}
public transform() {
const {formatter} = this.options!;
if (!formatter) {
throw new Error("Missing formatter, please set with setOptions()");
}
const transformedTree = this.transformTree(this.properties, this.rootName,
this.compareWithProperties, this.rootName,
this.stableId, this.transformOptions);
return transformedTree;
}
private transformTree(
properties: PropertiesTree | Terminal,
name: string | Terminal,
compareWithProperties: PropertiesTree | Terminal,
compareWithName: string | Terminal,
stableId: string,
transformOptions: TransformOptions,
) {
const originalProperties = properties;
const metadata = this.getMetadata(
originalProperties, transformOptions.metadataKey
);
const children: any[] = [];
if (!this.isTerminal(properties)) {
const transformedProperties = this.transformProperties(properties, transformOptions.metadataKey);
properties = transformedProperties.properties;
}
if (!this.isTerminal(compareWithProperties)) {
const transformedProperties = this.transformProperties(
compareWithProperties,
transformOptions.metadataKey
);
compareWithProperties = transformedProperties.properties;
}
for (const key in properties) {
if (properties[key]) {
let compareWithChild = new Terminal();
let compareWithChildName = new Terminal();
if (compareWithProperties[key]) {
compareWithChild = compareWithProperties[key];
compareWithChildName = key;
}
const child = this.transformTree(properties[key], key,
compareWithChild, compareWithChildName,
`${stableId}.${key}`, transformOptions);
children.push(child);
}
}
// Takes care of adding deleted items to final tree
for (const key in compareWithProperties) {
if (!properties[key] && compareWithProperties[key]) {
const child = this.transformTree(new Terminal(), new Terminal(),
compareWithProperties[key], key,
`${stableId}.${key}`, transformOptions);
children.push(child);
}
}
let transformedProperties: any;
if (
children.length == 1 &&
children[0].children?.length == 0 &&
!children[0].combined
) {
// Merge leaf key value pairs.
const child = children[0];
transformedProperties = {
kind: "",
name: (this.isTerminal(name) ? compareWithName : name) + ": " + child.name,
stableId,
children: child.children,
combined: true,
};
if (this.isShowDiff) {
transformedProperties.diffType = child.diffType;
}
} else {
transformedProperties = {
kind: "",
name,
stableId,
children,
};
if (this.isShowDiff) {
const diffType = this.getDiff(name, compareWithName);
transformedProperties.diffType = diffType;
if (diffType == DiffType.DELETED) {
transformedProperties.name = compareWithName;
}
}
}
if (transformOptions.keepOriginal) {
transformedProperties.properties = originalProperties;
}
if (metadata && transformOptions.metadataKey) {
transformedProperties[transformOptions.metadataKey] = metadata;
}
if (!this.isTerminal(transformedProperties.name)) {
transformedProperties.propertyKey = this.getPropertyKey(transformedProperties);
transformedProperties.propertyValue = this.getPropertyValue(transformedProperties);
}
if (!this.filterMatches(transformedProperties) &&
!this.hasChildMatchingFilter(transformedProperties?.children)) {
transformedProperties.propertyKey = new Terminal();
}
return transformOptions.freeze ? Object.freeze(transformedProperties) : transformedProperties;
}
private hasChildMatchingFilter(children: PropertiesTree[] | null | undefined) {
if (!children || children.length === 0) return false;
let match = false;
for (let i=0; i<children.length; i++) {
if (this.filterMatches(children[i]) || this.hasChildMatchingFilter(children[i].children)) {
match = true;
}
}
return match;
}
private getMetadata(obj: PropertiesTree, metadataKey: string | null) {
if (metadataKey && obj[metadataKey]) {
const metadata = obj[metadataKey];
obj[metadataKey] = undefined;
return metadata;
} else {
return null;
}
}
private getPropertyKey(item: PropertiesTree) {
if (!item.children || item.children.length === 0) {
return item.name.split(": ")[0];
}
return item.name;
}
private getPropertyValue(item: PropertiesTree) {
if (!item.children || item.children.length === 0) {
return item.name.split(": ").slice(1).join(": ");
}
return null;
}
private filterMatches(item: PropertiesTree | null): boolean {
return this.filter(item) ?? false;
}
private transformProperties(properties: PropertiesTree, metadataKey: string | null) {
const {skip, formatter} = this.options!;
const transformedProperties: TransformedPropertiesObject = {
properties: {},
};
let formatted = undefined;
if (skip && skip.includes(properties)) {
// skip
} else if ((formatted = formatter(properties))) {
// Obj has been formatted into a terminal node — has no children.
transformedProperties.properties[formatted] = new Terminal();
} else if (Array.isArray(properties)) {
properties.forEach((e, i) => {
transformedProperties.properties["" + i] = e;
});
} else if (typeof properties == "string") {
// Object is a primitive type — has no children. Set to terminal
// to differentiate between null object and Terminal element.
transformedProperties.properties[properties] = new Terminal();
} else if (typeof properties == "number" || typeof properties == "boolean") {
// Similar to above — primitive type node has no children.
transformedProperties.properties["" + properties] = new Terminal();
} else if (properties && typeof properties == "object") {
Object.keys(properties).forEach((key) => {
if (key === metadataKey) {
return;
}
transformedProperties.properties[key] = properties[key];
});
} else if (properties === null) {
// Null object has no children — set to be terminal node.
transformedProperties.properties.null = new Terminal();
}
return transformedProperties;
}
private getDiff(val: string | Terminal, compareVal: string | Terminal) {
if (val && this.isTerminal(compareVal)) {
return DiffType.ADDED;
} else if (this.isTerminal(val) && compareVal) {
return DiffType.DELETED;
} else if (compareVal != val) {
return DiffType.MODIFIED;
} else {
return DiffType.NONE;
}
}
private compatibleStableId(item: Tree) {
// For backwards compatibility
// (the only item that doesn't have a unique stable ID in the tree)
if (item.stableId === "winToken|-|") {
return item.stableId + item.children[0].stableId;
}
return item.stableId;
}
private formatProto(item: any) {
if (item?.prettyPrint) {
return item.prettyPrint();
}
}
private isTerminal(item: any) {
return item instanceof Terminal;
}
}

View File

@@ -14,14 +14,13 @@
* 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 = {
export type FilterType = (item: Tree | null) => boolean;
export type Tree = Layer | BaseLayerTraceEntry;
export type PropertiesTree = any; //TODO: make specific
export type TreeSummary = Array<{key: string, value: string}>
export const DiffType = {
NONE: "none",
ADDED: "added",
DELETED: "deleted",
@@ -29,18 +28,12 @@ const DiffType = {
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 class Terminal {}
export function diffClass(item: Tree): string {
const diff = item!.diff;
return diff ?? "";
const diffType = item!.diffType;
return diffType ?? "";
}
export function isHighlighted(item: Tree, highlightedItems: Array<string>) {
@@ -61,7 +54,7 @@ export function getFilter(filterString: string): FilterType {
positive.push((s:any) => regex.test(s));
}
});
const filter = (item:Tree | null) => {
const filter = (item: any) => {
if (item) {
const apply = (f:any) => f(`${item.name}`);
return (positive.length === 0 || positive.some(apply)) &&
@@ -71,379 +64,3 @@ export function getFilter(filterString: string): FilterType {
};
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);
}
}

View File

@@ -16,6 +16,7 @@
export interface UserOptions {
[key: string]: {
name: string,
enabled: boolean
enabled: boolean,
tooltip?: string
}
}

View File

@@ -17,5 +17,8 @@ export const ViewerEvents = {
HierarchyPinnedChange: "HierarchyPinnedChange",
HighlightedChange: "HighlightedChange",
HierarchyUserOptionsChange: "HierarchyUserOptionsChange",
HierarchyFilterChange: "HierarchyFilterChange"
};
HierarchyFilterChange: "HierarchyFilterChange",
SelectedTreeChange: "SelectedTreeChange",
PropertiesUserOptionsChange: "PropertiesUserOptionsChange",
PropertiesFilterChange: "PropertiesFilterChange"
};

View File

@@ -17,7 +17,7 @@ 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 { nodeStyles } from "viewers/components/styles/node.styles";
import { ViewerEvents } from "viewers/common/viewer_events";
import { TraceType } from "common/trace/trace_type";
@@ -56,7 +56,7 @@ import { TraceType } from "common/trace/trace_type";
[isPinned]="true"
[isInPinnedSection]="true"
(pinNodeChange)="pinnedItemChange($event)"
(click)="onPinnedNodeClick($event, pinnedItem.id)"
(click)="onPinnedNodeClick($event, pinnedItem.id, pinnedItem)"
></tree-node>
</div>
</mat-card-header>
@@ -76,6 +76,7 @@ import { TraceType } from "common/trace/trace_type";
[pinnedItems]="pinnedItems"
(highlightedItemChange)="highlightedItemChange($event)"
(pinnedItemChange)="pinnedItemChange($event)"
(selectedTreeChange)="selectedTreeChange($event)"
></tree-view>
</div>
</mat-card-content>
@@ -169,12 +170,13 @@ export class HierarchyComponent {
};
}
onPinnedNodeClick(event: MouseEvent, pinnedItemId: string) {
onPinnedNodeClick(event: MouseEvent, pinnedItemId: string, pinnedItem: Tree) {
event.preventDefault();
if (window.getSelection()?.type === "range") {
return;
}
this.highlightedItemChange(`${pinnedItemId}`);
this.selectedTreeChange(pinnedItem);
}
updateTree() {
@@ -207,6 +209,16 @@ export class HierarchyComponent {
this.elementRef.nativeElement.dispatchEvent(event);
}
selectedTreeChange(item: Tree) {
const event: CustomEvent = new CustomEvent(
ViewerEvents.SelectedTreeChange,
{
bubbles: true,
detail: { selectedItem: item }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
pinnedItemChange(item: Tree) {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HierarchyPinnedChange,

View File

@@ -0,0 +1,88 @@
/*
* 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 { PropertiesComponent } from "./properties.component";
import { CommonModule } from "@angular/common";
import { NO_ERRORS_SCHEMA } from "@angular/core";
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";
import { PropertyGroupsComponent } from "./property_groups.component";
import { TreeComponent } from "./tree.component";
describe("PropertiesComponent", () => {
let fixture: ComponentFixture<PropertiesComponent>;
let component: PropertiesComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
PropertiesComponent,
PropertyGroupsComponent,
TreeComponent
],
imports: [
CommonModule,
MatInputModule,
MatFormFieldModule,
MatCheckboxModule,
BrowserAnimationsModule
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PropertiesComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
component.selectedTree = {};
component.userOptions = {
showDefaults: {
name: "Show defaults",
enabled: false
},
};
});
it("can be created", () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it("creates title", () => {
fixture.detectChanges();
const title = htmlElement.querySelector(".properties-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();
});
});

View File

@@ -13,14 +13,172 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component } from "@angular/core";
import { Component, Input, Inject, ElementRef } from "@angular/core";
import { UserOptions } from "viewers/common/user_options";
import { ViewerEvents } from "viewers/common/viewer_events";
import { PropertiesTree, TreeSummary, Terminal } from "viewers/common/tree_utils";
import { Layer } from "common/trace/flickerlib/common";
@Component({
selector: "properties-view",
template: `
<mat-card-title class="trace-view-subtitle">Properties</mat-card-title>
<mat-card-header class="view-header">
<mat-card-title class="title-filter">
<span class="properties-title">Properties</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()"
[matTooltip]="userOptions[option].tooltip ?? ''"
>{{userOptions[option].name}}</mat-checkbox>
</div>
<div *ngIf="objectKeys(selectedLayer).length > 0 && propertyGroups" class="element-summary">
<property-groups
[item]="selectedLayer"
[summary]="summary"
></property-groups>
</div>
</mat-card-header>
<mat-card-content class="properties-content" [style]="maxPropertiesHeight()">
<div class="tree-wrapper">
<tree-view
class="tree-view"
[item]="selectedTree"
[showNode]="showNode"
[isLeaf]="isLeaf"
*ngIf="objectKeys(selectedTree).length > 0"
[isPropertiesTree]="true"
[isAlwaysCollapsed]="true"
></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%;
}
.properties-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
}
.properties-content{
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x:hidden
}
.element-summary {
padding: 1rem;
border-bottom: thin solid rgba(0,0,0,.12);
}
.element-summary .key {
font-weight: 500;
}
.element-summary .value {
color: rgba(0, 0, 0, 0.75);
}
.tree-view {
white-space: pre-line;
flex: 1 0 0;
height: 100%;
overflow-y: auto
}
`,
],
})
export class PropertiesComponent {
objectKeys = Object.keys;
filterString = "";
@Input() userOptions: UserOptions = {};
@Input() selectedTree: PropertiesTree = {};
@Input() selectedLayer: Layer = {};
@Input() propertyGroups = false;
@Input() summary?: TreeSummary = [];
constructor(
@Inject(ElementRef) private elementRef: ElementRef,
) {}
maxPropertiesHeight() {
const headerHeight = this.elementRef.nativeElement.querySelector(".view-header").clientHeight;
return {
height: `${800 - headerHeight}px`
};
}
filterTree() {
const event: CustomEvent = new CustomEvent(
ViewerEvents.PropertiesFilterChange,
{
bubbles: true,
detail: { filterString: this.filterString }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
updateTree() {
const event: CustomEvent = new CustomEvent(
ViewerEvents.PropertiesUserOptionsChange,
{
bubbles: true,
detail: { userOptions: this.userOptions }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
showNode(item: any) {
return !(item instanceof Terminal)
&& !(item.name instanceof Terminal)
&& !(item.propertyKey instanceof Terminal);
}
isLeaf(item: any) {
return !item.children || item.children.length === 0
|| item.children.filter((c: any) => !(c instanceof Terminal)).length === 0;
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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 { PropertyGroupsComponent } from "./property_groups.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { TransformMatrixComponent } from "./transform_matrix.component";
describe("PropertyGroupsComponent", () => {
let fixture: ComponentFixture<PropertyGroupsComponent>;
let component: PropertyGroupsComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
PropertyGroupsComponent,
TransformMatrixComponent
],
schemas: []
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PropertyGroupsComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
});
it("can be created", () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,305 @@
/*
* 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 { TreeSummary } from "viewers/common/tree_utils";
import { Layer } from "common/trace/flickerlib/common";
@Component({
selector: "property-groups",
template: `
<div>
<div class="group">
<span class="group-header">Geometry</span>
<div class="left-column">
<div class="column-header">Calculated</div>
<span class="key">Transform:</span>
<transform-matrix [transform]="item.transform" [formatFloat]="formatFloat"></transform-matrix>
<div></div>
<span
class="key"
matTooltip="Raw value read from proto.bounds. This is the buffer size or
requested crop cropped by parent bounds."
>Crop:</span>
<span class="value">{{ item.bounds }}</span>
<div></div>
<span
class="key"
matTooltip="Raw value read from proto.screenBounds. This is the calculated crop
transformed."
>Final Bounds:</span>
<span class="value">{{ item.screenBounds }}</span>
</div>
<div class="right-column">
<div class="column-header">Requested</div>
<span class="key">Transform:</span>
<transform-matrix [transform]="item.requestedTransform" [formatFloat]="formatFloat"></transform-matrix>
<div></div>
<span class="key">Crop:</span>
<span class="value">{{ item.crop ? item.crop : "[empty]" }}</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Buffer</span>
</span>
<div *ngIf="item.isBufferLayer" class="left-column">
<div></div>
<span class="key">Size:</span>
<span class="value">{{ item.activeBuffer }}</span>
<div></div>
<span class="key">Frame Number:</span>
<span class="value">{{ item.currFrame }}</span>
<div></div>
<span
class="key"
matTooltip="Rotates or flips the buffer in place. Used with display transform
hint to cancel out any buffer transformation when sending to
HWC."
>Transform:</span>
<span class="value">{{ item.bufferTransform }}</span>
</div>
<div *ngIf="item.isBufferLayer" class="right-column">
<div></div>
<span
class="key"
matTooltip="Scales buffer to the frame by overriding the requested transform
for this layer."
>Destination Frame:</span>
<span class="value">{{ getDestinationFrame() }}</span>
<div></div>
<span
*ngIf="hasIgnoreDestinationFrame()"
class="value"
>Destination Frame ignored because layer has eIgnoreDestinationFrame
flag set.
</span>
</div>
<div *ngIf="item.isContainerLayer" class="left-column">
<span class="key"></span>
<span class="value">Container layer</span>
</div>
<div *ngIf="item.isEffectLayer" class="left-column">
<span class="key"></span>
<span class="value">Effect layer</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Hierarchy</span>
</span>
<div class="left-column">
<div></div>
<span class="key">z-order:</span>
<span class="value">{{ item.z }}</span>
<div></div>
<span
class="key"
matTooltip="Layer is z-ordered relative to its relative parents but its bounds
and other properties are inherited from its parents."
>relative parent:</span>
<span class="value">
{{ item.zOrderRelativeOfId == -1 ? "none" : item.zOrderRelativeOfId }}
</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Effects</span>
</span>
<div class="left-column">
<div class="column-header">Calculated</div>
<span class="key">Color:</span>
<span class="value">{{ item.color }}</span>
<div></div>
<span class="key">Shadow:</span>
<span class="value">{{ item.shadowRadius }} px</span>
<div></div>
<span class="key">Corner Radius:</span>
<span class="value">{{ formatFloat(item.cornerRadius) }} px</span>
<div></div>
<span
class="key"
matTooltip="Crop used to define the bounds of the corner radii. If the bounds
are greater than the layer bounds then the rounded corner will not
be visible."
>Corner Radius Crop:</span>
<span class="value">{{ item.cornerRadiusCrop }}</span>
<div></div>
<span class="key">Blur:</span>
<span class="value">
{{
item.proto?.backgroundBlurRadius
? item.proto?.backgroundBlurRadius
: 0
}} px
</span>
</div>
<div class="right-column">
<div class="column-header">Requested</div>
<span class="key">Color:</span>
<span class="value">{{ item.requestedColor }}</span>
<div></div>
<span class="key">Shadow:</span>
<span class="value">
{{
item.proto?.requestedShadowRadius
? item.proto?.requestedShadowRadius
: 0
}} px
</span>
<div></div>
<span class="key">Corner Radius:</span>
<span class="value">
{{
item.proto?.requestedCornerRadius
? formatFloat(item.proto?.requestedCornerRadius)
: 0
}} px
</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Input</span>
</span>
<div *ngIf="item.proto?.inputWindowInfo" class="left-column">
<span class="key">To Display Transform:</span>
<transform-matrix [transform]="item.inputTransform" [formatFloat]="formatFloat"></transform-matrix>
<div></div>
<span class="key">Touchable Region:</span>
<span class="value">{{ item.inputRegion }}</span>
</div>
<div *ngIf="item.proto?.inputWindowInfo" class="right-column">
<span class="key">Config:</span>
<span class="value"></span>
<div></div>
<span class="key">Focusable:</span>
<span class="value">{{ item.proto?.inputWindowInfo.focusable }}</span>
<div></div>
<span class="key">Crop touch region with layer:</span>
<span class="value">
{{
item.proto?.inputWindowInfo.cropLayerId &lt;= 0
? "none"
: item.proto?.inputWindowInfo.cropLayerId
}}
</span>
<div></div>
<span class="key">Replace touch region with crop:</span>
<span class="value">
{{
item.proto?.inputWindowInfo.replaceTouchableRegionWithCrop
}}
</span>
</div>
<div *ngIf="!item.proto?.inputWindowInfo" class="left-column">
<span class="key"></span>
<span class="value">No input channel set</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Visibility</span>
</span>
<div class="left-column">
<span class="key">Flags:</span>
<span class="value">{{ item.flags }}</span>
<div></div>
<div *ngIf="summary">
<div *ngFor="let reason of summary">
<span class="key">{{ reason.key }}:</span>
<span class="value">{{ reason.value }}</span>
</div>
</div>
</div>
</div>
</div>
`,
styles: [
`
.group {
padding: 0.5rem;
border-bottom: thin solid rgba(0, 0, 0, 0.12);
flex-direction: row;
display: flex;
}
.group .key {
font-weight: 500;
padding-right: 5px;
}
.group .value {
color: rgba(0, 0, 0, 0.75);
}
.group-header {
justify-content: center;
padding: 0px 5px;
width: 80px;
display: inline-block;
font-size: bigger;
color: grey;
}
.left-column {
width: 320px;
max-width: 100%;
display: inline-block;
vertical-align: top;
overflow: auto;
border-right: 5px solid rgba(#000, 0.12);
padding-right: 20px;
}
.right-column {
width: 320px;
max-width: 100%;
display: inline-block;
vertical-align: top;
overflow: auto;
border: 1px solid rgba(#000, 0.12);
}
.column-header {
font-weight: lighter;
font-size: smaller;
}
`
],
})
export class PropertyGroupsComponent {
@Input() item!: Layer;
@Input() summary?: TreeSummary | null = null;
getDestinationFrame() {
const frame = this.item.proto?.destinationFrame;
if (frame) {
return ` left: ${frame.left}, top: ${frame.top}, right: ${frame.right}, bottom: ${frame.bottom}`;
}
else return "";
}
hasIgnoreDestinationFrame() {
return (this.item.flags & 0x400) === 0x400;
}
formatFloat(num: number) {
return Math.round(num * 100) / 100;
}
}

View File

@@ -56,9 +56,9 @@ export class CanvasGraphics {
alpha: true
});
let labelRenderer: CSS2DRenderer;
if (document.querySelector("#labels-canvas")) {
if (document.querySelector(".labels-canvas")) {
labelRenderer = new CSS2DRenderer({
element: document.querySelector("#labels-canvas")! as HTMLElement
element: document.querySelector(".labels-canvas")! as HTMLElement
});
} else {
labelRenderer = new CSS2DRenderer();
@@ -66,7 +66,7 @@ export class CanvasGraphics {
labelRenderer.domElement.style.top = "0px";
labelRenderer.domElement.style.width = "100%";
labelRenderer.domElement.style.height = "40rem";
labelRenderer.domElement.id = "labels-canvas";
labelRenderer.domElement.className = "labels-canvas";
labelRenderer.domElement.style.pointerEvents = "none";
document.querySelector(".canvas-container")?.appendChild(labelRenderer.domElement);
}
@@ -141,6 +141,7 @@ export class CanvasGraphics {
lowestY: number
) {
this.targetObjects = [];
this.clearLabelElements();
this.rects.forEach(rect => {
const mustNotDrawInVisibleView = this.visibleView && !rect.isVisible;
const mustNotDrawInXrayViewWithoutVirtualDisplays = !this.visibleView && !this.showVirtualDisplays && rect.isDisplay && rect.isVirtual;
@@ -175,17 +176,18 @@ export class CanvasGraphics {
scene.add(edgeSegments);
}
// label circular marker
const circle = this.setCircleMaterial(planeRect, rect);
scene.add(circle);
// 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);
scene.add(line);
scene.add(rectLabel);
// labelling elements
if (rect.label.length > 0) {
const circle = this.setCircleMaterial(planeRect, rect);
scene.add(circle);
const [line, rectLabel] = this.createLabel(rect, circle, lowestY, rectCounter);
scene.add(line);
scene.add(rectLabel);
}
rectCounter++;
});
}
@@ -261,10 +263,8 @@ export class CanvasGraphics {
linePoints.push(endPos);
//add rectangle label
document.querySelector(`.label-${rectCounter}`)?.remove();
const rectLabelDiv: HTMLElement = document.createElement("div");
this.labelElements.push(rectLabelDiv);
rectLabelDiv.className = `label-${rectCounter}`;
rectLabelDiv.textContent = labelText;
rectLabelDiv.style.fontSize = "10px";
if (isGrey) {

View File

@@ -62,15 +62,15 @@ describe("RectsComponent", () => {
it("check that layer separation slider causes view to change", () => {
const slider = htmlElement.querySelector(".spacing-slider");
spyOn(component.rectsComponent.canvasGraphics, "updateLayerSeparation");
spyOn(component.rectsComponent, "updateLayerSeparation");
slider?.dispatchEvent(new MouseEvent("mousedown"));
fixture.detectChanges();
expect(component.rectsComponent.canvasGraphics.updateLayerSeparation).toHaveBeenCalled();
expect(component.rectsComponent.updateLayerSeparation).toHaveBeenCalled();
});
it("check that rects canvas is rendered", () => {
fixture.detectChanges();
const rectsCanvas = htmlElement.querySelector("#rects-canvas");
const rectsCanvas = htmlElement.querySelector(".rects-canvas");
expect(rectsCanvas).toBeTruthy();
});

View File

@@ -80,7 +80,7 @@ import { ViewerEvents } from "viewers/common/viewer_events";
</mat-card-header>
<mat-card-content class="rects-content">
<div class="canvas-container">
<canvas id="rects-canvas" (click)="onRectClick($event)">
<canvas class="rects-canvas" (click)="onRectClick($event)">
</canvas>
</div>
<div class="tabs" *ngIf="displayIds.length > 1">
@@ -92,8 +92,8 @@ import { ViewerEvents } from "viewers/common/viewer_events";
"@import 'https://fonts.googleapis.com/icon?family=Material+Icons';",
".rects-content {position: relative}",
".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}",
".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: 4rem; width: 100%;}",
".slider-view-controls {display: inline-block; position: relative; height: 3rem; width: 100%;}",
".slider {display: inline-block}",
@@ -131,10 +131,15 @@ export class RectsComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnInit() {
window.addEventListener('resize', () => this.refreshCanvas());
window.addEventListener("resize", () => this.refreshCanvas());
}
ngOnChanges(changes: SimpleChanges) {
if (changes["displayIds"]) {
if (!this.displayIds.includes(this.currentDisplayId)) {
this.currentDisplayId = this.displayIds[0];
}
}
if (changes["highlightedItems"]) {
this.canvasGraphics.updateHighlightedItems(this.highlightedItems);
}
@@ -157,7 +162,7 @@ export class RectsComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnDestroy() {
window.removeEventListener('resize', () => this.refreshCanvas());
window.removeEventListener("resize", () => this.refreshCanvas());
}
onRectClick(event:MouseEvent) {
@@ -193,7 +198,7 @@ export class RectsComponent implements OnInit, OnChanges, OnDestroy {
}
drawRects() {
const canvas = document.getElementById("rects-canvas") as HTMLCanvasElement;
const canvas = this.elementRef.nativeElement.querySelector(".rects-canvas") as HTMLCanvasElement;
this.canvasGraphics.initialise(canvas);
this.refreshCanvas();
}

View File

@@ -17,23 +17,18 @@ 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 {
.node:not(.selected).addedMove {
background: #03ff35;
}
.node:not(.selected).deleted,
.node:not(.selected).deletedMove,
.expand-tree-btn.deleted,
.expand-tree-btn.deletedMove {
.node:not(.selected).deletedMove {
background: #ff6b6b;
}
.node:hover:not(.selected) {background: #f1f1f1;}
.node:not(.selected).modified,
.expand-tree-btn.modified {
.node:not(.selected).modified {
background: cyan;
}
@@ -50,7 +45,7 @@ export const nodeStyles = `
.selected {background-color: #365179;color: white;}
`;
export const treeNodeStyles = `
export const treeNodeDataViewStyles = `
.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;}
@@ -65,4 +60,24 @@ export const nodeInnerItemStyles = `
.description {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;}
.expand-tree-btn {
float: right;
padding-left: 0px;
padding-right: 0px;
}
.expand-tree-btn.modified {
background: cyan;
}
.expand-tree-btn.deleted,
.expand-tree-btn.deletedMove {
background: #ff6b6b;
}
.expand-tree-btn.added,
.expand-tree-btn.addedMove {
background: #03ff35;
}
`;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const treeElementStyles = `
export const treeNodeDataViewStyles = `
.kind {font-weight: bold}
span {overflow-wrap: break-word; flex: 1 1 auto; width: 0; word-break: break-all}
@@ -49,3 +49,24 @@ export const treeElementStyles = `
color: black;
}
`;
export const treeNodePropertiesDataViewStyles = `
.key {
color: #4b4b4b;
}
.value {
color: #8A2BE2;
}
.value.null {
color: #e1e1e1;
}
.value.number {
color: #4c75fd;
}
.value.true {
color: #2ECC40;
}
.value.false {
color: #FF4136;
}
`;

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
import {ComponentFixture, TestBed} from "@angular/core/testing";
import { TreeElementComponent } from "./tree_element.component";
import { TransformMatrixComponent } from "./transform_matrix.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("TreeElementComponent", () => {
let fixture: ComponentFixture<TreeElementComponent>;
let component: TreeElementComponent;
describe("TransformMatrixComponent", () => {
let fixture: ComponentFixture<TransformMatrixComponent>;
let component: TransformMatrixComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
@@ -29,14 +29,14 @@ describe("TreeElementComponent", () => {
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeElementComponent
TransformMatrixComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeElementComponent);
fixture = TestBed.createComponent(TransformMatrixComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
});

View File

@@ -0,0 +1,59 @@
/*
* 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 { Transform } from "common/trace/flickerlib/common";
@Component({
selector: "transform-matrix",
template: `
<div class="matrix" *ngIf="transform" [matTooltip]="transform.getTypeAsString()">
<div class="cell">{{ formatFloat(transform.matrix.dsdx) }}</div>
<div class="cell">{{ formatFloat(transform.matrix.dsdy) }}</div>
<div class="cell" matTooltip="Translate x">
{{ formatFloat(transform.matrix.tx) }}
</div>
<div class="cell">{{ formatFloat(transform.matrix.dtdx) }}</div>
<div class="cell">{{ formatFloat(transform.matrix.dtdy) }}</div>
<div class="cell" matTooltip="Translate y">
{{ formatFloat(transform.matrix.ty) }}
</div>
<div class="cell">0</div>
<div class="cell">0</div>
<div class="cell">1</div>
</div>
`,
styles: [
`
.matrix {
display: grid;
grid-gap: 1px;
grid-template-columns: repeat(3, 1fr);
}
.cell {
padding-left: 10px;
background-color: #F8F9FA;
}
`
],
})
export class TransformMatrixComponent {
@Input() transform!: Transform;
@Input() formatFloat!: (num: number) => number;
}

View File

@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from "@angular/core/testing";
import { ComponentFixture, TestBed, ComponentFixtureAutoDetect } from "@angular/core/testing";
import { TreeComponent } from "./tree.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { Component, ViewChild, NO_ERRORS_SCHEMA } from "@angular/core";
import { PersistentStore } from "common/persistent_store";
describe("TreeComponent", () => {
let fixture: ComponentFixture<TreeComponent>;
let component: TreeComponent;
let fixture: ComponentFixture<TestHostComponent>;
let component: TestHostComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
@@ -29,28 +29,16 @@ describe("TreeComponent", () => {
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeComponent
TreeComponent, TestHostComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeComponent);
fixture = TestBed.createComponent(TestHostComponent);
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", () => {
@@ -58,9 +46,35 @@ describe("TreeComponent", () => {
expect(component).toBeTruthy();
});
it("creates node element", () => {
fixture.detectChanges();
const nodeElement = htmlElement.querySelector(".node");
expect(nodeElement).toBeTruthy();
});
@Component({
selector: "host-component",
template: `
<tree-view
[item]="item"
[store]="store"
[isFlattened]="isFlattened"
[diffClass]="diffClass"
[isHighlighted]="isHighlighted"
[hasChildren]="hasChildren"
></tree-view>
`
})
class TestHostComponent {
isFlattened = true;
item = {
simplifyNames: false,
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
chips: [],
children: [{kind: "3", id: "3", name: "Child1"}]
};
store = new PersistentStore();
diffClass = jasmine.createSpy().and.returnValue("none");
isHighlighted = jasmine.createSpy().and.returnValue(false);
hasChildren = jasmine.createSpy().and.returnValue(true);
@ViewChild(TreeComponent)
public treeComponent!: TreeComponent;
}
});

View File

@@ -15,9 +15,10 @@
*/
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 { nodeStyles, treeNodeDataViewStyles } from "viewers/components/styles/node.styles";
import { Tree, diffClass, isHighlighted, PropertiesTree, Terminal } from "viewers/common/tree_utils";
import { TraceType } from "common/trace/trace_type";
import { TreeNodePropertiesDataViewComponent } from "./tree_node_properties_data_view.component";
@Component({
selector: "tree-view",
@@ -25,41 +26,48 @@ import { TraceType } from "common/trace/trace_type";
<div class="tree-view">
<tree-node
class="node"
[class.leaf]="isLeaf()"
*ngIf="showNode(item)"
[class.leaf]="isLeaf(this.item)"
[class.selected]="isHighlighted(item, highlightedItems)"
[class.clickable]="isClickable()"
[class.shaded]="isShaded"
[class.hover]="nodeHover"
[class.childHover]="childHover"
[isAlwaysCollapsed]="isAlwaysCollapsed"
[class]="diffClass(item)"
[style]="nodeOffsetStyle()"
[item]="item"
[flattened]="isFlattened"
[isLeaf]="isLeaf()"
[isCollapsed]="isCollapsed()"
[isLeaf]="isLeaf(this.item)"
[isCollapsed]="isAlwaysCollapsed ?? isCollapsed()"
[isPropertiesTreeNode]="isPropertiesTree"
[hasChildren]="hasChildren()"
[isPinned]="isPinned()"
(toggleTreeChange)="toggleTree()"
(click)="onNodeClick($event)"
(expandTreeChange)="expandTree()"
(pinNodeChange)="sendNewPinnedItemToHierarchy($event)"
(pinNodeChange)="propagateNewPinnedItem($event)"
></tree-node>
<div class="children" *ngIf="hasChildren()" [hidden]="isCollapsed()" [style]="childrenIndentation()">
<div class="children" *ngIf="hasChildren()" [hidden]="!isCollapsed()" [style]="childrenIndentation()">
<ng-container *ngFor="let child of children()">
<tree-view
class="childrenTree"
[item]="child"
[store]="store"
[showNode]="showNode"
[isLeaf]="isLeaf"
[dependencies]="dependencies"
[isFlattened]="isFlattened"
[isPropertiesTree]="isPropertiesTree"
[isShaded]="!isShaded"
[useGlobalCollapsedState]="useGlobalCollapsedState"
[initialDepth]="initialDepth + 1"
[highlightedItems]="highlightedItems"
[pinnedItems]="pinnedItems"
(highlightedItemChange)="sendNewHighlightedItemToHierarchy($event)"
(pinnedItemChange)="sendNewPinnedItemToHierarchy($event)"
(highlightedItemChange)="propagateNewHighlightedItem($event)"
(pinnedItemChange)="propagateNewPinnedItem($event)"
(selectedTreeChange)="propagateNewSelectedTree($event)"
[itemsClickable]="itemsClickable"
(hoverStart)="childHover = true"
(hoverEnd)="childHover = false"
@@ -68,14 +76,14 @@ import { TraceType } from "common/trace/trace_type";
</div>
</div>
`,
styles: [nodeStyles, treeNodeStyles]
styles: [nodeStyles, treeNodeDataViewStyles]
})
export class TreeComponent {
diffClass = diffClass;
isHighlighted = isHighlighted;
@Input() item!: Tree;
@Input() item!: Tree | PropertiesTree | Terminal;
@Input() dependencies: Array<TraceType> = [];
@Input() store!: PersistentStore;
@Input() isFlattened? = false;
@@ -85,8 +93,13 @@ export class TreeComponent {
@Input() pinnedItems?: Array<Tree> = [];
@Input() itemsClickable?: boolean;
@Input() useGlobalCollapsedState?: boolean;
@Input() isPropertiesTree?: boolean;
@Input() isAlwaysCollapsed?: boolean;
@Input() showNode: (item?: any) => boolean = () => true;
@Input() isLeaf: (item: any) => boolean = (item: any) => !item.children || item.children.length === 0;
@Output() highlightedItemChange = new EventEmitter<string>();
@Output() selectedTreeChange = new EventEmitter<Tree>();
@Output() pinnedItemChange = new EventEmitter<Tree>();
@Output() hoverStart = new EventEmitter<void>();
@Output() hoverEnd = new EventEmitter<void>();
@@ -99,7 +112,7 @@ export class TreeComponent {
nodeElement: HTMLElement;
constructor(
@Inject(ElementRef) elementRef: ElementRef,
@Inject(ElementRef) public elementRef: ElementRef,
) {
this.nodeElement = elementRef.nativeElement.querySelector(".node");
this.nodeElement?.addEventListener("mousedown", this.nodeMouseDownEventListener);
@@ -107,6 +120,18 @@ export class TreeComponent {
this.nodeElement?.addEventListener("mouseleave", this.nodeMouseLeaveEventListener);
}
ngOnInit() {
if (this.isCollapsedByDefault) {
this.setCollapseValue(this.isCollapsedByDefault);
}
}
ngOnChanges() {
if (isHighlighted(this.item, this.highlightedItems)) {
this.selectedTreeChange.emit(this.item);
}
}
ngOnDestroy() {
this.nodeElement?.removeEventListener("mousedown", this.nodeMouseDownEventListener);
this.nodeElement?.removeEventListener("mouseenter", this.nodeMouseEnterEventListener);
@@ -119,7 +144,7 @@ export class TreeComponent {
return;
}
if (!this.isLeaf() && event.detail % 2 === 0) {
if (!this.isLeaf(this.item) && event.detail % 2 === 0) {
// Double click collapsable node
event.preventDefault();
this.toggleTree();
@@ -140,6 +165,8 @@ export class TreeComponent {
updateHighlightedItems() {
if (this.item && this.item.id) {
this.highlightedItemChange.emit(`${this.item.id}`);
} else if (!this.item.id) {
this.selectedTreeChange.emit(this.item);
}
}
@@ -150,20 +177,20 @@ export class TreeComponent {
return false;
}
sendNewHighlightedItemToHierarchy(newId: string) {
propagateNewHighlightedItem(newId: string) {
this.highlightedItemChange.emit(newId);
}
sendNewPinnedItemToHierarchy(newPinnedItem: Tree) {
propagateNewPinnedItem(newPinnedItem: Tree) {
this.pinnedItemChange.emit(newPinnedItem);
}
isLeaf() {
return !this.item.children || this.item.children.length === 0;
propagateNewSelectedTree(newTree: Tree) {
this.selectedTreeChange.emit(newTree);
}
isClickable() {
return !this.isLeaf() || this.itemsClickable;
return !this.isLeaf(this.item) || this.itemsClickable;
}
toggleTree() {
@@ -171,19 +198,18 @@ export class TreeComponent {
}
expandTree() {
this.setCollapseValue(false);
this.setCollapseValue(true);
}
isCollapsed() {
if (this.isLeaf()) {
return false;
if (this.isAlwaysCollapsed || this.isLeaf(this.item)) {
return true;
}
if (this.useGlobalCollapsedState) {
return this.store.getFromStore(`collapsedState.item.${this.dependencies}.${this.item.id}`)==="true"
?? this.isCollapsedByDefault;
}
return this.localCollapsedState;
}
@@ -193,10 +219,10 @@ export class TreeComponent {
hasChildren() {
const isParentEntryInFlatView = this.item.kind === "entry" && this.isFlattened;
return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf();
return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf(this.item);
}
setCollapseValue(isCollapsed:boolean) {
setCollapseValue(isCollapsed: boolean) {
if (this.useGlobalCollapsedState) {
this.store.addToStore(`collapsedState.item.${this.dependencies}.${this.item.id}`, `${isCollapsed}`);
} else {

View File

@@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from "@angular/core/testing";
import { ComponentFixture, TestBed, ComponentFixtureAutoDetect } from "@angular/core/testing";
import { TreeNodeComponent } from "./tree_node.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { Component, ViewChild, NO_ERRORS_SCHEMA } from "@angular/core";
describe("TreeNodeComponent", () => {
let fixture: ComponentFixture<TreeNodeComponent>;
let component: TreeNodeComponent;
let fixture: ComponentFixture<TestHostComponent>;
let component: TestHostComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
@@ -29,27 +28,16 @@ describe("TreeNodeComponent", () => {
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeNodeComponent
TreeNodeComponent, TestHostComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeNodeComponent);
fixture = TestBed.createComponent(TestHostComponent);
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", () => {
@@ -57,9 +45,29 @@ describe("TreeNodeComponent", () => {
expect(component).toBeTruthy();
});
it("creates tree element", () => {
fixture.detectChanges();
const treeElement = htmlElement.querySelector("tree-element");
expect(treeElement).toBeTruthy();
});
});
@Component({
selector: "host-component",
template: `
<tree-node
[item]="item"
[isCollapsed]="true"
[isPinned]="false"
[isInPinnedSection]="false"
[hasChildren]="false"
[isPropertiesTreeNode]="false"
></tree-node>
`
})
class TestHostComponent {
item = {
simplifyNames: false,
kind: "entry",
name: "BaseLayerTraceEntry",
shortName: "BLTE",
chips: [],
};
@ViewChild(TreeNodeComponent)
public treeNodeComponent!: TreeNodeComponent;
}
});

View File

@@ -14,8 +14,8 @@
* 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";
import { nodeInnerItemStyles } from "viewers/components/styles/node.styles";
import { PropertiesTree, Tree, DiffType } from "viewers/common/tree_utils";
@Component({
selector: "tree-node",
@@ -26,7 +26,7 @@ import { Tree } from "viewers/common/tree_utils";
*ngIf="showChevron()"
>
<mat-icon class="icon-button">
{{isCollapsed ? "chevron_right" : "arrow_drop_down"}}
{{isCollapsed ? "arrow_drop_down" : "chevron_right"}}
</mat-icon>
</button>
@@ -40,7 +40,7 @@ import { Tree } from "viewers/common/tree_utils";
<button
class="icon-button pin-node-btn"
(click)="pinNode($event)"
*ngIf="!isEntryNode()"
*ngIf="showPinNodeIcon()"
>
<mat-icon class="icon-button">
{{isPinned ? "star" : "star_border"}}
@@ -48,15 +48,21 @@ import { Tree } from "viewers/common/tree_utils";
</button>
<div class="description">
<tree-element
<tree-node-data-view
[item]="item"
></tree-element>
*ngIf="!isPropertiesTreeNode"
></tree-node-data-view>
<tree-node-properties-data-view
[item]="item"
*ngIf="isPropertiesTreeNode"
></tree-node-properties-data-view>
</div>
<button
*ngIf="hasChildren && isCollapsed"
*ngIf="hasChildren && !isCollapsed"
(click)="expandTree($event)"
class="icon-button expand-tree-btn"
[class]="collapseDiffClass"
>
<mat-icon
aria-hidden="true"
@@ -70,25 +76,35 @@ import { Tree } from "viewers/common/tree_utils";
})
export class TreeNodeComponent {
@Input() item!: Tree | null;
@Input() item!: Tree | PropertiesTree;
@Input() isLeaf?: boolean;
@Input() flattened?: boolean;
@Input() isCollapsed?: boolean;
@Input() hasChildren?: boolean = false;
@Input() isPinned?: boolean = false;
@Input() isInPinnedSection?: boolean = false;
@Input() isPropertiesTreeNode?: boolean;
@Input() isAlwaysCollapsed?: boolean;
@Output() toggleTreeChange = new EventEmitter<void>();
@Output() expandTreeChange = new EventEmitter<boolean>();
@Output() pinNodeChange = new EventEmitter<Tree>();
isEntryNode() {
return this.item.kind === "entry" ?? false;
collapseDiffClass = "";
ngOnChanges() {
this.collapseDiffClass = this.updateCollapseDiffClass();
}
showPinNodeIcon() {
return (!this.isPropertiesTreeNode && this.item.kind !== "entry") ?? false;
}
toggleTree(event: MouseEvent) {
event.stopPropagation();
this.toggleTreeChange.emit();
if (!this.isAlwaysCollapsed) {
event.stopPropagation();
this.toggleTreeChange.emit();
}
}
showChevron() {
@@ -108,4 +124,42 @@ export class TreeNodeComponent {
event.stopPropagation();
this.pinNodeChange.emit(this.item);
}
updateCollapseDiffClass() {
if (this.isCollapsed) {
return "";
}
const childrenDiffClasses = this.getAllDiffTypesOfChildren(this.item);
childrenDiffClasses.delete(DiffType.NONE);
childrenDiffClasses.delete(undefined);
if (childrenDiffClasses.size === 0) {
return "";
}
if (childrenDiffClasses.size === 1) {
const diffType = childrenDiffClasses.values().next().value;
return diffType;
}
return DiffType.MODIFIED;
}
getAllDiffTypesOfChildren(item: Tree | PropertiesTree) {
if (!item.children) {
return new Set();
}
const classes = new Set();
for (const child of item.children) {
if (child.diffType) {
classes.add(child.diffType);
}
for (const diffClass of this.getAllDiffTypesOfChildren(child)) {
classes.add(diffClass);
}
}
return classes;
}
}

View File

@@ -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 { TreeNodeDataViewComponent } from "./tree_node_data_view.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("TreeNodeDataViewComponent", () => {
let fixture: ComponentFixture<TreeNodeDataViewComponent>;
let component: TreeNodeDataViewComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeNodeDataViewComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeNodeDataViewComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
});
it("can be created", () => {
expect(component).toBeTruthy();
});
});

View File

@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { Component, Input } from "@angular/core";
import { treeElementStyles } from "viewers/styles/tree_element.styles";
import { treeNodeDataViewStyles } from "viewers/components/styles/tree_node_data_view.styles";
import { Tree } from "viewers/common/tree_utils";
import Chip from "viewers/common/chip";
@Component({
selector: "tree-element",
selector: "tree-node-data-view",
template: `
<span>
<span class="kind">{{item.kind}}</span>
@@ -33,10 +33,10 @@ import Chip from "viewers/common/chip";
>{{chip.short}}</div>
</span>
`,
styles: [ treeElementStyles ]
styles: [ treeNodeDataViewStyles ]
})
export class TreeElementComponent {
export class TreeNodeDataViewComponent {
@Input() item!: Tree;
showShortName() {

View File

@@ -0,0 +1,45 @@
/*
* 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 { TreeNodePropertiesDataViewComponent } from "./tree_node_properties_data_view.component";
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
describe("TreeNodePropertiesDataViewComponent", () => {
let fixture: ComponentFixture<TreeNodePropertiesDataViewComponent>;
let component: TreeNodePropertiesDataViewComponent;
let htmlElement: HTMLElement;
beforeAll(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
],
declarations: [
TreeNodePropertiesDataViewComponent
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TreeNodePropertiesDataViewComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
});
it("can be created", () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,57 @@
/*
* 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 { treeNodePropertiesDataViewStyles } from "viewers/components/styles/tree_node_data_view.styles";
import { PropertiesTree } from "viewers/common/tree_utils";
@Component({
selector: "tree-node-properties-data-view",
template: `
<span>
<span class="key">{{ item.propertyKey }}</span>
<span *ngIf="item.propertyValue">: </span>
<span class="value" *ngIf="item.propertyValue" [class]="[valueClass()]">{{ item.propertyValue }}</span>
</span>
`,
styles: [ treeNodePropertiesDataViewStyles ]
})
export class TreeNodePropertiesDataViewComponent {
@Input() item!: PropertiesTree;
valueClass() {
if (!this.item.propertyValue) {
return null;
}
if (this.item.propertyValue == "null") {
return "null";
}
if (this.item.propertyValue == "true") {
return "true";
}
if (this.item.propertyValue == "false") {
return "false";
}
if (!isNaN(this.item.propertyValue)) {
return "number";
}
return null;
}
}

View File

@@ -16,7 +16,6 @@
import { TraceType } from "common/trace/trace_type";
interface Viewer {
//TODO: add TraceEntry data type
notifyCurrentTraceEntries(entries: Map<TraceType, any>): void;
getView(): HTMLElement;
getTitle(): string;

View File

@@ -16,19 +16,20 @@
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";
import { getFilter, FilterType, Tree, TreeSummary } from "viewers/common/tree_utils";
import { TreeGenerator } from "viewers/common/tree_generator";
import { TreeTransformer } from "viewers/common/tree_transformer";
type NotifyViewCallbackType = (uiData: UiData) => void;
class Presenter {
export class Presenter {
constructor(notifyViewCallback: NotifyViewCallbackType) {
this.notifyViewCallback = notifyViewCallback;
this.uiData = new UiData();
this.notifyViewCallback(this.uiData);
}
public updatePinnedItems(event: CustomEvent) {
const pinnedItem = event.detail.pinnedItem;
public updatePinnedItems(pinnedItem: Tree) {
const pinnedId = `${pinnedItem.id}`;
if (this.pinnedItems.map(item => `${item.id}`).includes(pinnedId)) {
this.pinnedItems = this.pinnedItems.filter(pinned => `${pinned.id}` != pinnedId);
@@ -40,8 +41,7 @@ class Presenter {
this.notifyViewCallback(this.uiData);
}
public updateHighlightedItems(event: CustomEvent) {
const id = `${event.detail.id}`;
public updateHighlightedItems(id: string) {
if (this.highlightedItems.includes(id)) {
this.highlightedItems = this.highlightedItems.filter(hl => hl != id);
} else {
@@ -52,23 +52,86 @@ class Presenter {
this.notifyViewCallback(this.uiData);
}
public updateHierarchyTree(event: CustomEvent) {
this.hierarchyUserOptions = event.detail.userOptions;
public updateHierarchyTree(userOptions: UserOptions) {
this.hierarchyUserOptions = 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);
public filterHierarchyTree(filterString: string) {
this.hierarchyFilter = getFilter(filterString);
this.uiData.tree = this.generateTree();
this.notifyViewCallback(this.uiData);
}
public updatePropertiesTree(userOptions: UserOptions) {
this.propertiesUserOptions = userOptions;
this.uiData.propertiesUserOptions = this.propertiesUserOptions;
this.updateSelectedTreeUiData();
}
public filterPropertiesTree(filterString: string) {
this.propertiesFilter = getFilter(filterString);
this.updateSelectedTreeUiData();
}
public newPropertiesTree(selectedItem: any) {
this.selectedTree = selectedItem;
this.updateSelectedTreeUiData();
}
private updateSelectedTreeUiData() {
this.uiData.selectedTree = this.getTreeWithTransformedProperties(this.selectedTree);
this.uiData.selectedTreeSummary = this.getSelectedTreeSummary(this.selectedTree);
this.notifyViewCallback(this.uiData);
}
private getSelectedTreeSummary(layer: Tree): TreeSummary | undefined {
const summary = [];
if (layer?.visibilityReason?.length > 0) {
let reason = "";
if (Array.isArray(layer.visibilityReason)) {
reason = layer.visibilityReason.join(", ");
} else {
reason = layer.visibilityReason;
}
summary.push({key: "Invisible due to", value: reason});
}
if (layer?.occludedBy?.length > 0) {
summary.push({key: "Occluded by", value: layer.occludedBy.map((it:Tree) => it.id).join(", ")});
}
if (layer?.partiallyOccludedBy?.length > 0) {
summary.push({
key: "Partially occluded by",
value: layer.partiallyOccludedBy.map((it:Tree) => it.id).join(", "),
});
}
if (layer?.coveredBy?.length > 0) {
summary.push({key: "Covered by", value: layer.coveredBy.map((it:Tree) => it.id).join(", ")});
}
if (summary.length === 0) {
return undefined;
}
return summary;
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>) {
this.uiData = new UiData();
const entry = entries.get(TraceType.SURFACE_FLINGER)[0];
this.uiData.rects = [];
this.previousEntry = entries.get(TraceType.SURFACE_FLINGER)[1];
this.uiData = new UiData();
this.uiData.highlightedItems = this.highlightedItems;
const displayRects = entry.displays.map((display: any) => {
const rect = display.layerStackSpace;
rect.label = display.name;
@@ -78,7 +141,6 @@ class Presenter {
rect.isVirtual = display.isVirtual ?? false;
return rect;
}) ?? [];
this.displayIds = [];
const rects = entry.visibleLayers
.sort((a: any, b: any) => (b.absoluteZ > a.absoluteZ) ? 1 : (a.absoluteZ == b.absoluteZ) ? 0 : -1)
@@ -92,13 +154,12 @@ 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.entry = entry;
this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
this.uiData.propertiesUserOptions = this.propertiesUserOptions;
this.uiData.tree = this.generateTree();
this.notifyViewCallback(this.uiData);
}
@@ -106,7 +167,11 @@ class Presenter {
if (!this.entry) {
return null;
}
const generator = new TreeGenerator(this.entry, this.hierarchyUserOptions, this.hierarchyFilter, this.pinnedIds)
const generator = new TreeGenerator(this.entry, this.hierarchyFilter, this.pinnedIds)
.setIsOnlyVisibleView(this.hierarchyUserOptions["onlyVisible"]?.enabled)
.setIsSimplifyNames(this.hierarchyUserOptions["simplifyNames"]?.enabled)
.setIsFlatView(this.hierarchyUserOptions["flat"]?.enabled)
.withUniqueNodeId();
let tree: Tree;
if (!this.hierarchyUserOptions["showDiff"]?.enabled) {
@@ -172,13 +237,26 @@ class Presenter {
}
}
private getTreeWithTransformedProperties(selectedTree: Tree) {
const transformer = new TreeTransformer(selectedTree, this.propertiesFilter)
.setIsShowDefaults(this.propertiesUserOptions["showDefaults"]?.enabled)
.setIsShowDiff(this.propertiesUserOptions["showDiff"]?.enabled)
.setTransformerOptions({skip: selectedTree.skip})
.setDiffProperties(this.previousEntry);
this.uiData.selectedLayer = transformer.getOriginalLayer(this.entry, selectedTree.stableId);
const transformedTree = transformer.transform();
return transformedTree;
}
private readonly notifyViewCallback: NotifyViewCallbackType;
private uiData: UiData;
private displayIds: Array<number> = [];
private hierarchyFilter: FilterType = getFilter("");
private propertiesFilter: FilterType = getFilter("");
private highlightedItems: Array<string> = [];
private displayIds: Array<number> = [];
private pinnedItems: Array<Tree> = [];
private pinnedIds: Array<string> = [];
private selectedTree: any = null;
private previousEntry: any = null;
private entry: any = null;
private hierarchyUserOptions: UserOptions = {
@@ -199,6 +277,20 @@ class Presenter {
enabled: false
}
};
}
export {Presenter};
private propertiesUserOptions: UserOptions = {
showDiff: {
name: "Show diff",
enabled: false
},
showDefaults: {
name: "Show defaults",
enabled: true,
tooltip: `
If checked, shows the value of all properties.
Otherwise, hides all properties whose value is
the default for its data type.
`
},
};
}

View File

@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TraceType } from "common/trace/trace_type";
import { Tree } from "viewers/common/tree_utils";
import { Tree, TreeSummary } from "viewers/common/tree_utils";
import { UserOptions } from "viewers/common/user_options";
import { Layer } from "common/trace/flickerlib/common";
import { TraceType } from "common/trace/trace_type";
export class UiData {
dependencies: Array<TraceType> = [TraceType.SURFACE_FLINGER];
@@ -24,7 +25,11 @@ export class UiData {
highlightedItems?: Array<string> = [];
pinnedItems?: Array<Tree> = [];
hierarchyUserOptions?: UserOptions = {};
propertiesUserOptions?: UserOptions = {};
tree?: Tree | null = null;
selectedTree?: any = {};
selectedLayer?: Layer = {};
selectedTreeSummary?: TreeSummary = [];
}
export interface Rectangle {

View File

@@ -31,6 +31,7 @@ import { PersistentStore } from "common/persistent_store";
[rects]="inputData?.rects ?? []"
[displayIds]="inputData?.displayIds ?? []"
[highlightedItems]="inputData?.highlightedItems ?? []"
[displayIds]="inputData?.displayIds ?? []"
></rects-view>
</mat-card>
<div fxLayout="row wrap" fxLayoutGap="10px grid" class="card-grid">
@@ -45,7 +46,13 @@ import { PersistentStore } from "common/persistent_store";
></hierarchy-view>
</mat-card>
<mat-card id="sf-properties-view" class="properties-view">
<properties-view></properties-view>
<properties-view
[userOptions]="inputData?.propertiesUserOptions ?? {}"
[selectedTree]="inputData?.selectedTree ?? {}"
[selectedLayer]="inputData?.selectedLayer ?? {}"
[summary]="inputData?.selectedTreeSummary ?? []"
[propertyGroups]="true"
></properties-view>
</mat-card>
</div>
</div>

View File

@@ -25,10 +25,13 @@ class ViewerSurfaceFlinger implements Viewer {
this.presenter = new Presenter((uiData: UiData) => {
(this.view as any).inputData = uiData;
});
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)));
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
}
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {