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