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 {Component, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core'; 18import {TRACE_INFO} from 'app/trace_info'; 19import {PersistentStore} from 'common/persistent_store'; 20import {View, Viewer, ViewType} from 'viewers/viewer'; 21 22interface Tab extends View { 23 addedToDom: boolean; 24} 25 26@Component({ 27 selector: 'trace-view', 28 template: ` 29 <div class="overlay"> 30 <div class="draggable-container" cdkDrag cdkDragBoundary=".overlay"> 31 <!-- 32 TODO: 33 this draggable div is a temporary hack. We should remove the div and move the cdkDrag 34 directives into the overlay view (e.g. ViewerScreenReocordingComponent) as soon as the new 35 Angular's directive composition API is available 36 (https://github.com/angular/angular/issues/8785). 37 --> 38 </div> 39 </div> 40 <div class="header-items-wrapper"> 41 <nav mat-tab-nav-bar class="tabs-navigation-bar"> 42 <a 43 *ngFor="let tab of tabs" 44 mat-tab-link 45 [active]="isCurrentActiveTab(tab)" 46 (click)="onTabClick(tab)" 47 class="tab"> 48 <mat-icon 49 class="icon" 50 [matTooltip]="TRACE_INFO[tab.traceType].name" 51 [style]="{color: TRACE_INFO[tab.traceType].color, marginRight: '0.5rem'}"> 52 {{ TRACE_INFO[tab.traceType].icon }} 53 </mat-icon> 54 <p> 55 {{ tab.title }} 56 </p> 57 </a> 58 </nav> 59 <button 60 color="primary" 61 mat-button 62 class="save-button" 63 (click)="downloadTracesButtonClick.emit()"> 64 Download all traces 65 </button> 66 </div> 67 <mat-divider></mat-divider> 68 <div class="trace-view-content"></div> 69 `, 70 styles: [ 71 ` 72 .overlay { 73 z-index: 10; 74 position: fixed; 75 top: 0px; 76 left: 0px; 77 width: 100%; 78 height: 100%; 79 pointer-events: none; 80 } 81 82 .overlay .draggable-container { 83 position: absolute; 84 right: 0; 85 top: 20vh; 86 } 87 88 .header-items-wrapper { 89 display: flex; 90 flex-direction: row; 91 justify-content: space-between; 92 } 93 94 .tabs-navigation-bar { 95 height: 100%; 96 } 97 98 .trace-view-content { 99 height: 100%; 100 overflow: auto; 101 } 102 `, 103 ], 104}) 105export class TraceViewComponent { 106 @Input() viewers!: Viewer[]; 107 @Input() store!: PersistentStore; 108 @Output() downloadTracesButtonClick = new EventEmitter<void>(); 109 @Output() activeViewChanged = new EventEmitter<View>(); 110 111 TRACE_INFO = TRACE_INFO; 112 113 private elementRef: ElementRef; 114 115 tabs: Tab[] = []; 116 private currentActiveTab: undefined | Tab; 117 118 constructor(@Inject(ElementRef) elementRef: ElementRef) { 119 this.elementRef = elementRef; 120 } 121 122 ngOnChanges() { 123 this.renderViewsTab(); 124 this.renderViewsOverlay(); 125 } 126 127 onTabClick(tab: Tab) { 128 this.showTab(tab); 129 } 130 131 private renderViewsTab() { 132 this.tabs = this.viewers 133 .map((viewer) => viewer.getViews()) 134 .flat() 135 .filter((view) => view.type === ViewType.TAB) 136 .map((view) => { 137 return { 138 type: view.type, 139 htmlElement: view.htmlElement, 140 title: view.title, 141 addedToDom: false, 142 dependencies: view.dependencies, 143 traceType: view.traceType, 144 }; 145 }); 146 147 this.tabs.forEach((tab) => { 148 // TODO: setting "store" this way is a hack. 149 // Store should be part of View's interface. 150 (tab.htmlElement as any).store = this.store; 151 }); 152 153 if (this.tabs.length > 0) { 154 this.showTab(this.tabs[0]); 155 } 156 } 157 158 private renderViewsOverlay() { 159 const views: View[] = this.viewers 160 .map((viewer) => viewer.getViews()) 161 .flat() 162 .filter((view) => view.type === ViewType.OVERLAY); 163 164 if (views.length > 1) { 165 throw new Error( 166 'Only one overlay view is supported. To allow more overlay views, either create more than' + 167 ' one draggable containers in this component or move the cdkDrag directives into the' + 168 " overlay view when the new Angular's directive composition API is available" + 169 ' (https://github.com/angular/angular/issues/8785).' 170 ); 171 } 172 173 views.forEach((view) => { 174 view.htmlElement.style.pointerEvents = 'all'; 175 const container = this.elementRef.nativeElement.querySelector( 176 '.overlay .draggable-container' 177 )!; 178 container.appendChild(view.htmlElement); 179 }); 180 } 181 182 private showTab(tab: Tab) { 183 if (this.currentActiveTab) { 184 this.currentActiveTab.htmlElement.style.display = 'none'; 185 } 186 187 if (!tab.addedToDom) { 188 // Workaround for b/255966194: 189 // make sure that the first time a tab content is rendered 190 // (added to the DOM) it has style.display == "". This fixes the 191 // initialization/rendering issues with cdk-virtual-scroll-viewport 192 // components inside the tab contents. 193 const traceViewContent = this.elementRef.nativeElement.querySelector('.trace-view-content')!; 194 traceViewContent.appendChild(tab.htmlElement); 195 tab.addedToDom = true; 196 } else { 197 tab.htmlElement.style.display = ''; 198 } 199 200 this.currentActiveTab = tab; 201 this.activeViewChanged.emit(tab); 202 } 203 204 isCurrentActiveTab(tab: Tab) { 205 return tab === this.currentActiveTab; 206 } 207} 208