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 */ 16 17import { 18 Component, 19 ElementRef, 20 EventEmitter, 21 HostListener, 22 Inject, 23 Input, 24 Output, 25} from '@angular/core'; 26import {Color} from 'app/colors'; 27import {InMemoryStorage} from 'common/store/in_memory_storage'; 28import {PersistentStore} from 'common/store/persistent_store'; 29import {Analytics} from 'logging/analytics'; 30import {UserWarning} from 'messaging/user_warning'; 31import {TraceType} from 'trace/trace_type'; 32import {RectShowState} from 'viewers/common/rect_show_state'; 33import {TableProperties} from 'viewers/common/table_properties'; 34import {TextFilter} from 'viewers/common/text_filter'; 35import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; 36import {UiTreeUtils} from 'viewers/common/ui_tree_utils'; 37import {UserOptions} from 'viewers/common/user_options'; 38import {ViewerEvents} from 'viewers/common/viewer_events'; 39import {nodeStyles} from 'viewers/components/styles/node.styles'; 40import {viewerCardInnerStyle} from './styles/viewer_card.styles'; 41 42@Component({ 43 selector: 'hierarchy-view', 44 template: ` 45 <div class="view-header"> 46 <div class="title-section"> 47 <collapsible-section-title 48 class="hierarchy-title" 49 title="HIERARCHY" 50 (collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title> 51 <search-box 52 formFieldClass="applied-field" 53 [textFilter]="textFilter" 54 (filterChange)="onFilterChange($event)"></search-box> 55 </div> 56 <user-options 57 class="view-controls" 58 [userOptions]="userOptions" 59 [eventType]="ViewerEvents.HierarchyUserOptionsChange" 60 [traceType]="dependencies[0]" 61 [logCallback]="Analytics.Navigation.logHierarchySettingsChanged"> 62 </user-options> 63 <ng-container *ngIf="getWarnings().length > 0"> 64 <span 65 *ngFor="let warning of getWarnings()" 66 class="mat-body-1 warning" 67 [matTooltip]="warning.getMessage()" 68 [matTooltipDisabled]="disableTooltip(warningEl)"> 69 <mat-icon class="warning-icon"> warning </mat-icon> 70 <span class="warning-message" #warningEl>{{warning.getMessage()}}</span> 71 </span> 72 </ng-container> 73 <properties-table 74 *ngIf="tableProperties" 75 class="properties-table" 76 [properties]="tableProperties"></properties-table> 77 <div 78 *ngIf="pinnedItems.length > 0" 79 class="pinned-items" 80 [style.padding]="getPinnedItemsPadding()"> 81 <tree-node 82 *ngFor="let pinnedItem of pinnedItems" 83 class="node full-opacity" 84 [class]="pinnedItem.getDiff()" 85 [class.selected]="isHighlighted(pinnedItem, highlightedItem)" 86 [class.clickable]="true" 87 [node]="pinnedItem" 88 [isPinned]="true" 89 [isInPinnedSection]="true" 90 [isSelected]="isHighlighted(pinnedItem, highlightedItem)" 91 (pinNodeChange)="onPinnedItemChange($event)" 92 (click)="onPinnedNodeClick($event, pinnedItem)"></tree-node> 93 </div> 94 </div> 95 <mat-divider></mat-divider> 96 <span class="mat-body-1 placeholder-text" *ngIf="showPlaceholderText()"> {{ placeholderText + ' Try changing timeline position.' }} </span> 97 <div class="hierarchy-content tree-wrapper"> 98 <div class="trees"> 99 <tree-view 100 *ngFor="let tree of trees; trackBy: trackById" 101 class="tree" 102 [node]="tree" 103 [isFlattened]="isFlattened()" 104 [useStoredExpandedState]="true" 105 [highlightedItem]="highlightedItem" 106 [pinnedItems]="pinnedItems" 107 [itemsClickable]="true" 108 [rectIdToShowState]="rectIdToShowState" 109 [store]="treeStorage" 110 (highlightedChange)="onHighlightedItemChange($event)" 111 (pinnedItemChange)="onPinnedItemChange($event)" 112 (selectedTreeChange)="onSelectedTreeChange($event)"></tree-view> 113 </div> 114 </div> 115 `, 116 styles: [ 117 ` 118 .view-header { 119 display: flex; 120 flex-direction: column; 121 } 122 123 .properties-table { 124 padding-top: 5px; 125 } 126 127 .hierarchy-content { 128 height: 100%; 129 overflow: auto; 130 padding: 0px 12px; 131 } 132 133 .pinned-items { 134 width: 100%; 135 box-sizing: border-box; 136 border: 2px solid ${Color.PINNED_ITEM_BORDER}; 137 } 138 139 tree-view { 140 overflow: auto; 141 } 142 `, 143 nodeStyles, 144 viewerCardInnerStyle, 145 ], 146}) 147export class HierarchyComponent { 148 isHighlighted = UiTreeUtils.isHighlighted; 149 ViewerEvents = ViewerEvents; 150 Analytics = Analytics; 151 treeStorage = new InMemoryStorage(); 152 153 @Input() trees: UiHierarchyTreeNode[] = []; 154 @Input() tableProperties: TableProperties | undefined; 155 @Input() dependencies: TraceType[] = []; 156 @Input() highlightedItem = ''; 157 @Input() pinnedItems: UiHierarchyTreeNode[] = []; 158 @Input() store: PersistentStore | undefined; 159 @Input() userOptions: UserOptions = {}; 160 @Input() rectIdToShowState?: Map<string, RectShowState>; 161 @Input() placeholderText = 'No entry found.'; 162 @Input() textFilter: TextFilter | undefined; 163 164 @Output() collapseButtonClicked = new EventEmitter(); 165 166 constructor( 167 @Inject(ElementRef) private elementRef: ElementRef<HTMLElement>, 168 ) {} 169 170 trackById(index: number, child: UiHierarchyTreeNode): string { 171 return child.id; 172 } 173 174 isFlattened(): boolean { 175 return this.userOptions['flat']?.enabled; 176 } 177 178 showPlaceholderText(): boolean { 179 return this.trees.length === 0 && !!this.placeholderText; 180 } 181 182 getWarnings(): UserWarning[] { 183 return this.trees.flatMap((tree) => tree.getWarnings()); 184 } 185 186 onPinnedNodeClick(event: MouseEvent, pinnedItem: UiHierarchyTreeNode) { 187 event.preventDefault(); 188 if (window.getSelection()?.type === 'range') { 189 return; 190 } 191 this.onHighlightedItemChange(pinnedItem); 192 } 193 194 onFilterChange(detail: TextFilter) { 195 const event = new CustomEvent(ViewerEvents.HierarchyFilterChange, { 196 bubbles: true, 197 detail, 198 }); 199 this.elementRef.nativeElement.dispatchEvent(event); 200 } 201 202 onHighlightedItemChange(node: UiHierarchyTreeNode) { 203 const event = new CustomEvent(ViewerEvents.HighlightedNodeChange, { 204 bubbles: true, 205 detail: {node}, 206 }); 207 this.elementRef.nativeElement.dispatchEvent(event); 208 } 209 210 onPinnedItemChange(item: UiHierarchyTreeNode) { 211 const event = new CustomEvent(ViewerEvents.HierarchyPinnedChange, { 212 bubbles: true, 213 detail: {pinnedItem: item}, 214 }); 215 this.elementRef.nativeElement.dispatchEvent(event); 216 } 217 218 disableTooltip(el: HTMLElement) { 219 return el.scrollWidth === el.clientWidth; 220 } 221 222 getPinnedItemsPadding() { 223 const addGutter = (this.rectIdToShowState?.size ?? 0) > 0; 224 return `0px 10.5px 0px ${addGutter ? 22.5 : 10.5}px`; 225 } 226 227 @HostListener('document:keydown', ['$event']) 228 async handleKeyboardEvent(event: KeyboardEvent) { 229 const domRect = this.elementRef.nativeElement.getBoundingClientRect(); 230 const componentVisible = domRect.height > 0 && domRect.width > 0; 231 if ( 232 componentVisible && 233 (event.key === 'ArrowDown' || event.key === 'ArrowUp') 234 ) { 235 event.preventDefault(); 236 const details = {bubbles: true, detail: this.treeStorage}; 237 if (event.key === 'ArrowDown') { 238 const arrowEvent = new CustomEvent( 239 ViewerEvents.ArrowDownPress, 240 details, 241 ); 242 this.elementRef.nativeElement.dispatchEvent(arrowEvent); 243 } else { 244 const arrowEvent = new CustomEvent(ViewerEvents.ArrowUpPress, details); 245 this.elementRef.nativeElement.dispatchEvent(arrowEvent); 246 } 247 } 248 } 249} 250