blob: 99845a43d3b10074765ee72adbb1dc18f49bf2a6 [file] [log] [blame]
<!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/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;
/**
* 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);
// The following code uses an interval to detect when the parent element
// is attached to the document. That is a trigger to run the setup function
// and install a resize listener.
this.checkForAttachInterval_ = setInterval(
this.checkForAttach_.bind(this), 250);
this.majorMarkPositions = [];
this.interestRange_ = new TimelineInterestRange(this);
this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
}
TimelineViewport.prototype = {
__proto__: tr.b.EventTarget.prototype,
/**
* Allows initialization of the viewport when the viewport's parent element
* has been attached to the document and given a size.
* @param {Function} fn Function to call when the viewport can be safely
* initialized.
*/
setWhenPossible: function(fn) {
this.pendingSetFunction_ = fn;
},
/**
* @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();
},
/**
* Checks whether the parentNode is attached to the document.
* When it is, it installs the iframe-based resize detection hook
* and then runs the pendingSetFunction_, if present.
*/
checkForAttach_: function() {
if (!this.isAttachedToDocumentOrInTestMode || this.clientWidth == 0)
return;
window.addEventListener('resize', this.dispatchChangeEvent.bind(this));
var curSize = this.parentEl_.clientWidth + 'x' +
this.parentEl_.clientHeight;
if (this.pendingSetFunction_) {
this.lastSize_ = curSize;
try {
this.pendingSetFunction_();
} catch (ex) {
console.log('While running setWhenPossible:',
ex.message ? ex.message + '\n' + ex.stack : ex.stack);
}
this.pendingSetFunction_ = undefined;
}
window.clearInterval(this.checkForAttachInterval_);
this.checkForAttachInterval_ = undefined;
},
/**
* 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() {
if (this.checkForAttachInterval_) {
window.clearInterval(this.checkForAttachInterval_);
this.checkForAttachInterval_ = undefined;
}
window.removeEventListener('resize', this.dispatchChangeEvent.bind(this));
},
initAnimationController_: function() {
this.dtAnimationController_ = new tr.ui.b.AnimationController();
this.dtAnimationController_.addEventListener(
'didtick', function(e) {
this.onCurentDisplayTransformChange_(e.oldTargetState);
}.bind(this));
var that = this;
this.dtAnimationController_.target = {
get panX() {
return that.currentDisplayTransform_.panX;
},
set panX(panX) {
that.currentDisplayTransform_.panX = panX;
},
get panY() {
return that.currentDisplayTransform_.panY;
},
set panY(panY) {
that.currentDisplayTransform_.panY = panY;
},
get scaleX() {
return that.currentDisplayTransform_.scaleX;
},
set scaleX(scaleX) {
that.currentDisplayTransform_.scaleX = scaleX;
},
cloneAnimationState: function() {
return that.currentDisplayTransform_.clone();
},
xPanWorldPosToViewPos: function(xWorld, xView) {
that.currentDisplayTransform_.xPanWorldPosToViewPos(
xWorld, xView, that.modelTrackContainer_.canvas.clientWidth);
}
};
},
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.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_;
},
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 idx in this.majorMarkPositions) {
var x = Math.floor(this.majorMarkPositions[idx]);
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: TimelineViewport
};
});
</script>