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.
8 <link rel="stylesheet" href="/tvcm/ui/common.css">
9 <link rel="stylesheet" href="/tracing/timeline_track_view.css">
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">
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
36 * Visually, the TimelineTrackView produces (prettier) visualizations like the
38 * Thread1: AAAAAAAAAA AAAAA
40 * Thread2: CCCCCC CCCCC
43 tvcm.exportTo('tracing', function() {
44 var Selection = tracing.Selection;
45 var SelectionState = tracing.trace_model.SelectionState;
46 var Viewport = tracing.TimelineViewport;
48 var tempDisplayTransform = new tracing.TimelineDisplayTransform();
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) {
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);
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
72 * @extends {HTMLDivElement}
74 var TimelineTrackView = tvcm.ui.define('div');
76 TimelineTrackView.prototype = {
77 __proto__: HTMLDivElement.prototype,
81 decorate: function() {
83 this.classList.add('timeline-track-view');
85 this.viewport_ = new Viewport(this);
86 this.viewportDisplayTransformAtMouseDown_ = null;
88 this.rulerTrackContainer_ =
89 new tracing.tracks.DrawingContainer(this.viewport_);
90 this.appendChild(this.rulerTrackContainer_);
91 this.rulerTrackContainer_.invalidate();
93 this.rulerTrack_ = new tracing.tracks.RulerTrack(this.viewport_);
94 this.rulerTrackContainer_.appendChild(this.rulerTrack_);
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();
102 this.viewport_.modelTrackContainer = this.modelTrackContainer_;
104 this.modelTrack_ = new tracing.tracks.TraceModelTrack(this.viewport_);
105 this.modelTrackContainer_.appendChild(this.modelTrack_);
107 this.timingTool_ = new tracing.TimingTool(this.viewport_,
110 this.initMouseModeSelector();
112 this.dragBox_ = this.ownerDocument.createElement('div');
113 this.dragBox_.className = 'drag-box';
114 this.appendChild(this.dragBox_);
117 this.initHintText_();
119 this.bindEventListener_(document, 'keypress', this.onKeypress_, this);
120 this.bindEventListener_(document, 'keydown', this.onKeydown_, this);
121 this.bindEventListener_(document, 'keyup', this.onKeyup_, this);
123 this.bindEventListener_(this, 'dblclick', this.onDblClick_, this);
124 this.bindEventListener_(this, 'mousewheel', this.onMouseWheel_, this);
126 this.addEventListener('mousemove', this.onMouseMove_);
128 this.addEventListener('touchstart', this.onTouchStart_);
129 this.addEventListener('touchmove', this.onTouchMove_);
130 this.addEventListener('touchend', this.onTouchEnd_);
132 this.mouseViewPosAtMouseDown_ = {x: 0, y: 0};
133 this.lastMouseViewPos_ = {x: 0, y: 0};
135 this.lastTouchViewPositions_ = [];
137 this.selection_ = new Selection();
138 this.highlight_ = new Selection();
140 this.isPanningAndScanning_ = false;
141 this.isZooming_ = false;
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.
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,
155 boundFunc: boundFunc});
156 object.addEventListener(event, boundFunc);
159 initMouseModeSelector: function() {
160 this.mouseModeSelector_ = new tvcm.ui.MouseModeSelector(this);
161 this.appendChild(this.mouseModeSelector_);
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));
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));
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));
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_));
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));
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);
214 this.modelTrack_.detach();
216 for (var i = 0; i < this.boundListeners_.length; i++) {
217 var binding = this.boundListeners_[i];
218 binding.object.removeEventListener(binding.event, binding.boundFunc);
220 this.boundListeners_ = undefined;
221 this.viewport_.detach();
225 return this.viewport_;
234 throw new Error('Model cannot be null');
236 var modelInstanceChanged = this.model_ !== model;
238 this.modelTrack_.model = model;
240 // Set up a reasonable viewport.
241 if (modelInstanceChanged)
242 this.viewport_.setWhenPossible(this.setInitialViewport_.bind(this));
244 tvcm.setPropertyAndDispatchChange(this, 'model', model);
247 get hasVisibleContent() {
248 return this.modelTrack_.hasVisibleContent;
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;
261 if (this.model_.bounds.isEmpty) {
264 } else if (this.model_.bounds.range === 0) {
265 min = this.model_.bounds.min;
268 min = this.model_.bounds.min;
269 range = this.model_.bounds.range;
272 var boost = range * 0.15;
273 tempDisplayTransform.set(this.viewport_.currentDisplayTransform);
274 tempDisplayTransform.xSetWorldBounds(min - boost,
277 this.viewport_.setDisplayTransformImmediately(tempDisplayTransform);
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
286 addAllObjectsMatchingFilterToSelection: function(filter, selection) {
287 this.modelTrack_.addAllObjectsMatchingFilterToSelection(
292 * @return {Element} The element whose focused state determines
293 * whether to respond to keyboard inputs.
294 * Defaults to the parent element.
297 if (this.focusElement_)
298 return this.focusElement_;
299 return this.parentElement;
303 * Sets the element whose focus state will determine whether
304 * to respond to keybaord input.
306 set focusElement(value) {
307 this.focusElement_ = value;
310 get listenToKeys_() {
311 if (!this.viewport_.isAttachedToDocumentOrInTestMode)
313 if (this.activeElement instanceof TracingFindControl)
315 if (!this.focusElement_)
317 if (this.focusElement.tabIndex >= 0) {
318 if (document.activeElement == this.focusElement)
320 return tvcm.ui.elementIsChildOf(document.activeElement,
326 onMouseMove_: function(e) {
328 // Zooming requires the delta since the last mousemove so we need to avoid
329 // tracking it when the zoom interaction is active.
333 this.storeLastMousePos_(e);
336 onTouchStart_: function(e) {
337 this.storeLastTouchPositions_(e);
338 this.focusElements_();
341 onTouchMove_: function(e) {
343 this.onUpdateTransformForTouch_(e);
346 onTouchEnd_: function(e) {
347 this.storeLastTouchPositions_(e);
348 this.focusElements_();
351 onKeypress_: function(e) {
352 var vp = this.viewport_;
353 if (!this.listenToKeys_)
355 if (document.activeElement.nodeName == 'INPUT')
357 var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
358 var curMouseV, curCenterW;
363 this.zoomBy_(1.5, true);
367 this.zoomBy_(1 / 1.5, true);
370 this.onGridToggle_(true);
373 this.onGridToggle_(false);
377 this.zoomBy_(10, true);
381 this.zoomBy_(1 / 10, true);
384 this.queueSmoothPan_(viewWidth * 0.3, 0);
388 this.queueSmoothPan_(viewWidth * -0.3, 0);
391 this.queueSmoothPan_(viewWidth * 0.5, 0);
394 this.queueSmoothPan_(viewWidth * -0.5, 0);
397 this.setInitialViewport_();
400 this.zoomToSelection();
402 case 'm'.charCodeAt(0):
403 this.setCurrentSelectionAsInterestRange_();
408 // Not all keys send a keypress.
409 onKeydown_: function(e) {
410 if (!this.listenToKeys_)
413 var vp = this.viewport_;
414 var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
417 case 37: // left arrow
418 sel = this.selection.getShiftedSelection(
422 this.setSelectionAndClearHighlight(sel);
423 this.panToSelection();
426 this.queueSmoothPan_(viewWidth * 0.3, 0);
429 case 39: // right arrow
430 sel = this.selection.getShiftedSelection(
433 this.setSelectionAndClearHighlight(sel);
434 this.panToSelection();
437 this.queueSmoothPan_(-viewWidth * 0.3, 0);
441 if (this.focusElement.tabIndex == -1) {
443 this.selectPrevious_(e);
452 onKeyup_: function(e) {
453 if (!this.listenToKeys_)
456 if (this.dragBeginEvent_) {
457 this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
458 this.dragBoxXEnd_, this.dragBoxYEnd_);
464 onDblClick_: function(e) {
465 if (this.mouseModeSelector_.mode !==
466 tvcm.ui.MOUSE_SELECTOR_MODE.SELECTION)
469 if (!this.selection.length || !this.selection[0].title)
472 var selection = new Selection();
473 var filter = new tracing.ExactTitleFilter(this.selection[0].title);
474 this.addAllObjectsMatchingFilterToSelection(filter, selection);
476 this.setSelectionAndClearHighlight(selection);
479 onMouseWheel_: function(e) {
483 var delta = e.wheelDelta / 120;
484 var zoomScale = Math.pow(1.5, delta);
485 this.zoomBy_(zoomScale);
489 queueSmoothPan_: function(viewDeltaX, deltaY) {
490 var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld(
492 var animation = new tracing.TimelineDisplayTransformPanAnimation(
494 this.viewport_.queueDisplayTransformAnimation(animation);
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.
502 zoomBy_: function(scale, smooth) {
504 var vp = this.viewport_;
505 var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
506 var pixelRatio = window.devicePixelRatio || 1;
508 var goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio;
509 var goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld(
510 goalFocalPointXView);
512 var animation = new tracing.TimelineDisplayTransformZoomToAnimation(
513 goalFocalPointXWorld, goalFocalPointXView,
514 vp.currentDisplayTransform.panY,
516 vp.queueDisplayTransformAnimation(animation);
518 tempDisplayTransform.set(vp.currentDisplayTransform);
519 tempDisplayTransform.scaleX = tempDisplayTransform.scaleX * scale;
520 tempDisplayTransform.xPanWorldPosToViewPos(
521 goalFocalPointXWorld, goalFocalPointXView, viewWidth);
522 vp.setDisplayTransformImmediately(tempDisplayTransform);
527 * Zoom into the current selection.
529 zoomToSelection: function() {
530 if (!this.selectionOfInterest.length)
533 var bounds = this.selectionOfInterest.bounds;
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 /
542 var zoomInRatio = newScale / this.viewport_.currentDisplayTransform.scaleX;
543 var animation = new tracing.TimelineDisplayTransformZoomToAnimation(
544 worldCenter, viewCenter,
545 this.viewport_.currentDisplayTransform.panY,
547 this.viewport_.queueDisplayTransformAnimation(animation);
551 * Pan the view so the current selection becomes visible.
553 panToSelection: function() {
554 if (!this.selectionOfInterest.length)
557 var bounds = this.selectionOfInterest.bounds;
558 var worldCenter = bounds.center;
559 var viewWidth = this.modelTrackContainer_.canvas.width;
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(
571 this.viewport_.queueDisplayTransformAnimation(animation);
576 tempDisplayTransform.set(dt);
577 tempDisplayTransform.xPanWorldBoundsIntoView(
581 var deltaX = tempDisplayTransform.panX - dt.panX;
582 var animation = new tracing.TimelineDisplayTransformPanAnimation(
584 this.viewport_.queueDisplayTransformAnimation(animation);
587 setCurrentSelectionAsInterestRange_: function() {
588 var selectionBounds = this.selection.bounds;
589 if (selectionBounds.empty) {
590 this.viewport_.interestRange.reset();
594 if (this.viewport_.interestRange.min == selectionBounds.min &&
595 this.viewport_.interestRange.max == selectionBounds.max)
596 this.viewport_.interestRange.reset();
598 this.viewport_.interestRange.set(selectionBounds);
602 * Sets the selected events and changes the SelectionState of the events to
604 * @param {Selection} selection A Selection of the new selected events.
606 set selection(selection) {
607 this.setSelectionAndHighlight(selection, this.highlight_);
611 return this.selection_;
615 * Sets the highlighted events and changes the SelectionState of the events
616 * to HIGHLIGHTED. All other events are set to DIMMED, except SELECTED
618 * @param {Selection} selection A Selection of the new selected events.
620 set highlight(highlight) {
621 this.setSelectionAndHighlight(this.selection_, highlight);
625 return this.highlight_;
629 * Getter for events of interest, primarily SELECTED and secondarily
630 * HIGHLIGHTED events.
632 get selectionOfInterest() {
633 if (!this.selection_.length && this.highlight_.length)
634 return this.highlight_;
635 return this.selection_;
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.
643 setSelectionAndClearHighlight: function(selection) {
644 this.setSelectionAndHighlight(selection, null);
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
651 * @param {Selection} highlight A Selection of the new highlighted events.
653 setHighlightAndClearSelection: function(highlight) {
654 this.setSelectionAndHighlight(null, highlight);
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.
663 setSelectionAndHighlight: function(selection, highlight) {
664 if (selection === this.selection_ && highlight === this.highlight_)
667 if ((selection !== null && !(selection instanceof Selection)) ||
668 (highlight !== null && !(highlight instanceof Selection))) {
669 throw new Error('Expected Selection');
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);
679 // Switch the highlight.
680 if (highlight !== this.highlight_)
681 this.highlight_ = highlight;
683 // Set HIGHLIGHTED on the events of the new highlight.
684 this.setSelectionState_(highlight, SelectionState.HIGHLIGHTED);
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();
693 if (selection && selection.length) {
694 // Switch the selection
695 if (selection !== this.selection_)
696 this.selection_ = selection;
698 // Set SELECTED on the events of the new highlight.
699 this.setSelectionState_(selection, SelectionState.SELECTED);
701 this.selection_ = new Selection();
703 tvcm.dispatchSimpleEvent(this, 'selectionChange');
704 this.showHintText_('Press \'m\' to mark current selection');
706 if (this.selectionOfInterest.length) {
707 var track = this.viewport_.trackForEvent(this.selectionOfInterest[0]);
709 track.scrollIntoViewIfNeeded();
712 this.viewport_.dispatchChangeEvent(); // Triggers a redraw.
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.
720 setSelectionState_: function(selection, selectionState) {
721 for (var i = 0; i < selection.length; i++)
722 selection[i].selectionState = selectionState;
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.
730 resetEventsTo_: function(selectionState) {
731 var dimmed = this.highlight_.length;
732 var resetAll = (dimmed && selectionState !== SelectionState.DIMMED) ||
733 (!dimmed && selectionState === SelectionState.DIMMED);
735 this.model.iterateAllEvents(
736 function(event) { event.selectionState = selectionState; });
738 this.setSelectionState_(this.selection_, selectionState);
739 this.setSelectionState_(this.highlight_, selectionState);
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;
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};
758 dragRect.right = dragRect.left + dragRect.width;
759 dragRect.bottom = dragRect.top + dragRect.height;
761 var modelTrackContainerRect =
762 this.modelTrackContainer_.getBoundingClientRect();
764 left: modelTrackContainerRect.left,
765 top: modelTrackContainerRect.top,
766 right: modelTrackContainerRect.right,
767 bottom: modelTrackContainerRect.bottom
770 var headingWidth = window.getComputedStyle(
771 this.querySelector('heading')).width;
772 var trackTitleWidth = parseInt(headingWidth);
773 clipRect.left = clipRect.left + trackTitleWidth;
775 var finalDragBox = intersectRect_(clipRect, dragRect);
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';
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);
790 var roundedDuration = Math.round((hiWX - loWX) * 100) / 100;
791 this.dragBox_.textContent = roundedDuration + 'ms';
793 var e = new tvcm.Event('selectionChanging');
796 this.dispatchEvent(e);
799 onGridToggle_: function(left) {
800 var tb = left ? this.selection.bounds.min : this.selection.bounds.max;
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;
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_);
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_;
824 storeLastMousePos_: function(e) {
825 this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
828 storeLastTouchPositions_: function(e) {
829 this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e);
832 extractRelativeMousePosition_: function(e) {
833 var canv = this.modelTrackContainer_.canvas;
835 x: e.clientX - canv.offsetLeft,
836 y: e.clientY - canv.offsetTop
840 extractRelativeTouchPositions_: function(e) {
841 var canv = this.modelTrackContainer_.canvas;
844 for (var i = 0; i < e.touches.length; ++i) {
846 x: e.touches[i].clientX - canv.offsetLeft,
847 y: e.touches[i].clientY - canv.offsetTop
853 storeInitialMouseDownPos_: function(e) {
855 var position = this.extractRelativeMousePosition_(e);
857 this.mouseViewPosAtMouseDown_.x = position.x;
858 this.mouseViewPosAtMouseDown_.y = position.y;
861 focusElements_: function() {
862 if (document.activeElement)
863 document.activeElement.blur();
864 if (this.focusElement.tabIndex >= 0)
865 this.focusElement.focus();
868 storeInitialInteractionPositionsAndFocus_: function(e) {
870 this.storeInitialMouseDownPos_(e);
871 this.storeLastMousePos_(e);
873 this.focusElements_();
876 onBeginPanScan_: function(e) {
877 var vp = this.viewport_;
878 this.viewportDisplayTransformAtMouseDown_ =
879 vp.currentDisplayTransform.clone();
880 this.isPanningAndScanning_ = true;
882 this.storeInitialInteractionPositionsAndFocus_(e);
886 onUpdatePanScan_: function(e) {
887 if (!this.isPanningAndScanning_)
890 var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
892 var pixelRatio = window.devicePixelRatio || 1;
893 var xDeltaView = pixelRatio * (this.lastMouseViewPos_.x -
894 this.mouseViewPosAtMouseDown_.x);
896 var yDelta = this.lastMouseViewPos_.y -
897 this.mouseViewPosAtMouseDown_.y;
899 tempDisplayTransform.set(this.viewportDisplayTransformAtMouseDown_);
900 tempDisplayTransform.incrementPanXInViewUnits(xDeltaView);
901 tempDisplayTransform.panY -= yDelta;
902 this.viewport_.setDisplayTransformImmediately(tempDisplayTransform);
907 this.storeLastMousePos_(e);
910 onEndPanScan_: function(e) {
911 this.isPanningAndScanning_ = false;
913 this.storeLastMousePos_(e);
919 onBeginSelection_: function(e) {
920 var canv = this.modelTrackContainer_.canvas;
921 var rect = this.modelTrack_.getBoundingClientRect();
922 var canvRect = canv.getBoundingClientRect();
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;
935 this.dragBeginEvent_ = e;
937 this.storeInitialInteractionPositionsAndFocus_(e);
941 onUpdateSelection_: function(e) {
942 if (!this.dragBeginEvent_)
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_);
955 onEndSelection_: function(e) {
958 if (!this.dragBeginEvent_)
961 // Stop the dragging.
963 var eDown = this.dragBeginEvent_;
964 this.dragBeginEvent_ = null;
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);
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;
978 // Figure out what has been selected.
979 var selection = new Selection();
980 this.modelTrack_.addIntersectingItemsInRangeToSelection(
981 loVX, hiVX, loY, hiY, selection);
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);
989 onBeginZoom_: function(e) {
990 this.isZooming_ = true;
992 this.storeInitialInteractionPositionsAndFocus_(e);
996 onUpdateZoom_: function(e) {
997 if (!this.isZooming_)
999 var newPosition = this.extractRelativeMousePosition_(e);
1001 var zoomScaleValue = 1 + (this.lastMouseViewPos_.y -
1002 newPosition.y) * 0.01;
1004 this.zoomBy_(zoomScaleValue, false);
1005 this.storeLastMousePos_(e);
1008 onEndZoom_: function(e) {
1009 this.isZooming_ = false;
1015 computeTouchCenter_: function(positions) {
1018 for (var i = 0; i < positions.length; ++i) {
1019 xSum += positions[i].x;
1020 ySum += positions[i].y;
1023 x: xSum / positions.length,
1024 y: ySum / positions.length
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);
1039 return Math.sqrt((xMin - xMax) * (xMin - xMax) +
1040 (yMin - yMax) * (yMin - yMax));
1043 onUpdateTransformForTouch_: function(e) {
1044 var newPositions = this.extractRelativeTouchPositions_(e);
1045 var currentPositions = this.lastTouchViewPositions_;
1047 var newCenter = this.computeTouchCenter_(newPositions);
1048 var currentCenter = this.computeTouchCenter_(currentPositions);
1050 var newSpan = this.computeTouchSpan_(newPositions);
1051 var currentSpan = this.computeTouchSpan_(currentPositions);
1053 var vp = this.viewport_;
1054 var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
1055 var pixelRatio = window.devicePixelRatio || 1;
1057 var xDelta = pixelRatio * (newCenter.x - currentCenter.x);
1058 var yDelta = newCenter.y - currentCenter.y;
1059 var zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1;
1061 var viewFocus = pixelRatio * newCenter.x;
1062 var worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus);
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);
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_);
1080 this.pendingHintTextClearTimeout_ = undefined;
1083 showHintText_: function(text) {
1084 if (this.pendingHintTextClearTimeout_) {
1085 window.clearTimeout(this.pendingHintTextClearTimeout_);
1086 this.pendingHintTextClearTimeout_ = undefined;
1088 this.pendingHintTextClearTimeout_ = setTimeout(
1089 this.hideHintText_.bind(this), 1000);
1090 this.hintTextBox_.textContent = text;
1091 this.hintTextBox_.style.display = '';
1094 hideHintText_: function() {
1095 this.pendingHintTextClearTimeout_ = undefined;
1096 this.hintTextBox_.style.display = 'none';
1101 TimelineTrackView: TimelineTrackView