• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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