| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/tracing/base/event.html"> |
| <link rel="import" href="/tracing/base/unit.html"> |
| <link rel="import" href="/tracing/model/event_set.html"> |
| <link rel="import" href="/tracing/ui/base/animation.html"> |
| <link rel="import" href="/tracing/ui/base/animation_controller.html"> |
| <link rel="import" href="/tracing/ui/base/dom_helpers.html"> |
| <link rel="import" href="/tracing/ui/base/draw_helpers.html"> |
| <link rel="import" href="/tracing/ui/timeline_display_transform.html"> |
| <link rel="import" href="/tracing/ui/timeline_interest_range.html"> |
| <link rel="import" href="/tracing/ui/tracks/container_to_track_map.html"> |
| <link rel="import" href="/tracing/ui/tracks/event_to_track_map.html"> |
| |
| <script> |
| 'use strict'; |
| |
| /** |
| * @fileoverview Code for the viewport. |
| */ |
| tr.exportTo('tr.ui', function() { |
| var TimelineDisplayTransform = tr.ui.TimelineDisplayTransform; |
| var TimelineInterestRange = tr.ui.TimelineInterestRange; |
| |
| var IDEAL_MAJOR_MARK_DISTANCE_PX = 150; |
| // Keep 5 digits of precision when rounding the major mark distances. |
| var MAJOR_MARK_ROUNDING_FACTOR = 100000; |
| |
| class AnimationControllerProxy { |
| constructor(target) { |
| this.target_ = target; |
| } |
| |
| get panX() { |
| return this.target_.currentDisplayTransform_.panX; |
| } |
| |
| set panX(panX) { |
| this.target_.currentDisplayTransform_.panX = panX; |
| } |
| |
| get panY() { |
| return this.target_.currentDisplayTransform_.panY; |
| } |
| |
| set panY(panY) { |
| this.target_.currentDisplayTransform_.panY = panY; |
| } |
| |
| get scaleX() { |
| return this.target_.currentDisplayTransform_.scaleX; |
| } |
| |
| set scaleX(scaleX) { |
| this.target_.currentDisplayTransform_.scaleX = scaleX; |
| } |
| |
| cloneAnimationState() { |
| return this.target_.currentDisplayTransform_.clone(); |
| } |
| |
| xPanWorldPosToViewPos(xWorld, xView) { |
| this.target_.currentDisplayTransform_.xPanWorldPosToViewPos( |
| xWorld, xView, this.target_.modelTrackContainer_.canvas.clientWidth); |
| } |
| } |
| |
| /** |
| * The TimelineViewport manages the transform used for navigating |
| * within the timeline. It is a simple transform: |
| * x' = (x+pan) * scale |
| * |
| * The timeline code tries to avoid directly accessing this transform, |
| * instead using this class to do conversion between world and viewspace, |
| * as well as the math for centering the viewport in various interesting |
| * ways. |
| * |
| * @constructor |
| * @extends {tr.b.EventTarget} |
| */ |
| function TimelineViewport(parentEl) { |
| this.parentEl_ = parentEl; |
| this.modelTrackContainer_ = undefined; |
| this.currentDisplayTransform_ = new TimelineDisplayTransform(); |
| this.initAnimationController_(); |
| |
| // Flow events |
| this.showFlowEvents_ = false; |
| |
| // Highlights. |
| this.highlightVSync_ = false; |
| |
| // High details. |
| this.highDetails_ = false; |
| |
| // Grid system. |
| this.gridTimebase_ = 0; |
| this.gridStep_ = 1000 / 60; |
| this.gridEnabled_ = false; |
| |
| // Init logic. |
| this.hasCalledSetupFunction_ = false; |
| |
| this.onResize_ = this.onResize_.bind(this); |
| this.onModelTrackControllerScroll_ = |
| this.onModelTrackControllerScroll_.bind(this); |
| |
| this.timeMode_ = TimelineViewport.TimeMode.TIME_IN_MS; |
| // Major mark positions are where the gridlines/ruler marks are placed along |
| // the x-axis. |
| this.majorMarkWorldPositions_ = []; |
| this.majorMarkUnit_ = undefined; |
| this.interestRange_ = new TimelineInterestRange(this); |
| |
| this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap(); |
| this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap(); |
| |
| this.dispatchChangeEvent = this.dispatchChangeEvent.bind(this); |
| } |
| |
| TimelineViewport.TimeMode = { |
| TIME_IN_MS: 0, |
| REVISIONS: 1 |
| }; |
| |
| TimelineViewport.prototype = { |
| __proto__: tr.b.EventTarget.prototype, |
| |
| /** |
| * @return {boolean} Whether the current timeline is attached to the |
| * document. |
| */ |
| get isAttachedToDocumentOrInTestMode() { |
| // Allow not providing a parent element, used by tests. |
| if (this.parentEl_ === undefined) |
| return; |
| return tr.ui.b.isElementAttachedToDocument(this.parentEl_); |
| }, |
| |
| onResize_: function() { |
| this.dispatchChangeEvent(); |
| }, |
| |
| /** |
| * Fires the change event on this viewport. Used to notify listeners |
| * to redraw when the underlying model has been mutated. |
| */ |
| dispatchChangeEvent: function() { |
| tr.b.dispatchSimpleEvent(this, 'change'); |
| }, |
| |
| detach: function() { |
| window.removeEventListener('resize', this.dispatchChangeEvent); |
| }, |
| |
| initAnimationController_: function() { |
| this.dtAnimationController_ = new tr.ui.b.AnimationController(); |
| this.dtAnimationController_.addEventListener( |
| 'didtick', function(e) { |
| this.onCurentDisplayTransformChange_(e.oldTargetState); |
| }.bind(this)); |
| |
| this.dtAnimationController_.target = new AnimationControllerProxy(this); |
| }, |
| |
| get currentDisplayTransform() { |
| return this.currentDisplayTransform_; |
| }, |
| |
| setDisplayTransformImmediately: function(displayTransform) { |
| this.dtAnimationController_.cancelActiveAnimation(); |
| |
| var oldDisplayTransform = |
| this.dtAnimationController_.target.cloneAnimationState(); |
| this.currentDisplayTransform_.set(displayTransform); |
| this.onCurentDisplayTransformChange_(oldDisplayTransform); |
| }, |
| |
| queueDisplayTransformAnimation: function(animation) { |
| if (!(animation instanceof tr.ui.b.Animation)) |
| throw new Error('animation must be instanceof tr.ui.b.Animation'); |
| this.dtAnimationController_.queueAnimation(animation); |
| }, |
| |
| onCurentDisplayTransformChange_: function(oldDisplayTransform) { |
| // Ensure panY stays clamped in the track container's scroll range. |
| if (this.modelTrackContainer_) { |
| this.currentDisplayTransform.panY = tr.b.math.clamp( |
| this.currentDisplayTransform.panY, |
| 0, |
| this.modelTrackContainer_.scrollHeight - |
| this.modelTrackContainer_.clientHeight); |
| } |
| |
| var changed = !this.currentDisplayTransform.equals(oldDisplayTransform); |
| var yChanged = this.currentDisplayTransform.panY !== |
| oldDisplayTransform.panY; |
| if (yChanged) |
| this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY; |
| if (changed) |
| this.dispatchChangeEvent(); |
| }, |
| |
| onModelTrackControllerScroll_: function(e) { |
| if (this.dtAnimationController_.activeAnimation && |
| this.dtAnimationController_.activeAnimation.affectsPanY) |
| this.dtAnimationController_.cancelActiveAnimation(); |
| var panY = this.modelTrackContainer_.scrollTop; |
| this.currentDisplayTransform_.panY = panY; |
| }, |
| |
| get modelTrackContainer() { |
| return this.modelTrackContainer_; |
| }, |
| |
| set modelTrackContainer(m) { |
| if (this.modelTrackContainer_) |
| this.modelTrackContainer_.removeEventListener('scroll', |
| this.onModelTrackControllerScroll_); |
| |
| this.modelTrackContainer_ = m; |
| this.modelTrackContainer_.addEventListener('scroll', |
| this.onModelTrackControllerScroll_); |
| }, |
| |
| get showFlowEvents() { |
| return this.showFlowEvents_; |
| }, |
| |
| set showFlowEvents(showFlowEvents) { |
| this.showFlowEvents_ = showFlowEvents; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get highlightVSync() { |
| return this.highlightVSync_; |
| }, |
| |
| set highlightVSync(highlightVSync) { |
| this.highlightVSync_ = highlightVSync; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get highDetails() { |
| return this.highDetails_; |
| }, |
| |
| set highDetails(highDetails) { |
| this.highDetails_ = highDetails; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get gridEnabled() { |
| return this.gridEnabled_; |
| }, |
| |
| set gridEnabled(enabled) { |
| if (this.gridEnabled_ === enabled) |
| return; |
| |
| this.gridEnabled_ = enabled && true; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get gridTimebase() { |
| return this.gridTimebase_; |
| }, |
| |
| set gridTimebase(timebase) { |
| if (this.gridTimebase_ === timebase) |
| return; |
| this.gridTimebase_ = timebase; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get gridStep() { |
| return this.gridStep_; |
| }, |
| |
| get interestRange() { |
| return this.interestRange_; |
| }, |
| |
| get majorMarkWorldPositions() { |
| return this.majorMarkWorldPositions_; |
| }, |
| |
| get majorMarkUnit() { |
| switch (this.timeMode_) { |
| case TimelineViewport.TimeMode.TIME_IN_MS: |
| return tr.b.Unit.byName.timeInMsAutoFormat; |
| case TimelineViewport.TimeMode.REVISIONS: |
| return tr.b.Unit.byName.count; |
| default: |
| throw new Error( |
| 'Cannot get Unit for unsupported time mode ' + this.timeMode_); |
| } |
| }, |
| |
| get timeMode() { |
| return this.timeMode_; |
| }, |
| |
| set timeMode(mode) { |
| this.timeMode_ = mode; |
| this.dispatchChangeEvent(); |
| }, |
| |
| updateMajorMarkData: function(viewLWorld, viewRWorld) { |
| var pixelRatio = window.devicePixelRatio || 1; |
| var dt = this.currentDisplayTransform; |
| |
| var idealMajorMarkDistancePix = |
| IDEAL_MAJOR_MARK_DISTANCE_PX * pixelRatio; |
| var idealMajorMarkDistanceWorld = |
| dt.xViewVectorToWorld(idealMajorMarkDistancePix); |
| |
| var majorMarkDistanceWorld = tr.b.math.preferredNumberLargerThanMin( |
| idealMajorMarkDistanceWorld); |
| |
| var firstMajorMark = Math.floor( |
| viewLWorld / majorMarkDistanceWorld) * majorMarkDistanceWorld; |
| |
| this.majorMarkWorldPositions_ = []; |
| for (var curX = firstMajorMark; |
| curX < viewRWorld; |
| curX += majorMarkDistanceWorld) { |
| this.majorMarkWorldPositions_.push( |
| Math.floor(MAJOR_MARK_ROUNDING_FACTOR * curX) / |
| MAJOR_MARK_ROUNDING_FACTOR); |
| } |
| }, |
| |
| drawMajorMarkLines: function(ctx) { |
| // Apply subpixel translate to get crisp lines. |
| // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ |
| ctx.save(); |
| ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); |
| |
| ctx.beginPath(); |
| for (var majorMark of this.majorMarkWorldPositions_) { |
| var x = this.currentDisplayTransform.xWorldToView(majorMark); |
| tr.ui.b.drawLine(ctx, x, 0, x, ctx.canvas.height); |
| } |
| ctx.strokeStyle = '#ddd'; |
| ctx.stroke(); |
| |
| ctx.restore(); |
| }, |
| |
| drawGridLines: function(ctx, viewLWorld, viewRWorld) { |
| if (!this.gridEnabled) |
| return; |
| |
| var dt = this.currentDisplayTransform; |
| var x = this.gridTimebase; |
| |
| // Apply subpixel translate to get crisp lines. |
| // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ |
| ctx.save(); |
| ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); |
| |
| ctx.beginPath(); |
| while (x < viewRWorld) { |
| if (x >= viewLWorld) { |
| // Do conversion to viewspace here rather than on |
| // x to avoid precision issues. |
| var vx = Math.floor(dt.xWorldToView(x)); |
| tr.ui.b.drawLine(ctx, vx, 0, vx, ctx.canvas.height); |
| } |
| |
| x += this.gridStep; |
| } |
| ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)'; |
| ctx.stroke(); |
| |
| ctx.restore(); |
| }, |
| |
| /** |
| * Helper for selection previous or next. |
| * @param {boolean} offset If positive, select one forward (next). |
| * Else, select previous. |
| * |
| * @return {boolean} true if current selection changed. |
| */ |
| getShiftedSelection: function(selection, offset) { |
| var newSelection = new tr.model.EventSet(); |
| for (var event of selection) { |
| // If this is a flow event, then move to its slice based on the |
| // offset direction. |
| if (event instanceof tr.model.FlowEvent) { |
| if (offset > 0) { |
| newSelection.push(event.endSlice); |
| } else if (offset < 0) { |
| newSelection.push(event.startSlice); |
| } else { |
| /* Do nothing. Zero offsets don't do anything. */ |
| } |
| continue; |
| } |
| |
| var track = this.trackForEvent(event); |
| track.addEventNearToProvidedEventToSelection( |
| event, offset, newSelection); |
| } |
| |
| if (newSelection.length === 0) |
| return undefined; |
| return newSelection; |
| }, |
| |
| rebuildEventToTrackMap: function() { |
| // TODO(charliea): Make the event to track map have a similar interface |
| // to the container to track map so that we can just clear() here. |
| this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap(); |
| this.modelTrackContainer_.addEventsToTrackMap(this.eventToTrackMap_); |
| }, |
| |
| rebuildContainerToTrackMap: function() { |
| this.containerToTrackMap.clear(); |
| this.modelTrackContainer_.addContainersToTrackMap( |
| this.containerToTrackMap); |
| }, |
| |
| trackForEvent: function(event) { |
| return this.eventToTrackMap_[event.guid]; |
| } |
| }; |
| |
| return { |
| TimelineViewport, |
| }; |
| }); |
| </script> |