1/* 2 * Copyright (C) 2022 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 Inject, 21 Input, 22 SimpleChanges, 23} from '@angular/core'; 24import {assertDefined} from 'common/assert_utils'; 25import {FunctionUtils} from 'common/function_utils'; 26import {PersistentStore} from 'common/persistent_store'; 27import {Analytics} from 'logging/analytics'; 28import { 29 TabbedViewSwitched, 30 WinscopeEvent, 31 WinscopeEventType, 32} from 'messaging/winscope_event'; 33import { 34 EmitEvent, 35 WinscopeEventEmitter, 36} from 'messaging/winscope_event_emitter'; 37import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 38import {TRACE_INFO} from 'trace/trace_info'; 39import {View, Viewer, ViewType} from 'viewers/viewer'; 40 41interface Tab { 42 view: View; 43 addedToDom: boolean; 44} 45 46@Component({ 47 selector: 'trace-view', 48 template: ` 49 <div class="overlay-container"> 50 </div> 51 <div class="header-items-wrapper"> 52 <nav mat-tab-nav-bar class="tabs-navigation-bar"> 53 <a 54 *ngFor="let tab of tabs; last as isLast" 55 mat-tab-link 56 [active]="isCurrentActiveTab(tab)" 57 [class.active]="isCurrentActiveTab(tab)" 58 (click)="onTabClick(tab)" 59 (focus)="$event.target.blur()" 60 [class.last]="isLast" 61 class="tab"> 62 <mat-icon 63 class="icon" 64 [style]="{color: TRACE_INFO[tab.view.traces[0].type].color, marginRight: '0.5rem'}"> 65 {{ TRACE_INFO[tab.view.traces[0].type].icon }} 66 </mat-icon> 67 <span> 68 {{ tab.view.title }} 69 </span> 70 </a> 71 </nav> 72 </div> 73 <mat-divider></mat-divider> 74 <div class="trace-view-content"></div> 75 `, 76 styles: [ 77 ` 78 .tab.active { 79 opacity: 100%; 80 } 81 82 .header-items-wrapper { 83 display: flex; 84 flex-direction: row; 85 justify-content: space-between; 86 } 87 88 .tabs-navigation-bar { 89 height: 100%; 90 border-bottom: 0px; 91 } 92 93 .trace-view-content { 94 height: 100%; 95 overflow: auto; 96 background-color: var(--trace-view-background-color); 97 } 98 99 .tab { 100 overflow-x: hidden; 101 text-overflow: ellipsis; 102 } 103 104 .tab:not(.last):after { 105 content: ''; 106 position: absolute; 107 right: 0; 108 height: 60%; 109 width: 1px; 110 background-color: #C4C0C0; 111 } 112 `, 113 ], 114}) 115export class TraceViewComponent 116 implements WinscopeEventEmitter, WinscopeEventListener 117{ 118 @Input() viewers: Viewer[] = []; 119 @Input() store: PersistentStore | undefined; 120 121 TRACE_INFO = TRACE_INFO; 122 tabs: Tab[] = []; 123 124 private elementRef: ElementRef; 125 private currentActiveTab: undefined | Tab; 126 private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 127 128 constructor(@Inject(ElementRef) elementRef: ElementRef) { 129 this.elementRef = elementRef; 130 } 131 132 ngOnChanges(changes: SimpleChanges) { 133 this.renderViewsTab(changes['viewers']?.firstChange ?? false); 134 this.renderViewsOverlay(); 135 } 136 137 async onTabClick(tab: Tab) { 138 await this.showTab(tab, false); 139 } 140 141 async onWinscopeEvent(event: WinscopeEvent) { 142 await event.visit( 143 WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST, 144 async (event) => { 145 const tab = this.tabs.find((tab) => 146 tab.view.traces.some((trace) => trace === event.newActiveTrace), 147 ); 148 await this.showTab(assertDefined(tab), false); 149 }, 150 ); 151 } 152 153 setEmitEvent(callback: EmitEvent) { 154 this.emitAppEvent = callback; 155 } 156 157 isCurrentActiveTab(tab: Tab) { 158 return tab === this.currentActiveTab; 159 } 160 161 private renderViewsTab(firstToRender: boolean) { 162 this.tabs = this.viewers 163 .map((viewer) => viewer.getViews()) 164 .flat() 165 .filter((view) => view.type === ViewType.TAB) 166 .map((view) => { 167 return { 168 view, 169 addedToDom: false, 170 }; 171 }); 172 173 this.tabs.forEach((tab) => { 174 // TODO: setting "store" this way is a hack. 175 // Store should be part of View's interface. 176 (tab.view.htmlElement as any).store = this.store; 177 }); 178 179 if (this.tabs.length > 0) { 180 this.showTab(this.tabs[0], firstToRender); 181 } 182 } 183 184 private renderViewsOverlay() { 185 const views: View[] = this.viewers 186 .map((viewer) => viewer.getViews()) 187 .flat() 188 .filter((view) => view.type === ViewType.OVERLAY); 189 190 if (views.length > 1) { 191 throw new Error( 192 'Only one overlay view is supported. To allow more overlay views, either create more than' + 193 ' one draggable containers in this component or move the cdkDrag directives into the' + 194 " overlay view when the new Angular's directive composition API is available" + 195 ' (https://github.com/angular/angular/issues/8785).', 196 ); 197 } 198 199 views.forEach((view) => { 200 view.htmlElement.style.pointerEvents = 'all'; 201 const container = assertDefined( 202 this.elementRef.nativeElement.querySelector('.overlay-container'), 203 ); 204 container.appendChild(view.htmlElement); 205 }); 206 } 207 208 private async showTab(tab: Tab, firstToRender: boolean) { 209 if (this.currentActiveTab) { 210 this.currentActiveTab.view.htmlElement.style.display = 'none'; 211 } 212 213 if (!tab.addedToDom) { 214 // Workaround for b/255966194: 215 // make sure that the first time a tab content is rendered 216 // (added to the DOM) it has style.display == "". This fixes the 217 // initialization/rendering issues with cdk-virtual-scroll-viewport 218 // components inside the tab contents. 219 const traceViewContent = assertDefined( 220 this.elementRef.nativeElement.querySelector('.trace-view-content'), 221 ); 222 traceViewContent.appendChild(tab.view.htmlElement); 223 tab.addedToDom = true; 224 } else { 225 tab.view.htmlElement.style.display = ''; 226 } 227 228 this.currentActiveTab = tab; 229 230 if (!firstToRender) { 231 Analytics.Navigation.logTabSwitched( 232 TRACE_INFO[tab.view.traces[0].type].name, 233 ); 234 await this.emitAppEvent(new TabbedViewSwitched(tab.view)); 235 } 236 } 237} 238