1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {Draft, produce} from 'immer'; 16import * as m from 'mithril'; 17 18import {Actions} from '../common/actions'; 19import {RecordConfig} from '../common/state'; 20 21import {copyToClipboard} from './clipboard'; 22import {globals} from './globals'; 23import {assertExists} from '../base/logging'; 24 25 26declare type Setter<T> = (draft: Draft<RecordConfig>, val: T) => void; 27declare type Getter<T> = (cfg: RecordConfig) => T; 28 29// +---------------------------------------------------------------------------+ 30// | Docs link with 'i' in circle icon. | 31// +---------------------------------------------------------------------------+ 32 33interface DocsChipAttrs { 34 href: string; 35} 36 37class DocsChip implements m.ClassComponent<DocsChipAttrs> { 38 view({attrs}: m.CVnode<DocsChipAttrs>) { 39 return m( 40 'a.inline-chip', 41 {href: attrs.href, title: 'Open docs in new tab', target: '_blank'}, 42 m('i.material-icons', 'info'), 43 ' Docs'); 44 } 45} 46 47// +---------------------------------------------------------------------------+ 48// | Probe: the rectangular box on the right-hand-side with a toggle box. | 49// +---------------------------------------------------------------------------+ 50 51export interface ProbeAttrs { 52 title: string; 53 img: string|null; 54 descr: m.Children; 55 isEnabled: Getter<boolean>; 56 setEnabled: Setter<boolean>; 57} 58 59export class Probe implements m.ClassComponent<ProbeAttrs> { 60 view({attrs, children}: m.CVnode<ProbeAttrs>) { 61 const onToggle = (enabled: boolean) => { 62 const traceCfg = produce(globals.state.recordConfig, draft => { 63 attrs.setEnabled(draft, enabled); 64 }); 65 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 66 }; 67 68 const enabled = attrs.isEnabled(globals.state.recordConfig); 69 70 return m( 71 `.probe${enabled ? '.enabled' : ''}`, 72 attrs.img && m('img', { 73 src: `${globals.root}assets/${attrs.img}`, 74 onclick: () => onToggle(!enabled), 75 }), 76 m('label', 77 m(`input[type=checkbox]`, { 78 checked: enabled, 79 oninput: (e: InputEvent) => { 80 onToggle((e.target as HTMLInputElement).checked); 81 }, 82 }), 83 m('span', attrs.title)), 84 m('div', m('div', attrs.descr), m('.probe-config', children))); 85 } 86} 87 88// +-------------------------------------------------------------+ 89// | Toggle: an on/off switch. 90// +-------------------------------------------------------------+ 91 92export interface ToggleAttrs { 93 title: string; 94 descr: string; 95 cssClass?: string; 96 isEnabled: Getter<boolean>; 97 setEnabled: Setter<boolean>; 98} 99 100export class Toggle implements m.ClassComponent<ToggleAttrs> { 101 view({attrs}: m.CVnode<ToggleAttrs>) { 102 const onToggle = (enabled: boolean) => { 103 const traceCfg = produce(globals.state.recordConfig, draft => { 104 attrs.setEnabled(draft, enabled); 105 }); 106 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 107 }; 108 109 const enabled = attrs.isEnabled(globals.state.recordConfig); 110 111 return m( 112 `.toggle${enabled ? '.enabled' : ''}${attrs.cssClass || ''}`, 113 m('label', 114 m(`input[type=checkbox]`, { 115 checked: enabled, 116 oninput: (e: InputEvent) => { 117 onToggle((e.target as HTMLInputElement).checked); 118 }, 119 }), 120 m('span', attrs.title)), 121 m('.descr', attrs.descr)); 122 } 123} 124 125// +---------------------------------------------------------------------------+ 126// | Slider: draggable horizontal slider with numeric spinner. | 127// +---------------------------------------------------------------------------+ 128 129export interface SliderAttrs { 130 title: string; 131 icon?: string; 132 cssClass?: string; 133 isTime?: boolean; 134 unit: string; 135 values: number[]; 136 get: Getter<number>; 137 set: Setter<number>; 138 min?: number; 139 description?: string; 140 disabled?: boolean; 141} 142 143export class Slider implements m.ClassComponent<SliderAttrs> { 144 onValueChange(attrs: SliderAttrs, newVal: number) { 145 const traceCfg = produce(globals.state.recordConfig, draft => { 146 attrs.set(draft, newVal); 147 }); 148 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 149 } 150 151 152 onTimeValueChange(attrs: SliderAttrs, hms: string) { 153 try { 154 const date = new Date(`1970-01-01T${hms}.000Z`); 155 if (isNaN(date.getTime())) return; 156 this.onValueChange(attrs, date.getTime()); 157 } catch { 158 } 159 } 160 161 onSliderChange(attrs: SliderAttrs, newIdx: number) { 162 this.onValueChange(attrs, attrs.values[newIdx]); 163 } 164 165 view({attrs}: m.CVnode<SliderAttrs>) { 166 const id = attrs.title.replace(/[^a-z0-9]/gmi, '_').toLowerCase(); 167 const maxIdx = attrs.values.length - 1; 168 const val = attrs.get(globals.state.recordConfig); 169 const min = attrs.min; 170 const description = attrs.description; 171 const disabled = attrs.disabled; 172 173 // Find the index of the closest value in the slider. 174 let idx = 0; 175 for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) { 176 } 177 178 let spinnerCfg = {}; 179 if (attrs.isTime) { 180 spinnerCfg = { 181 type: 'text', 182 pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss 183 value: new Date(val).toISOString().substr(11, 8), 184 oninput: (e: InputEvent) => { 185 this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value); 186 }, 187 }; 188 } else { 189 spinnerCfg = { 190 type: 'number', 191 value: val, 192 oninput: (e: InputEvent) => { 193 this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value); 194 }, 195 }; 196 } 197 return m( 198 '.slider' + (attrs.cssClass || ''), 199 m('header', attrs.title), 200 description ? m('header.descr', attrs.description) : '', 201 attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [], 202 m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}] 203 ${disabled ? '[disabled]' : ''}`, 204 { 205 oninput: (e: InputEvent) => { 206 this.onSliderChange(attrs, +(e.target as HTMLInputElement).value); 207 }, 208 }), 209 m(`input.spinner[min=${min !== undefined ? min : 1}][for=${id}]`, 210 spinnerCfg), 211 m('.unit', attrs.unit)); 212 } 213} 214 215// +---------------------------------------------------------------------------+ 216// | Dropdown: wrapper around <select>. Supports single an multiple selection. | 217// +---------------------------------------------------------------------------+ 218 219export interface DropdownAttrs { 220 title: string; 221 cssClass?: string; 222 options: Map<string, string>; 223 get: Getter<string[]>; 224 set: Setter<string[]>; 225} 226 227export class Dropdown implements m.ClassComponent<DropdownAttrs> { 228 resetScroll(dom: HTMLSelectElement) { 229 // Chrome seems to override the scroll offset on creation without this, 230 // even though we call it after having marked the options as selected. 231 setTimeout(() => { 232 // Don't reset the scroll position if the element is still focused. 233 if (dom !== document.activeElement) dom.scrollTop = 0; 234 }, 0); 235 } 236 237 onChange(attrs: DropdownAttrs, e: Event) { 238 const dom = e.target as HTMLSelectElement; 239 const selKeys: string[] = []; 240 for (let i = 0; i < dom.selectedOptions.length; i++) { 241 const item = assertExists(dom.selectedOptions.item(i)); 242 selKeys.push(item.value); 243 } 244 const traceCfg = produce(globals.state.recordConfig, draft => { 245 attrs.set(draft, selKeys); 246 }); 247 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 248 } 249 250 view({attrs}: m.CVnode<DropdownAttrs>) { 251 const options: m.Children = []; 252 const selItems = attrs.get(globals.state.recordConfig); 253 let numSelected = 0; 254 const entries = [...attrs.options.entries()]; 255 entries.sort((a, b) => a[1].localeCompare(b[1])); 256 for (const [key, label] of entries) { 257 const opts = {value: key, selected: false}; 258 if (selItems.includes(key)) { 259 opts.selected = true; 260 numSelected++; 261 } 262 options.push(m('option', opts, label)); 263 } 264 const label = `${attrs.title} ${numSelected ? `(${numSelected})` : ''}`; 265 return m( 266 `select.dropdown${attrs.cssClass || ''}[multiple=multiple]`, 267 { 268 onblur: (e: Event) => this.resetScroll(e.target as HTMLSelectElement), 269 onmouseleave: (e: Event) => 270 this.resetScroll(e.target as HTMLSelectElement), 271 oninput: (e: Event) => this.onChange(attrs, e), 272 oncreate: (vnode) => this.resetScroll(vnode.dom as HTMLSelectElement), 273 }, 274 m('optgroup', {label}, options)); 275 } 276} 277 278 279// +---------------------------------------------------------------------------+ 280// | Textarea: wrapper around <textarea>. | 281// +---------------------------------------------------------------------------+ 282 283export interface TextareaAttrs { 284 placeholder: string; 285 docsLink?: string; 286 cssClass?: string; 287 get: Getter<string>; 288 set: Setter<string>; 289 title?: string; 290} 291 292export class Textarea implements m.ClassComponent<TextareaAttrs> { 293 onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) { 294 const traceCfg = produce(globals.state.recordConfig, draft => { 295 attrs.set(draft, dom.value); 296 }); 297 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 298 } 299 300 view({attrs}: m.CVnode<TextareaAttrs>) { 301 return m( 302 '.textarea-holder', 303 m('header', 304 attrs.title, 305 attrs.docsLink && [' ', m(DocsChip, {href: attrs.docsLink})]), 306 m(`textarea.extra-input${attrs.cssClass || ''}`, { 307 onchange: (e: Event) => 308 this.onChange(attrs, e.target as HTMLTextAreaElement), 309 placeholder: attrs.placeholder, 310 value: attrs.get(globals.state.recordConfig) 311 })); 312 } 313} 314 315// +---------------------------------------------------------------------------+ 316// | CodeSnippet: command-prompt-like box with code snippets to copy/paste. | 317// +---------------------------------------------------------------------------+ 318 319export interface CodeSnippetAttrs { 320 text: string; 321 hardWhitespace?: boolean; 322} 323 324export class CodeSnippet implements m.ClassComponent<CodeSnippetAttrs> { 325 view({attrs}: m.CVnode<CodeSnippetAttrs>) { 326 return m( 327 '.code-snippet', 328 m('button', 329 { 330 title: 'Copy to clipboard', 331 onclick: () => copyToClipboard(attrs.text), 332 }, 333 m('i.material-icons', 'assignment')), 334 m('code', attrs.text), 335 ); 336 } 337} 338