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// | Probe: the rectangular box on the right-hand-side with a toggle box. | 31// +---------------------------------------------------------------------------+ 32 33export interface ProbeAttrs { 34 title: string; 35 img: string; 36 descr: string; 37 isEnabled: Getter<boolean>; 38 setEnabled: Setter<boolean>; 39} 40 41export class Probe implements m.ClassComponent<ProbeAttrs> { 42 view({attrs, children}: m.CVnode<ProbeAttrs>) { 43 const onToggle = (enabled: boolean) => { 44 const traceCfg = produce(globals.state.recordConfig, draft => { 45 attrs.setEnabled(draft, enabled); 46 }); 47 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 48 }; 49 50 const enabled = attrs.isEnabled(globals.state.recordConfig); 51 52 return m( 53 `.probe${enabled ? '.enabled' : ''}`, 54 m(`img[src=assets/${attrs.img}]`, {onclick: () => onToggle(!enabled)}), 55 m('label', 56 m(`input[type=checkbox]`, 57 {checked: enabled, oninput: m.withAttr('checked', onToggle)}), 58 m('span', attrs.title)), 59 m('div', m('div', attrs.descr), m('.probe-config', children))); 60 } 61} 62 63// +---------------------------------------------------------------------------+ 64// | Slider: draggable horizontal slider with numeric spinner. | 65// +---------------------------------------------------------------------------+ 66 67export interface SliderAttrs { 68 title: string; 69 icon?: string; 70 cssClass?: string; 71 isTime?: boolean; 72 unit: string; 73 values: number[]; 74 get: Getter<number>; 75 set: Setter<number>; 76} 77 78export class Slider implements m.ClassComponent<SliderAttrs> { 79 onValueChange(attrs: SliderAttrs, newVal: number) { 80 const traceCfg = produce(globals.state.recordConfig, draft => { 81 attrs.set(draft, newVal); 82 }); 83 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 84 } 85 86 87 onTimeValueChange(attrs: SliderAttrs, hms: string) { 88 try { 89 const date = new Date(`1970-01-01T${hms}.000Z`); 90 this.onValueChange(attrs, date.getTime()); 91 } catch { 92 } 93 } 94 95 onSliderChange(attrs: SliderAttrs, newIdx: number) { 96 this.onValueChange(attrs, attrs.values[newIdx]); 97 } 98 99 view({attrs}: m.CVnode<SliderAttrs>) { 100 const id = attrs.title.replace(/[^a-z0-9]/gmi, '_').toLowerCase(); 101 const maxIdx = attrs.values.length - 1; 102 const val = attrs.get(globals.state.recordConfig); 103 104 // Find the index of the closest value in the slider. 105 let idx = 0; 106 for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) { 107 } 108 109 let spinnerCfg = {}; 110 if (attrs.isTime) { 111 spinnerCfg = { 112 type: 'text', 113 pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss 114 value: new Date(val).toISOString().substr(11, 8), 115 oninput: m.withAttr('value', v => this.onTimeValueChange(attrs, v)) 116 }; 117 } else { 118 spinnerCfg = { 119 type: 'number', 120 value: val, 121 oninput: m.withAttr('value', v => this.onValueChange(attrs, v)) 122 }; 123 } 124 return m( 125 '.slider' + (attrs.cssClass || ''), 126 m('header', attrs.title), 127 attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [], 128 m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]`, 129 {oninput: m.withAttr('value', v => this.onSliderChange(attrs, v))}), 130 m(`input.spinner[min=1][for=${id}]`, spinnerCfg), 131 m('.unit', attrs.unit)); 132 } 133} 134 135// +---------------------------------------------------------------------------+ 136// | Dropdown: wrapper around <select>. Supports single an multiple selection. | 137// +---------------------------------------------------------------------------+ 138 139export interface DropdownAttrs { 140 title: string; 141 cssClass?: string; 142 options: Map<string, string>; 143 get: Getter<string[]>; 144 set: Setter<string[]>; 145} 146 147export class Dropdown implements m.ClassComponent<DropdownAttrs> { 148 resetScroll(dom: HTMLSelectElement) { 149 // Chrome seems to override the scroll offset on creation without this, 150 // even though we call it after having marked the options as selected. 151 setTimeout(() => { 152 // Don't reset the scroll position if the element is still focused. 153 if (dom !== document.activeElement) dom.scrollTop = 0; 154 }, 0); 155 } 156 157 onChange(attrs: DropdownAttrs, e: Event) { 158 const dom = e.target as HTMLSelectElement; 159 const selKeys: string[] = []; 160 for (let i = 0; i < dom.selectedOptions.length; i++) { 161 const item = assertExists(dom.selectedOptions.item(i)); 162 selKeys.push(item.value); 163 } 164 const traceCfg = produce(globals.state.recordConfig, draft => { 165 attrs.set(draft, selKeys); 166 }); 167 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 168 } 169 170 view({attrs}: m.CVnode<DropdownAttrs>) { 171 const options: m.Children = []; 172 const selItems = attrs.get(globals.state.recordConfig); 173 let numSelected = 0; 174 for (const [key, label] of attrs.options) { 175 const opts = {value: key, selected: false}; 176 if (selItems.includes(key)) { 177 opts.selected = true; 178 numSelected++; 179 } 180 options.push(m('option', opts, label)); 181 } 182 const label = `${attrs.title} ${numSelected ? `(${numSelected})` : ''}`; 183 return m( 184 `select.dropdown${attrs.cssClass || ''}[multiple=multiple]`, 185 { 186 onblur: (e: Event) => this.resetScroll(e.target as HTMLSelectElement), 187 onmouseleave: (e: Event) => 188 this.resetScroll(e.target as HTMLSelectElement), 189 oninput: (e: Event) => this.onChange(attrs, e), 190 oncreate: (vnode) => this.resetScroll(vnode.dom as HTMLSelectElement), 191 }, 192 m('optgroup', {label}, options)); 193 } 194} 195 196 197// +---------------------------------------------------------------------------+ 198// | Textarea: wrapper around <textarea>. | 199// +---------------------------------------------------------------------------+ 200 201export interface TextareaAttrs { 202 placeholder: string; 203 cssClass?: string; 204 get: Getter<string>; 205 set: Setter<string>; 206} 207 208export class Textarea implements m.ClassComponent<TextareaAttrs> { 209 onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) { 210 const traceCfg = produce(globals.state.recordConfig, draft => { 211 attrs.set(draft, dom.value); 212 }); 213 globals.dispatch(Actions.setRecordConfig({config: traceCfg})); 214 } 215 216 view({attrs}: m.CVnode<TextareaAttrs>) { 217 return m(`textarea.extra-input${attrs.cssClass || ''}`, { 218 onchange: (e: Event) => 219 this.onChange(attrs, e.target as HTMLTextAreaElement), 220 placeholder: attrs.placeholder, 221 value: attrs.get(globals.state.recordConfig) 222 }); 223 } 224} 225 226// +---------------------------------------------------------------------------+ 227// | CodeSnippet: command-prompt-like box with code snippets to copy/paste. | 228// +---------------------------------------------------------------------------+ 229 230export interface CodeSnippetAttrs { 231 text: string; 232 hardWhitespace?: boolean; 233} 234 235export class CodeSnippet implements m.ClassComponent<CodeSnippetAttrs> { 236 view({attrs}: m.CVnode<CodeSnippetAttrs>) { 237 return m( 238 '.code-snippet', 239 m('button', 240 { 241 title: 'Copy to clipboard', 242 onclick: () => copyToClipboard(attrs.text), 243 }, 244 m('i.material-icons', 'assignment')), 245 m('code', 246 { 247 style: { 248 'white-space': attrs.hardWhitespace ? 'pre' : null, 249 }, 250 }, 251 attrs.text), ); 252 } 253} 254