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 */ 20cr.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 this.viewportTrack = new tracing.TimelineViewportTrack(); 406 407 this.tracks_ = this.ownerDocument.createElement('div'); 408 this.appendChild(this.tracks_); 409 410 this.dragBox_ = this.ownerDocument.createElement('div'); 411 this.dragBox_.className = 'timeline-drag-box'; 412 this.appendChild(this.dragBox_); 413 this.hideDragBox_(); 414 415 this.bindEventListener_(document, 'keypress', this.onKeypress_, this); 416 this.bindEventListener_(document, 'keydown', this.onKeydown_, this); 417 this.bindEventListener_(document, 'mousedown', this.onMouseDown_, this); 418 this.bindEventListener_(document, 'mousemove', this.onMouseMove_, this); 419 this.bindEventListener_(document, 'mouseup', this.onMouseUp_, this); 420 this.bindEventListener_(document, 'dblclick', this.onDblClick_, this); 421 422 this.lastMouseViewPos_ = {x: 0, y: 0}; 423 424 this.selection_ = new TimelineSelection(); 425 }, 426 427 /** 428 * Wraps the standard addEventListener but automatically binds the provided 429 * func to the provided target, tracking the resulting closure. When detach 430 * is called, these listeners will be automatically removed. 431 */ 432 bindEventListener_: function(object, event, func, target) { 433 if (!this.boundListeners_) 434 this.boundListeners_ = []; 435 var boundFunc = func.bind(target); 436 this.boundListeners_.push({object: object, 437 event: event, 438 boundFunc: boundFunc}); 439 object.addEventListener(event, boundFunc); 440 }, 441 442 detach: function() { 443 for (var i = 0; i < this.tracks_.children.length; i++) 444 this.tracks_.children[i].detach(); 445 446 for (var i = 0; i < this.boundListeners_.length; i++) { 447 var binding = this.boundListeners_[i]; 448 binding.object.removeEventListener(binding.event, binding.boundFunc); 449 } 450 this.boundListeners_ = undefined; 451 this.viewport_.detach(); 452 }, 453 454 get viewport() { 455 return this.viewport_; 456 }, 457 458 get model() { 459 return this.model_; 460 }, 461 462 set model(model) { 463 if (!model) 464 throw Error('Model cannot be null'); 465 if (this.model) { 466 throw Error('Cannot set model twice.'); 467 } 468 this.model_ = model; 469 470 // Figure out all the headings. 471 var allHeadings = []; 472 model.getAllThreads().forEach(function(t) { 473 allHeadings.push(t.userFriendlyName); 474 }); 475 model.getAllCounters().forEach(function(c) { 476 allHeadings.push(c.name); 477 }); 478 model.getAllCpus().forEach(function(c) { 479 allHeadings.push('CPU ' + c.cpuNumber); 480 }); 481 482 // Figure out the maximum heading size. 483 var maxHeadingWidth = 0; 484 var measuringStick = new tracing.MeasuringStick(); 485 var headingEl = document.createElement('div'); 486 headingEl.style.position = 'fixed'; 487 headingEl.className = 'timeline-canvas-based-track-title'; 488 allHeadings.forEach(function(text) { 489 headingEl.textContent = text + ':__'; 490 var w = measuringStick.measure(headingEl).width; 491 // Limit heading width to 300px. 492 if (w > 300) 493 w = 300; 494 if (w > maxHeadingWidth) 495 maxHeadingWidth = w; 496 }); 497 maxHeadingWidth = maxHeadingWidth + 'px'; 498 499 // Reset old tracks. 500 for (var i = 0; i < this.tracks_.children.length; i++) 501 this.tracks_.children[i].detach(); 502 this.tracks_.textContent = ''; 503 504 // Set up the viewport track 505 this.viewportTrack.headingWidth = maxHeadingWidth; 506 this.viewportTrack.viewport = this.viewport_; 507 508 // Get a sorted list of CPUs 509 var cpus = model.getAllCpus(); 510 cpus.sort(tracing.TimelineCpu.compare); 511 512 // Create tracks for each CPU. 513 cpus.forEach(function(cpu) { 514 var track = new tracing.TimelineCpuTrack(); 515 track.heading = 'CPU ' + cpu.cpuNumber + ':'; 516 track.headingWidth = maxHeadingWidth; 517 track.viewport = this.viewport_; 518 track.cpu = cpu; 519 this.tracks_.appendChild(track); 520 521 for (var counterName in cpu.counters) { 522 var counter = cpu.counters[counterName]; 523 track = new tracing.TimelineCounterTrack(); 524 track.heading = 'CPU ' + cpu.cpuNumber + ' ' + counter.name + ':'; 525 track.headingWidth = maxHeadingWidth; 526 track.viewport = this.viewport_; 527 track.counter = counter; 528 this.tracks_.appendChild(track); 529 } 530 }.bind(this)); 531 532 // Get a sorted list of processes. 533 var processes = model.getAllProcesses(); 534 processes.sort(tracing.TimelineProcess.compare); 535 536 // Create tracks for each process. 537 processes.forEach(function(process) { 538 // Add counter tracks for this process. 539 var counters = []; 540 for (var tid in process.counters) 541 counters.push(process.counters[tid]); 542 counters.sort(tracing.TimelineCounter.compare); 543 544 // Create the counters for this process. 545 counters.forEach(function(counter) { 546 var track = new tracing.TimelineCounterTrack(); 547 track.heading = counter.name + ':'; 548 track.headingWidth = maxHeadingWidth; 549 track.viewport = this.viewport_; 550 track.counter = counter; 551 this.tracks_.appendChild(track); 552 }.bind(this)); 553 554 // Get a sorted list of threads. 555 var threads = []; 556 for (var tid in process.threads) 557 threads.push(process.threads[tid]); 558 threads.sort(tracing.TimelineThread.compare); 559 560 // Create the threads. 561 threads.forEach(function(thread) { 562 var track = new tracing.TimelineThreadTrack(); 563 track.heading = thread.userFriendlyName + ':'; 564 track.tooltip = thread.userFriendlyDetails; 565 track.headingWidth = maxHeadingWidth; 566 track.viewport = this.viewport_; 567 track.thread = thread; 568 this.tracks_.appendChild(track); 569 }.bind(this)); 570 }.bind(this)); 571 572 // Set up a reasonable viewport. 573 this.viewport_.setWhenPossible(function() { 574 var w = this.firstCanvas.width; 575 this.viewport_.xSetWorldRange(this.model_.minTimestamp, 576 this.model_.maxTimestamp, 577 w); 578 }.bind(this)); 579 }, 580 581 /** 582 * @param {TimelineFilter} filter The filter to use for finding matches. 583 * @param {TimelineSelection} selection The selection to add matches to. 584 * @return {Array} An array of objects that match the provided 585 * TimelineFilter. 586 */ 587 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 588 for (var i = 0; i < this.tracks_.children.length; ++i) 589 this.tracks_.children[i].addAllObjectsMatchingFilterToSelection( 590 filter, selection); 591 }, 592 593 /** 594 * @return {Element} The element whose focused state determines 595 * whether to respond to keyboard inputs. 596 * Defaults to the parent element. 597 */ 598 get focusElement() { 599 if (this.focusElement_) 600 return this.focusElement_; 601 return this.parentElement; 602 }, 603 604 /** 605 * Sets the element whose focus state will determine whether 606 * to respond to keybaord input. 607 */ 608 set focusElement(value) { 609 this.focusElement_ = value; 610 }, 611 612 get listenToKeys_() { 613 if (!this.viewport_.isAttachedToDocument_) 614 return false; 615 if (!this.focusElement_) 616 return true; 617 if (this.focusElement.tabIndex >= 0) 618 return document.activeElement == this.focusElement; 619 return true; 620 }, 621 622 onKeypress_: function(e) { 623 var vp = this.viewport_; 624 if (!this.firstCanvas) 625 return; 626 if (!this.listenToKeys_) 627 return; 628 var viewWidth = this.firstCanvas.clientWidth; 629 var curMouseV, curCenterW; 630 switch (e.keyCode) { 631 case 101: // e 632 var vX = this.lastMouseViewPos_.x; 633 var wX = vp.xViewToWorld(this.lastMouseViewPos_.x); 634 var distFromCenter = vX - (viewWidth / 2); 635 var percFromCenter = distFromCenter / viewWidth; 636 var percFromCenterSq = percFromCenter * percFromCenter; 637 vp.xPanWorldPosToViewPos(wX, 'center', viewWidth); 638 break; 639 case 119: // w 640 this.zoomBy_(1.5); 641 break; 642 case 115: // s 643 this.zoomBy_(1 / 1.5); 644 break; 645 case 103: // g 646 this.onGridToggle_(true); 647 break; 648 case 71: // G 649 this.onGridToggle_(false); 650 break; 651 case 87: // W 652 this.zoomBy_(10); 653 break; 654 case 83: // S 655 this.zoomBy_(1 / 10); 656 break; 657 case 97: // a 658 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); 659 break; 660 case 100: // d 661 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); 662 break; 663 case 65: // A 664 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5); 665 break; 666 case 68: // D 667 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5); 668 break; 669 } 670 }, 671 672 // Not all keys send a keypress. 673 onKeydown_: function(e) { 674 if (!this.listenToKeys_) 675 return; 676 var sel; 677 switch (e.keyCode) { 678 case 37: // left arrow 679 sel = this.selection.getShiftedSelection(-1); 680 if (sel) { 681 this.setSelectionAndMakeVisible(sel); 682 e.preventDefault(); 683 } 684 break; 685 case 39: // right arrow 686 sel = this.selection.getShiftedSelection(1); 687 if (sel) { 688 this.setSelectionAndMakeVisible(sel); 689 e.preventDefault(); 690 } 691 break; 692 case 9: // TAB 693 if (this.focusElement.tabIndex == -1) { 694 if (e.shiftKey) 695 this.selectPrevious_(e); 696 else 697 this.selectNext_(e); 698 e.preventDefault(); 699 } 700 break; 701 } 702 }, 703 704 /** 705 * Zoom in or out on the timeline by the given scale factor. 706 * @param {integer} scale The scale factor to apply. If <1, zooms out. 707 */ 708 zoomBy_: function(scale) { 709 if (!this.firstCanvas) 710 return; 711 var vp = this.viewport_; 712 var viewWidth = this.firstCanvas.clientWidth; 713 var curMouseV = this.lastMouseViewPos_.x; 714 var curCenterW = vp.xViewToWorld(curMouseV); 715 vp.scaleX = vp.scaleX * scale; 716 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); 717 }, 718 719 get keyHelp() { 720 var help = 'Keyboard shortcuts:\n' + 721 ' w/s : Zoom in/out (with shift: go faster)\n' + 722 ' a/d : Pan left/right\n' + 723 ' e : Center on mouse\n' + 724 ' g/G : Shows grid at the start/end of the selected task\n'; 725 726 if (this.focusElement.tabIndex) { 727 help += ' <- : Select previous event on current timeline\n' + 728 ' -> : Select next event on current timeline\n'; 729 } else { 730 help += ' <-,^TAB : Select previous event on current timeline\n' + 731 ' ->, TAB : Select next event on current timeline\n'; 732 } 733 help += 734 '\n' + 735 'Dbl-click to zoom in; Shift dbl-click to zoom out\n'; 736 return help; 737 }, 738 739 get selection() { 740 return this.selection_; 741 }, 742 743 set selection(selection) { 744 if (!(selection instanceof TimelineSelection)) 745 throw 'Expected TimelineSelection'; 746 747 // Clear old selection. 748 var i; 749 for (i = 0; i < this.selection_.length; i++) 750 this.selection_[i].selected = false; 751 752 this.selection_ = selection; 753 754 cr.dispatchSimpleEvent(this, 'selectionChange'); 755 for (i = 0; i < this.selection_.length; i++) 756 this.selection_[i].selected = true; 757 this.viewport_.dispatchChangeEvent(); // Triggers a redraw. 758 }, 759 760 setSelectionAndMakeVisible: function(selection, zoomAllowed) { 761 if (!(selection instanceof TimelineSelection)) 762 throw 'Expected TimelineSelection'; 763 this.selection = selection; 764 var range = this.selection.range; 765 var size = this.viewport_.xWorldVectorToView(range.max - range.min); 766 if (zoomAllowed && size < 50) { 767 var worldCenter = range.min + (range.max - range.min) * 0.5; 768 var worldRange = (range.max - range.min) * 5; 769 this.viewport_.xSetWorldRange(worldCenter - worldRange * 0.5, 770 worldCenter + worldRange * 0.5, 771 this.firstCanvas.width); 772 return; 773 } 774 775 this.viewport_.xPanWorldRangeIntoView(range.min, range.max, 776 this.firstCanvas.width); 777 }, 778 779 get firstCanvas() { 780 return this.tracks_.firstChild ? 781 this.tracks_.firstChild.firstCanvas : undefined; 782 }, 783 784 hideDragBox_: function() { 785 this.dragBox_.style.left = '-1000px'; 786 this.dragBox_.style.top = '-1000px'; 787 this.dragBox_.style.width = 0; 788 this.dragBox_.style.height = 0; 789 }, 790 791 setDragBoxPosition_: function(eDown, eCur) { 792 var loX = Math.min(eDown.clientX, eCur.clientX); 793 var hiX = Math.max(eDown.clientX, eCur.clientX); 794 var loY = Math.min(eDown.clientY, eCur.clientY); 795 var hiY = Math.max(eDown.clientY, eCur.clientY); 796 797 this.dragBox_.style.left = loX + 'px'; 798 this.dragBox_.style.top = loY + 'px'; 799 this.dragBox_.style.width = hiX - loX + 'px'; 800 this.dragBox_.style.height = hiY - loY + 'px'; 801 802 var canv = this.firstCanvas; 803 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); 804 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); 805 806 var roundedDuration = Math.round((hiWX - loWX) * 100) / 100; 807 this.dragBox_.textContent = roundedDuration + 'ms'; 808 809 var e = new cr.Event('selectionChanging'); 810 e.loWX = loWX; 811 e.hiWX = hiWX; 812 this.dispatchEvent(e); 813 }, 814 815 onGridToggle_: function(left) { 816 var tb; 817 if (left) 818 tb = this.selection_.range.min; 819 else 820 tb = this.selection_.range.max; 821 822 // Shift the timebase left until its just left of minTimestamp. 823 var numInterfvalsSinceStart = Math.ceil((tb - this.model_.minTimestamp) / 824 this.viewport_.gridStep_); 825 this.viewport_.gridTimebase = tb - 826 (numInterfvalsSinceStart + 1) * this.viewport_.gridStep_; 827 this.viewport_.gridEnabled = true; 828 }, 829 830 onMouseDown_: function(e) { 831 var canv = this.firstCanvas; 832 var rect = this.tracks_.getClientRects()[0]; 833 var inside = rect && 834 e.clientX >= rect.left && 835 e.clientX < rect.right && 836 e.clientY >= rect.top && 837 e.clientY < rect.bottom && 838 e.x >= canv.offsetLeft; 839 if (!inside) 840 return; 841 842 var pos = { 843 x: e.clientX - canv.offsetLeft, 844 y: e.clientY - canv.offsetTop 845 }; 846 847 var wX = this.viewport_.xViewToWorld(pos.x); 848 849 this.dragBeginEvent_ = e; 850 e.preventDefault(); 851 if (this.focusElement.tabIndex >= 0) 852 this.focusElement.focus(); 853 }, 854 855 onMouseMove_: function(e) { 856 if (!this.firstCanvas) 857 return; 858 var canv = this.firstCanvas; 859 var pos = { 860 x: e.clientX - canv.offsetLeft, 861 y: e.clientY - canv.offsetTop 862 }; 863 864 // Remember position. Used during keyboard zooming. 865 this.lastMouseViewPos_ = pos; 866 867 // Update the drag box 868 if (this.dragBeginEvent_) { 869 this.setDragBoxPosition_(this.dragBeginEvent_, e); 870 } 871 }, 872 873 onMouseUp_: function(e) { 874 var i; 875 if (this.dragBeginEvent_) { 876 // Stop the dragging. 877 this.hideDragBox_(); 878 var eDown = this.dragBeginEvent_; 879 this.dragBeginEvent_ = null; 880 881 // Figure out extents of the drag. 882 var loX = Math.min(eDown.clientX, e.clientX); 883 var hiX = Math.max(eDown.clientX, e.clientX); 884 var loY = Math.min(eDown.clientY, e.clientY); 885 var hiY = Math.max(eDown.clientY, e.clientY); 886 887 // Convert to worldspace. 888 var canv = this.firstCanvas; 889 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); 890 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); 891 892 // Figure out what has been hit. 893 var selection = new TimelineSelection(); 894 for (i = 0; i < this.tracks_.children.length; i++) { 895 var track = this.tracks_.children[i]; 896 897 // Only check tracks that insersect the rect. 898 var trackClientRect = track.getBoundingClientRect(); 899 var a = Math.max(loY, trackClientRect.top); 900 var b = Math.min(hiY, trackClientRect.bottom); 901 if (a <= b) { 902 track.addIntersectingItemsInRangeToSelection( 903 loWX, hiWX, loY, hiY, selection); 904 } 905 } 906 // Activate the new selection. 907 this.selection = selection; 908 } 909 }, 910 911 onDblClick_: function(e) { 912 var canv = this.firstCanvas; 913 if (e.x < canv.offsetLeft) 914 return; 915 916 var scale = 4; 917 if (e.shiftKey) 918 scale = 1 / scale; 919 this.zoomBy_(scale); 920 e.preventDefault(); 921 } 922 }; 923 924 /** 925 * The TimelineModel being viewed by the timeline 926 * @type {TimelineModel} 927 */ 928 cr.defineProperty(Timeline, 'model', cr.PropertyKind.JS); 929 930 return { 931 Timeline: Timeline, 932 TimelineSelectionSliceHit: TimelineSelectionSliceHit, 933 TimelineSelectionCounterSampleHit: TimelineSelectionCounterSampleHit, 934 TimelineSelection: TimelineSelection, 935 TimelineViewport: TimelineViewport 936 }; 937}); 938