// Copyright (C) 2019 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {Draft, produce} from 'immer'; import * as m from 'mithril'; import {Actions} from '../common/actions'; import {RecordConfig} from '../common/state'; import {copyToClipboard} from './clipboard'; import {globals} from './globals'; import {assertExists} from '../base/logging'; declare type Setter = (draft: Draft, val: T) => void; declare type Getter = (cfg: RecordConfig) => T; // +---------------------------------------------------------------------------+ // | Docs link with 'i' in circle icon. | // +---------------------------------------------------------------------------+ interface DocsChipAttrs { href: string; } class DocsChip implements m.ClassComponent { view({attrs}: m.CVnode) { return m( 'a.inline-chip', {href: attrs.href, title: 'Open docs in new tab', target: '_blank'}, m('i.material-icons', 'info'), ' Docs'); } } // +---------------------------------------------------------------------------+ // | Probe: the rectangular box on the right-hand-side with a toggle box. | // +---------------------------------------------------------------------------+ export interface ProbeAttrs { title: string; img: string|null; descr: m.Children; isEnabled: Getter; setEnabled: Setter; } export class Probe implements m.ClassComponent { view({attrs, children}: m.CVnode) { const onToggle = (enabled: boolean) => { const traceCfg = produce(globals.state.recordConfig, draft => { attrs.setEnabled(draft, enabled); }); globals.dispatch(Actions.setRecordConfig({config: traceCfg})); }; const enabled = attrs.isEnabled(globals.state.recordConfig); return m( `.probe${enabled ? '.enabled' : ''}`, attrs.img && m('img', { src: `${globals.root}assets/${attrs.img}`, onclick: () => onToggle(!enabled), }), m('label', m(`input[type=checkbox]`, { checked: enabled, oninput: (e: InputEvent) => { onToggle((e.target as HTMLInputElement).checked); }, }), m('span', attrs.title)), m('div', m('div', attrs.descr), m('.probe-config', children))); } } // +-------------------------------------------------------------+ // | Toggle: an on/off switch. // +-------------------------------------------------------------+ export interface ToggleAttrs { title: string; descr: string; cssClass?: string; isEnabled: Getter; setEnabled: Setter; } export class Toggle implements m.ClassComponent { view({attrs}: m.CVnode) { const onToggle = (enabled: boolean) => { const traceCfg = produce(globals.state.recordConfig, draft => { attrs.setEnabled(draft, enabled); }); globals.dispatch(Actions.setRecordConfig({config: traceCfg})); }; const enabled = attrs.isEnabled(globals.state.recordConfig); return m( `.toggle${enabled ? '.enabled' : ''}${attrs.cssClass || ''}`, m('label', m(`input[type=checkbox]`, { checked: enabled, oninput: (e: InputEvent) => { onToggle((e.target as HTMLInputElement).checked); }, }), m('span', attrs.title)), m('.descr', attrs.descr)); } } // +---------------------------------------------------------------------------+ // | Slider: draggable horizontal slider with numeric spinner. | // +---------------------------------------------------------------------------+ export interface SliderAttrs { title: string; icon?: string; cssClass?: string; isTime?: boolean; unit: string; values: number[]; get: Getter; set: Setter; min?: number; description?: string; disabled?: boolean; } export class Slider implements m.ClassComponent { onValueChange(attrs: SliderAttrs, newVal: number) { const traceCfg = produce(globals.state.recordConfig, draft => { attrs.set(draft, newVal); }); globals.dispatch(Actions.setRecordConfig({config: traceCfg})); } onTimeValueChange(attrs: SliderAttrs, hms: string) { try { const date = new Date(`1970-01-01T${hms}.000Z`); if (isNaN(date.getTime())) return; this.onValueChange(attrs, date.getTime()); } catch { } } onSliderChange(attrs: SliderAttrs, newIdx: number) { this.onValueChange(attrs, attrs.values[newIdx]); } view({attrs}: m.CVnode) { const id = attrs.title.replace(/[^a-z0-9]/gmi, '_').toLowerCase(); const maxIdx = attrs.values.length - 1; const val = attrs.get(globals.state.recordConfig); const min = attrs.min; const description = attrs.description; const disabled = attrs.disabled; // Find the index of the closest value in the slider. let idx = 0; for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) { } let spinnerCfg = {}; if (attrs.isTime) { spinnerCfg = { type: 'text', pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss value: new Date(val).toISOString().substr(11, 8), oninput: (e: InputEvent) => { this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value); }, }; } else { spinnerCfg = { type: 'number', value: val, oninput: (e: InputEvent) => { this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value); }, }; } return m( '.slider' + (attrs.cssClass || ''), m('header', attrs.title), description ? m('header.descr', attrs.description) : '', attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [], m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}] ${disabled ? '[disabled]' : ''}`, { oninput: (e: InputEvent) => { this.onSliderChange(attrs, +(e.target as HTMLInputElement).value); }, }), m(`input.spinner[min=${min !== undefined ? min : 1}][for=${id}]`, spinnerCfg), m('.unit', attrs.unit)); } } // +---------------------------------------------------------------------------+ // | Dropdown: wrapper around