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