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 ChangeDetectionStrategy, 18 ChangeDetectorRef, 19 Component, 20 ElementRef, 21 EventEmitter, 22 Inject, 23 Input, 24 Output, 25 SimpleChanges, 26} from '@angular/core'; 27import {assertDefined} from 'common/assert_utils'; 28import {InMemoryStorage} from 'common/store/in_memory_storage'; 29import {RectShowState} from 'viewers/common/rect_show_state'; 30import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; 31import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node'; 32import {UiTreeUtils} from 'viewers/common/ui_tree_utils'; 33import {ViewerEvents} from 'viewers/common/viewer_events'; 34import { 35 nodeInnerItemStyles, 36 nodeStyles, 37 treeNodeDataViewStyles, 38} from 'viewers/components/styles/node.styles'; 39 40@Component({ 41 selector: 'tree-view', 42 changeDetection: ChangeDetectionStrategy.OnPush, 43 template: ` 44 <tree-node 45 *ngIf="node && showNode(node)" 46 [id]="'node' + node.name" 47 class="node" 48 [id]="'node' + node.name" 49 [class.leaf]="isLeaf(node)" 50 [class.selected]="isHighlighted(node, highlightedItem)" 51 [class.clickable]="isClickable()" 52 [class.child-selected]="hasSelectedChild()" 53 [class.child-hover]="childHover" 54 [class.full-opacity]="showFullOpacity(node)" 55 [class]="node.getDiff()" 56 [style]="nodeOffsetStyle()" 57 [node]="node" 58 [flattened]="isFlattened" 59 [isLeaf]="isLeaf(node)" 60 [isExpanded]="isExpanded()" 61 [isPinned]="isPinned()" 62 [isSelected]="isHighlighted(node, highlightedItem)" 63 [showStateIcon]="getShowStateIcon(node)" 64 (toggleTreeChange)="toggleTree()" 65 (rectShowStateChange)="toggleRectShowState()" 66 (click)="onNodeClick($event)" 67 (expandTreeChange)="expandTree()" 68 (pinNodeChange)="propagateNewPinnedItem($event)"></tree-node> 69 70 <div 71 *ngIf="!isLeaf(node)" 72 class="children" 73 [class.flattened]="isFlattened" 74 [class.with-gutter]="addGutter()" 75 [hidden]="!isExpanded()"> 76 <tree-view 77 *ngFor="let child of node.children.values(); trackBy: childTrackById" 78 class="subtree" 79 [node]="child" 80 [store]="store" 81 [showNode]="showNode" 82 [isFlattened]="isFlattened" 83 [useStoredExpandedState]="useStoredExpandedState" 84 [initialDepth]="initialDepth + 1" 85 [highlightedItem]="highlightedItem" 86 [pinnedItems]="pinnedItems" 87 [itemsClickable]="itemsClickable" 88 [rectIdToShowState]="rectIdToShowState" 89 (highlightedChange)="propagateNewHighlightedItem($event)" 90 (pinnedItemChange)="propagateNewPinnedItem($event)" 91 (hoverStart)="childHover = true" 92 (hoverEnd)="childHover = false" 93 (expandParent)="expandTree()"></tree-view> 94 </div> 95 `, 96 styles: [nodeStyles, treeNodeDataViewStyles, nodeInnerItemStyles], 97}) 98export class TreeComponent { 99 isHighlighted = UiTreeUtils.isHighlighted; 100 101 @Input() node?: UiPropertyTreeNode | UiHierarchyTreeNode; 102 @Input() store: InMemoryStorage | undefined; 103 @Input() isFlattened? = false; 104 @Input() initialDepth = 0; 105 @Input() highlightedItem = ''; 106 @Input() pinnedItems?: UiHierarchyTreeNode[] = []; 107 @Input() itemsClickable?: boolean; 108 @Input() rectIdToShowState?: Map<string, RectShowState>; 109 110 // Conditionally use stored states. Some traces (e.g. transactions) do not provide items with the "stable id" field needed to search values in the storage. 111 @Input() useStoredExpandedState = false; 112 113 @Input() showNode = (node: UiPropertyTreeNode | UiHierarchyTreeNode) => true; 114 115 @Output() readonly highlightedChange = new EventEmitter< 116 UiHierarchyTreeNode | UiPropertyTreeNode 117 >(); 118 @Output() readonly pinnedItemChange = new EventEmitter<UiHierarchyTreeNode>(); 119 @Output() readonly hoverStart = new EventEmitter<void>(); 120 @Output() readonly hoverEnd = new EventEmitter<void>(); 121 @Output() readonly expandParent = new EventEmitter<void>(); 122 123 childHover = false; 124 readonly levelOffset = 24; 125 nodeElement: HTMLElement; 126 127 private localExpandedState = true; 128 private storeKeyCollapsedState = ''; 129 130 childTrackById( 131 index: number, 132 child: UiPropertyTreeNode | UiHierarchyTreeNode, 133 ): string { 134 return child.id; 135 } 136 137 constructor( 138 @Inject(ElementRef) public elementRef: ElementRef, 139 @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, 140 ) { 141 this.nodeElement = elementRef.nativeElement.querySelector('.node'); 142 this.nodeElement?.addEventListener( 143 'mousedown', 144 this.nodeMouseDownEventListener, 145 ); 146 this.nodeElement?.addEventListener( 147 'mouseenter', 148 this.nodeMouseEnterEventListener, 149 ); 150 this.nodeElement?.addEventListener( 151 'mouseleave', 152 this.nodeMouseLeaveEventListener, 153 ); 154 } 155 156 ngOnChanges(changes: SimpleChanges) { 157 if (changes['node'] && this.node) { 158 if (this.node.isRoot() && !this.store) { 159 this.store = new InMemoryStorage(); 160 } 161 this.storeKeyCollapsedState = `${this.node.id}.collapsedState`; 162 if (this.store) { 163 this.setExpandedValue(!this.isCollapsedInStore()); 164 } else { 165 this.setExpandedValue(true); 166 } 167 } 168 } 169 170 ngOnDestroy() { 171 this.nodeElement?.removeEventListener( 172 'mousedown', 173 this.nodeMouseDownEventListener, 174 ); 175 this.nodeElement?.removeEventListener( 176 'mouseenter', 177 this.nodeMouseEnterEventListener, 178 ); 179 this.nodeElement?.removeEventListener( 180 'mouseleave', 181 this.nodeMouseLeaveEventListener, 182 ); 183 } 184 185 isLeaf(node?: UiPropertyTreeNode | UiHierarchyTreeNode): boolean { 186 if (node === undefined) return true; 187 if (node instanceof UiHierarchyTreeNode) { 188 return node.getAllChildren().length === 0; 189 } 190 return ( 191 node.formattedValue().length > 0 || node.getAllChildren().length === 0 192 ); 193 } 194 195 onNodeClick(event: MouseEvent) { 196 event.preventDefault(); 197 if (window.getSelection()?.type === 'range') { 198 return; 199 } 200 201 const isDoubleClick = event.detail % 2 === 0; 202 if (!this.isFlattened && !this.isLeaf(this.node) && isDoubleClick) { 203 event.preventDefault(); 204 this.toggleTree(); 205 } else { 206 this.updateHighlightedItem(); 207 } 208 } 209 210 nodeOffsetStyle() { 211 const offset = this.levelOffset * this.initialDepth; 212 const gutterOffset = this.addGutter() ? this.levelOffset / 2 : 0; 213 return { 214 marginLeft: '-' + offset + 'px', 215 paddingLeft: offset + gutterOffset + 'px', 216 }; 217 } 218 219 isPinned() { 220 if (this.node instanceof UiHierarchyTreeNode) { 221 return this.pinnedItems?.map((item) => item.id).includes(this.node!.id); 222 } 223 return false; 224 } 225 226 propagateNewHighlightedItem( 227 newItem: UiPropertyTreeNode | UiHierarchyTreeNode, 228 ) { 229 this.highlightedChange.emit(newItem); 230 } 231 232 propagateNewPinnedItem(newPinnedItem: UiHierarchyTreeNode) { 233 this.pinnedItemChange.emit(newPinnedItem); 234 } 235 236 isClickable() { 237 return !this.isLeaf(this.node) || this.itemsClickable; 238 } 239 240 toggleTree() { 241 this.setExpandedValue(!this.isExpanded()); 242 } 243 244 expandTree() { 245 this.setExpandedValue(true); 246 this.changeDetectorRef.detectChanges(); 247 this.expandParent.emit(); 248 } 249 250 isExpanded() { 251 if (this.isLeaf(this.node)) { 252 return true; 253 } 254 255 if (this.useStoredExpandedState && this.store) { 256 return !this.isCollapsedInStore(); 257 } 258 259 return this.localExpandedState; 260 } 261 262 hasSelectedChild() { 263 if (this.isLeaf(this.node)) { 264 return false; 265 } 266 for (const child of assertDefined(this.node).getAllChildren()) { 267 if (this.highlightedItem === child.id) { 268 return true; 269 } 270 } 271 return false; 272 } 273 274 getShowStateIcon( 275 node: UiPropertyTreeNode | UiHierarchyTreeNode, 276 ): string | undefined { 277 const showState = this.rectIdToShowState?.get(node.id); 278 if (showState === undefined || node instanceof UiPropertyTreeNode) { 279 return undefined; 280 } 281 return showState === RectShowState.SHOW ? 'visibility' : 'visibility_off'; 282 } 283 284 showFullOpacity(node: UiPropertyTreeNode | UiHierarchyTreeNode) { 285 if (node instanceof UiPropertyTreeNode) return true; 286 if (this.rectIdToShowState === undefined) return true; 287 const showState = this.rectIdToShowState.get(node.id); 288 return showState === RectShowState.SHOW; 289 } 290 291 toggleRectShowState() { 292 const nodeId = assertDefined(this.node).id; 293 const currentShowState = assertDefined(this.rectIdToShowState?.get(nodeId)); 294 const newShowState = 295 currentShowState === RectShowState.HIDE 296 ? RectShowState.SHOW 297 : RectShowState.HIDE; 298 const event = new CustomEvent(ViewerEvents.RectShowStateChange, { 299 bubbles: true, 300 detail: {rectId: nodeId, state: newShowState}, 301 }); 302 this.elementRef.nativeElement.dispatchEvent(event); 303 } 304 305 addGutter() { 306 return (this.rectIdToShowState?.size ?? 0) > 0; 307 } 308 309 private updateHighlightedItem() { 310 if (this.node) this.highlightedChange.emit(this.node); 311 } 312 313 private setExpandedValue( 314 isExpanded: boolean, 315 shouldUpdateStoredState = true, 316 ) { 317 if (this.store && this.useStoredExpandedState && shouldUpdateStoredState) { 318 if (isExpanded) { 319 this.store.clear(this.storeKeyCollapsedState); 320 } else { 321 this.store.add(this.storeKeyCollapsedState, 'true'); 322 } 323 } else { 324 this.localExpandedState = isExpanded; 325 } 326 } 327 328 private nodeMouseDownEventListener = (event: MouseEvent) => { 329 if (event.detail > 1) { 330 event.preventDefault(); 331 return false; 332 } 333 return true; 334 }; 335 336 private nodeMouseEnterEventListener = () => { 337 this.hoverStart.emit(); 338 }; 339 340 private nodeMouseLeaveEventListener = () => { 341 this.hoverEnd.emit(); 342 }; 343 344 private isCollapsedInStore(): boolean { 345 return ( 346 assertDefined(this.store).get(this.storeKeyCollapsedState) !== undefined 347 ); 348 } 349} 350