• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7/**
8 * @fileoverview View visualizes TRACE_EVENT events using the
9 * tracing.Timeline component and adds in selection summary and control buttons.
10 */
11base.requireStylesheet('tracing.timeline_view');
12base.requireTemplate('tracing.timeline_view');
13
14base.require('base.utils');
15base.require('base.settings');
16base.require('tracing.analysis.analysis_view');
17base.require('tracing.category_filter_dialog');
18base.require('tracing.filter');
19base.require('tracing.find_control');
20base.require('tracing.timeline_track_view');
21base.require('ui.overlay');
22base.require('ui.drag_handle');
23
24base.exportTo('tracing', function() {
25
26  /**
27   * View
28   * @constructor
29   * @extends {HTMLDivElement}
30   */
31  var TimelineView = ui.define('div');
32
33  TimelineView.prototype = {
34    __proto__: HTMLDivElement.prototype,
35
36    decorate: function() {
37      this.classList.add('timeline-view');
38
39      var node = base.instantiateTemplate('#timeline-view-template');
40      this.appendChild(node);
41
42      this.titleEl_ = this.querySelector('.title');
43      this.leftControlsEl_ = this.querySelector('#left-controls');
44      this.rightControlsEl_ = this.querySelector('#right-controls');
45      this.timelineContainer_ = this.querySelector('.container');
46
47      this.categoryFilterButton_ = this.createCategoryFilterButton_();
48      this.categoryFilterButton_.callback =
49          this.updateCategoryFilter_.bind(this);
50
51      this.findCtl_ = new tracing.FindControl();
52      this.findCtl_.controller = new tracing.FindController();
53
54      this.rightControls.appendChild(this.createImportErrorsButton_());
55      this.rightControls.appendChild(this.categoryFilterButton_);
56      this.rightControls.appendChild(this.createMetadataButton_());
57      this.rightControls.appendChild(this.findCtl_);
58      this.rightControls.appendChild(this.createHelpButton_());
59
60      this.dragEl_ = new ui.DragHandle();
61      this.appendChild(this.dragEl_);
62
63      this.analysisEl_ = new tracing.analysis.AnalysisView();
64      this.analysisEl_.addEventListener(
65          'requestSelectionChange',
66          this.onRequestSelectionChange_.bind(this));
67      this.appendChild(this.analysisEl_);
68
69      // Bookkeeping.
70      this.onSelectionChanged_ = this.onSelectionChanged_.bind(this);
71      document.addEventListener('keypress', this.onKeypress_.bind(this), true);
72
73      this.dragEl_.target = this.analysisEl_;
74    },
75
76    createImportErrorsButton_: function() {
77      var node = base.instantiateTemplate('#import-errors-btn-template');
78      var showEl = node.querySelector('.view-import-errors-button');
79      var containerEl = node.querySelector('.info-button-container');
80      var textEl = containerEl.querySelector('.info-button-text');
81
82      var dlg = new ui.Overlay();
83      dlg.classList.add('view-import-errors-overlay');
84      dlg.obeyCloseEvents = true;
85      dlg.appendChild(containerEl);
86
87      function onClick() {
88        dlg.visible = true;
89        textEl.textContent = this.model.importErrors.join('\n');
90      }
91      showEl.addEventListener('click', onClick.bind(this));
92
93      function updateVisibility() {
94        showEl.style.display =
95            (this.model && this.model.importErrors.length) ? '' : 'none';
96      }
97      var updateVisibility_ = updateVisibility.bind(this);
98      updateVisibility_();
99      this.addEventListener('modelChange', updateVisibility_);
100
101      return showEl;
102    },
103
104    updateCategoryFilter_: function(categories) {
105      if (!this.timeline_)
106        return;
107      this.timeline_.categoryFilter = new tracing.CategoryFilter(categories);
108    },
109
110    createCategoryFilterButton_: function() {
111      var node = base.instantiateTemplate('#category-filter-btn-template');
112      var showEl = node.querySelector('.view-info-button');
113
114      function onClick() {
115        var dlg = new tracing.CategoryFilterDialog();
116        dlg.categories = this.model.categories;
117        dlg.settings_key = 'categories';
118        dlg.settingUpdatedCallback = this.updateCategoryFilter_.bind(this);
119        dlg.visible = true;
120      }
121      showEl.addEventListener('click', onClick.bind(this));
122
123      function updateVisibility() {
124        showEl.style.display = this.model ? '' : 'none';
125      }
126      var updateVisibility_ = updateVisibility.bind(this);
127      updateVisibility_();
128      this.addEventListener('modelChange', updateVisibility_);
129
130      return showEl;
131    },
132
133    createHelpButton_: function() {
134      var node = base.instantiateTemplate('#help-btn-template');
135      var showEl = node.querySelector('.view-help-button');
136      var helpTextEl = node.querySelector('.view-help-text');
137
138      var dlg = new ui.Overlay();
139      dlg.classList.add('view-help-overlay');
140      dlg.obeyCloseEvents = true;
141      dlg.additionalCloseKeyCodes.push('?'.charCodeAt(0));
142      dlg.appendChild(helpTextEl);
143
144      function onClick(e) {
145        dlg.visible = true;
146
147        helpTextEl.textContent = this.timeline_ ? this.timeline_.keyHelp :
148            'No content loaded. For interesting help, load something.';
149
150        // Stop event so it doesn't trigger new click listener on document.
151        e.stopPropagation();
152        return false;
153      }
154      showEl.addEventListener('click', onClick.bind(this));
155
156      return showEl;
157    },
158
159    createMetadataButton_: function() {
160      var node = base.instantiateTemplate('#metadata-btn-template');
161      var showEl = node.querySelector('.view-metadata-button');
162      var containerEl = node.querySelector('.info-button-container');
163      var textEl = containerEl.querySelector('.info-button-text');
164
165      var dlg = new ui.Overlay();
166      dlg.classList.add('view-metadata-overlay');
167      dlg.obeyCloseEvents = true;
168      dlg.appendChild(containerEl);
169
170      function onClick() {
171        dlg.visible = true;
172
173        var metadataStrings = [];
174
175        var model = this.model;
176        for (var data in model.metadata) {
177          var meta = model.metadata[data];
178          var name = JSON.stringify(meta.name);
179          var value = JSON.stringify(meta.value, undefined, ' ');
180
181          metadataStrings.push(name + ': ' + value);
182        }
183        textEl.textContent = metadataStrings.join('\n');
184      }
185      showEl.addEventListener('click', onClick.bind(this));
186
187      function updateVisibility() {
188        showEl.style.display =
189            (this.model && this.model.metadata.length) ? '' : 'none';
190      }
191      var updateVisibility_ = updateVisibility.bind(this);
192      updateVisibility_();
193      this.addEventListener('modelChange', updateVisibility_);
194
195      return showEl;
196    },
197
198    get leftControls() {
199      return this.leftControlsEl_;
200    },
201
202    get rightControls() {
203      return this.rightControlsEl_;
204    },
205
206    get viewTitle() {
207      return this.titleEl_.textContent.substring(
208          this.titleEl_.textContent.length - 2);
209    },
210
211    set viewTitle(text) {
212      if (text === undefined) {
213        this.titleEl_.textContent = '';
214        this.titleEl_.hidden = true;
215        return;
216      }
217      this.titleEl_.hidden = false;
218      this.titleEl_.textContent = text;
219    },
220
221    set traceData(traceData) {
222      this.model = new tracing.TraceModel(traceData);
223    },
224
225    get model() {
226      if (this.timeline_)
227        return this.timeline_.model;
228      return undefined;
229    },
230
231    set model(model) {
232      var modelInstanceChanged = model != this.model;
233      var modelValid = model && !model.bounds.isEmpty;
234
235      // Remove old timeline if the model has completely changed.
236      if (modelInstanceChanged) {
237        this.timelineContainer_.textContent = '';
238        if (this.timeline_) {
239          this.timeline_.removeEventListener(
240              'selectionChange', this.onSelectionChanged_);
241          this.timeline_.detach();
242          this.timeline_ = undefined;
243          this.findCtl_.controller.timeline = undefined;
244        }
245      }
246
247      // Create new timeline if needed.
248      if (modelValid && !this.timeline_) {
249        this.timeline_ = new tracing.TimelineTrackView();
250        this.timeline_.focusElement =
251            this.focusElement_ ? this.focusElement_ : this.parentElement;
252        this.timelineContainer_.appendChild(this.timeline_);
253        this.findCtl_.controller.timeline = this.timeline_;
254        this.timeline_.addEventListener(
255            'selectionChange', this.onSelectionChanged_);
256
257        this.analysisEl_.clearSelectionHistory();
258      }
259
260      // Set the model.
261      if (modelValid)
262        this.timeline_.model = model;
263      base.dispatchSimpleEvent(this, 'modelChange');
264
265      // Do things that are selection specific
266      if (modelInstanceChanged)
267        this.onSelectionChanged_();
268    },
269
270    get timeline() {
271      return this.timeline_;
272    },
273
274    get settings() {
275      if (!this.settings_)
276        this.settings_ = new base.Settings();
277      return this.settings_;
278    },
279
280    /**
281     * Sets the element whose focus state will determine whether
282     * to respond to keybaord input.
283     */
284    set focusElement(value) {
285      this.focusElement_ = value;
286      if (this.timeline_)
287        this.timeline_.focusElement = value;
288    },
289
290    /**
291     * @return {Element} The element whose focused state determines
292     * whether to respond to keyboard inputs.
293     * Defaults to the parent element.
294     */
295    get focusElement() {
296      if (this.focusElement_)
297        return this.focusElement_;
298      return this.parentElement;
299    },
300
301    /**
302     * @return {boolean} Whether the current timeline is attached to the
303     * document.
304     */
305    get isAttachedToDocument_() {
306      var cur = this;
307      while (cur.parentNode)
308        cur = cur.parentNode;
309      return cur == this.ownerDocument;
310    },
311
312    get listenToKeys_() {
313      if (!this.isAttachedToDocument_)
314        return;
315      if (!this.focusElement_)
316        return true;
317      if (this.focusElement.tabIndex >= 0)
318        return document.activeElement == this.focusElement;
319      return true;
320    },
321
322    onKeypress_: function(e) {
323      if (!this.listenToKeys_)
324        return;
325
326      if (event.keyCode == '/'.charCodeAt(0)) { // / key
327        this.findCtl_.focus();
328        event.preventDefault();
329        return;
330      } else if (e.keyCode == '?'.charCodeAt(0)) {
331        this.querySelector('.view-help-button').click();
332        e.preventDefault();
333      }
334    },
335
336    beginFind: function() {
337      if (this.findInProgress_)
338        return;
339      this.findInProgress_ = true;
340      var dlg = tracing.FindControl();
341      dlg.controller = new tracing.FindController();
342      dlg.controller.timeline = this.timeline;
343      dlg.visible = true;
344      dlg.addEventListener('close', function() {
345        this.findInProgress_ = false;
346      }.bind(this));
347      dlg.addEventListener('findNext', function() {
348      });
349      dlg.addEventListener('findPrevious', function() {
350      });
351    },
352
353    onSelectionChanged_: function(e) {
354      var oldScrollTop = this.timelineContainer_.scrollTop;
355
356      var selection = this.timeline_ ?
357          this.timeline_.selection :
358          new tracing.Selection();
359      this.analysisEl_.selection = selection;
360      this.timelineContainer_.scrollTop = oldScrollTop;
361    },
362
363    onRequestSelectionChange_: function(e) {
364      this.timeline_.selection = e.selection;
365      e.stopPropagation();
366    }
367  };
368
369  return {
370    TimelineView: TimelineView
371  };
372});
373