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