Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / third_party / trace-viewer / src / tracing / timeline_track_view.js
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 tvcm.requireStylesheet('tvcm.ui.common');
22 tvcm.requireStylesheet('tracing.timeline_track_view');
23 tvcm.require('tvcm.events');
24 tvcm.require('tvcm.properties');
25 tvcm.require('tvcm.settings');
26 tvcm.require('tracing.filter');
27 tvcm.require('tracing.selection');
28 tvcm.require('tracing.timeline_viewport');
29 tvcm.require('tracing.timeline_display_transform_animations');
30 tvcm.require('tracing.timing_tool');
31 tvcm.require('tracing.trace_model.event');
32 tvcm.require('tracing.tracks.drawing_container');
33 tvcm.require('tracing.tracks.trace_model_track');
34 tvcm.require('tracing.tracks.ruler_track');
35 tvcm.require('tvcm.ui');
36 tvcm.require('tvcm.ui.mouse_mode_selector');
37
38 tvcm.exportTo('tracing', function() {
39
40   var Selection = tracing.Selection;
41   var SelectionState = tracing.trace_model.SelectionState;
42   var Viewport = tracing.TimelineViewport;
43
44   var tempDisplayTransform = new tracing.TimelineDisplayTransform();
45
46   function intersectRect_(r1, r2) {
47     var results = new Object;
48     if (r2.left > r1.right || r2.right < r1.left ||
49         r2.top > r1.bottom || r2.bottom < r1.top) {
50       return false;
51     }
52     results.left = Math.max(r1.left, r2.left);
53     results.top = Math.max(r1.top, r2.top);
54     results.right = Math.min(r1.right, r2.right);
55     results.bottom = Math.min(r1.bottom, r2.bottom);
56     results.width = (results.right - results.left);
57     results.height = (results.bottom - results.top);
58     return results;
59   }
60
61   /**
62    * Renders a TraceModel into a div element, making one
63    * Track for each subrow in each thread of the model, managing
64    * overall track layout, and handling user interaction with the
65    * viewport.
66    *
67    * @constructor
68    * @extends {HTMLDivElement}
69    */
70   var TimelineTrackView = tvcm.ui.define('div');
71
72   TimelineTrackView.prototype = {
73     __proto__: HTMLDivElement.prototype,
74
75     model_: null,
76
77     decorate: function() {
78
79       this.classList.add('timeline-track-view');
80
81       this.viewport_ = new Viewport(this);
82       this.viewportDisplayTransformAtMouseDown_ = null;
83
84       this.rulerTrackContainer_ =
85           new tracing.tracks.DrawingContainer(this.viewport_);
86       this.appendChild(this.rulerTrackContainer_);
87       this.rulerTrackContainer_.invalidate();
88
89       this.rulerTrack_ = new tracing.tracks.RulerTrack(this.viewport_);
90       this.rulerTrackContainer_.appendChild(this.rulerTrack_);
91
92       this.modelTrackContainer_ =
93           new tracing.tracks.DrawingContainer(this.viewport_);
94       this.appendChild(this.modelTrackContainer_);
95       this.modelTrackContainer_.style.display = 'block';
96       this.modelTrackContainer_.invalidate();
97
98       this.viewport_.modelTrackContainer = this.modelTrackContainer_;
99
100       this.modelTrack_ = new tracing.tracks.TraceModelTrack(this.viewport_);
101       this.modelTrackContainer_.appendChild(this.modelTrack_);
102
103       this.timingTool_ = new tracing.TimingTool(this.viewport_,
104                                                 this);
105
106       this.initMouseModeSelector();
107
108       this.dragBox_ = this.ownerDocument.createElement('div');
109       this.dragBox_.className = 'drag-box';
110       this.appendChild(this.dragBox_);
111       this.hideDragBox_();
112
113       this.bindEventListener_(document, 'keypress', this.onKeypress_, this);
114       this.bindEventListener_(document, 'keydown', this.onKeydown_, this);
115       this.bindEventListener_(document, 'keyup', this.onKeyup_, this);
116
117       this.bindEventListener_(this, 'dblclick', this.onDblClick_, this);
118       this.bindEventListener_(this, 'mousewheel', this.onMouseWheel_, this);
119
120       this.addEventListener('mousemove', this.onMouseMove_);
121
122       this.mouseViewPosAtMouseDown_ = {x: 0, y: 0};
123       this.lastMouseViewPos_ = {x: 0, y: 0};
124
125       this.selection_ = new Selection();
126       this.highlight_ = new Selection();
127
128       this.isPanningAndScanning_ = false;
129       this.isZooming_ = false;
130     },
131
132     /**
133      * Wraps the standard addEventListener but automatically binds the provided
134      * func to the provided target, tracking the resulting closure. When detach
135      * is called, these listeners will be automatically removed.
136      */
137     bindEventListener_: function(object, event, func, target) {
138       if (!this.boundListeners_)
139         this.boundListeners_ = [];
140       var boundFunc = func.bind(target);
141       this.boundListeners_.push({object: object,
142         event: event,
143         boundFunc: boundFunc});
144       object.addEventListener(event, boundFunc);
145     },
146
147     initMouseModeSelector: function() {
148       this.mouseModeSelector_ = new tvcm.ui.MouseModeSelector(this);
149       this.appendChild(this.mouseModeSelector_);
150
151       this.mouseModeSelector_.addEventListener('beginpan',
152           this.onBeginPanScan_.bind(this));
153       this.mouseModeSelector_.addEventListener('updatepan',
154           this.onUpdatePanScan_.bind(this));
155       this.mouseModeSelector_.addEventListener('endpan',
156           this.onEndPanScan_.bind(this));
157
158       this.mouseModeSelector_.addEventListener('beginselection',
159           this.onBeginSelection_.bind(this));
160       this.mouseModeSelector_.addEventListener('updateselection',
161           this.onUpdateSelection_.bind(this));
162       this.mouseModeSelector_.addEventListener('endselection',
163           this.onEndSelection_.bind(this));
164
165       this.mouseModeSelector_.addEventListener('beginzoom',
166           this.onBeginZoom_.bind(this));
167       this.mouseModeSelector_.addEventListener('updatezoom',
168           this.onUpdateZoom_.bind(this));
169       this.mouseModeSelector_.addEventListener('endzoom',
170           this.onEndZoom_.bind(this));
171
172       this.mouseModeSelector_.addEventListener('entertiming',
173           this.timingTool_.onEnterTiming.bind(this.timingTool_));
174       this.mouseModeSelector_.addEventListener('begintiming',
175           this.timingTool_.onBeginTiming.bind(this.timingTool_));
176       this.mouseModeSelector_.addEventListener('updatetiming',
177           this.timingTool_.onUpdateTiming.bind(this.timingTool_));
178       this.mouseModeSelector_.addEventListener('endtiming',
179           this.timingTool_.onEndTiming.bind(this.timingTool_));
180       this.mouseModeSelector_.addEventListener('exittiming',
181           this.timingTool_.onExitTiming.bind(this.timingTool_));
182
183       var m = tvcm.ui.MOUSE_SELECTOR_MODE;
184       this.mouseModeSelector_.supportedModeMask =
185           m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING;
186       this.mouseModeSelector_.settingsKey =
187           'timelineTrackView.mouseModeSelector';
188       this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0));
189       this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0));
190       this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0));
191       this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0));
192
193       this.mouseModeSelector_.setModifierForAlternateMode(
194           m.SELECTION, tvcm.ui.MODIFIER.SHIFT);
195       this.mouseModeSelector_.setModifierForAlternateMode(
196           m.PANSCAN, tvcm.ui.MODIFIER.SPACE);
197       this.mouseModeSelector_.setModifierForAlternateMode(
198           m.ZOOM, tvcm.ui.MODIFIER.CMD_OR_CTRL);
199     },
200
201     detach: function() {
202       this.modelTrack_.detach();
203
204       for (var i = 0; i < this.boundListeners_.length; i++) {
205         var binding = this.boundListeners_[i];
206         binding.object.removeEventListener(binding.event, binding.boundFunc);
207       }
208       this.boundListeners_ = undefined;
209       this.viewport_.detach();
210     },
211
212     get viewport() {
213       return this.viewport_;
214     },
215
216     get model() {
217       return this.model_;
218     },
219
220     set model(model) {
221       if (!model)
222         throw new Error('Model cannot be null');
223
224       var modelInstanceChanged = this.model_ != model;
225       this.model_ = model;
226       this.modelTrack_.model = model;
227
228       // Set up a reasonable viewport.
229       if (modelInstanceChanged)
230         this.viewport_.setWhenPossible(this.setInitialViewport_.bind(this));
231
232       tvcm.setPropertyAndDispatchChange(this, 'model', model);
233     },
234
235     get hasVisibleContent() {
236       return this.modelTrack_.hasVisibleContent;
237     },
238
239     setInitialViewport_: function() {
240       var w = this.modelTrackContainer_.canvas.width;
241
242       var min;
243       var range;
244
245       if (this.model_.bounds.isEmpty) {
246         min = 0;
247         range = 1000;
248       } else if (this.model_.bounds.range == 0) {
249         min = this.model_.bounds.min;
250         range = 1000;
251       } else {
252         min = this.model_.bounds.min;
253         range = this.model_.bounds.range;
254       }
255       var boost = range * 0.15;
256       tempDisplayTransform.set(this.viewport.currentDisplayTransform);
257       tempDisplayTransform.xSetWorldBounds(min - boost,
258                                            min + range + boost,
259                                            w);
260       this.viewport.setDisplayTransformImmediately(tempDisplayTransform);
261     },
262
263     /**
264      * @param {Filter} filter The filter to use for finding matches.
265      * @param {Selection} selection The selection to add matches to.
266      * @return {Array} An array of objects that match the provided
267      * TitleFilter.
268      */
269     addAllObjectsMatchingFilterToSelection: function(filter, selection) {
270       this.modelTrack_.addAllObjectsMatchingFilterToSelection(
271           filter, selection);
272     },
273
274     /**
275      * @return {Element} The element whose focused state determines
276      * whether to respond to keyboard inputs.
277      * Defaults to the parent element.
278      */
279     get focusElement() {
280       if (this.focusElement_)
281         return this.focusElement_;
282       return this.parentElement;
283     },
284
285     /**
286      * Sets the element whose focus state will determine whether
287      * to respond to keybaord input.
288      */
289     set focusElement(value) {
290       this.focusElement_ = value;
291     },
292
293     get listenToKeys_() {
294       if (!this.viewport_.isAttachedToDocument_)
295         return false;
296       if (this.activeElement instanceof tracing.FindControl)
297         return false;
298       if (!this.focusElement_)
299         return true;
300       if (this.focusElement.tabIndex >= 0) {
301         if (document.activeElement == this.focusElement)
302           return true;
303         return tvcm.ui.elementIsChildOf(document.activeElement,
304                                         this.focusElement);
305       }
306       return true;
307     },
308
309     onMouseMove_: function(e) {
310
311       // Zooming requires the delta since the last mousemove so we need to avoid
312       // tracking it when the zoom interaction is active.
313       if (this.isZooming_)
314         return;
315
316       this.storeLastMousePos_(e);
317     },
318
319     onKeypress_: function(e) {
320       var vp = this.viewport_;
321       if (!this.listenToKeys_)
322         return;
323       if (document.activeElement.nodeName == 'INPUT')
324         return;
325       var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
326       var curMouseV, curCenterW;
327       switch (e.keyCode) {
328
329         case 119:  // w
330         case 44:   // ,
331           this.zoomBy_(1.5, true);
332           break;
333         case 115:  // s
334         case 111:  // o
335           this.zoomBy_(1 / 1.5, true);
336           break;
337         case 103:  // g
338           this.onGridToggle_(true);
339           break;
340         case 71:  // G
341           this.onGridToggle_(false);
342           break;
343         case 87:  // W
344         case 60:  // <
345           this.zoomBy_(10, true);
346           break;
347         case 83:  // S
348         case 79:  // O
349           this.zoomBy_(1 / 10, true);
350           break;
351         case 97:  // a
352           this.queueSmoothPan_(viewWidth * 0.3, 0);
353           break;
354         case 100:  // d
355         case 101:  // e
356           this.queueSmoothPan_(viewWidth * -0.3, 0);
357           break;
358         case 65:  // A
359           this.queueSmoothPan_(viewWidth * 0.5, 0);
360           break;
361         case 68:  // D
362           this.queueSmoothPan_(viewWidth * -0.5, 0);
363           break;
364         case 48:  // 0
365           this.setInitialViewport_();
366           break;
367         case 102:  // f
368           this.zoomToSelection();
369           break;
370         case 'm'.charCodeAt(0):
371           this.putMarkAroundCurrentSelection_();
372           break;
373       }
374     },
375
376     // Not all keys send a keypress.
377     onKeydown_: function(e) {
378       if (!this.listenToKeys_)
379         return;
380       var sel;
381       var vp = this.viewport;
382       var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
383
384       switch (e.keyCode) {
385         case 37:   // left arrow
386           sel = this.selection.getShiftedSelection(
387               this.viewport, -1);
388           if (sel) {
389             this.setSelectionAndClearHighlight(sel);
390             this.panToSelection();
391             e.preventDefault();
392           } else {
393             this.queueSmoothPan_(viewWidth * 0.3, 0);
394           }
395           break;
396         case 39:   // right arrow
397           sel = this.selection.getShiftedSelection(
398               this.viewport, 1);
399           if (sel) {
400             this.setSelectionAndClearHighlight(sel);
401             this.panToSelection();
402             e.preventDefault();
403           } else {
404             this.queueSmoothPan_(-viewWidth * 0.3, 0);
405           }
406           break;
407         case 9:    // TAB
408           if (this.focusElement.tabIndex == -1) {
409             if (e.shiftKey)
410               this.selectPrevious_(e);
411             else
412               this.selectNext_(e);
413             e.preventDefault();
414           }
415           break;
416       }
417     },
418
419     onKeyup_: function(e) {
420       if (!this.listenToKeys_)
421         return;
422       if (!e.shiftKey) {
423         if (this.dragBeginEvent_) {
424           this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
425               this.dragBoxXEnd_, this.dragBoxYEnd_);
426         }
427       }
428
429     },
430
431     onDblClick_: function(e) {
432       if (this.mouseModeSelector_.mode !==
433           tvcm.ui.MOUSE_SELECTOR_MODE.SELECTION)
434         return;
435
436       if (!this.selection.length || !this.selection[0].title)
437         return;
438
439       var selection = new Selection();
440       var filter = new tracing.ExactTitleFilter(this.selection[0].title);
441       this.addAllObjectsMatchingFilterToSelection(filter, selection);
442
443       this.setSelectionAndClearHighlight(selection);
444     },
445
446     onMouseWheel_: function(e) {
447       if (!e.altKey)
448         return;
449
450       var delta = e.wheelDelta / 120;
451       var zoomScale = Math.pow(1.5, delta);
452       this.zoomBy_(zoomScale);
453       e.preventDefault();
454     },
455
456     queueSmoothPan_: function(viewDeltaX, deltaY) {
457       var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld(
458           viewDeltaX);
459       var animation = new tracing.TimelineDisplayTransformPanAnimation(
460           deltaX, deltaY);
461       this.viewport_.queueDisplayTransformAnimation(animation);
462     },
463
464     /**
465      * Zoom in or out on the timeline by the given scale factor.
466      * @param {Number} scale The scale factor to apply.  If <1, zooms out.
467      * @param {boolean} Whether to change the zoom level smoothly.
468      */
469     zoomBy_: function(scale, smooth) {
470       smooth = !!smooth;
471       var vp = this.viewport;
472       var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
473       var pixelRatio = window.devicePixelRatio || 1;
474
475       var goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio;
476       var goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld(
477           goalFocalPointXView);
478       if (smooth) {
479         var animation = new tracing.TimelineDisplayTransformZoomToAnimation(
480             goalFocalPointXWorld, goalFocalPointXView,
481             vp.currentDisplayTransform.panY,
482             scale);
483         vp.queueDisplayTransformAnimation(animation);
484       } else {
485         tempDisplayTransform.set(vp.currentDisplayTransform);
486         tempDisplayTransform.scaleX = tempDisplayTransform.scaleX * scale;
487         tempDisplayTransform.xPanWorldPosToViewPos(
488             goalFocalPointXWorld, goalFocalPointXView, viewWidth);
489         vp.setDisplayTransformImmediately(tempDisplayTransform);
490       }
491     },
492
493     /**
494      * Zoom into the current selection.
495      */
496     zoomToSelection: function() {
497       if (!this.selectionOfInterest.length)
498         return;
499
500       var bounds = this.selectionOfInterest.bounds;
501       if (!bounds.range)
502         return;
503
504       var worldCenter = bounds.center;
505       var adjustedWorldRange = bounds.range * 1.25;
506       var newScale = this.modelTrackContainer_.canvas.width /
507           adjustedWorldRange;
508       var zoomInRatio = newScale / this.viewport.currentDisplayTransform.scaleX;
509       var animation = new tracing.TimelineDisplayTransformZoomToAnimation(
510           worldCenter, 'center',
511           this.viewport.currentDisplayTransform.panY,
512           zoomInRatio);
513       this.viewport.queueDisplayTransformAnimation(animation);
514     },
515
516     /**
517      * Pan the view so the current selection becomes visible.
518      */
519     panToSelection: function() {
520       if (!this.selectionOfInterest.length)
521         return;
522
523       var bounds = this.selectionOfInterest.bounds;
524       var worldCenter = bounds.center;
525       var viewWidth = this.modelTrackContainer_.canvas.width;
526
527       var dt = this.viewport.currentDisplayTransform;
528       if (false && !bounds.range) {
529         if (dt.xWorldToView(bounds.center) < 0 ||
530             dt.xWorldToView(bounds.center) > viewWidth) {
531           tempDisplayTransform.set(dt);
532           tempDisplayTransform.xPanWorldPosToViewPos(
533               worldCenter, 'center', viewWidth);
534           var deltaX = tempDisplayTransform.panX - dt.panX;
535           var animation = new tracing.TimelineDisplayTransformPanAnimation(
536               deltaX, 0);
537           this.viewport.queueDisplayTransformAnimation(animation);
538         }
539         return;
540       }
541
542       tempDisplayTransform.set(dt);
543       tempDisplayTransform.xPanWorldBoundsIntoView(
544           bounds.min,
545           bounds.max,
546           viewWidth);
547       var deltaX = tempDisplayTransform.panX - dt.panX;
548       var animation = new tracing.TimelineDisplayTransformPanAnimation(
549           deltaX, 0);
550       this.viewport.queueDisplayTransformAnimation(animation);
551     },
552
553     putMarkAroundCurrentSelection_: function() {
554       var selectionBounds = this.selection.bounds;
555       if (selectionBounds.empty)
556         return this.viewport.removeAllMarkers();
557       var markerBounds = this.viewport.getMarkerBounds();
558
559
560       if (selectionBounds.equals(markerBounds)) {
561         return this.viewport.removeAllMarkers();
562       }
563       this.viewport.removeAllMarkers();
564       this.viewport.addMarker(this.viewport.createMarker(selectionBounds.min));
565       this.viewport.addMarker(this.viewport.createMarker(selectionBounds.max));
566     },
567
568     /**
569      * Sets the selected events and changes the SelectionState of the events to
570      *   SELECTED.
571      * @param {Selection} selection A Selection of the new selected events.
572      */
573     set selection(selection) {
574       this.setSelectionAndHighlight(selection, this.highlight_);
575     },
576
577     get selection() {
578       return this.selection_;
579     },
580
581     /**
582      * Sets the highlighted events and changes the SelectionState of the events
583      *   to HIGHLIGHTED. All other events are set to DIMMED, except SELECTED
584      *   ones.
585      * @param {Selection} selection A Selection of the new selected events.
586      */
587     set highlight(highlight) {
588       this.setSelectionAndHighlight(this.selection_, highlight);
589     },
590
591     get highlight() {
592       return this.highlight_;
593     },
594
595     /**
596      * Getter for events of interest, primarily SELECTED and secondarily
597      *   HIGHLIGHTED events.
598      */
599     get selectionOfInterest() {
600       if (!this.selection_.length && this.highlight_.length)
601         return this.highlight_;
602       return this.selection_;
603     },
604
605     /**
606      * Sets the selected events, changes the SelectionState of the events to
607      *   SELECTED and clears the highlighted events.
608      * @param {Selection} selection A Selection of the new selected events.
609      */
610     setSelectionAndClearHighlight: function(selection) {
611       this.setSelectionAndHighlight(selection, null);
612     },
613
614     /**
615      * Sets the highlighted events, changes the SelectionState of the events to
616      *   HIGHLIGHTED and clears the selected events. All other events are set to
617      *   DIMMED.
618      * @param {Selection} highlight A Selection of the new highlighted events.
619      */
620     setHighlightAndClearSelection: function(highlight) {
621       this.setSelectionAndHighlight(null, highlight);
622     },
623
624     /**
625      * Sets both selected and highlighted events. If an event is both it will be
626      *   set to SELECTED. All other events are set to DIMMED.
627      * @param {Selection} selection A Selection of the new selected events.
628      * @param {Selection} highlight A Selection of the new highlighted events.
629      */
630     setSelectionAndHighlight: function(selection, highlight) {
631       if (selection === this.selection_ && highlight === this.highlight_)
632         return;
633
634       if ((selection !== null && !(selection instanceof Selection)) ||
635           (highlight !== null && !(highlight instanceof Selection))) {
636         throw new Error('Expected Selection');
637       }
638
639       if (highlight && highlight.length) {
640         // Set all events to DIMMED. This needs to be done before clearing the
641         // old highlight, so that the old events are still available. This is
642         // also necessary when the highlight doesn't change, because it might
643         // have overlapping events with selection.
644         this.resetEventsTo_(SelectionState.DIMMED);
645
646         // Switch the highlight.
647         if (highlight !== this.highlight_)
648           this.highlight_ = highlight;
649
650         // Set HIGHLIGHTED on the events of the new highlight.
651         this.setSelectionState_(highlight, SelectionState.HIGHLIGHTED);
652       } else {
653         // If no highlight is active the SelectionState needs to be cleared.
654         // Note that this also clears old SELECTED events, so it doesn't need
655         // to be called again when setting the selection.
656         this.resetEventsTo_(SelectionState.NONE);
657         this.highlight_ = new Selection();
658       }
659
660       if (selection && selection.length) {
661         // Switch the selection
662         if (selection !== this.selection_)
663           this.selection_ = selection;
664
665         // Set SELECTED on the events of the new highlight.
666         this.setSelectionState_(selection, SelectionState.SELECTED);
667       } else
668         this.selection_ = new Selection();
669
670       tvcm.dispatchSimpleEvent(this, 'selectionChange');
671
672       if (this.selectionOfInterest.length) {
673         var track = this.viewport.trackForEvent(this.selectionOfInterest[0]);
674         if (track)
675           track.scrollIntoViewIfNeeded();
676       }
677
678       this.viewport.dispatchChangeEvent(); // Triggers a redraw.
679     },
680
681     /**
682      * Sets a new SelectionState on all events in the selection.
683      * @param {Selection} selection The affected selection.
684      * @param {SelectionState} selectionState The new selection state.
685      */
686     setSelectionState_: function(selection, selectionState) {
687       for (var i = 0; i < selection.length; i++)
688         selection[i].selectionState = selectionState;
689     },
690
691     /**
692      * Resets all events to the provided SelectionState. When the SelectionState
693      *   changes from or to DIMMED all events in the model need to get updated.
694      * @param {SelectionState} selectionState The SelectionState to reset to.
695      */
696     resetEventsTo_: function(selectionState) {
697       var dimmed = this.highlight_.length;
698       var resetAll = (dimmed && selectionState !== SelectionState.DIMMED) ||
699                      (!dimmed && selectionState === SelectionState.DIMMED);
700       if (resetAll) {
701         this.model.iterateAllEvents(
702             function(event) { event.selectionState = selectionState; });
703       } else {
704         this.setSelectionState_(this.selection_, selectionState);
705         this.setSelectionState_(this.highlight_, selectionState);
706       }
707     },
708
709     hideDragBox_: function() {
710       this.dragBox_.style.left = '-1000px';
711       this.dragBox_.style.top = '-1000px';
712       this.dragBox_.style.width = 0;
713       this.dragBox_.style.height = 0;
714     },
715
716     setDragBoxPosition_: function(xStart, yStart, xEnd, yEnd) {
717       var loY = Math.min(yStart, yEnd);
718       var hiY = Math.max(yStart, yEnd);
719       var loX = Math.min(xStart, xEnd);
720       var hiX = Math.max(xStart, xEnd);
721       var modelTrackRect = this.modelTrack_.getBoundingClientRect();
722       var dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY};
723
724       dragRect.right = dragRect.left + dragRect.width;
725       dragRect.bottom = dragRect.top + dragRect.height;
726
727       var modelTrackContainerRect =
728           this.modelTrackContainer_.getBoundingClientRect();
729       var clipRect = {
730         left: modelTrackContainerRect.left,
731         top: modelTrackContainerRect.top,
732         right: modelTrackContainerRect.right,
733         bottom: modelTrackContainerRect.bottom
734       };
735
736       var headingWidth = window.getComputedStyle(
737           this.querySelector('heading')).width;
738       var trackTitleWidth = parseInt(headingWidth);
739       clipRect.left = clipRect.left + trackTitleWidth;
740
741       var finalDragBox = intersectRect_(clipRect, dragRect);
742
743       this.dragBox_.style.left = finalDragBox.left + 'px';
744       this.dragBox_.style.width = finalDragBox.width + 'px';
745       this.dragBox_.style.top = finalDragBox.top + 'px';
746       this.dragBox_.style.height = finalDragBox.height + 'px';
747
748       var pixelRatio = window.devicePixelRatio || 1;
749       var canv = this.modelTrackContainer_.canvas;
750       var dt = this.viewport.currentDisplayTransform;
751       var loWX = dt.xViewToWorld(
752           (loX - canv.offsetLeft) * pixelRatio);
753       var hiWX = dt.xViewToWorld(
754           (hiX - canv.offsetLeft) * pixelRatio);
755
756       var roundedDuration = Math.round((hiWX - loWX) * 100) / 100;
757       this.dragBox_.textContent = roundedDuration + 'ms';
758
759       var e = new tvcm.Event('selectionChanging');
760       e.loWX = loWX;
761       e.hiWX = hiWX;
762       this.dispatchEvent(e);
763     },
764
765     onGridToggle_: function(left) {
766       var tb = left ? this.selection.bounds.min : this.selection.bounds.max;
767
768       // Toggle the grid off if the grid is on, the marker position is the same
769       // and the same element is selected (same timebase).
770       if (this.viewport.gridEnabled &&
771           this.viewport.gridSide === left &&
772           this.viewport.gridInitialTimebase === tb) {
773         this.viewport.gridside = undefined;
774         this.viewport.gridEnabled = false;
775         this.viewport.gridInitialTimebase = undefined;
776         return;
777       }
778
779       // Shift the timebase left until its just left of model_.bounds.min.
780       var numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) /
781           this.viewport.gridStep_);
782
783       this.viewport.gridEnabled = true;
784       this.viewport.gridSide = left;
785       this.viewport.gridInitialTimebase = tb;
786       this.viewport.gridTimebase = tb -
787           (numIntervalsSinceStart + 1) * this.viewport.gridStep_;
788     },
789
790     storeLastMousePos_: function(e) {
791       this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
792     },
793
794     extractRelativeMousePosition_: function(e) {
795       var canv = this.modelTrackContainer_.canvas;
796       return {
797         x: e.clientX - canv.offsetLeft,
798         y: e.clientY - canv.offsetTop
799       };
800     },
801
802     storeInitialMouseDownPos_: function(e) {
803
804       var position = this.extractRelativeMousePosition_(e);
805
806       this.mouseViewPosAtMouseDown_.x = position.x;
807       this.mouseViewPosAtMouseDown_.y = position.y;
808     },
809
810     focusElements_: function() {
811       if (document.activeElement)
812         document.activeElement.blur();
813       if (this.focusElement.tabIndex >= 0)
814         this.focusElement.focus();
815     },
816
817     storeInitialInteractionPositionsAndFocus_: function(e) {
818
819       this.storeInitialMouseDownPos_(e);
820       this.storeLastMousePos_(e);
821
822       this.focusElements_();
823     },
824
825     onBeginPanScan_: function(e) {
826       var vp = this.viewport;
827       this.viewportDisplayTransformAtMouseDown_ =
828           vp.currentDisplayTransform.clone();
829       this.isPanningAndScanning_ = true;
830
831       this.storeInitialInteractionPositionsAndFocus_(e);
832       e.preventDefault();
833     },
834
835     onUpdatePanScan_: function(e) {
836       if (!this.isPanningAndScanning_)
837         return;
838
839       var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
840
841       var pixelRatio = window.devicePixelRatio || 1;
842       var xDeltaView = pixelRatio * (this.lastMouseViewPos_.x -
843           this.mouseViewPosAtMouseDown_.x);
844
845       var yDelta = this.lastMouseViewPos_.y -
846           this.mouseViewPosAtMouseDown_.y;
847
848       tempDisplayTransform.set(this.viewportDisplayTransformAtMouseDown_);
849       tempDisplayTransform.incrementPanXInViewUnits(xDeltaView);
850       tempDisplayTransform.panY -= yDelta;
851       this.viewport.setDisplayTransformImmediately(tempDisplayTransform);
852
853       e.preventDefault();
854       e.stopPropagation();
855
856       this.storeLastMousePos_(e);
857     },
858
859     onEndPanScan_: function(e) {
860       this.isPanningAndScanning_ = false;
861
862       this.storeLastMousePos_(e);
863
864       if (!e.isClick)
865         e.preventDefault();
866     },
867
868     onBeginSelection_: function(e) {
869       var canv = this.modelTrackContainer_.canvas;
870       var rect = this.modelTrack_.getBoundingClientRect();
871       var canvRect = canv.getBoundingClientRect();
872
873       var inside = rect &&
874           e.clientX >= rect.left &&
875           e.clientX < rect.right &&
876           e.clientY >= rect.top &&
877           e.clientY < rect.bottom &&
878           e.clientX >= canvRect.left &&
879           e.clientX < canvRect.right;
880
881       if (!inside)
882         return;
883
884       this.dragBeginEvent_ = e;
885
886       this.storeInitialInteractionPositionsAndFocus_(e);
887       e.preventDefault();
888     },
889
890     onUpdateSelection_: function(e) {
891       if (!this.dragBeginEvent_)
892         return;
893
894       // Update the drag box
895       this.dragBoxXStart_ = this.dragBeginEvent_.clientX;
896       this.dragBoxXEnd_ = e.clientX;
897       this.dragBoxYStart_ = this.dragBeginEvent_.clientY;
898       this.dragBoxYEnd_ = e.clientY;
899       this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
900           this.dragBoxXEnd_, this.dragBoxYEnd_);
901
902     },
903
904     onEndSelection_: function(e) {
905       e.preventDefault();
906
907       if (!this.dragBeginEvent_)
908         return;
909
910       // Stop the dragging.
911       this.hideDragBox_();
912       var eDown = this.dragBeginEvent_;
913       this.dragBeginEvent_ = null;
914
915       // Figure out extents of the drag.
916       var loY = Math.min(eDown.clientY, e.clientY);
917       var hiY = Math.max(eDown.clientY, e.clientY);
918       var loX = Math.min(eDown.clientX, e.clientX);
919       var hiX = Math.max(eDown.clientX, e.clientX);
920       var tracksContainerBoundingRect =
921           this.modelTrackContainer_.getBoundingClientRect();
922       var topBoundary = tracksContainerBoundingRect.height;
923
924       // Convert to worldspace.
925       var canv = this.modelTrackContainer_.canvas;
926       var loVX = loX - canv.offsetLeft;
927       var hiVX = hiX - canv.offsetLeft;
928
929       // Figure out what has been selected.
930       var selection = new Selection();
931       this.modelTrack_.addIntersectingItemsInRangeToSelection(
932           loVX, hiVX, loY, hiY, selection);
933
934       // Activate the new selection.
935       this.setSelectionAndClearHighlight(selection);
936     },
937
938     onBeginZoom_: function(e) {
939       this.isZooming_ = true;
940
941       this.storeInitialInteractionPositionsAndFocus_(e);
942       e.preventDefault();
943     },
944
945     onUpdateZoom_: function(e) {
946       if (!this.isZooming_)
947         return;
948       var newPosition = this.extractRelativeMousePosition_(e);
949
950       var zoomScaleValue = 1 + (this.lastMouseViewPos_.y -
951           newPosition.y) * 0.01;
952
953       this.zoomBy_(zoomScaleValue, false);
954       this.storeLastMousePos_(e);
955     },
956
957     onEndZoom_: function(e) {
958       this.isZooming_ = false;
959
960       if (!e.isClick)
961         e.preventDefault();
962     }
963   };
964
965   return {
966     TimelineTrackView: TimelineTrackView
967   };
968 });