xref: /aosp_15_r20/development/tools/winscope/src/viewers/components/tree_node_component.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import {
17  Component,
18  ElementRef,
19  EventEmitter,
20  Inject,
21  Input,
22  Output,
23} from '@angular/core';
24import {assertDefined} from 'common/assert_utils';
25import {DiffType} from 'viewers/common/diff_type';
26import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
27import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
28import {nodeInnerItemStyles} from 'viewers/components/styles/node.styles';
29
30@Component({
31  selector: 'tree-node',
32  template: `
33    <div *ngIf="showStateIcon" class="icon-wrapper-show-state" [style]="getShowStateIconStyle()">
34      <button
35        mat-icon-button
36        class="icon-button toggle-rect-show-state-btn"
37        (click)="toggleRectShowState($event)">
38        <mat-icon class="material-symbols-outlined">
39          {{ showStateIcon }}
40        </mat-icon>
41      </button>
42    </div>
43    <div *ngIf="showChevron()" class="icon-wrapper">
44      <button
45        mat-icon-button
46        class="icon-button toggle-tree-btn"
47        (click)="toggleTree($event)">
48        <mat-icon>
49          {{ isExpanded ? 'arrow_drop_down' : 'chevron_right' }}
50        </mat-icon>
51      </button>
52    </div>
53
54    <div *ngIf="!showChevron()" class="icon-wrapper leaf-node-icon-wrapper">
55      <mat-icon class="leaf-node-icon"></mat-icon>
56    </div>
57
58    <div *ngIf="showPinNodeIcon()" class="icon-wrapper">
59      <button
60        mat-icon-button
61        class="icon-button pin-node-btn"
62        (click)="pinNode($event)">
63        <mat-icon [class.material-symbols-outlined]="!isPinned"> push_pin </mat-icon>
64      </button>
65    </div>
66
67    <div class="description">
68      <hierarchy-tree-node-data-view
69        *ngIf="node && !isPropertyTreeNode()"
70        [node]="node"></hierarchy-tree-node-data-view>
71      <property-tree-node-data-view
72        *ngIf="isPropertyTreeNode()"
73        [node]="node"></property-tree-node-data-view>
74    </div>
75
76    <div *ngIf="!isLeaf && !isExpanded && !isPinned" class="icon-wrapper">
77      <button
78        mat-icon-button
79        class="icon-button expand-tree-btn"
80        [class]="collapseDiffClass"
81        (click)="expandTree($event)">
82        <mat-icon aria-hidden="true"> more_horiz </mat-icon>
83      </button>
84    </div>
85    <div *ngIf="showCopyButton()" class="icon-wrapper-copy">
86      <button
87        mat-icon-button
88        class="icon-button copy-btn"
89        [cdkCopyToClipboard]="getCopyText()"
90        (click)="$event.stopPropagation()">
91        <mat-icon class="material-symbols-outlined">content_copy</mat-icon>
92      </button>
93    </div>
94  `,
95  styles: [nodeInnerItemStyles],
96})
97export class TreeNodeComponent {
98  @Input() node?: UiHierarchyTreeNode | UiPropertyTreeNode;
99  @Input() isLeaf?: boolean;
100  @Input() flattened?: boolean;
101  @Input() isExpanded?: boolean;
102  @Input() isPinned = false;
103  @Input() isInPinnedSection = false;
104  @Input() isSelected = false;
105  @Input() showStateIcon?: string;
106
107  @Output() readonly toggleTreeChange = new EventEmitter<void>();
108  @Output() readonly rectShowStateChange = new EventEmitter<void>();
109  @Output() readonly expandTreeChange = new EventEmitter<boolean>();
110  @Output() readonly pinNodeChange = new EventEmitter<UiHierarchyTreeNode>();
111
112  collapseDiffClass = '';
113  private el: HTMLElement;
114  private treeWrapper: HTMLElement | undefined;
115  private readonly gutterOffset = -13;
116
117  constructor(@Inject(ElementRef) public elementRef: ElementRef) {
118    this.el = elementRef.nativeElement;
119  }
120
121  ngAfterViewInit() {
122    this.treeWrapper = this.getTreeWrapper();
123  }
124
125  ngOnChanges() {
126    this.collapseDiffClass = this.updateCollapseDiffClass();
127    if (!this.isPinned && this.isSelected && !this.isNodeInView()) {
128      this.el.scrollIntoView({block: 'center', inline: 'nearest'});
129    }
130  }
131
132  isNodeInView(): boolean {
133    if (!this.treeWrapper) {
134      return false;
135    }
136    const rect = this.el.getBoundingClientRect();
137    const parentRect = this.treeWrapper.getBoundingClientRect();
138    return rect.top >= parentRect.top && rect.bottom <= parentRect.bottom;
139  }
140
141  getTreeWrapper(): HTMLElement | undefined {
142    let parent = this.el;
143    while (
144      !parent.className.includes('tree-wrapper') &&
145      parent?.parentElement
146    ) {
147      parent = parent.parentElement;
148    }
149    if (!parent.className.includes('tree-wrapper')) {
150      return undefined;
151    }
152    return parent;
153  }
154
155  isPropertyTreeNode(): boolean {
156    return this.node instanceof UiPropertyTreeNode;
157  }
158
159  showPinNodeIcon(): boolean {
160    return this.node instanceof UiHierarchyTreeNode && !this.node.isRoot();
161  }
162
163  toggleTree(event: MouseEvent) {
164    event.stopPropagation();
165    this.toggleTreeChange.emit();
166  }
167
168  toggleRectShowState(event: MouseEvent) {
169    event.stopPropagation();
170    this.rectShowStateChange.emit();
171  }
172
173  showChevron(): boolean {
174    return !this.isLeaf && !this.flattened && !this.isInPinnedSection;
175  }
176
177  expandTree(event: MouseEvent) {
178    event.stopPropagation();
179    this.expandTreeChange.emit();
180  }
181
182  pinNode(event: MouseEvent) {
183    event.stopPropagation();
184    this.pinNodeChange.emit(assertDefined(this.node) as UiHierarchyTreeNode);
185  }
186
187  updateCollapseDiffClass() {
188    if (this.isExpanded) {
189      return '';
190    }
191
192    const childrenDiffClasses = this.getAllDiffTypesOfChildren(
193      assertDefined(this.node),
194    );
195
196    childrenDiffClasses.delete(DiffType.NONE);
197
198    if (childrenDiffClasses.size === 0) {
199      return '';
200    }
201    if (childrenDiffClasses.size === 1) {
202      const diffType = childrenDiffClasses.values().next().value;
203      return diffType;
204    }
205    return DiffType.MODIFIED;
206  }
207
208  getShowStateIconStyle() {
209    const nodeMargin = this.flattened
210      ? 0
211      : Number(this.el.style.marginLeft.split('px')[0]);
212    return {
213      marginLeft: nodeMargin + this.gutterOffset + 'px',
214    };
215  }
216
217  showCopyButton(): boolean {
218    return (
219      this.node instanceof UiPropertyTreeNode &&
220      (this.node.isRoot() || !this.showChevron())
221    );
222  }
223
224  getCopyText(): string {
225    const node = assertDefined(this.node) as UiPropertyTreeNode;
226    if (this.showChevron()) {
227      return node.name;
228    }
229    return `${node.name}: ${node.formattedValue()}`;
230  }
231
232  private getAllDiffTypesOfChildren(
233    node: UiHierarchyTreeNode | UiPropertyTreeNode,
234  ): Set<DiffType> {
235    const classes = new Set<DiffType>();
236    for (const child of node.getAllChildren()) {
237      classes.add(child.getDiff());
238      for (const diffClass of this.getAllDiffTypesOfChildren(child)) {
239        classes.add(diffClass);
240      }
241    }
242
243    return classes;
244  }
245}
246