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