• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {Store} from 'common/store/store';
29import {
30  CheckboxConfiguration,
31  SelectionConfiguration,
32  TraceConfigurationMap,
33  updateConfigsFromStore,
34} from 'trace_collection/ui/ui_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].config.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.checkboxConfigs.length > 0"
60        class="enable-config-opt">
61        <mat-checkbox
62          *ngFor="let checkboxConfig of getSortedConfigs(this.traceConfig[traceKey].config.checkboxConfigs)"
63          color="primary"
64          class="enable-config"
65          [disabled]="!this.traceConfig[traceKey].config.enabled"
66          [(ngModel)]="checkboxConfig.enabled"
67          (ngModelChange)="onTraceConfigChange()"
68          >{{ checkboxConfig.name }}</mat-checkbox
69        >
70      </div>
71
72      <div
73        *ngIf="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].config.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
159  @Input() title: string | undefined;
160  @Input() traceConfigStoreKey: string | undefined;
161  @Input() traceConfig: TraceConfigurationMap | undefined;
162  @Input() storage: Store | undefined;
163  @Output() readonly traceConfigChange =
164    new EventEmitter<TraceConfigurationMap>();
165
166  private tooltipsWithStablePosition = new Set<string>();
167
168  constructor(
169    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
170    @Inject(NgZone) private ngZone: NgZone,
171  ) {}
172
173  ngOnInit() {
174    this.traceConfig = updateConfigsFromStore(
175      assertDefined(
176        JSON.parse(JSON.stringify(assertDefined(this.traceConfig))),
177        () => 'component initialized without config',
178      ),
179      assertDefined(this.storage),
180      assertDefined(this.traceConfigStoreKey),
181    );
182    if (globalConfig.MODE !== 'KARMA_TEST') {
183      this.changeDetectionWorker = window.setInterval(
184        () => this.changeDetectorRef.detectChanges(),
185        200,
186      );
187    }
188    this.traceConfigChange.emit(this.traceConfig);
189  }
190
191  ngOnDestroy() {
192    window.clearInterval(this.changeDetectionWorker);
193  }
194
195  getTraceCheckboxContainerHeight(): string {
196    const config = assertDefined(this.traceConfig);
197    return Math.ceil(Object.keys(config).length / 3) * 24 + 'px';
198  }
199
200  getSortedTraceKeys(): string[] {
201    const config = assertDefined(this.traceConfig);
202    return Object.keys(config).sort((a, b) => {
203      return config[a].name < config[b].name ? -1 : 1;
204    });
205  }
206
207  getSortedConfigKeys(): string[] {
208    const advancedConfigs: string[] = [];
209    Object.keys(assertDefined(this.traceConfig)).forEach((traceKey: string) => {
210      const c = assertDefined(this.traceConfig)[traceKey].config;
211      if (c.checkboxConfigs.length > 0 || c.selectionConfigs.length > 0) {
212        advancedConfigs.push(traceKey);
213      }
214    });
215    return advancedConfigs.sort();
216  }
217
218  getSortedConfigs(
219    configs: CheckboxConfiguration[] | SelectionConfiguration[],
220  ): CheckboxConfiguration[] | 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