| OLD | NEW |
| (Empty) |
| 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 Interactive visualizaiton of TimelineModel objects | |
| 9 * based loosely on gantt charts. Each thread in the TimelineModel is given a | |
| 10 * set of TimelineTracks, one per subrow in the thread. The Timeline class | |
| 11 * acts as a controller, creating the individual tracks, while TimelineTracks | |
| 12 * do actual drawing. | |
| 13 * | |
| 14 * Visually, the Timeline produces (prettier) visualizations like the following: | |
| 15 * Thread1: AAAAAAAAAA AAAAA | |
| 16 * BBBB BB | |
| 17 * Thread2: CCCCCC CCCCC | |
| 18 * | |
| 19 */ | |
| 20 cr.define('tracing', function() { | |
| 21 | |
| 22 /** | |
| 23 * The TimelineViewport manages the transform used for navigating | |
| 24 * within the timeline. It is a simple transform: | |
| 25 * x' = (x+pan) * scale | |
| 26 * | |
| 27 * The timeline code tries to avoid directly accessing this transform, | |
| 28 * instead using this class to do conversion between world and view space, | |
| 29 * as well as the math for centering the viewport in various interesting | |
| 30 * ways. | |
| 31 * | |
| 32 * @constructor | |
| 33 * @extends {cr.EventTarget} | |
| 34 */ | |
| 35 function TimelineViewport(parentEl) { | |
| 36 this.parentEl_ = parentEl; | |
| 37 this.scaleX_ = 1; | |
| 38 this.panX_ = 0; | |
| 39 this.gridTimebase_ = 0; | |
| 40 this.gridStep_ = 1000 / 60; | |
| 41 this.gridEnabled_ = false; | |
| 42 this.hasCalledSetupFunction_ = false; | |
| 43 | |
| 44 this.onResizeBoundToThis_ = this.onResize_.bind(this); | |
| 45 | |
| 46 // The following code uses an interval to detect when the parent element | |
| 47 // is attached to the document. That is a trigger to run the setup function | |
| 48 // and install a resize listener. | |
| 49 this.checkForAttachInterval_ = setInterval( | |
| 50 this.checkForAttach_.bind(this), 250); | |
| 51 } | |
| 52 | |
| 53 TimelineViewport.prototype = { | |
| 54 __proto__: cr.EventTarget.prototype, | |
| 55 | |
| 56 /** | |
| 57 * Allows initialization of the viewport when the viewport's parent element | |
| 58 * has been attached to the document and given a size. | |
| 59 * @param {Function} fn Function to call when the viewport can be safely | |
| 60 * initialized. | |
| 61 */ | |
| 62 setWhenPossible: function(fn) { | |
| 63 this.pendingSetFunction_ = fn; | |
| 64 }, | |
| 65 | |
| 66 /** | |
| 67 * @return {boolean} Whether the current timeline is attached to the | |
| 68 * document. | |
| 69 */ | |
| 70 get isAttachedToDocument_() { | |
| 71 var cur = this.parentEl_; | |
| 72 while (cur.parentNode) | |
| 73 cur = cur.parentNode; | |
| 74 return cur == this.parentEl_.ownerDocument; | |
| 75 }, | |
| 76 | |
| 77 onResize_: function() { | |
| 78 this.dispatchChangeEvent(); | |
| 79 }, | |
| 80 | |
| 81 /** | |
| 82 * Checks whether the parentNode is attached to the document. | |
| 83 * When it is, it installs the iframe-based resize detection hook | |
| 84 * and then runs the pendingSetFunction_, if present. | |
| 85 */ | |
| 86 checkForAttach_: function() { | |
| 87 if (!this.isAttachedToDocument_ || this.clientWidth == 0) | |
| 88 return; | |
| 89 | |
| 90 if (!this.iframe_) { | |
| 91 this.iframe_ = document.createElement('iframe'); | |
| 92 this.iframe_.style.cssText = | |
| 93 'position:absolute;width:100%;height:0;border:0;visibility:hidden;'; | |
| 94 this.parentEl_.appendChild(this.iframe_); | |
| 95 | |
| 96 this.iframe_.contentWindow.addEventListener('resize', | |
| 97 this.onResizeBoundToThis_); | |
| 98 } | |
| 99 | |
| 100 var curSize = this.clientWidth + 'x' + this.clientHeight; | |
| 101 if (this.pendingSetFunction_) { | |
| 102 this.lastSize_ = curSize; | |
| 103 this.pendingSetFunction_(); | |
| 104 this.pendingSetFunction_ = undefined; | |
| 105 } | |
| 106 | |
| 107 window.clearInterval(this.checkForAttachInterval_); | |
| 108 this.checkForAttachInterval_ = undefined; | |
| 109 }, | |
| 110 | |
| 111 /** | |
| 112 * Fires the change event on this viewport. Used to notify listeners | |
| 113 * to redraw when the underlying model has been mutated. | |
| 114 */ | |
| 115 dispatchChangeEvent: function() { | |
| 116 cr.dispatchSimpleEvent(this, 'change'); | |
| 117 }, | |
| 118 | |
| 119 detach: function() { | |
| 120 if (this.checkForAttachInterval_) { | |
| 121 window.clearInterval(this.checkForAttachInterval_); | |
| 122 this.checkForAttachInterval_ = undefined; | |
| 123 } | |
| 124 this.iframe_.removeEventListener('resize', this.onResizeBoundToThis_); | |
| 125 this.parentEl_.removeChild(this.iframe_); | |
| 126 }, | |
| 127 | |
| 128 get scaleX() { | |
| 129 return this.scaleX_; | |
| 130 }, | |
| 131 set scaleX(s) { | |
| 132 var changed = this.scaleX_ != s; | |
| 133 if (changed) { | |
| 134 this.scaleX_ = s; | |
| 135 this.dispatchChangeEvent(); | |
| 136 } | |
| 137 }, | |
| 138 | |
| 139 get panX() { | |
| 140 return this.panX_; | |
| 141 }, | |
| 142 set panX(p) { | |
| 143 var changed = this.panX_ != p; | |
| 144 if (changed) { | |
| 145 this.panX_ = p; | |
| 146 this.dispatchChangeEvent(); | |
| 147 } | |
| 148 }, | |
| 149 | |
| 150 setPanAndScale: function(p, s) { | |
| 151 var changed = this.scaleX_ != s || this.panX_ != p; | |
| 152 if (changed) { | |
| 153 this.scaleX_ = s; | |
| 154 this.panX_ = p; | |
| 155 this.dispatchChangeEvent(); | |
| 156 } | |
| 157 }, | |
| 158 | |
| 159 xWorldToView: function(x) { | |
| 160 return (x + this.panX_) * this.scaleX_; | |
| 161 }, | |
| 162 | |
| 163 xWorldVectorToView: function(x) { | |
| 164 return x * this.scaleX_; | |
| 165 }, | |
| 166 | |
| 167 xViewToWorld: function(x) { | |
| 168 return (x / this.scaleX_) - this.panX_; | |
| 169 }, | |
| 170 | |
| 171 xViewVectorToWorld: function(x) { | |
| 172 return x / this.scaleX_; | |
| 173 }, | |
| 174 | |
| 175 xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) { | |
| 176 if (typeof viewX == 'string') { | |
| 177 if (viewX == 'left') { | |
| 178 viewX = 0; | |
| 179 } else if (viewX == 'center') { | |
| 180 viewX = viewWidth / 2; | |
| 181 } else if (viewX == 'right') { | |
| 182 viewX = viewWidth - 1; | |
| 183 } else { | |
| 184 throw Error('unrecognized string for viewPos. left|center|right'); | |
| 185 } | |
| 186 } | |
| 187 this.panX = (viewX / this.scaleX_) - worldX; | |
| 188 }, | |
| 189 | |
| 190 xPanWorldRangeIntoView: function(worldMin, worldMax, viewWidth) { | |
| 191 if (this.xWorldToView(worldMin) < 0) | |
| 192 this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth); | |
| 193 else if (this.xWorldToView(worldMax) > viewWidth) | |
| 194 this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth); | |
| 195 }, | |
| 196 | |
| 197 xSetWorldRange: function(worldMin, worldMax, viewWidth) { | |
| 198 var worldRange = worldMax - worldMin; | |
| 199 var scaleX = viewWidth / worldRange; | |
| 200 var panX = -worldMin; | |
| 201 this.setPanAndScale(panX, scaleX); | |
| 202 }, | |
| 203 | |
| 204 get gridEnabled() { | |
| 205 return this.gridEnabled_; | |
| 206 }, | |
| 207 | |
| 208 set gridEnabled(enabled) { | |
| 209 if (this.gridEnabled_ == enabled) | |
| 210 return; | |
| 211 this.gridEnabled_ = enabled && true; | |
| 212 this.dispatchChangeEvent(); | |
| 213 }, | |
| 214 | |
| 215 get gridTimebase() { | |
| 216 return this.gridTimebase_; | |
| 217 }, | |
| 218 | |
| 219 set gridTimebase(timebase) { | |
| 220 if (this.gridTimebase_ == timebase) | |
| 221 return; | |
| 222 this.gridTimebase_ = timebase; | |
| 223 cr.dispatchSimpleEvent(this, 'change'); | |
| 224 }, | |
| 225 | |
| 226 get gridStep() { | |
| 227 return this.gridStep_; | |
| 228 }, | |
| 229 | |
| 230 applyTransformToCanavs: function(ctx) { | |
| 231 ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0); | |
| 232 } | |
| 233 }; | |
| 234 | |
| 235 function TimelineSelectionSliceHit(track, slice) { | |
| 236 this.track = track; | |
| 237 this.slice = slice; | |
| 238 } | |
| 239 TimelineSelectionSliceHit.prototype = { | |
| 240 get selected() { | |
| 241 return this.slice.selected; | |
| 242 }, | |
| 243 set selected(v) { | |
| 244 this.slice.selected = v; | |
| 245 } | |
| 246 }; | |
| 247 | |
| 248 function TimelineSelectionCounterSampleHit(track, counter, sampleIndex) { | |
| 249 this.track = track; | |
| 250 this.counter = counter; | |
| 251 this.sampleIndex = sampleIndex; | |
| 252 } | |
| 253 TimelineSelectionCounterSampleHit.prototype = { | |
| 254 get selected() { | |
| 255 return this.track.selectedSamples[this.sampleIndex] == true; | |
| 256 }, | |
| 257 set selected(v) { | |
| 258 if (v) | |
| 259 this.track.selectedSamples[this.sampleIndex] = true; | |
| 260 else | |
| 261 this.track.selectedSamples[this.sampleIndex] = false; | |
| 262 this.track.invalidate(); | |
| 263 } | |
| 264 }; | |
| 265 | |
| 266 | |
| 267 /** | |
| 268 * Represents a selection within a Timeline and its associated set of tracks. | |
| 269 * @constructor | |
| 270 */ | |
| 271 function TimelineSelection() { | |
| 272 this.range_dirty_ = true; | |
| 273 this.range_ = {}; | |
| 274 this.length_ = 0; | |
| 275 } | |
| 276 TimelineSelection.prototype = { | |
| 277 __proto__: Object.prototype, | |
| 278 | |
| 279 get range() { | |
| 280 if (this.range_dirty_) { | |
| 281 var wmin = Infinity; | |
| 282 var wmax = -wmin; | |
| 283 for (var i = 0; i < this.length_; i++) { | |
| 284 var hit = this[i]; | |
| 285 if (hit.slice) { | |
| 286 wmin = Math.min(wmin, hit.slice.start); | |
| 287 wmax = Math.max(wmax, hit.slice.end); | |
| 288 } | |
| 289 } | |
| 290 this.range_ = { | |
| 291 min: wmin, | |
| 292 max: wmax | |
| 293 }; | |
| 294 this.range_dirty_ = false; | |
| 295 } | |
| 296 return this.range_; | |
| 297 }, | |
| 298 | |
| 299 get duration() { | |
| 300 return this.range.max - this.range.min; | |
| 301 }, | |
| 302 | |
| 303 get length() { | |
| 304 return this.length_; | |
| 305 }, | |
| 306 | |
| 307 clear: function() { | |
| 308 for (var i = 0; i < this.length_; ++i) | |
| 309 delete this[i]; | |
| 310 this.length_ = 0; | |
| 311 this.range_dirty_ = true; | |
| 312 }, | |
| 313 | |
| 314 push_: function(hit) { | |
| 315 this[this.length_++] = hit; | |
| 316 this.range_dirty_ = true; | |
| 317 return hit; | |
| 318 }, | |
| 319 | |
| 320 addSlice: function(track, slice) { | |
| 321 return this.push_(new TimelineSelectionSliceHit(track, slice)); | |
| 322 }, | |
| 323 | |
| 324 addCounterSample: function(track, counter, sampleIndex) { | |
| 325 return this.push_( | |
| 326 new TimelineSelectionCounterSampleHit( | |
| 327 track, counter, sampleIndex)); | |
| 328 }, | |
| 329 | |
| 330 subSelection: function(index, count) { | |
| 331 count = count || 1; | |
| 332 | |
| 333 var selection = new TimelineSelection(); | |
| 334 selection.range_dirty_ = true; | |
| 335 if (index < 0 || index + count > this.length_) | |
| 336 throw 'Index out of bounds'; | |
| 337 | |
| 338 for (var i = index; i < index + count; i++) | |
| 339 selection.push_(this[i]); | |
| 340 | |
| 341 return selection; | |
| 342 }, | |
| 343 | |
| 344 getCounterSampleHits: function() { | |
| 345 var selection = new TimelineSelection(); | |
| 346 for (var i = 0; i < this.length_; i++) | |
| 347 if (this[i] instanceof TimelineSelectionCounterSampleHit) | |
| 348 selection.push_(this[i]); | |
| 349 return selection; | |
| 350 }, | |
| 351 | |
| 352 getSliceHits: function() { | |
| 353 var selection = new TimelineSelection(); | |
| 354 for (var i = 0; i < this.length_; i++) | |
| 355 if (this[i] instanceof TimelineSelectionSliceHit) | |
| 356 selection.push_(this[i]); | |
| 357 return selection; | |
| 358 }, | |
| 359 | |
| 360 map: function(fn) { | |
| 361 for (var i = 0; i < this.length_; i++) | |
| 362 fn(this[i]); | |
| 363 }, | |
| 364 | |
| 365 /** | |
| 366 * Helper for selection previous or next. | |
| 367 * @param {boolean} forwardp If true, select one forward (next). | |
| 368 * Else, select previous. | |
| 369 * @return {boolean} true if current selection changed. | |
| 370 */ | |
| 371 getShiftedSelection: function(offset) { | |
| 372 var newSelection = new TimelineSelection(); | |
| 373 for (var i = 0; i < this.length_; i++) { | |
| 374 var hit = this[i]; | |
| 375 hit.track.addItemNearToProvidedHitToSelection( | |
| 376 hit, offset, newSelection); | |
| 377 } | |
| 378 | |
| 379 if (newSelection.length == 0) | |
| 380 return undefined; | |
| 381 return newSelection; | |
| 382 }, | |
| 383 }; | |
| 384 | |
| 385 /** | |
| 386 * Renders a TimelineModel into a div element, making one | |
| 387 * TimelineTrack for each subrow in each thread of the model, managing | |
| 388 * overall track layout, and handling user interaction with the | |
| 389 * viewport. | |
| 390 * | |
| 391 * @constructor | |
| 392 * @extends {HTMLDivElement} | |
| 393 */ | |
| 394 var Timeline = cr.ui.define('div'); | |
| 395 | |
| 396 Timeline.prototype = { | |
| 397 __proto__: HTMLDivElement.prototype, | |
| 398 | |
| 399 model_: null, | |
| 400 | |
| 401 decorate: function() { | |
| 402 this.classList.add('timeline'); | |
| 403 | |
| 404 this.viewport_ = new TimelineViewport(this); | |
| 405 | |
| 406 this.tracks_ = this.ownerDocument.createElement('div'); | |
| 407 this.appendChild(this.tracks_); | |
| 408 | |
| 409 this.dragBox_ = this.ownerDocument.createElement('div'); | |
| 410 this.dragBox_.className = 'timeline-drag-box'; | |
| 411 this.appendChild(this.dragBox_); | |
| 412 this.hideDragBox_(); | |
| 413 | |
| 414 this.bindEventListener_(document, 'keypress', this.onKeypress_, this); | |
| 415 this.bindEventListener_(document, 'keydown', this.onKeydown_, this); | |
| 416 this.bindEventListener_(document, 'mousedown', this.onMouseDown_, this); | |
| 417 this.bindEventListener_(document, 'mousemove', this.onMouseMove_, this); | |
| 418 this.bindEventListener_(document, 'mouseup', this.onMouseUp_, this); | |
| 419 this.bindEventListener_(document, 'dblclick', this.onDblClick_, this); | |
| 420 | |
| 421 this.lastMouseViewPos_ = {x: 0, y: 0}; | |
| 422 | |
| 423 this.selection_ = new TimelineSelection(); | |
| 424 }, | |
| 425 | |
| 426 /** | |
| 427 * Wraps the standard addEventListener but automatically binds the provided | |
| 428 * func to the provided target, tracking the resulting closure. When detach | |
| 429 * is called, these listeners will be automatically removed. | |
| 430 */ | |
| 431 bindEventListener_: function(object, event, func, target) { | |
| 432 if (!this.boundListeners_) | |
| 433 this.boundListeners_ = []; | |
| 434 var boundFunc = func.bind(target); | |
| 435 this.boundListeners_.push({object: object, | |
| 436 event: event, | |
| 437 boundFunc: boundFunc}); | |
| 438 object.addEventListener(event, boundFunc); | |
| 439 }, | |
| 440 | |
| 441 detach: function() { | |
| 442 for (var i = 0; i < this.tracks_.children.length; i++) | |
| 443 this.tracks_.children[i].detach(); | |
| 444 | |
| 445 for (var i = 0; i < this.boundListeners_.length; i++) { | |
| 446 var binding = this.boundListeners_[i]; | |
| 447 binding.object.removeEventListener(binding.event, binding.boundFunc); | |
| 448 } | |
| 449 this.boundListeners_ = undefined; | |
| 450 this.viewport_.detach(); | |
| 451 }, | |
| 452 | |
| 453 get viewport() { | |
| 454 return this.viewport_; | |
| 455 }, | |
| 456 | |
| 457 get model() { | |
| 458 return this.model_; | |
| 459 }, | |
| 460 | |
| 461 set model(model) { | |
| 462 if (!model) | |
| 463 throw Error('Model cannot be null'); | |
| 464 if (this.model) { | |
| 465 throw Error('Cannot set model twice.'); | |
| 466 } | |
| 467 this.model_ = model; | |
| 468 | |
| 469 // Figure out all the headings. | |
| 470 var allHeadings = []; | |
| 471 model.getAllThreads().forEach(function(t) { | |
| 472 allHeadings.push(t.userFriendlyName); | |
| 473 }); | |
| 474 model.getAllCounters().forEach(function(c) { | |
| 475 allHeadings.push(c.name); | |
| 476 }); | |
| 477 model.getAllCpus().forEach(function(c) { | |
| 478 allHeadings.push('CPU ' + c.cpuNumber); | |
| 479 }); | |
| 480 | |
| 481 // Figure out the maximum heading size. | |
| 482 var maxHeadingWidth = 0; | |
| 483 var measuringStick = new tracing.MeasuringStick(); | |
| 484 var headingEl = document.createElement('div'); | |
| 485 headingEl.style.position = 'fixed'; | |
| 486 headingEl.className = 'timeline-canvas-based-track-title'; | |
| 487 allHeadings.forEach(function(text) { | |
| 488 headingEl.textContent = text + ':__'; | |
| 489 var w = measuringStick.measure(headingEl).width; | |
| 490 // Limit heading width to 300px. | |
| 491 if (w > 300) | |
| 492 w = 300; | |
| 493 if (w > maxHeadingWidth) | |
| 494 maxHeadingWidth = w; | |
| 495 }); | |
| 496 maxHeadingWidth = maxHeadingWidth + 'px'; | |
| 497 | |
| 498 // Reset old tracks. | |
| 499 for (var i = 0; i < this.tracks_.children.length; i++) | |
| 500 this.tracks_.children[i].detach(); | |
| 501 this.tracks_.textContent = ''; | |
| 502 | |
| 503 // Add the viewport track | |
| 504 var viewportTrack = new tracing.TimelineViewportTrack(); | |
| 505 viewportTrack.headingWidth = maxHeadingWidth; | |
| 506 viewportTrack.viewport = this.viewport_; | |
| 507 this.tracks_.appendChild(viewportTrack); | |
| 508 | |
| 509 // Get a sorted list of CPUs | |
| 510 var cpus = model.getAllCpus(); | |
| 511 cpus.sort(tracing.TimelineCpu.compare); | |
| 512 | |
| 513 // Create tracks for each CPU. | |
| 514 cpus.forEach(function(cpu) { | |
| 515 var track = new tracing.TimelineCpuTrack(); | |
| 516 track.heading = 'CPU ' + cpu.cpuNumber + ':'; | |
| 517 track.headingWidth = maxHeadingWidth; | |
| 518 track.viewport = this.viewport_; | |
| 519 track.cpu = cpu; | |
| 520 this.tracks_.appendChild(track); | |
| 521 | |
| 522 for (var counterName in cpu.counters) { | |
| 523 var counter = cpu.counters[counterName]; | |
| 524 track = new tracing.TimelineCounterTrack(); | |
| 525 track.heading = 'CPU ' + cpu.cpuNumber + ' ' + counter.name + ':'; | |
| 526 track.headingWidth = maxHeadingWidth; | |
| 527 track.viewport = this.viewport_; | |
| 528 track.counter = counter; | |
| 529 this.tracks_.appendChild(track); | |
| 530 } | |
| 531 }.bind(this)); | |
| 532 | |
| 533 // Get a sorted list of processes. | |
| 534 var processes = model.getAllProcesses(); | |
| 535 processes.sort(tracing.TimelineProcess.compare); | |
| 536 | |
| 537 // Create tracks for each process. | |
| 538 processes.forEach(function(process) { | |
| 539 // Add counter tracks for this process. | |
| 540 var counters = []; | |
| 541 for (var tid in process.counters) | |
| 542 counters.push(process.counters[tid]); | |
| 543 counters.sort(tracing.TimelineCounter.compare); | |
| 544 | |
| 545 // Create the counters for this process. | |
| 546 counters.forEach(function(counter) { | |
| 547 var track = new tracing.TimelineCounterTrack(); | |
| 548 track.heading = counter.name + ':'; | |
| 549 track.headingWidth = maxHeadingWidth; | |
| 550 track.viewport = this.viewport_; | |
| 551 track.counter = counter; | |
| 552 this.tracks_.appendChild(track); | |
| 553 }.bind(this)); | |
| 554 | |
| 555 // Get a sorted list of threads. | |
| 556 var threads = []; | |
| 557 for (var tid in process.threads) | |
| 558 threads.push(process.threads[tid]); | |
| 559 threads.sort(tracing.TimelineThread.compare); | |
| 560 | |
| 561 // Create the threads. | |
| 562 threads.forEach(function(thread) { | |
| 563 var track = new tracing.TimelineThreadTrack(); | |
| 564 track.heading = thread.userFriendlyName + ':'; | |
| 565 track.tooltip = thread.userFriendlyDetails; | |
| 566 track.headingWidth = maxHeadingWidth; | |
| 567 track.viewport = this.viewport_; | |
| 568 track.thread = thread; | |
| 569 this.tracks_.appendChild(track); | |
| 570 }.bind(this)); | |
| 571 }.bind(this)); | |
| 572 | |
| 573 // Set up a reasonable viewport. | |
| 574 this.viewport_.setWhenPossible(function() { | |
| 575 var w = this.firstCanvas.width; | |
| 576 this.viewport_.xSetWorldRange(this.model_.minTimestamp, | |
| 577 this.model_.maxTimestamp, | |
| 578 w); | |
| 579 }.bind(this)); | |
| 580 }, | |
| 581 | |
| 582 /** | |
| 583 * @param {TimelineFilter} filter The filter to use for finding matches. | |
| 584 * @param {TimelineSelection} selection The selection to add matches to. | |
| 585 * @return {Array} An array of objects that match the provided | |
| 586 * TimelineFilter. | |
| 587 */ | |
| 588 addAllObjectsMatchingFilterToSelection: function(filter, selection) { | |
| 589 for (var i = 0; i < this.tracks_.children.length; ++i) | |
| 590 this.tracks_.children[i].addAllObjectsMatchingFilterToSelection( | |
| 591 filter, selection); | |
| 592 }, | |
| 593 | |
| 594 /** | |
| 595 * @return {Element} The element whose focused state determines | |
| 596 * whether to respond to keyboard inputs. | |
| 597 * Defaults to the parent element. | |
| 598 */ | |
| 599 get focusElement() { | |
| 600 if (this.focusElement_) | |
| 601 return this.focusElement_; | |
| 602 return this.parentElement; | |
| 603 }, | |
| 604 | |
| 605 /** | |
| 606 * Sets the element whose focus state will determine whether | |
| 607 * to respond to keybaord input. | |
| 608 */ | |
| 609 set focusElement(value) { | |
| 610 this.focusElement_ = value; | |
| 611 }, | |
| 612 | |
| 613 get listenToKeys_() { | |
| 614 if (!this.viewport_.isAttachedToDocument_) | |
| 615 return false; | |
| 616 if (!this.focusElement_) | |
| 617 return true; | |
| 618 if (this.focusElement.tabIndex >= 0) | |
| 619 return document.activeElement == this.focusElement; | |
| 620 return true; | |
| 621 }, | |
| 622 | |
| 623 onKeypress_: function(e) { | |
| 624 var vp = this.viewport_; | |
| 625 if (!this.firstCanvas) | |
| 626 return; | |
| 627 if (!this.listenToKeys_) | |
| 628 return; | |
| 629 var viewWidth = this.firstCanvas.clientWidth; | |
| 630 var curMouseV, curCenterW; | |
| 631 switch (e.keyCode) { | |
| 632 case 101: // e | |
| 633 var vX = this.lastMouseViewPos_.x; | |
| 634 var wX = vp.xViewToWorld(this.lastMouseViewPos_.x); | |
| 635 var distFromCenter = vX - (viewWidth / 2); | |
| 636 var percFromCenter = distFromCenter / viewWidth; | |
| 637 var percFromCenterSq = percFromCenter * percFromCenter; | |
| 638 vp.xPanWorldPosToViewPos(wX, 'center', viewWidth); | |
| 639 break; | |
| 640 case 119: // w | |
| 641 this.zoomBy_(1.5); | |
| 642 break; | |
| 643 case 115: // s | |
| 644 this.zoomBy_(1 / 1.5); | |
| 645 break; | |
| 646 case 103: // g | |
| 647 this.onGridToggle_(true); | |
| 648 break; | |
| 649 case 71: // G | |
| 650 this.onGridToggle_(false); | |
| 651 break; | |
| 652 case 87: // W | |
| 653 this.zoomBy_(10); | |
| 654 break; | |
| 655 case 83: // S | |
| 656 this.zoomBy_(1 / 10); | |
| 657 break; | |
| 658 case 97: // a | |
| 659 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); | |
| 660 break; | |
| 661 case 100: // d | |
| 662 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); | |
| 663 break; | |
| 664 case 65: // A | |
| 665 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5); | |
| 666 break; | |
| 667 case 68: // D | |
| 668 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5); | |
| 669 break; | |
| 670 } | |
| 671 }, | |
| 672 | |
| 673 // Not all keys send a keypress. | |
| 674 onKeydown_: function(e) { | |
| 675 if (!this.listenToKeys_) | |
| 676 return; | |
| 677 var sel; | |
| 678 switch (e.keyCode) { | |
| 679 case 37: // left arrow | |
| 680 sel = this.selection.getShiftedSelection(-1); | |
| 681 if (sel) { | |
| 682 this.setSelectionAndMakeVisible(sel); | |
| 683 e.preventDefault(); | |
| 684 } | |
| 685 break; | |
| 686 case 39: // right arrow | |
| 687 sel = this.selection.getShiftedSelection(1); | |
| 688 if (sel) { | |
| 689 this.setSelectionAndMakeVisible(sel); | |
| 690 e.preventDefault(); | |
| 691 } | |
| 692 break; | |
| 693 case 9: // TAB | |
| 694 if (this.focusElement.tabIndex == -1) { | |
| 695 if (e.shiftKey) | |
| 696 this.selectPrevious_(e); | |
| 697 else | |
| 698 this.selectNext_(e); | |
| 699 e.preventDefault(); | |
| 700 } | |
| 701 break; | |
| 702 } | |
| 703 }, | |
| 704 | |
| 705 /** | |
| 706 * Zoom in or out on the timeline by the given scale factor. | |
| 707 * @param {integer} scale The scale factor to apply. If <1, zooms out. | |
| 708 */ | |
| 709 zoomBy_: function(scale) { | |
| 710 if (!this.firstCanvas) | |
| 711 return; | |
| 712 var vp = this.viewport_; | |
| 713 var viewWidth = this.firstCanvas.clientWidth; | |
| 714 var curMouseV = this.lastMouseViewPos_.x; | |
| 715 var curCenterW = vp.xViewToWorld(curMouseV); | |
| 716 vp.scaleX = vp.scaleX * scale; | |
| 717 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); | |
| 718 }, | |
| 719 | |
| 720 get keyHelp() { | |
| 721 var help = 'Keyboard shortcuts:\n' + | |
| 722 ' w/s : Zoom in/out (with shift: go faster)\n' + | |
| 723 ' a/d : Pan left/right\n' + | |
| 724 ' e : Center on mouse\n' + | |
| 725 ' g/G : Shows grid at the start/end of the selected task\n'; | |
| 726 | |
| 727 if (this.focusElement.tabIndex) { | |
| 728 help += ' <- : Select previous event on current timeline\n' + | |
| 729 ' -> : Select next event on current timeline\n'; | |
| 730 } else { | |
| 731 help += ' <-,^TAB : Select previous event on current timeline\n' + | |
| 732 ' ->, TAB : Select next event on current timeline\n'; | |
| 733 } | |
| 734 help += | |
| 735 '\n' + | |
| 736 'Dbl-click to zoom in; Shift dbl-click to zoom out\n'; | |
| 737 return help; | |
| 738 }, | |
| 739 | |
| 740 get selection() { | |
| 741 return this.selection_; | |
| 742 }, | |
| 743 | |
| 744 set selection(selection) { | |
| 745 if (!(selection instanceof TimelineSelection)) | |
| 746 throw 'Expected TimelineSelection'; | |
| 747 | |
| 748 // Clear old selection. | |
| 749 var i; | |
| 750 for (i = 0; i < this.selection_.length; i++) | |
| 751 this.selection_[i].selected = false; | |
| 752 | |
| 753 this.selection_ = selection; | |
| 754 | |
| 755 cr.dispatchSimpleEvent(this, 'selectionChange'); | |
| 756 for (i = 0; i < this.selection_.length; i++) | |
| 757 this.selection_[i].selected = true; | |
| 758 this.viewport_.dispatchChangeEvent(); // Triggers a redraw. | |
| 759 }, | |
| 760 | |
| 761 setSelectionAndMakeVisible: function(selection, zoomAllowed) { | |
| 762 if (!(selection instanceof TimelineSelection)) | |
| 763 throw 'Expected TimelineSelection'; | |
| 764 this.selection = selection; | |
| 765 var range = this.selection.range; | |
| 766 var size = this.viewport_.xWorldVectorToView(range.max - range.min); | |
| 767 if (zoomAllowed && size < 50) { | |
| 768 var worldCenter = range.min + (range.max - range.min) * 0.5; | |
| 769 var worldRange = (range.max - range.min) * 5; | |
| 770 this.viewport_.xSetWorldRange(worldCenter - worldRange * 0.5, | |
| 771 worldCenter + worldRange * 0.5, | |
| 772 this.firstCanvas.width); | |
| 773 return; | |
| 774 } | |
| 775 | |
| 776 this.viewport_.xPanWorldRangeIntoView(range.min, range.max, | |
| 777 this.firstCanvas.width); | |
| 778 }, | |
| 779 | |
| 780 get firstCanvas() { | |
| 781 return this.tracks_.firstChild ? | |
| 782 this.tracks_.firstChild.firstCanvas : undefined; | |
| 783 }, | |
| 784 | |
| 785 hideDragBox_: function() { | |
| 786 this.dragBox_.style.left = '-1000px'; | |
| 787 this.dragBox_.style.top = '-1000px'; | |
| 788 this.dragBox_.style.width = 0; | |
| 789 this.dragBox_.style.height = 0; | |
| 790 }, | |
| 791 | |
| 792 setDragBoxPosition_: function(eDown, eCur) { | |
| 793 var loX = Math.min(eDown.clientX, eCur.clientX); | |
| 794 var hiX = Math.max(eDown.clientX, eCur.clientX); | |
| 795 var loY = Math.min(eDown.clientY, eCur.clientY); | |
| 796 var hiY = Math.max(eDown.clientY, eCur.clientY); | |
| 797 | |
| 798 this.dragBox_.style.left = loX + 'px'; | |
| 799 this.dragBox_.style.top = loY + 'px'; | |
| 800 this.dragBox_.style.width = hiX - loX + 'px'; | |
| 801 this.dragBox_.style.height = hiY - loY + 'px'; | |
| 802 | |
| 803 var canv = this.firstCanvas; | |
| 804 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); | |
| 805 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); | |
| 806 | |
| 807 var roundedDuration = Math.round((hiWX - loWX) * 100) / 100; | |
| 808 this.dragBox_.textContent = roundedDuration + 'ms'; | |
| 809 | |
| 810 var e = new cr.Event('selectionChanging'); | |
| 811 e.loWX = loWX; | |
| 812 e.hiWX = hiWX; | |
| 813 this.dispatchEvent(e); | |
| 814 }, | |
| 815 | |
| 816 onGridToggle_: function(left) { | |
| 817 var tb; | |
| 818 if (left) | |
| 819 tb = this.selection_.range.min; | |
| 820 else | |
| 821 tb = this.selection_.range.max; | |
| 822 | |
| 823 // Shift the timebase left until its just left of minTimestamp. | |
| 824 var numInterfvalsSinceStart = Math.ceil((tb - this.model_.minTimestamp) / | |
| 825 this.viewport_.gridStep_); | |
| 826 this.viewport_.gridTimebase = tb - | |
| 827 (numInterfvalsSinceStart + 1) * this.viewport_.gridStep_; | |
| 828 this.viewport_.gridEnabled = true; | |
| 829 }, | |
| 830 | |
| 831 onMouseDown_: function(e) { | |
| 832 var canv = this.firstCanvas; | |
| 833 var rect = this.tracks_.getClientRects()[0]; | |
| 834 var inside = rect && | |
| 835 e.clientX >= rect.left && | |
| 836 e.clientX < rect.right && | |
| 837 e.clientY >= rect.top && | |
| 838 e.clientY < rect.bottom && | |
| 839 e.x >= canv.offsetLeft; | |
| 840 if (!inside) | |
| 841 return; | |
| 842 | |
| 843 var pos = { | |
| 844 x: e.clientX - canv.offsetLeft, | |
| 845 y: e.clientY - canv.offsetTop | |
| 846 }; | |
| 847 | |
| 848 var wX = this.viewport_.xViewToWorld(pos.x); | |
| 849 | |
| 850 this.dragBeginEvent_ = e; | |
| 851 e.preventDefault(); | |
| 852 if (this.focusElement.tabIndex >= 0) | |
| 853 this.focusElement.focus(); | |
| 854 }, | |
| 855 | |
| 856 onMouseMove_: function(e) { | |
| 857 if (!this.firstCanvas) | |
| 858 return; | |
| 859 var canv = this.firstCanvas; | |
| 860 var pos = { | |
| 861 x: e.clientX - canv.offsetLeft, | |
| 862 y: e.clientY - canv.offsetTop | |
| 863 }; | |
| 864 | |
| 865 // Remember position. Used during keyboard zooming. | |
| 866 this.lastMouseViewPos_ = pos; | |
| 867 | |
| 868 // Update the drag box | |
| 869 if (this.dragBeginEvent_) { | |
| 870 this.setDragBoxPosition_(this.dragBeginEvent_, e); | |
| 871 } | |
| 872 }, | |
| 873 | |
| 874 onMouseUp_: function(e) { | |
| 875 var i; | |
| 876 if (this.dragBeginEvent_) { | |
| 877 // Stop the dragging. | |
| 878 this.hideDragBox_(); | |
| 879 var eDown = this.dragBeginEvent_; | |
| 880 this.dragBeginEvent_ = null; | |
| 881 | |
| 882 // Figure out extents of the drag. | |
| 883 var loX = Math.min(eDown.clientX, e.clientX); | |
| 884 var hiX = Math.max(eDown.clientX, e.clientX); | |
| 885 var loY = Math.min(eDown.clientY, e.clientY); | |
| 886 var hiY = Math.max(eDown.clientY, e.clientY); | |
| 887 | |
| 888 // Convert to worldspace. | |
| 889 var canv = this.firstCanvas; | |
| 890 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); | |
| 891 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); | |
| 892 | |
| 893 // Figure out what has been hit. | |
| 894 var selection = new TimelineSelection(); | |
| 895 for (i = 0; i < this.tracks_.children.length; i++) { | |
| 896 var track = this.tracks_.children[i]; | |
| 897 | |
| 898 // Only check tracks that insersect the rect. | |
| 899 var trackClientRect = track.getBoundingClientRect(); | |
| 900 var a = Math.max(loY, trackClientRect.top); | |
| 901 var b = Math.min(hiY, trackClientRect.bottom); | |
| 902 if (a <= b) { | |
| 903 track.addIntersectingItemsInRangeToSelection( | |
| 904 loWX, hiWX, loY, hiY, selection); | |
| 905 } | |
| 906 } | |
| 907 // Activate the new selection. | |
| 908 this.selection = selection; | |
| 909 } | |
| 910 }, | |
| 911 | |
| 912 onDblClick_: function(e) { | |
| 913 var canv = this.firstCanvas; | |
| 914 if (e.x < canv.offsetLeft) | |
| 915 return; | |
| 916 | |
| 917 var scale = 4; | |
| 918 if (e.shiftKey) | |
| 919 scale = 1 / scale; | |
| 920 this.zoomBy_(scale); | |
| 921 e.preventDefault(); | |
| 922 } | |
| 923 }; | |
| 924 | |
| 925 /** | |
| 926 * The TimelineModel being viewed by the timeline | |
| 927 * @type {TimelineModel} | |
| 928 */ | |
| 929 cr.defineProperty(Timeline, 'model', cr.PropertyKind.JS); | |
| 930 | |
| 931 return { | |
| 932 Timeline: Timeline, | |
| 933 TimelineSelectionSliceHit: TimelineSelectionSliceHit, | |
| 934 TimelineSelectionCounterSampleHit: TimelineSelectionCounterSampleHit, | |
| 935 TimelineSelection: TimelineSelection, | |
| 936 TimelineViewport: TimelineViewport | |
| 937 }; | |
| 938 }); | |
| OLD | NEW |