1<!DOCTYPE html>
2<!--
3Copyright (c) 2013 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7<link rel="import" href="/tracing/core/filter.html">
8<link rel="import" href="/tracing/ui/base/overlay.html">
9<link rel="import" href="/tracing/ui/base/dom_helpers.html">
10<link rel="import" href="/tracing/ui/base/info_bar_group.html">
11<link rel="import" href="/tracing/ui/base/utils.html">
12
13<template id="record-selection-dialog-template">
14  <style>
15  .categories-column-view {
16    display: -webkit-flex;
17    -webkit-flex-direction: column;
18    font-family: sans-serif;
19    max-width: 640px;
20    min-height: 0;
21    min-width: 0;
22    opacity: 1;
23    transition: max-height 1s ease, max-width 1s ease, opacity 1s ease;
24    will-change: opacity;
25  }
26
27  .categories-column-view-hidden {
28    max-height: 0;
29    max-width: 0;
30    opacity: 0;
31    overflow: hidden;
32  }
33
34  .categories-selection {
35    display: -webkit-flex;
36    -webkit-flex-direction: row;
37  }
38
39  .category-presets {
40    padding: 4px;
41  }
42
43  .category-description {
44    color: #aaa;
45    font-size: small;
46    max-height: 1em;
47    opacity: 1;
48    padding-left: 4px;
49    padding-right: 4px;
50    text-align: right;
51    transition: max-height 1s ease, opacity 1s ease;
52    will-change: opacity;
53  }
54
55  .category-description-hidden {
56    max-height: 0;
57    opacity: 0;
58  }
59
60  .default-enabled-categories,
61  .default-disabled-categories {
62    -webkit-flex: 1 1 auto;
63    display: -webkit-flex;
64    -webkit-flex-direction: column;
65    padding: 4px;
66    width: 300px;
67  }
68
69  .default-enabled-categories > div,
70  .default-disabled-categories > div {
71    padding: 4px;
72  }
73
74  .tracing-modes {
75    -webkit-flex: 1 0 auto;
76    display: -webkit-flex;
77    -webkit-flex-direction: reverse;
78    padding: 4px;
79    border-bottom: 2px solid #ddd;
80    border-top: 2px solid #ddd;
81  }
82
83  .default-disabled-categories {
84    border-left: 2px solid #ddd;
85  }
86
87  .warning-default-disabled-categories {
88    display: inline-block;
89    font-weight: bold;
90    text-align: center;
91    color: #BD2E2E;
92    width: 2.0ex;
93    height: 2.0ex;
94    border-radius: 2.0ex;
95    border: 1px solid #BD2E2E;
96  }
97
98  .categories {
99    font-size: 80%;
100    padding: 10px;
101    -webkit-flex: 1 1 auto;
102  }
103
104  .group-selectors {
105    font-size: 80%;
106    border-bottom: 1px solid #ddd;
107    padding-bottom: 6px;
108    -webkit-flex: 0 0 auto;
109  }
110
111  .group-selectors button {
112    padding: 1px;
113  }
114
115  .record-selection-dialog .labeled-option-group {
116    -webkit-flex: 0 0 auto;
117    -webkit-flex-direction: column;
118    display: -webkit-flex;
119  }
120
121  .record-selection-dialog .labeled-option {
122    border-top: 5px solid white;
123    border-bottom: 5px solid white;
124  }
125
126  .record-selection-dialog .edit-categories {
127    padding-left: 6px;
128  }
129
130  .record-selection-dialog .edit-categories:after {
131    padding-left: 15px;
132    font-size: 125%;
133  }
134
135  .record-selection-dialog .labeled-option-group:not(.categories-expanded)
136      .edit-categories:after {
137    content: '\25B8'; /* Right triangle */
138  }
139
140  .record-selection-dialog .labeled-option-group.categories-expanded
141      .edit-categories:after {
142    content: '\25BE'; /* Down triangle */
143  }
144
145  </style>
146
147  <div class="record-selection-dialog">
148    <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group>
149    <div class="category-presets">
150    </div>
151    <div class="category-description"></div>
152    <div class="categories-column-view">
153      <div class="tracing-modes"></div>
154      <div class="categories-selection">
155        <div class="default-enabled-categories">
156          <div>Record&nbsp;Categories</div>
157          <div class="group-selectors">
158            Select
159            <button class="all-btn">All</button>
160            <button class="none-btn">None</button>
161          </div>
162          <div class="categories"></div>
163        </div>
164        <div class="default-disabled-categories">
165          <div>Disabled&nbsp;by&nbsp;Default&nbsp;Categories
166            <a class="warning-default-disabled-categories">!</a>
167          </div>
168          <div class="group-selectors">
169            Select
170            <button class="all-btn">All</button>
171            <button class="none-btn">None</button>
172          </div>
173          <div class="categories"></div>
174        </div>
175      </div>
176    </div>
177  </div>
178</template>
179
180<script>
181'use strict';
182
183/**
184 * @fileoverview RecordSelectionDialog presents the available categories
185 * to be enabled/disabled during tr.c.
186 */
187tr.exportTo('tr.ui.e.about_tracing', function() {
188  var THIS_DOC = document.currentScript.ownerDocument;
189  var RecordSelectionDialog = tr.ui.b.define('div');
190
191  var DEFAULT_PRESETS = [
192    {title: 'Web developer',
193      categoryFilter: ['blink', 'cc', 'netlog', 'renderer.scheduler',
194        'toplevel', 'v8']},
195    {title: 'Input latency',
196      categoryFilter: ['benchmark', 'input', 'evdev', 'renderer.scheduler',
197        'toplevel']},
198    {title: 'Rendering',
199      categoryFilter: ['blink', 'cc', 'gpu', 'toplevel']},
200    {title: 'Javascript and rendering',
201      categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', 'v8',
202        'toplevel']},
203    {title: 'Frame Viewer',
204      categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', 'v8',
205        'toplevel',
206        'disabled-by-default-cc.debug',
207        'disabled-by-default-cc.debug.picture',
208        'disabled-by-default-cc.debug.display_items']},
209    {title: 'Manually select settings',
210      categoryFilter: []}
211  ];
212  var RECORDING_MODES = [
213      {'label': 'Record until full',
214        'value': 'record-until-full'},
215      {'label': 'Record continuously',
216        'value': 'record-continuously'},
217      {'label': 'Record as much as possible',
218        'value': 'record-as-much-as-possible'}];
219  var DEFAULT_RECORD_MODE = 'record-until-full';
220  var DEFAULT_CONTINUOUS_TRACING = true;
221  var DEFAULT_SYSTEM_TRACING = true;
222  var DEFAULT_SAMPLING_TRACING = false;
223
224  RecordSelectionDialog.prototype = {
225    __proto__: tr.ui.b.Overlay.prototype,
226
227    decorate: function() {
228      tr.ui.b.Overlay.prototype.decorate.call(this);
229      this.title = 'Record a new trace...';
230
231      this.classList.add('record-dialog-overlay');
232
233      var node =
234          tr.ui.b.instantiateTemplate('#record-selection-dialog-template',
235          THIS_DOC);
236      this.appendChild(node);
237
238      this.recordButtonEl_ = document.createElement('button');
239      this.recordButtonEl_.textContent = 'Record';
240      this.recordButtonEl_.addEventListener(
241          'click',
242          this.onRecordButtonClicked_.bind(this));
243      this.recordButtonEl_.style.fontSize = '110%';
244      this.buttons.appendChild(this.recordButtonEl_);
245
246      this.categoriesView_ = this.querySelector('.categories-column-view');
247      this.presetsEl_ = this.querySelector('.category-presets');
248      this.presetsEl_.appendChild(tr.ui.b.createOptionGroup(
249          this, 'currentlyChosenPreset',
250          'about_tracing.record_selection_dialog_preset',
251          DEFAULT_PRESETS[0].categoryFilter,
252          DEFAULT_PRESETS.map(function(p) {
253            return { label: p.title, value: p.categoryFilter };
254          })));
255
256      this.tracingRecordModeSltr_ = tr.ui.b.createSelector(
257          this, 'tracingRecordMode',
258          'recordSelectionDialog.tracingRecordMode',
259          DEFAULT_RECORD_MODE, RECORDING_MODES);
260
261      this.systemTracingBn_ = tr.ui.b.createCheckBox(
262          undefined, undefined,
263          'recordSelectionDialog.useSystemTracing', DEFAULT_SYSTEM_TRACING,
264          'System tracing');
265      this.samplingTracingBn_ = tr.ui.b.createCheckBox(
266          undefined, undefined,
267          'recordSelectionDialog.useSampling', DEFAULT_SAMPLING_TRACING,
268          'State sampling');
269      this.tracingModesContainerEl_ = this.querySelector('.tracing-modes');
270      this.tracingModesContainerEl_.appendChild(this.tracingRecordModeSltr_);
271      this.tracingModesContainerEl_.appendChild(this.systemTracingBn_);
272      this.tracingModesContainerEl_.appendChild(this.samplingTracingBn_);
273
274
275      this.enabledCategoriesContainerEl_ =
276          this.querySelector('.default-enabled-categories .categories');
277
278      this.disabledCategoriesContainerEl_ =
279          this.querySelector('.default-disabled-categories .categories');
280
281      this.createGroupSelectButtons_(
282          this.querySelector('.default-enabled-categories'));
283      this.createGroupSelectButtons_(
284          this.querySelector('.default-disabled-categories'));
285      this.createDefaultDisabledWarningDialog_(
286          this.querySelector('.warning-default-disabled-categories'));
287      this.editCategoriesOpened_ = false;
288
289      // TODO(chrishenry): When used with tr.ui.b.Overlay (such as in
290      // chrome://tracing, this does not yet look quite right due to
291      // the 10px overlay content padding (but it's good enough).
292      this.infoBarGroup_ = this.querySelector('tr-ui-b-info-bar-group');
293
294      this.addEventListener('visible-change', this.onVisibleChange_.bind(this));
295    },
296
297    set supportsSystemTracing(s) {
298      if (s) {
299        this.systemTracingBn_.style.display = undefined;
300      } else {
301        this.systemTracingBn_.style.display = 'none';
302        this.useSystemTracing = false;
303      }
304    },
305
306    get tracingRecordMode() {
307      return this.tracingRecordModeSltr_.selectedValue;
308    },
309    set tracingRecordMode(value) {
310      this.tracingRecordMode_ = value;
311    },
312
313    get useSystemTracing() {
314      return this.systemTracingBn_.checked;
315    },
316    set useSystemTracing(value) {
317      this.systemTracingBn_.checked = !!value;
318    },
319
320    get useSampling() {
321      return this.samplingTracingBn_.checked;
322    },
323    set useSampling(value) {
324      this.samplingTracingBn_.checked = !!value;
325    },
326
327    set categories(c) {
328      if (!(c instanceof Array))
329        throw new Error('categories must be an array');
330      this.categories_ = c;
331
332      for (var i = 0; i < this.categories_.length; i++) {
333        var split = this.categories_[i].split(',');
334        this.categories_[i] = split.shift();
335        if (split.length > 0)
336          this.categories_ = this.categories_.concat(split);
337      }
338    },
339
340    set settings_key(k) {
341      this.settings_key_ = k;
342    },
343
344    set settings(s) {
345      throw new Error('Dont use this!');
346    },
347
348    usingPreset_: function() {
349      return this.currentlyChosenPreset_.length > 0 ||
350             this.isPresetSelected_;
351    },
352
353    get currentlyChosenPreset() {
354      return this.currentlyChosenPreset_;
355    },
356
357    set currentlyChosenPreset(preset) {
358      if (!(preset instanceof Array))
359        throw new Error('RecordSelectionDialog.currentlyChosenPreset:' +
360            ' preset must be an array.');
361      this.currentlyChosenPreset_ = preset;
362      this.isPresetSelected_ = false;
363
364      if (this.currentlyChosenPreset_.length) {
365        this.isPresetSelected_ = true;
366        this.changeEditCategoriesState_(false);
367      } else {
368        this.updateCategoryColumnView_(true);
369        this.changeEditCategoriesState_(true);
370      }
371      this.updateManualSelectionView_();
372      this.updatePresetDescription_();
373    },
374
375    updateManualSelectionView_: function() {
376      var classList = this.categoriesView_.classList;
377      if (!this.usingPreset_()) {
378        classList.remove('categories-column-view-hidden');
379      } else {
380        if (this.editCategoriesOpened_)
381          classList.remove('categories-column-view-hidden');
382        else
383          classList.add('categories-column-view-hidden');
384      }
385    },
386
387    updateCategoryColumnView_: function(shouldReadFromSettings) {
388      var categorySet = this.querySelectorAll('.categories');
389      for (var i = 0; i < categorySet.length; ++i) {
390        var categoryGroup = categorySet[i].children;
391        for (var j = 0; j < categoryGroup.length; ++j) {
392          var categoryEl = categoryGroup[j].children[0];
393          categoryEl.checked = shouldReadFromSettings ?
394              tr.b.Settings.get(categoryEl.value, false, this.settings_key_) :
395              false;
396        }
397      }
398    },
399
400    onClickEditCategories: function() {
401      if (!this.usingPreset_())
402        return;
403
404      if (!this.editCategoriesOpened_) {
405        this.updateCategoryColumnView_(false);
406        for (var i = 0; i < this.currentlyChosenPreset_.length; ++i) {
407          var categoryEl = document.getElementById(
408              this.currentlyChosenPreset_[i]);
409          if (!categoryEl)
410            continue;
411          categoryEl.checked = true;
412        }
413      }
414
415      this.changeEditCategoriesState_(!this.editCategoriesOpened_);
416      this.updateManualSelectionView_();
417      this.recordButtonEl_.focus();
418    },
419
420    changeEditCategoriesState_: function(editCategoriesState) {
421      var presetOptionsGroup = this.querySelector('.labeled-option-group');
422      if (!presetOptionsGroup)
423        return;
424
425      this.editCategoriesOpened_ = editCategoriesState;
426      if (this.editCategoriesOpened_)
427          presetOptionsGroup.classList.add('categories-expanded');
428      else
429          presetOptionsGroup.classList.remove('categories-expanded');
430    },
431
432    updatePresetDescription_: function() {
433      var description = this.querySelector('.category-description');
434      if (this.usingPreset_()) {
435        description.innerText = this.currentlyChosenPreset_;
436        description.classList.remove('category-description-hidden');
437      } else {
438        description.innerText = '';
439        if (!description.classList.contains('category-description-hidden'))
440          description.classList.add('category-description-hidden');
441      }
442    },
443
444    categoryFilter: function() {
445      if (this.usingPreset_()) {
446        var categories = [];
447        var allCategories = this.allCategories_();
448        for (var category in allCategories) {
449          var disabled = category.indexOf('disabled-by-default-') == 0;
450          if (this.currentlyChosenPreset_.indexOf(category) >= 0) {
451            if (disabled)
452              categories.push(category);
453          } else {
454            if (!disabled)
455              categories.push('-' + category);
456          }
457        }
458        return categories.join(',');
459      }
460
461      var categories = this.unselectedCategories_();
462      var categories_length = categories.length;
463      var negated_categories = [];
464      for (var i = 0; i < categories_length; ++i) {
465        // Skip any category with a , as it will cause issues when we negate.
466        // Both sides should have been added as separate categories, these can
467        // only come from settings.
468        if (categories[i].match(/,/))
469          continue;
470        negated_categories.push('-' + categories[i]);
471      }
472      categories = negated_categories.join(',');
473
474      var disabledCategories = this.enabledDisabledByDefaultCategories_();
475      disabledCategories = disabledCategories.join(',');
476
477      var results = [];
478      if (categories !== '')
479        results.push(categories);
480      if (disabledCategories !== '')
481        results.push(disabledCategories);
482      return results.join(',');
483    },
484
485    clickRecordButton: function() {
486      this.recordButtonEl_.click();
487    },
488
489    onRecordButtonClicked_: function() {
490      this.visible = false;
491      tr.b.dispatchSimpleEvent(this, 'recordclick');
492      return false;
493    },
494
495    collectInputs_: function(inputs, isChecked) {
496      var inputs_length = inputs.length;
497      var categories = [];
498      for (var i = 0; i < inputs_length; ++i) {
499        var input = inputs[i];
500        if (input.checked === isChecked)
501          categories.push(input.value);
502      }
503      return categories;
504    },
505
506    unselectedCategories_: function() {
507      var inputs =
508          this.enabledCategoriesContainerEl_.querySelectorAll('input');
509      return this.collectInputs_(inputs, false);
510    },
511
512    enabledDisabledByDefaultCategories_: function() {
513      var inputs =
514          this.disabledCategoriesContainerEl_.querySelectorAll('input');
515      return this.collectInputs_(inputs, true);
516    },
517
518    onVisibleChange_: function() {
519      if (this.visible)
520        this.updateForm_();
521    },
522
523    buildInputs_: function(inputs, checkedDefault, parent) {
524      var inputs_length = inputs.length;
525      for (var i = 0; i < inputs_length; i++) {
526        var category = inputs[i];
527
528        var inputEl = document.createElement('input');
529        inputEl.type = 'checkbox';
530        inputEl.id = category;
531        inputEl.value = category;
532
533        inputEl.checked = tr.b.Settings.get(
534            category, checkedDefault, this.settings_key_);
535        inputEl.onclick = this.updateSetting_.bind(this);
536
537        var labelEl = document.createElement('label');
538        labelEl.textContent = category.replace('disabled-by-default-', '');
539        labelEl.setAttribute('for', category);
540
541        var divEl = document.createElement('div');
542        divEl.appendChild(inputEl);
543        divEl.appendChild(labelEl);
544
545        parent.appendChild(divEl);
546      }
547    },
548
549    allCategories_: function() {
550      // Dedup the categories. We may have things in settings that are also
551      // returned when we query the category list.
552      var categorySet = {};
553      var allCategories =
554          this.categories_.concat(tr.b.Settings.keys(this.settings_key_));
555      var allCategoriesLength = allCategories.length;
556      for (var i = 0; i < allCategoriesLength; ++i)
557        categorySet[allCategories[i]] = true;
558      return categorySet;
559    },
560
561    updateForm_: function() {
562      function ignoreCaseCompare(a, b) {
563        return a.toLowerCase().localeCompare(b.toLowerCase());
564      }
565
566      this.enabledCategoriesContainerEl_.innerHTML = ''; // Clear old categories
567      this.disabledCategoriesContainerEl_.innerHTML = '';
568
569      this.recordButtonEl_.focus();
570
571      var allCategories = this.allCategories_();
572      var categories = [];
573      var disabledCategories = [];
574      for (var category in allCategories) {
575        if (category.indexOf('disabled-by-default-') == 0)
576          disabledCategories.push(category);
577        else
578          categories.push(category);
579      }
580      disabledCategories = disabledCategories.sort(ignoreCaseCompare);
581      categories = categories.sort(ignoreCaseCompare);
582
583      if (this.categories_.length == 0) {
584        this.infoBarGroup_.addMessage(
585            'No categories found; recording will use default categories.');
586      }
587
588      this.buildInputs_(categories, true, this.enabledCategoriesContainerEl_);
589
590      if (disabledCategories.length > 0) {
591        this.disabledCategoriesContainerEl_.hidden = false;
592        this.buildInputs_(disabledCategories, false,
593            this.disabledCategoriesContainerEl_);
594      }
595    },
596
597    updateSetting_: function(e) {
598      var checkbox = e.target;
599      tr.b.Settings.set(checkbox.value, checkbox.checked, this.settings_key_);
600
601      // Change the current record mode to 'Manually select settings' from
602      // preset mode if and only if currently user is in preset record mode
603      // and user selects/deselects any category in 'Edit Categories' mode.
604      if (this.usingPreset_()) {
605        if (checkbox.checked) {
606          this.currentlyChosenPreset_.push(checkbox.value);
607        } else {
608          var pos = this.currentlyChosenPreset_.lastIndexOf(checkbox.value);
609          this.currentlyChosenPreset_.splice(pos, 1);
610        }
611        var categoryEl = document.getElementById(
612            'category-preset-Manually-select-settings');
613        categoryEl.checked = true;
614        var description = this.querySelector('.category-description');
615        description.innerText = '';
616        description.classList.add('category-description-hidden');
617      }
618    },
619
620    createGroupSelectButtons_: function(parent) {
621      var flipInputs = function(dir) {
622        var inputs = parent.querySelectorAll('input');
623        for (var i = 0; i < inputs.length; i++) {
624          if (inputs[i].checked === dir)
625            continue;
626          // click() is used so the settings will be correclty stored. Setting
627          // checked does not trigger the onclick (or onchange) callback.
628          inputs[i].click();
629        }
630      };
631
632      var allBtn = parent.querySelector('.all-btn');
633      allBtn.onclick = function(evt) {
634        flipInputs(true);
635        evt.preventDefault();
636      };
637
638      var noneBtn = parent.querySelector('.none-btn');
639      noneBtn.onclick = function(evt) {
640        flipInputs(false);
641        evt.preventDefault();
642      };
643    },
644
645    setWarningDialogOverlayText_: function(messages) {
646      var contentDiv = document.createElement('div');
647
648      for (var i = 0; i < messages.length; ++i) {
649        var messageDiv = document.createElement('div');
650        messageDiv.textContent = messages[i];
651        contentDiv.appendChild(messageDiv);
652      }
653      this.warningOverlay_.textContent = '';
654      this.warningOverlay_.appendChild(contentDiv);
655    },
656
657    createDefaultDisabledWarningDialog_: function(warningLink) {
658      function onClickHandler(evt) {
659        this.warningOverlay_ = tr.ui.b.Overlay();
660        this.warningOverlay_.parentEl_ = this;
661        this.warningOverlay_.title = 'Warning...';
662        this.warningOverlay_.userCanClose = true;
663        this.warningOverlay_.visible = true;
664
665        this.setWarningDialogOverlayText_([
666          'Enabling the default disabled categories may have',
667          'performance and memory impact while tr.c.'
668        ]);
669
670        evt.preventDefault();
671      }
672      warningLink.onclick = onClickHandler.bind(this);
673    }
674  };
675
676  return {
677    RecordSelectionDialog: RecordSelectionDialog
678  };
679});
680</script>
681