1 // Copyright (c) 2012 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 TimelineView visualizes TRACE_EVENT events using the 9 * tracing.Timeline component and adds in selection summary and control buttons. 10 */ 11 cr.define('tracing', function() { 12 /** 13 * TimelineFindControl 14 * @constructor 15 * @extends {tracing.Overlay} 16 */ 17 var TimelineFindControl = cr.ui.define('div'); 18 19 TimelineFindControl.prototype = { 20 __proto__: tracing.Overlay.prototype, 21 22 decorate: function() { 23 tracing.Overlay.prototype.decorate.call(this); 24 25 this.className = 'timeline-find-control'; 26 27 this.hitCountEl_ = document.createElement('div'); 28 this.hitCountEl_.className = 'hit-count-label'; 29 this.hitCountEl_.textContent = '1 of 7'; 30 31 var findPreviousBn = document.createElement('div'); 32 findPreviousBn.className = 'timeline-button find-previous'; 33 findPreviousBn.textContent = '\u2190'; 34 findPreviousBn.addEventListener('click', function() { 35 this.controller.findPrevious(); 36 this.updateHitCountEl_(); 37 }.bind(this)); 38 39 var findNextBn = document.createElement('div'); 40 findNextBn.className = 'timeline-button find-next'; 41 findNextBn.textContent = '\u2192'; 42 findNextBn.addEventListener('click', function() { 43 this.controller.findNext(); 44 this.updateHitCountEl_(); 45 }.bind(this)); 46 47 // Filter input element. 48 this.filterEl_ = document.createElement('input'); 49 this.filterEl_.type = 'input'; 50 51 this.filterEl_.addEventListener('input', function(e) { 52 this.controller.filterText = this.filterEl_.value; 53 this.updateHitCountEl_(); 54 }.bind(this)); 55 56 this.filterEl_.addEventListener('keydown', function(e) { 57 if (e.keyCode == 13) { 58 findNextBn.click(); 59 } else if (e.keyCode == 27) { 60 this.filterEl_.blur(); 61 this.updateHitCountEl_(); 62 } 63 }.bind(this)); 64 65 this.filterEl_.addEventListener('blur', function(e) { 66 this.updateHitCountEl_(); 67 }.bind(this)); 68 69 this.filterEl_.addEventListener('focus', function(e) { 70 this.updateHitCountEl_(); 71 }.bind(this)); 72 73 // Attach everything. 74 this.appendChild(this.filterEl_); 75 76 this.appendChild(findPreviousBn); 77 this.appendChild(findNextBn); 78 this.appendChild(this.hitCountEl_); 79 80 this.updateHitCountEl_(); 81 }, 82 83 get controller() { 84 return this.controller_; 85 }, 86 87 set controller(c) { 88 this.controller_ = c; 89 this.updateHitCountEl_(); 90 }, 91 92 focus: function() { 93 this.filterEl_.selectionStart = 0; 94 this.filterEl_.selectionEnd = this.filterEl_.value.length; 95 this.filterEl_.focus(); 96 }, 97 98 updateHitCountEl_: function() { 99 if (!this.controller || document.activeElement != this.filterEl_) { 100 this.hitCountEl_.textContent = ''; 101 return; 102 } 103 var i = this.controller.currentHitIndex; 104 var n = this.controller.filterHits.length; 105 if (n == 0) 106 this.hitCountEl_.textContent = '0 of 0'; 107 else 108 this.hitCountEl_.textContent = (i + 1) + ' of ' + n; 109 } 110 }; 111 112 function TimelineFindController() { 113 this.timeline_ = undefined; 114 this.model_ = undefined; 115 this.filterText_ = ''; 116 this.filterHits_ = new tracing.TimelineSelection(); 117 this.filterHitsDirty_ = true; 118 this.currentHitIndex_ = 0; 119 }; 120 121 TimelineFindController.prototype = { 122 __proto__: Object.prototype, 123 124 get timeline() { 125 return this.timeline_; 126 }, 127 128 set timeline(t) { 129 this.timeline_ = t; 130 this.filterHitsDirty_ = true; 131 }, 132 133 get filterText() { 134 return this.filterText_; 135 }, 136 137 set filterText(f) { 138 if (f == this.filterText_) 139 return; 140 this.filterText_ = f; 141 this.filterHitsDirty_ = true; 142 this.findNext(); 143 }, 144 145 get filterHits() { 146 if (this.filterHitsDirty_) { 147 this.filterHitsDirty_ = false; 148 if (this.timeline_) { 149 var filter = new tracing.TimelineFilter(this.filterText); 150 this.filterHits_.clear(); 151 this.timeline.addAllObjectsMatchingFilterToSelection( 152 filter, this.filterHits_); 153 this.currentHitIndex_ = this.filterHits_.length - 1; 154 } else { 155 this.filterHits_.clear(); 156 this.currentHitIndex_ = 0; 157 } 158 } 159 return this.filterHits_; 160 }, 161 162 get currentHitIndex() { 163 return this.currentHitIndex_; 164 }, 165 166 find_: function(dir) { 167 if (!this.timeline) 168 return; 169 170 var N = this.filterHits.length; 171 this.currentHitIndex_ = this.currentHitIndex_ + dir; 172 173 if (this.currentHitIndex_ < 0) this.currentHitIndex_ = N - 1; 174 if (this.currentHitIndex_ >= N) this.currentHitIndex_ = 0; 175 176 if (this.currentHitIndex_ < 0 || this.currentHitIndex_ >= N) { 177 this.timeline.selection = new tracing.TimelineSelection(); 178 return; 179 } 180 181 // We allow the zoom level to change on the first hit level. But, when 182 // then cycling through subsequent changes, restrict it to panning. 183 var zoomAllowed = this.currentHitIndex_ == 0; 184 var subSelection = this.filterHits.subSelection(this.currentHitIndex_); 185 this.timeline.setSelectionAndMakeVisible(subSelection, zoomAllowed); 186 }, 187 188 findNext: function() { 189 this.find_(1); 190 }, 191 192 findPrevious: function() { 193 this.find_(-1); 194 }, 195 }; 196 197 /** 198 * TimelineView 199 * @constructor 200 * @extends {HTMLDivElement} 201 */ 202 var TimelineView = cr.ui.define('div'); 203 204 TimelineView.prototype = { 205 __proto__: HTMLDivElement.prototype, 206 207 decorate: function() { 208 this.classList.add('timeline-view'); 209 210 // Create individual elements. 211 this.titleEl_ = document.createElement('div'); 212 this.titleEl_.textContent = 'Tracing: '; 213 214 this.controlDiv_ = document.createElement('div'); 215 this.controlDiv_.className = 'control'; 216 217 this.leftControlsEl_ = document.createElement('div'); 218 this.leftControlsEl_.className = 'controls'; 219 this.rightControlsEl_ = document.createElement('div'); 220 this.rightControlsEl_.className = 'controls'; 221 222 var spacingEl = document.createElement('div'); 223 spacingEl.className = 'spacer'; 224 225 this.timelineContainer_ = document.createElement('div'); 226 this.timelineContainer_.className = 'timeline-container'; 227 228 var analysisContainer_ = document.createElement('div'); 229 analysisContainer_.className = 'analysis-container'; 230 231 this.analysisEl_ = new tracing.TimelineAnalysisView(); 232 233 this.findCtl_ = new TimelineFindControl(); 234 this.findCtl_.controller = new TimelineFindController(); 235 236 // Connect everything up. 237 this.rightControls.appendChild(this.findCtl_); 238 this.controlDiv_.appendChild(this.titleEl_); 239 this.controlDiv_.appendChild(this.leftControlsEl_); 240 this.controlDiv_.appendChild(spacingEl); 241 this.controlDiv_.appendChild(this.rightControlsEl_); 242 this.appendChild(this.controlDiv_); 243 244 this.appendChild(this.timelineContainer_); 245 246 analysisContainer_.appendChild(this.analysisEl_); 247 this.appendChild(analysisContainer_); 248 249 this.rightControls.appendChild(this.createHelpButton_()); 250 251 // Bookkeeping. 252 this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this); 253 document.addEventListener('keypress', this.onKeypress_.bind(this), true); 254 }, 255 256 createHelpButton_: function() { 257 var dlg = new tracing.Overlay(); 258 dlg.classList.add('timeline-view-help-overlay'); 259 260 var showHelpEl = document.createElement('div'); 261 showHelpEl.className = 'timeline-button timeline-view-help-button'; 262 showHelpEl.textContent = '?'; 263 264 var helpTextEl = document.createElement('div'); 265 helpTextEl.style.whiteSpace = 'pre'; 266 helpTextEl.style.fontFamily = 'monospace'; 267 268 function onClick() { 269 dlg.visible = true; 270 helpTextEl.textContent = this.timeline_.keyHelp; 271 document.addEventListener('keydown', onKey, true); 272 } 273 274 function onKey(e) { 275 if (!dlg.visible) 276 return; 277 278 if (e.keyCode == 27 || e.keyCode == '?'.charCodeAt(0)) { 279 e.preventDefault(); 280 document.removeEventListener('keydown', onKey); 281 dlg.visible = false; 282 } 283 } 284 showHelpEl.addEventListener('click', onClick.bind(this)); 285 286 dlg.appendChild(helpTextEl); 287 288 return showHelpEl; 289 }, 290 291 get leftControls() { 292 return this.leftControlsEl_; 293 }, 294 295 get rightControls() { 296 return this.rightControlsEl_; 297 }, 298 299 get title() { 300 return this.titleEl_.textContent.substring( 301 this.titleEl_.textContent.length - 2); 302 }, 303 304 set title(text) { 305 this.titleEl_.textContent = text + ':'; 306 }, 307 308 set traceData(traceData) { 309 this.model = new tracing.TimelineModel(traceData); 310 }, 311 312 get model() { 313 return this.timelineModel_; 314 }, 315 316 set model(model) { 317 this.timelineModel_ = model; 318 319 // remove old timeline 320 this.timelineContainer_.textContent = ''; 321 322 // create new timeline if needed 323 if (this.timelineModel_.minTimestamp !== undefined) { 324 if (this.timeline_) { 325 this.timeline_.viewportTrack.detach(); 326 this.timeline_.detach(); 327 } 328 this.timeline_ = new tracing.Timeline(); 329 this.timeline_.model = this.timelineModel_; 330 this.timeline_.focusElement = 331 this.focusElement_ ? this.focusElement_ : this.parentElement; 332 this.insertBefore(this.timeline_.viewportTrack, this.timelineContainer_); 333 this.timelineContainer_.appendChild(this.timeline_); 334 this.timeline_.addEventListener('selectionChange', 335 this.onSelectionChangedBoundToThis_); 336 337 this.findCtl_.controller.timeline = this.timeline_; 338 this.onSelectionChanged_(); 339 } else { 340 this.timeline_ = undefined; 341 this.findCtl_.controller.timeline = undefined; 342 } 343 }, 344 345 get timeline() { 346 return this.timeline_; 347 }, 348 349 /** 350 * Sets the element whose focus state will determine whether 351 * to respond to keybaord input. 352 */ 353 set focusElement(value) { 354 this.focusElement_ = value; 355 if (this.timeline_) 356 this.timeline_.focusElement = value; 357 }, 358 359 /** 360 * @return {Element} The element whose focused state determines 361 * whether to respond to keyboard inputs. 362 * Defaults to the parent element. 363 */ 364 get focusElement() { 365 if (this.focusElement_) 366 return this.focusElement_; 367 return this.parentElement; 368 }, 369 370 /** 371 * @return {boolean} Whether the current timeline is attached to the 372 * document. 373 */ 374 get isAttachedToDocument_() { 375 var cur = this; 376 while (cur.parentNode) 377 cur = cur.parentNode; 378 return cur == this.ownerDocument; 379 }, 380 381 get listenToKeys_() { 382 if (!this.isAttachedToDocument_) 383 return; 384 if (!this.focusElement_) 385 return true; 386 if (this.focusElement.tabIndex >= 0) 387 return document.activeElement == this.focusElement; 388 return true; 389 }, 390 391 onKeypress_: function(e) { 392 if (!this.listenToKeys_) 393 return; 394 395 if (event.keyCode == '/'.charCodeAt(0)) { // / key 396 this.findCtl_.focus(); 397 event.preventDefault(); 398 return; 399 } else if (e.keyCode == '?'.charCodeAt(0)) { 400 this.querySelector('.timeline-view-help-button').click(); 401 e.preventDefault(); 402 } 403 }, 404 405 beginFind: function() { 406 if (this.findInProgress_) 407 return; 408 this.findInProgress_ = true; 409 var dlg = TimelineFindControl(); 410 dlg.controller = new TimelineFindController(); 411 dlg.controller.timeline = this.timeline; 412 dlg.visible = true; 413 dlg.addEventListener('close', function() { 414 this.findInProgress_ = false; 415 }.bind(this)); 416 dlg.addEventListener('findNext', function() { 417 }); 418 dlg.addEventListener('findPrevious', function() { 419 }); 420 }, 421 422 onSelectionChanged_: function(e) { 423 var oldScrollTop = this.timelineContainer_.scrollTop; 424 this.analysisEl_.selection = this.timeline_.selection; 425 this.timelineContainer_.scrollTop = oldScrollTop; 426 } 427 }; 428 429 return { 430 TimelineFindControl: TimelineFindControl, 431 TimelineFindController: TimelineFindController, 432 TimelineView: TimelineView 433 }; 434 }); 435