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