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