xref: /aosp_15_r20/development/tools/winscope/src/app/components/trace_config_component.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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 */
16import {
17  ChangeDetectorRef,
18  Component,
19  EventEmitter,
20  Inject,
21  Input,
22  NgZone,
23  Output,
24} from '@angular/core';
25import {MatSelect, MatSelectChange} from '@angular/material/select';
26import {assertDefined} from 'common/assert_utils';
27import {globalConfig} from 'common/global_config';
28import {PersistentStoreProxy} from 'common/persistent_store_proxy';
29import {Store} from 'common/store';
30import {
31  EnableConfiguration,
32  SelectionConfiguration,
33  TraceConfigurationMap,
34} from 'trace_collection/trace_configuration';
35import {userOptionStyle} from 'viewers/components/styles/user_option.styles';
36
37@Component({
38  selector: 'trace-config',
39  template: `
40    <h3 class="mat-subheading-2">{{title}}</h3>
41
42    <div class="checkboxes" [style.height]="getTraceCheckboxContainerHeight()">
43      <mat-checkbox
44        *ngFor="let traceKey of getSortedTraceKeys()"
45        color="primary"
46        class="trace-checkbox"
47        [disabled]="!this.traceConfig[traceKey].available"
48        [(ngModel)]="this.traceConfig[traceKey].enabled"
49        (ngModelChange)="onTraceConfigChange()"
50        >{{ this.traceConfig[traceKey].name }}</mat-checkbox>
51    </div>
52
53    <ng-container *ngFor="let traceKey of getSortedConfigKeys()">
54      <mat-divider></mat-divider>
55
56      <h3 class="config-heading mat-subheading-2">{{ this.traceConfig[traceKey].name }} configuration</h3>
57
58      <div
59        *ngIf="this.traceConfig[traceKey].config && this.traceConfig[traceKey].config.enableConfigs.length > 0"
60        class="enable-config-opt">
61        <mat-checkbox
62          *ngFor="let enableConfig of getSortedConfigs(this.traceConfig[traceKey].config.enableConfigs)"
63          color="primary"
64          class="enable-config"
65          [disabled]="!this.traceConfig[traceKey].enabled"
66          [(ngModel)]="enableConfig.enabled"
67          (ngModelChange)="onTraceConfigChange()"
68          >{{ enableConfig.name }}</mat-checkbox
69        >
70      </div>
71
72      <div
73        *ngIf="this.traceConfig[traceKey].config && this.traceConfig[traceKey].config.selectionConfigs.length > 0"
74        class="selection-config-opt">
75        <ng-container *ngFor="let selectionConfig of getSortedConfigs(this.traceConfig[traceKey].config.selectionConfigs)">
76          <div class="config-selection-with-desc" [class.wide-field]="selectionConfig.wideField">
77            <mat-form-field
78              class="config-selection"
79              [class.wide-field]="selectionConfig.wideField"
80              appearance="fill">
81              <mat-label>{{ selectionConfig.name }}</mat-label>
82
83              <mat-select
84                #matSelect
85                [multiple]="isMultipleSelect(selectionConfig)"
86                disableOptionCentering
87                class="selected-value"
88                [attr.label]="traceKey + selectionConfig.name"
89                [value]="selectionConfig.value"
90                [disabled]="!this.traceConfig[traceKey].enabled || selectionConfig.options.length === 0"
91                (selectionChange)="onSelectChange($event, selectionConfig)">
92                <span class="mat-option" *ngIf="matSelect.multiple || selectionConfig.optional">
93                  <button
94                    *ngIf="matSelect.multiple"
95                    mat-flat-button
96                    class="user-option"
97                    [color]="matSelect.value.length === selectionConfig.options.length ? 'primary' : undefined"
98                    [class.not-enabled]="matSelect.value.length !== selectionConfig.options.length"
99                    (click)="onAllButtonClick(matSelect, selectionConfig)"> All </button>
100                  <button
101                    *ngIf="selectionConfig.optional && !matSelect.multiple"
102                    mat-flat-button
103                    class="user-option"
104                    [color]="matSelect.value.length === 0 ? 'primary' : undefined"
105                    [class.not-enabled]="matSelect.value.length > 0"
106                    (click)="onNoneButtonClick(matSelect, selectionConfig)"> None </button>
107                </span>
108                <mat-option
109                  *ngFor="let option of selectionConfig.options"
110                  (click)="onOptionClick(matSelect, option, traceKey + selectionConfig.name)"
111                  [value]="option"
112                  (mouseenter)="onSelectOptionHover($event, option)"
113                  [matTooltip]="option"
114                  [matTooltipDisabled]="disableOptionTooltip(option, optionEl)"
115                  matTooltipPosition="right">
116                    <span #optionEl> {{ option }} </span>
117                </mat-option>
118              </mat-select>
119            </mat-form-field>
120            <span class="config-desc" *ngIf="selectionConfig.desc"> {{selectionConfig.desc}} </span>
121          </div>
122        </ng-container>
123      </div>
124    </ng-container>
125  `,
126  styles: [
127    `
128      .checkboxes {
129        display: flex;
130        flex-direction: column;
131        flex-wrap: wrap;
132      }
133      .enable-config-opt,
134      .selection-config-opt {
135        display: flex;
136        flex-direction: row;
137        flex-wrap: wrap;
138        gap: 10px;
139      }
140      .config-selection-with-desc {
141        display: flex;
142        flex-direction: column;
143      }
144      .wide-field {
145        width: 100%;
146      }
147      .config-panel {
148        position: absolute;
149        left: 0px;
150        top: 100px;
151      }
152    `,
153    userOptionStyle,
154  ],
155})
156export class TraceConfigComponent {
157  changeDetectionWorker: number | undefined;
158  traceConfig: TraceConfigurationMap | undefined;
159
160  @Input() title: string | undefined;
161  @Input() traceConfigStoreKey: string | undefined;
162  @Input() initialTraceConfig: TraceConfigurationMap | undefined;
163  @Input() storage: Store | undefined;
164  @Output() readonly traceConfigChange =
165    new EventEmitter<TraceConfigurationMap>();
166
167  private tooltipsWithStablePosition = new Set<string>();
168
169  constructor(
170    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
171    @Inject(NgZone) private ngZone: NgZone,
172  ) {}
173
174  ngOnInit() {
175    this.traceConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
176      assertDefined(this.traceConfigStoreKey),
177      assertDefined(
178        JSON.parse(JSON.stringify(assertDefined(this.initialTraceConfig))),
179        () => 'component initialized without config',
180      ),
181      assertDefined(this.storage),
182    );
183    if (globalConfig.MODE !== 'KARMA_TEST') {
184      this.changeDetectionWorker = window.setInterval(
185        () => this.changeDetectorRef.detectChanges(),
186        200,
187      );
188    }
189    this.traceConfigChange.emit(this.traceConfig);
190  }
191
192  ngOnDestroy() {
193    window.clearInterval(this.changeDetectionWorker);
194  }
195
196  getTraceCheckboxContainerHeight(): string {
197    const config = assertDefined(this.traceConfig);
198    return Math.ceil(Object.keys(config).length / 3) * 24 + 'px';
199  }
200
201  getSortedTraceKeys(): string[] {
202    const config = assertDefined(this.traceConfig);
203    return Object.keys(config).sort((a, b) => {
204      return config[a].name < config[b].name ? -1 : 1;
205    });
206  }
207
208  getSortedConfigKeys(): string[] {
209    const advancedConfigs: string[] = [];
210    Object.keys(assertDefined(this.traceConfig)).forEach((traceKey: string) => {
211      if (assertDefined(this.traceConfig)[traceKey].config) {
212        advancedConfigs.push(traceKey);
213      }
214    });
215    return advancedConfigs.sort();
216  }
217
218  getSortedConfigs(
219    configs: EnableConfiguration[] | SelectionConfiguration[],
220  ): EnableConfiguration[] | SelectionConfiguration[] {
221    return configs.sort((a, b) => {
222      return a.name < b.name ? -1 : 1;
223    });
224  }
225
226  onSelectOptionHover(event: MouseEvent, option: string) {
227    if (this.tooltipsWithStablePosition.has(option)) {
228      return;
229    }
230    this.ngZone.run(() => {
231      (event.target as HTMLElement).dispatchEvent(new Event('mouseleave'));
232      this.tooltipsWithStablePosition.add(option);
233      this.changeDetectorRef.detectChanges();
234      (event.target as HTMLElement).dispatchEvent(new Event('mouseenter'));
235    });
236  }
237
238  disableOptionTooltip(option: string, optionText: HTMLElement): boolean {
239    const optionEl = assertDefined(optionText.parentElement);
240    return (
241      !this.tooltipsWithStablePosition.has(option) ||
242      optionEl.offsetWidth >= optionText.offsetWidth
243    );
244  }
245
246  onSelectChange(event: MatSelectChange, config: SelectionConfiguration) {
247    config.value = event.value;
248    if (!event.source.multiple) {
249      event.source.close();
250    }
251    this.onTraceConfigChange();
252  }
253
254  onNoneButtonClick(select: MatSelect, config: SelectionConfiguration) {
255    if (config.value.length > 0) {
256      select.value = '';
257      config.value = '';
258      this.onTraceConfigChange();
259    }
260  }
261
262  onAllButtonClick(select: MatSelect, config: SelectionConfiguration) {
263    if (config.value.length !== config.options.length) {
264      config.value = config.options;
265      select.value = config.options;
266    } else {
267      config.value = [];
268      select.value = [];
269    }
270    this.onTraceConfigChange();
271  }
272
273  onOptionClick(select: MatSelect, option: string, configName: string) {
274    if (select.value === option) {
275      const selectElement = assertDefined(
276        document.querySelector<HTMLElement>(
277          `mat-select[label="${configName}"]`,
278        ),
279      );
280      selectElement.blur();
281    }
282  }
283
284  onTraceConfigChange() {
285    this.traceConfigChange.emit(this.traceConfig);
286  }
287
288  isMultipleSelect(config: SelectionConfiguration): boolean {
289    return Array.isArray(config.value);
290  }
291}
292