• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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<void>();
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    if (!this.isInPinnedSection && this.isSelected) {
127      this.expandTreeChange.emit();
128    }
129    this.collapseDiffClass = this.updateCollapseDiffClass();
130    if (!this.isInPinnedSection && this.isSelected && !this.isNodeInView()) {
131      this.el.scrollIntoView({block: 'center', inline: 'nearest'});
132    }
133  }
134
135  isNodeInView(): boolean {
136    if (!this.treeWrapper) {
137      return false;
138    }
139    const rect = this.el.getBoundingClientRect();
140    const parentRect = this.treeWrapper.getBoundingClientRect();
141    return rect.top >= parentRect.top && rect.bottom <= parentRect.bottom;
142  }
143
144  getTreeWrapper(): HTMLElement | undefined {
145    let parent = this.el;
146    while (
147      !parent.className.includes('tree-wrapper') &&
148      parent?.parentElement
149    ) {
150      parent = parent.parentElement;
151    }
152    if (!parent.className.includes('tree-wrapper')) {
153      return undefined;
154    }
155    return parent;
156  }
157
158  isPropertyTreeNode(): boolean {
159    return this.node instanceof UiPropertyTreeNode;
160  }
161
162  showPinNodeIcon(): boolean {
163    return this.node instanceof UiHierarchyTreeNode && !this.node.isRoot();
164  }
165
166  toggleTree(event: MouseEvent) {
167    event.stopPropagation();
168    this.toggleTreeChange.emit();
169  }
170
171  toggleRectShowState(event: MouseEvent) {
172    event.stopPropagation();
173    this.rectShowStateChange.emit();
174  }
175
176  showChevron(): boolean {
177    return !this.isLeaf && !this.flattened && !this.isInPinnedSection;
178  }
179
180  expandTree(event: MouseEvent) {
181    event.stopPropagation();
182    this.expandTreeChange.emit();
183  }
184
185  pinNode(event: MouseEvent) {
186    event.stopPropagation();
187    this.pinNodeChange.emit(assertDefined(this.node) as UiHierarchyTreeNode);
188  }
189
190  updateCollapseDiffClass(): string {
191    if (this.isExpanded) {
192      return '';
193    }
194
195    const childrenDiffClasses = this.getAllDiffTypesOfChildren(
196      assertDefined(this.node),
197    );
198
199    childrenDiffClasses.delete(DiffType.NONE);
200
201    if (childrenDiffClasses.size === 0) {
202      return '';
203    }
204    if (childrenDiffClasses.size === 1) {
205      const diffType = assertDefined(childrenDiffClasses.values().next().value);
206      return diffType;
207    }
208    return DiffType.MODIFIED;
209  }
210
211  getShowStateIconStyle() {
212    const nodeMargin = this.flattened
213      ? 0
214      : Number(this.el.style.marginLeft.split('px')[0]);
215    return {
216      marginLeft: nodeMargin + this.gutterOffset + 'px',
217    };
218  }
219
220  showCopyButton(): boolean {
221    return (
222      this.node instanceof UiPropertyTreeNode &&
223      (this.node.isRoot() || !this.showChevron())
224    );
225  }
226
227  getCopyText(): string {
228    const node = assertDefined(this.node) as UiPropertyTreeNode;
229    if (this.showChevron()) {
230      return node.name;
231    }
232    return `${node.name}: ${node.formattedValue()}`;
233  }
234
235  private getAllDiffTypesOfChildren(
236    node: UiHierarchyTreeNode | UiPropertyTreeNode,
237  ): Set<DiffType> {
238    const classes = new Set<DiffType>();
239    for (const child of node.getAllChildren()) {
240      classes.add(child.getDiff());
241      for (const diffClass of this.getAllDiffTypesOfChildren(child)) {
242        classes.add(diffClass);
243      }
244    }
245
246    return classes;
247  }
248}
249