• 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// | 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