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