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 TraceModel objects 9 * based loosely on gantt charts. Each thread in the TraceModel is given a 10 * set of Tracks, one per subrow in the thread. The TimelineTrackView class 11 * acts as a controller, creating the individual tracks, while Tracks 12 * do actual drawing. 13 * 14 * Visually, the TimelineTrackView produces (prettier) visualizations like the 15 * following: 16 * Thread1: AAAAAAAAAA AAAAA 17 * BBBB BB 18 * Thread2: CCCCCC CCCCC 19 * 20 */ 21base.requireStylesheet('tracing.timeline_track_view'); 22base.require('base.events'); 23base.require('base.properties'); 24base.require('base.settings'); 25base.require('tracing.filter'); 26base.require('tracing.selection'); 27base.require('tracing.timeline_viewport'); 28base.require('tracing.mouse_mode_constants'); 29base.require('tracing.tracks.drawing_container'); 30base.require('tracing.tracks.trace_model_track'); 31base.require('tracing.tracks.ruler_track'); 32base.require('ui'); 33base.require('ui.mouse_mode_selector'); 34 35base.exportTo('tracing', function() { 36 37 var Selection = tracing.Selection; 38 var Viewport = tracing.TimelineViewport; 39 var MIN_SELECTION_DISTANCE = 4; 40 41 function intersectRect_(r1, r2) { 42 var results = new Object; 43 if (r2.left > r1.right || r2.right < r1.left || 44 r2.top > r1.bottom || r2.bottom < r1.top) { 45 return false; 46 } 47 results.left = Math.max(r1.left, r2.left); 48 results.top = Math.max(r1.top, r2.top); 49 results.right = Math.min(r1.right, r2.right); 50 results.bottom = Math.min(r1.bottom, r2.bottom); 51 results.width = (results.right - results.left); 52 results.height = (results.bottom - results.top); 53 return results; 54 } 55 56 /** 57 * Renders a TraceModel into a div element, making one 58 * Track for each subrow in each thread of the model, managing 59 * overall track layout, and handling user interaction with the 60 * viewport. 61 * 62 * @constructor 63 * @extends {HTMLDivElement} 64 */ 65 var TimelineTrackView = ui.define('div'); 66 67 TimelineTrackView.prototype = { 68 __proto__: HTMLDivElement.prototype, 69 70 model_: null, 71 72 decorate: function() { 73 74 this.classList.add('timeline-track-view'); 75 76 this.categoryFilter_ = new tracing.CategoryFilter(); 77 78 this.viewport_ = new Viewport(this); 79 this.viewportStateAtMouseDown_ = null; 80 81 this.rulerTrackContainer_ = 82 new tracing.tracks.DrawingContainer(this.viewport_); 83 this.appendChild(this.rulerTrackContainer_); 84 this.rulerTrackContainer_.invalidate(); 85 86 this.rulerTrack_ = new tracing.tracks.RulerTrack(this.viewport_); 87 this.rulerTrackContainer_.appendChild(this.rulerTrack_); 88 89 this.modelTrackContainer_ = 90 new tracing.tracks.DrawingContainer(this.viewport_); 91 this.appendChild(this.modelTrackContainer_); 92 this.modelTrackContainer_.style.display = 'block'; 93 this.modelTrackContainer_.invalidate(); 94 95 this.viewport_.modelTrackContainer = this.modelTrackContainer_; 96 97 this.modelTrack_ = new tracing.tracks.TraceModelTrack(this.viewport_); 98 this.modelTrackContainer_.appendChild(this.modelTrack_); 99 100 this.mouseModeSelector_ = new ui.MouseModeSelector(this); 101 this.appendChild(this.mouseModeSelector_); 102 103 this.dragBox_ = this.ownerDocument.createElement('div'); 104 this.dragBox_.className = 'drag-box'; 105 this.appendChild(this.dragBox_); 106 this.hideDragBox_(); 107 108 this.bindEventListener_(document, 'keypress', this.onKeypress_, this); 109 110 this.bindEventListener_(document, 'beginpan', this.onBeginPanScan_, this); 111 this.bindEventListener_(document, 'updatepan', 112 this.onUpdatePanScan_, this); 113 this.bindEventListener_(document, 'endpan', this.onEndPanScan_, this); 114 115 this.bindEventListener_(document, 'beginselection', 116 this.onBeginSelection_, this); 117 this.bindEventListener_(document, 'updateselection', 118 this.onUpdateSelection_, this); 119 this.bindEventListener_(document, 'endselection', 120 this.onEndSelection_, this); 121 122 this.bindEventListener_(document, 'beginzoom', this.onBeginZoom_, this); 123 this.bindEventListener_(document, 'updatezoom', this.onUpdateZoom_, this); 124 this.bindEventListener_(document, 'endzoom', this.onEndZoom_, this); 125 126 this.bindEventListener_(document, 'keydown', this.onKeydown_, this); 127 this.bindEventListener_(document, 'keyup', this.onKeyup_, this); 128 129 this.addEventListener('mousemove', this.onMouseMove_); 130 this.addEventListener('dblclick', this.onDblClick_); 131 132 this.mouseViewPosAtMouseDown_ = {x: 0, y: 0}; 133 this.lastMouseViewPos_ = {x: 0, y: 0}; 134 this.selection_ = new Selection(); 135 136 this.isPanningAndScanning_ = false; 137 this.isZooming_ = false; 138 139 }, 140 141 distanceCoveredInPanScan_: function(e) { 142 var x = this.lastMouseViewPos_.x - this.mouseViewPosAtMouseDown_.x; 143 var y = this.lastMouseViewPos_.y - this.mouseViewPosAtMouseDown_.y; 144 145 return Math.sqrt(x * x + y * y); 146 }, 147 148 /** 149 * Wraps the standard addEventListener but automatically binds the provided 150 * func to the provided target, tracking the resulting closure. When detach 151 * is called, these listeners will be automatically removed. 152 */ 153 bindEventListener_: function(object, event, func, target) { 154 if (!this.boundListeners_) 155 this.boundListeners_ = []; 156 var boundFunc = func.bind(target); 157 this.boundListeners_.push({object: object, 158 event: event, 159 boundFunc: boundFunc}); 160 object.addEventListener(event, boundFunc); 161 }, 162 163 detach: function() { 164 this.modelTrack_.detach(); 165 166 for (var i = 0; i < this.boundListeners_.length; i++) { 167 var binding = this.boundListeners_[i]; 168 binding.object.removeEventListener(binding.event, binding.boundFunc); 169 } 170 this.boundListeners_ = undefined; 171 this.viewport_.detach(); 172 }, 173 174 get viewport() { 175 return this.viewport_; 176 }, 177 178 get categoryFilter() { 179 return this.categoryFilter_; 180 }, 181 182 set categoryFilter(filter) { 183 this.modelTrackContainer_.invalidate(); 184 185 this.categoryFilter_ = filter; 186 this.modelTrack_.categoryFilter = filter; 187 }, 188 189 get model() { 190 return this.model_; 191 }, 192 193 set model(model) { 194 if (!model) 195 throw new Error('Model cannot be null'); 196 197 var modelInstanceChanged = this.model_ != model; 198 this.model_ = model; 199 this.modelTrack_.model = model; 200 this.modelTrack_.categoryFilter = this.categoryFilter; 201 202 // Set up a reasonable viewport. 203 if (modelInstanceChanged) 204 this.viewport_.setWhenPossible(this.setInitialViewport_.bind(this)); 205 206 base.setPropertyAndDispatchChange(this, 'model', model); 207 }, 208 209 get hasVisibleContent() { 210 return this.modelTrack_.hasVisibleContent; 211 }, 212 213 setInitialViewport_: function() { 214 var w = this.modelTrackContainer_.canvas.width; 215 216 var min; 217 var range; 218 219 if (this.model_.bounds.isEmpty) { 220 min = 0; 221 range = 1000; 222 } else if (this.model_.bounds.range == 0) { 223 min = this.model_.bounds.min; 224 range = 1000; 225 } else { 226 min = this.model_.bounds.min; 227 range = this.model_.bounds.range; 228 } 229 var boost = range * 0.15; 230 this.viewport_.xSetWorldBounds(min - boost, 231 min + range + boost, 232 w); 233 }, 234 235 /** 236 * @param {Filter} filter The filter to use for finding matches. 237 * @param {Selection} selection The selection to add matches to. 238 * @return {Array} An array of objects that match the provided 239 * TitleFilter. 240 */ 241 addAllObjectsMatchingFilterToSelection: function(filter, selection) { 242 this.modelTrack_.addAllObjectsMatchingFilterToSelection(filter, 243 selection); 244 }, 245 246 /** 247 * @return {Element} The element whose focused state determines 248 * whether to respond to keyboard inputs. 249 * Defaults to the parent element. 250 */ 251 get focusElement() { 252 if (this.focusElement_) 253 return this.focusElement_; 254 return this.parentElement; 255 }, 256 257 /** 258 * Sets the element whose focus state will determine whether 259 * to respond to keybaord input. 260 */ 261 set focusElement(value) { 262 this.focusElement_ = value; 263 }, 264 265 get listenToKeys_() { 266 if (!this.viewport_.isAttachedToDocument_) 267 return false; 268 if (this.activeElement instanceof tracing.FindControl) 269 return false; 270 if (!this.focusElement_) 271 return true; 272 if (this.focusElement.tabIndex >= 0) { 273 if (document.activeElement == this.focusElement) 274 return true; 275 return ui.elementIsChildOf(document.activeElement, this.focusElement); 276 } 277 return true; 278 }, 279 280 onMouseMove_: function(e) { 281 282 // Zooming requires the delta since the last mousemove so we need to avoid 283 // tracking it when the zoom interaction is active. 284 if (this.isZooming_) 285 return; 286 287 this.storeLastMousePos_(e); 288 }, 289 290 onKeypress_: function(e) { 291 var mouseModeConstants = tracing.mouseModeConstants; 292 var vp = this.viewport_; 293 if (!this.listenToKeys_) 294 return; 295 if (document.activeElement.nodeName == 'INPUT') 296 return; 297 var viewWidth = this.modelTrackContainer_.canvas.clientWidth; 298 var curMouseV, curCenterW; 299 switch (e.keyCode) { 300 301 case 119: // w 302 case 44: // , 303 this.zoomBy_(1.5); 304 break; 305 case 115: // s 306 case 111: // o 307 this.zoomBy_(1 / 1.5); 308 break; 309 case 103: // g 310 this.onGridToggle_(true); 311 break; 312 case 71: // G 313 this.onGridToggle_(false); 314 break; 315 case 87: // W 316 case 60: // < 317 this.zoomBy_(10); 318 break; 319 case 83: // S 320 case 79: // O 321 this.zoomBy_(1 / 10); 322 break; 323 case 97: // a 324 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); 325 break; 326 case 100: // d 327 case 101: // e 328 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); 329 break; 330 case 65: // A 331 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5); 332 break; 333 case 68: // D 334 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5); 335 break; 336 case 48: // 0 337 case 122: // z 338 this.setInitialViewport_(); 339 break; 340 case 102: // f 341 this.zoomToSelection(); 342 break; 343 } 344 }, 345 346 // Not all keys send a keypress. 347 onKeydown_: function(e) { 348 if (!this.listenToKeys_) 349 return; 350 var sel; 351 var mouseModeConstants = tracing.mouseModeConstants; 352 var vp = this.viewport_; 353 var viewWidth = this.modelTrackContainer_.canvas.clientWidth; 354 355 switch (e.keyCode) { 356 case 37: // left arrow 357 sel = this.selection.getShiftedSelection(-1); 358 if (sel) { 359 this.selection = sel; 360 this.panToSelection(); 361 e.preventDefault(); 362 } else { 363 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); 364 } 365 break; 366 case 39: // right arrow 367 sel = this.selection.getShiftedSelection(1); 368 if (sel) { 369 this.selection = sel; 370 this.panToSelection(); 371 e.preventDefault(); 372 } else { 373 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); 374 } 375 break; 376 case 9: // TAB 377 if (this.focusElement.tabIndex == -1) { 378 if (e.shiftKey) 379 this.selectPrevious_(e); 380 else 381 this.selectNext_(e); 382 e.preventDefault(); 383 } 384 break; 385 } 386 }, 387 388 onKeyup_: function(e) { 389 if (!this.listenToKeys_) 390 return; 391 if (!e.shiftKey) { 392 if (this.dragBeginEvent_) { 393 this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, 394 this.dragBoxXEnd_, this.dragBoxYEnd_); 395 } 396 } 397 398 }, 399 400 /** 401 * Zoom in or out on the timeline by the given scale factor. 402 * @param {integer} scale The scale factor to apply. If <1, zooms out. 403 */ 404 zoomBy_: function(scale) { 405 var vp = this.viewport_; 406 var viewWidth = this.modelTrackContainer_.canvas.clientWidth; 407 var pixelRatio = window.devicePixelRatio || 1; 408 var curMouseV = this.lastMouseViewPos_.x * pixelRatio; 409 var curCenterW = vp.xViewToWorld(curMouseV); 410 vp.scaleX = vp.scaleX * scale; 411 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); 412 }, 413 414 /** 415 * Zoom into the current selection. 416 */ 417 zoomToSelection: function() { 418 if (!this.selection || !this.selection.length) 419 return; 420 421 var bounds = this.selection.bounds; 422 if (!bounds.range) 423 return; 424 425 var worldCenter = bounds.center; 426 var worldRangeHalf = bounds.range * 0.5; 427 var boost = worldRangeHalf * 0.5; 428 this.viewport_.xSetWorldBounds(worldCenter - worldRangeHalf - boost, 429 worldCenter + worldRangeHalf + boost, 430 this.modelTrackContainer_.canvas.width); 431 }, 432 433 /** 434 * Pan the view so the current selection becomes visible. 435 */ 436 panToSelection: function() { 437 if (!this.selection || !this.selection.length) 438 return; 439 440 var bounds = this.selection.bounds; 441 var worldCenter = bounds.center; 442 var viewWidth = this.modelTrackContainer_.canvas.width; 443 444 if (!bounds.range) { 445 if (this.viewport_.xWorldToView(bounds.center) < 0 || 446 this.viewport_.xWorldToView(bounds.center) > viewWidth) { 447 this.viewport_.xPanWorldPosToViewPos( 448 worldCenter, 'center', viewWidth); 449 } 450 return; 451 } 452 453 var worldRangeHalf = bounds.range * 0.5; 454 var boost = worldRangeHalf * 0.5; 455 this.viewport_.xPanWorldBoundsIntoView( 456 worldCenter - worldRangeHalf - boost, 457 worldCenter + worldRangeHalf + boost, 458 viewWidth); 459 460 this.viewport_.xPanWorldBoundsIntoView(bounds.min, bounds.max, viewWidth); 461 }, 462 463 get keyHelp() { 464 var mod = navigator.platform.indexOf('Mac') == 0 ? 'cmd' : 'ctrl'; 465 var help = 'Qwerty Controls\n' + 466 ' w/s : Zoom in/out (with shift: go faster)\n' + 467 ' a/d : Pan left/right\n\n' + 468 'Dvorak Controls\n' + 469 ' ,/o : Zoom in/out (with shift: go faster)\n' + 470 ' a/e : Pan left/right\n\n' + 471 'Mouse Controls\n' + 472 ' drag (Selection mode) : Select slices (with ' + mod + 473 ': zoom to slices)\n' + 474 ' drag (Pan mode) : Pan left/right/up/down)\n\n'; 475 476 if (this.focusElement.tabIndex) { 477 help += 478 ' <- : Select previous event on current ' + 479 'timeline\n' + 480 ' -> : Select next event on current timeline\n'; 481 } else { 482 help += 'General Navigation\n' + 483 ' g/General : Shows grid at the start/end of the ' + 484 ' selected task\n' + 485 ' <-,^TAB : Select previous event on current ' + 486 'timeline\n' + 487 ' ->, TAB : Select next event on current timeline\n'; 488 } 489 help += 490 '\n' + 491 'Space to switch between select / pan modes\n' + 492 'Shift to temporarily switch between select / pan modes\n' + 493 'Scroll to zoom in/out (in pan mode)\n' + 494 'Dbl-click to add timing markers\n' + 495 'f to zoom into selection\n' + 496 'z to reset zoom and pan to initial view\n' + 497 '/ to search\n'; 498 return help; 499 }, 500 501 get selection() { 502 return this.selection_; 503 }, 504 505 set selection(selection) { 506 if (!(selection instanceof Selection)) 507 throw new Error('Expected Selection'); 508 509 // Clear old selection. 510 var i; 511 for (i = 0; i < this.selection_.length; i++) 512 this.selection_[i].selected = false; 513 514 this.selection_.clear(); 515 this.selection_.addSelection(selection); 516 517 base.dispatchSimpleEvent(this, 'selectionChange'); 518 for (i = 0; i < this.selection_.length; i++) 519 this.selection_[i].selected = true; 520 if (this.selection_.length && 521 this.selection_[0].track) 522 this.selection_[0].track.scrollIntoViewIfNeeded(); 523 this.viewport_.dispatchChangeEvent(); // Triggers a redraw. 524 }, 525 526 hideDragBox_: function() { 527 this.dragBox_.style.left = '-1000px'; 528 this.dragBox_.style.top = '-1000px'; 529 this.dragBox_.style.width = 0; 530 this.dragBox_.style.height = 0; 531 }, 532 533 setDragBoxPosition_: function(xStart, yStart, xEnd, yEnd) { 534 var loY = Math.min(yStart, yEnd); 535 var hiY = Math.max(yStart, yEnd); 536 var loX = Math.min(xStart, xEnd); 537 var hiX = Math.max(xStart, xEnd); 538 var modelTrackRect = this.modelTrack_.getBoundingClientRect(); 539 var dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY}; 540 541 dragRect.right = dragRect.left + dragRect.width; 542 dragRect.bottom = dragRect.top + dragRect.height; 543 544 var modelTrackContainerRect = 545 this.modelTrackContainer_.getBoundingClientRect(); 546 var clipRect = { 547 left: modelTrackContainerRect.left, 548 top: modelTrackContainerRect.top, 549 right: modelTrackContainerRect.right, 550 bottom: modelTrackContainerRect.bottom 551 }; 552 553 var headingWidth = window.getComputedStyle( 554 this.querySelector('heading')).width; 555 var trackTitleWidth = parseInt(headingWidth); 556 clipRect.left = clipRect.left + trackTitleWidth; 557 558 var finalDragBox = intersectRect_(clipRect, dragRect); 559 560 this.dragBox_.style.left = finalDragBox.left + 'px'; 561 this.dragBox_.style.width = finalDragBox.width + 'px'; 562 this.dragBox_.style.top = finalDragBox.top + 'px'; 563 this.dragBox_.style.height = finalDragBox.height + 'px'; 564 565 var pixelRatio = window.devicePixelRatio || 1; 566 var canv = this.modelTrackContainer_.canvas; 567 var loWX = this.viewport_.xViewToWorld( 568 (loX - canv.offsetLeft) * pixelRatio); 569 var hiWX = this.viewport_.xViewToWorld( 570 (hiX - canv.offsetLeft) * pixelRatio); 571 572 var roundedDuration = Math.round((hiWX - loWX) * 100) / 100; 573 this.dragBox_.textContent = roundedDuration + 'ms'; 574 575 var e = new base.Event('selectionChanging'); 576 e.loWX = loWX; 577 e.hiWX = hiWX; 578 this.dispatchEvent(e); 579 }, 580 581 onGridToggle_: function(left) { 582 var tb = left ? this.selection_.bounds.min : this.selection_.bounds.max; 583 584 // Toggle the grid off if the grid is on, the marker position is the same 585 // and the same element is selected (same timebase). 586 if (this.viewport_.gridEnabled && 587 this.viewport_.gridSide === left && 588 this.viewport_.gridTimebase === tb) { 589 this.viewport_.gridside = undefined; 590 this.viewport_.gridEnabled = false; 591 this.viewport_.gridTimebase = undefined; 592 return; 593 } 594 595 // Shift the timebase left until its just left of model_.bounds.min. 596 var numInterfvalsSinceStart = Math.ceil((tb - this.model_.bounds.min) / 597 this.viewport_.gridStep_); 598 this.viewport_.gridTimebase = tb - 599 (numInterfvalsSinceStart + 1) * this.viewport_.gridStep_; 600 601 this.viewport_.gridEnabled = true; 602 this.viewport_.gridSide = left; 603 this.viewport_.gridTimebase = tb; 604 }, 605 606 canBeginInteraction_: function(e) { 607 if (e.button != 0) 608 return false; 609 610 // Ensure that we do not interfere with the user adding markers. 611 if (ui.elementIsChildOf(e.target, this.rulerTrack_)) 612 return false; 613 614 return true; 615 }, 616 617 onDblClick_: function(e) { 618 619 if (this.isPanningAndScanning_) { 620 var endPanEvent = new base.Event('endpan'); 621 endPanEvent.data = e; 622 this.onEndPanScan_(endPanEvent); 623 } 624 625 if (this.isZooming_) { 626 var endZoomEvent = new base.Event('endzoom'); 627 endZoomEvent.data = e; 628 this.onEndZoom_(endZoomEvent); 629 } 630 631 this.rulerTrack_.placeAndBeginDraggingMarker(e.clientX); 632 e.preventDefault(); 633 }, 634 635 storeLastMousePos_: function(e) { 636 this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e); 637 }, 638 639 extractRelativeMousePosition_: function(e) { 640 var canv = this.modelTrackContainer_.canvas; 641 return { 642 x: e.clientX - canv.offsetLeft, 643 y: e.clientY - canv.offsetTop 644 }; 645 }, 646 647 storeInitialMouseDownPos_: function(e) { 648 649 var position = this.extractRelativeMousePosition_(e); 650 651 this.mouseViewPosAtMouseDown_.x = position.x; 652 this.mouseViewPosAtMouseDown_.y = position.y; 653 }, 654 655 focusElements_: function() { 656 if (document.activeElement) 657 document.activeElement.blur(); 658 if (this.focusElement.tabIndex >= 0) 659 this.focusElement.focus(); 660 }, 661 662 storeInitialInteractionPositionsAndFocus_: function(mouseEvent) { 663 664 this.storeInitialMouseDownPos_(mouseEvent); 665 this.storeLastMousePos_(mouseEvent); 666 667 this.focusElements_(); 668 }, 669 670 onBeginPanScan_: function(e) { 671 var vp = this.viewport_; 672 var mouseEvent = e.data; 673 674 if (!this.canBeginInteraction_(mouseEvent)) 675 return; 676 677 this.viewportStateAtMouseDown_ = vp.getStateInViewCoordinates(); 678 this.isPanningAndScanning_ = true; 679 680 this.storeInitialInteractionPositionsAndFocus_(mouseEvent); 681 mouseEvent.preventDefault(); 682 }, 683 684 onUpdatePanScan_: function(e) { 685 if (!this.isPanningAndScanning_) 686 return; 687 688 var vp = this.viewport_; 689 var viewWidth = this.modelTrackContainer_.canvas.clientWidth; 690 var mouseEvent = e.data; 691 692 var x = this.viewportStateAtMouseDown_.panX + (this.lastMouseViewPos_.x - 693 this.mouseViewPosAtMouseDown_.x); 694 var y = this.viewportStateAtMouseDown_.panY - (this.lastMouseViewPos_.y - 695 this.mouseViewPosAtMouseDown_.y); 696 697 vp.setStateInViewCoordinates({ 698 panX: x, 699 panY: y 700 }); 701 702 mouseEvent.preventDefault(); 703 mouseEvent.stopPropagation(); 704 705 this.storeLastMousePos_(mouseEvent); 706 }, 707 708 onEndPanScan_: function(e) { 709 var mouseEvent = e.data; 710 this.isPanningAndScanning_ = false; 711 712 this.storeLastMousePos_(mouseEvent); 713 714 if (this.distanceCoveredInPanScan_(mouseEvent) > MIN_SELECTION_DISTANCE) 715 return; 716 717 this.dragBeginEvent_ = mouseEvent; 718 this.onEndSelection_(e); 719 720 }, 721 722 onBeginSelection_: function(e) { 723 724 var mouseEvent = e.data; 725 726 if (!this.canBeginInteraction_(mouseEvent)) 727 return; 728 729 var canv = this.modelTrackContainer_.canvas; 730 var rect = this.modelTrack_.getBoundingClientRect(); 731 var canvRect = canv.getBoundingClientRect(); 732 733 var inside = rect && 734 mouseEvent.clientX >= rect.left && 735 mouseEvent.clientX < rect.right && 736 mouseEvent.clientY >= rect.top && 737 mouseEvent.clientY < rect.bottom && 738 mouseEvent.clientX >= canvRect.left && 739 mouseEvent.clientX < canvRect.right; 740 741 if (!inside) 742 return; 743 744 this.dragBeginEvent_ = mouseEvent; 745 746 this.storeInitialInteractionPositionsAndFocus_(mouseEvent); 747 mouseEvent.preventDefault(); 748 749 }, 750 751 onUpdateSelection_: function(e) { 752 var mouseEvent = e.data; 753 754 if (!this.dragBeginEvent_) 755 return; 756 757 // Update the drag box 758 this.dragBoxXStart_ = this.dragBeginEvent_.clientX; 759 this.dragBoxXEnd_ = mouseEvent.clientX; 760 this.dragBoxYStart_ = this.dragBeginEvent_.clientY; 761 this.dragBoxYEnd_ = mouseEvent.clientY; 762 this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, 763 this.dragBoxXEnd_, this.dragBoxYEnd_); 764 765 }, 766 767 onEndSelection_: function(e) { 768 769 if (!this.dragBeginEvent_) 770 return; 771 772 var mouseEvent = e.data; 773 774 // Stop the dragging. 775 this.hideDragBox_(); 776 var eDown = this.dragBeginEvent_ || mouseEvent; 777 this.dragBeginEvent_ = null; 778 779 // Figure out extents of the drag. 780 var loY = Math.min(eDown.clientY, mouseEvent.clientY); 781 var hiY = Math.max(eDown.clientY, mouseEvent.clientY); 782 var loX = Math.min(eDown.clientX, mouseEvent.clientX); 783 var hiX = Math.max(eDown.clientX, mouseEvent.clientX); 784 var tracksContainerBoundingRect = 785 this.modelTrackContainer_.getBoundingClientRect(); 786 var topBoundary = tracksContainerBoundingRect.height; 787 788 // Convert to worldspace. 789 var canv = this.modelTrackContainer_.canvas; 790 var loVX = loX - canv.offsetLeft; 791 var hiVX = hiX - canv.offsetLeft; 792 793 // Figure out what has been hit. 794 var selection = new Selection(); 795 this.modelTrack_.addIntersectingItemsInRangeToSelection( 796 loVX, hiVX, loY, hiY, selection); 797 798 // Activate the new selection, and zoom if ctrl key held down. 799 this.selection = selection; 800 if ((base.isMac && e.metaKey) || (!base.isMac && e.ctrlKey)) 801 this.zoomToSelection_(); 802 }, 803 804 onBeginZoom_: function(e) { 805 806 var mouseEvent = e.data; 807 808 if (!this.canBeginInteraction_(mouseEvent)) 809 return; 810 811 this.isZooming_ = true; 812 813 this.storeInitialInteractionPositionsAndFocus_(mouseEvent); 814 mouseEvent.preventDefault(); 815 }, 816 817 onUpdateZoom_: function(e) { 818 819 if (!this.isZooming_) 820 return; 821 var mouseEvent = e.data; 822 var newPosition = this.extractRelativeMousePosition_(mouseEvent); 823 824 var zoomScaleValue = 1 + (this.lastMouseViewPos_.y - 825 newPosition.y) * 0.01; 826 827 this.zoomBy_(zoomScaleValue); 828 this.storeLastMousePos_(mouseEvent); 829 }, 830 831 onEndZoom_: function(e) { 832 this.isZooming_ = false; 833 } 834 }; 835 836 return { 837 TimelineTrackView: TimelineTrackView 838 }; 839}); 840