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 class="icon-button toggle-rect-show-state-btn" (click)="toggleRectShowState($event)">
35        <mat-icon class="material-symbols-outlined">
36          {{ showStateIcon }}
37        </mat-icon>
38      </button>
39    </div>
40    <div *ngIf="showChevron()" class="icon-wrapper">
41      <button class="icon-button toggle-tree-btn" (click)="toggleTree($event)">
42        <mat-icon>
43          {{ isExpanded ? 'arrow_drop_down' : 'chevron_right' }}
44        </mat-icon>
45      </button>
46    </div>
47
48    <div *ngIf="showLeafNodeIcon()" class="icon-wrapper leaf-node-icon-wrapper">
49      <mat-icon class="leaf-node-icon"></mat-icon>
50    </div>
51
52    <div *ngIf="showPinNodeIcon()" class="icon-wrapper">
53      <button class="icon-button pin-node-btn" (click)="pinNode($event)">
54        <mat-icon [class.material-symbols-outlined]="!isPinned"> push_pin </mat-icon>
55      </button>
56    </div>
57
58    <div class="description">
59      <hierarchy-tree-node-data-view
60        *ngIf="node && !isPropertyTreeNode()"
61        [node]="node"></hierarchy-tree-node-data-view>
62      <property-tree-node-data-view
63        *ngIf="isPropertyTreeNode()"
64        [node]="node"></property-tree-node-data-view>
65    </div>
66
67    <div *ngIf="!isLeaf && !isExpanded && !isPinned" class="icon-wrapper">
68      <button
69        class="icon-button expand-tree-btn"
70        [class]="collapseDiffClass"
71        (click)="expandTree($event)">
72        <mat-icon aria-hidden="true"> more_horiz </mat-icon>
73      </button>
74    </div>
75  `,
76  styles: [nodeInnerItemStyles],
77})
78export class TreeNodeComponent {
79  @Input() node?: UiHierarchyTreeNode | UiPropertyTreeNode;
80  @Input() isLeaf?: boolean;
81  @Input() flattened?: boolean;
82  @Input() isExpanded?: boolean;
83  @Input() isPinned = false;
84  @Input() isInPinnedSection = false;
85  @Input() isSelected = false;
86  @Input() showStateIcon?: string;
87
88  @Output() readonly toggleTreeChange = new EventEmitter<void>();
89  @Output() readonly rectShowStateChange = new EventEmitter<void>();
90  @Output() readonly expandTreeChange = new EventEmitter<boolean>();
91  @Output() readonly pinNodeChange = new EventEmitter<UiHierarchyTreeNode>();
92
93  collapseDiffClass = '';
94  private el: HTMLElement;
95  private treeWrapper: HTMLElement | undefined;
96  private readonly gutterOffset = -13;
97
98  constructor(@Inject(ElementRef) public elementRef: ElementRef) {
99    this.el = elementRef.nativeElement;
100  }
101
102  ngAfterViewInit() {
103    this.treeWrapper = this.getTreeWrapper();
104  }
105
106  ngOnChanges() {
107    this.collapseDiffClass = this.updateCollapseDiffClass();
108    if (!this.isPinned && this.isSelected && !this.isNodeInView()) {
109      this.el.scrollIntoView({block: 'center', inline: 'nearest'});
110    }
111  }
112
113  isNodeInView() {
114    if (!this.treeWrapper) {
115      return false;
116    }
117    const rect = this.el.getBoundingClientRect();
118    const parentRect = this.treeWrapper.getBoundingClientRect();
119    return rect.top >= parentRect.top && rect.bottom <= parentRect.bottom;
120  }
121
122  getTreeWrapper(): HTMLElement | undefined {
123    let parent = this.el;
124    while (
125      !parent.className.includes('tree-wrapper') &&
126      parent?.parentElement
127    ) {
128      parent = parent.parentElement;
129    }
130    if (!parent.className.includes('tree-wrapper')) {
131      return undefined;
132    }
133    return parent;
134  }
135
136  isPropertyTreeNode() {
137    return this.node instanceof UiPropertyTreeNode;
138  }
139
140  showPinNodeIcon() {
141    return (
142      (this.node instanceof UiHierarchyTreeNode && !this.node.isRoot()) ?? false
143    );
144  }
145
146  toggleTree(event: MouseEvent) {
147    event.stopPropagation();
148    this.toggleTreeChange.emit();
149  }
150
151  toggleRectShowState(event: MouseEvent) {
152    event.stopPropagation();
153    this.rectShowStateChange.emit();
154  }
155
156  showChevron() {
157    return !this.isLeaf && !this.flattened && !this.isInPinnedSection;
158  }
159
160  showLeafNodeIcon() {
161    return !this.showChevron() && !this.isInPinnedSection;
162  }
163
164  expandTree(event: MouseEvent) {
165    event.stopPropagation();
166    this.expandTreeChange.emit();
167  }
168
169  pinNode(event: MouseEvent) {
170    event.stopPropagation();
171    this.pinNodeChange.emit(assertDefined(this.node) as UiHierarchyTreeNode);
172  }
173
174  updateCollapseDiffClass() {
175    if (this.isExpanded) {
176      return '';
177    }
178
179    const childrenDiffClasses = this.getAllDiffTypesOfChildren(
180      assertDefined(this.node),
181    );
182
183    childrenDiffClasses.delete(DiffType.NONE);
184    childrenDiffClasses.delete(undefined);
185
186    if (childrenDiffClasses.size === 0) {
187      return '';
188    }
189    if (childrenDiffClasses.size === 1) {
190      const diffType = childrenDiffClasses.values().next().value;
191      return diffType;
192    }
193    return DiffType.MODIFIED;
194  }
195
196  getShowStateIconStyle() {
197    const nodeMargin = this.flattened
198      ? 0
199      : Number(this.el.style.marginLeft.split('px')[0]);
200    return {
201      marginLeft: nodeMargin + this.gutterOffset + 'px',
202    };
203  }
204
205  private getAllDiffTypesOfChildren(
206    node: UiHierarchyTreeNode | UiPropertyTreeNode,
207  ) {
208    const classes = new Set();
209    for (const child of node.getAllChildren().values()) {
210      classes.add(child.getDiff());
211      for (const diffClass of this.getAllDiffTypesOfChildren(child)) {
212        classes.add(diffClass);
213      }
214    }
215
216    return classes;
217  }
218}
219