Home | History | Annotate | Download | only in tracing
      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  */
     21 base.requireStylesheet('tracing.timeline_track_view');
     22 base.require('base.events');
     23 base.require('base.properties');
     24 base.require('base.settings');
     25 base.require('tracing.filter');
     26 base.require('tracing.selection');
     27 base.require('tracing.timeline_viewport');
     28 base.require('tracing.mouse_mode_constants');
     29 base.require('tracing.tracks.drawing_container');
     30 base.require('tracing.tracks.trace_model_track');
     31 base.require('tracing.tracks.ruler_track');
     32 base.require('ui');
     33 base.require('ui.mouse_mode_selector');
     34 
     35 base.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