blob: b66c7a6734ff75ce23f3b0764a2bd2bedc489fd1 [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="stylesheet" href="/base/ui/common.css">
<link rel="stylesheet" href="/core/timeline_track_view.css">
<link rel="import" href="/base/events.html">
<link rel="import" href="/base/properties.html">
<link rel="import" href="/base/settings.html">
<link rel="import" href="/base/task.html">
<link rel="import" href="/base/ui.html">
<link rel="import" href="/base/ui/mouse_mode_selector.html">
<link rel="import" href="/core/filter.html">
<link rel="import" href="/core/selection.html">
<link rel="import" href="/core/timeline_viewport.html">
<link rel="import" href="/core/timeline_display_transform_animations.html">
<link rel="import" href="/core/timing_tool.html">
<link rel="import" href="/core/trace_model/event.html">
<link rel="import" href="/core/trace_model/x_marker_annotation.html">
<link rel="import" href="/core/tracks/drawing_container.html">
<link rel="import" href="/core/tracks/trace_model_track.html">
<link rel="import" href="/core/tracks/ruler_track.html">
<script>
'use strict';
/**
* @fileoverview Interactive visualizaiton of TraceModel objects
* based loosely on gantt charts. Each thread in the TraceModel is given a
* set of Tracks, one per subrow in the thread. The TimelineTrackView class
* acts as a controller, creating the individual tracks, while Tracks
* do actual drawing.
*
* Visually, the TimelineTrackView produces (prettier) visualizations like the
* following:
* Thread1: AAAAAAAAAA AAAAA
* BBBB BB
* Thread2: CCCCCC CCCCC
*
*/
tv.exportTo('tv.c', function() {
var Selection = tv.c.Selection;
var SelectionState = tv.c.trace_model.SelectionState;
var Viewport = tv.c.TimelineViewport;
var tempDisplayTransform = new tv.c.TimelineDisplayTransform();
function intersectRect_(r1, r2) {
var results = new Object;
if (r2.left > r1.right || r2.right < r1.left ||
r2.top > r1.bottom || r2.bottom < r1.top) {
return false;
}
results.left = Math.max(r1.left, r2.left);
results.top = Math.max(r1.top, r2.top);
results.right = Math.min(r1.right, r2.right);
results.bottom = Math.min(r1.bottom, r2.bottom);
results.width = (results.right - results.left);
results.height = (results.bottom - results.top);
return results;
}
/**
* Renders a TraceModel into a div element, making one
* Track for each subrow in each thread of the model, managing
* overall track layout, and handling user interaction with the
* viewport.
*
* @constructor
* @extends {HTMLDivElement}
*/
var TimelineTrackView = tv.b.ui.define('div');
TimelineTrackView.prototype = {
__proto__: HTMLDivElement.prototype,
model_: null,
decorate: function() {
this.classList.add('timeline-track-view');
this.viewport_ = new Viewport(this);
this.viewportDisplayTransformAtMouseDown_ = null;
this.rulerTrackContainer_ =
new tv.c.tracks.DrawingContainer(this.viewport_);
this.appendChild(this.rulerTrackContainer_);
this.rulerTrackContainer_.invalidate();
this.rulerTrack_ = new tv.c.tracks.RulerTrack(this.viewport_);
this.rulerTrackContainer_.appendChild(this.rulerTrack_);
this.upperModelTrack_ = new tv.c.tracks.TraceModelTrack(this.viewport_);
this.upperModelTrack_.upperMode = true;
this.rulerTrackContainer_.appendChild(this.upperModelTrack_);
this.modelTrackContainer_ =
new tv.c.tracks.DrawingContainer(this.viewport_);
this.appendChild(this.modelTrackContainer_);
this.modelTrackContainer_.style.display = 'block';
this.modelTrackContainer_.invalidate();
this.viewport_.modelTrackContainer = this.modelTrackContainer_;
this.modelTrack_ = new tv.c.tracks.TraceModelTrack(this.viewport_);
this.modelTrackContainer_.appendChild(this.modelTrack_);
this.timingTool_ = new tv.c.TimingTool(this.viewport_,
this);
this.initMouseModeSelector();
this.dragBox_ = this.ownerDocument.createElement('div');
this.dragBox_.className = 'drag-box';
this.appendChild(this.dragBox_);
this.hideDragBox_();
this.initHintText_();
this.bindEventListener_(document, 'keypress', this.onKeypress_, this);
this.bindEventListener_(document, 'keydown', this.onKeydown_, this);
this.bindEventListener_(document, 'keyup', this.onKeyup_, this);
this.bindEventListener_(this, 'dblclick', this.onDblClick_, this);
this.bindEventListener_(this, 'mousewheel', this.onMouseWheel_, this);
this.addEventListener('mousemove', this.onMouseMove_);
this.addEventListener('touchstart', this.onTouchStart_);
this.addEventListener('touchmove', this.onTouchMove_);
this.addEventListener('touchend', this.onTouchEnd_);
this.mouseViewPosAtMouseDown_ = {x: 0, y: 0};
this.lastMouseViewPos_ = {x: 0, y: 0};
this.lastTouchViewPositions_ = [];
this.selection_ = new Selection();
this.highlight_ = new Selection();
this.isPanningAndScanning_ = false;
this.isZooming_ = false;
},
/**
* Wraps the standard addEventListener but automatically binds the provided
* func to the provided target, tracking the resulting closure. When detach
* is called, these listeners will be automatically removed.
*/
bindEventListener_: function(object, event, func, target) {
if (!this.boundListeners_)
this.boundListeners_ = [];
var boundFunc = func.bind(target);
this.boundListeners_.push({object: object,
event: event,
boundFunc: boundFunc});
object.addEventListener(event, boundFunc);
},
initMouseModeSelector: function() {
this.mouseModeSelector_ = new tv.b.ui.MouseModeSelector(this);
this.appendChild(this.mouseModeSelector_);
this.mouseModeSelector_.addEventListener('beginpan',
this.onBeginPanScan_.bind(this));
this.mouseModeSelector_.addEventListener('updatepan',
this.onUpdatePanScan_.bind(this));
this.mouseModeSelector_.addEventListener('endpan',
this.onEndPanScan_.bind(this));
this.mouseModeSelector_.addEventListener('beginselection',
this.onBeginSelection_.bind(this));
this.mouseModeSelector_.addEventListener('updateselection',
this.onUpdateSelection_.bind(this));
this.mouseModeSelector_.addEventListener('endselection',
this.onEndSelection_.bind(this));
this.mouseModeSelector_.addEventListener('beginzoom',
this.onBeginZoom_.bind(this));
this.mouseModeSelector_.addEventListener('updatezoom',
this.onUpdateZoom_.bind(this));
this.mouseModeSelector_.addEventListener('endzoom',
this.onEndZoom_.bind(this));
this.mouseModeSelector_.addEventListener('entertiming',
this.timingTool_.onEnterTiming.bind(this.timingTool_));
this.mouseModeSelector_.addEventListener('begintiming',
this.timingTool_.onBeginTiming.bind(this.timingTool_));
this.mouseModeSelector_.addEventListener('updatetiming',
this.timingTool_.onUpdateTiming.bind(this.timingTool_));
this.mouseModeSelector_.addEventListener('endtiming',
this.timingTool_.onEndTiming.bind(this.timingTool_));
this.mouseModeSelector_.addEventListener('exittiming',
this.timingTool_.onExitTiming.bind(this.timingTool_));
var m = tv.b.ui.MOUSE_SELECTOR_MODE;
this.mouseModeSelector_.supportedModeMask =
m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING;
this.mouseModeSelector_.settingsKey =
'timelineTrackView.mouseModeSelector';
this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0));
this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0));
this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0));
this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0));
this.mouseModeSelector_.setKeyCodeCondition(function() {
// Return false when FindControl is active so that MouseMode keyboard
// shortcuts are ignored.
return this.listenToKeys_;
}.bind(this));
this.mouseModeSelector_.setModifierForAlternateMode(
m.SELECTION, tv.b.ui.MODIFIER.SHIFT);
this.mouseModeSelector_.setModifierForAlternateMode(
m.PANSCAN, tv.b.ui.MODIFIER.SPACE);
this.mouseModeSelector_.setModifierForAlternateMode(
m.ZOOM, tv.b.ui.MODIFIER.CMD_OR_CTRL);
},
detach: function() {
this.modelTrack_.detach();
this.upperModelTrack_.detach();
for (var i = 0; i < this.boundListeners_.length; i++) {
var binding = this.boundListeners_[i];
binding.object.removeEventListener(binding.event, binding.boundFunc);
}
this.boundListeners_ = undefined;
this.viewport_.detach();
},
get viewport() {
return this.viewport_;
},
get model() {
return this.model_;
},
set model(model) {
if (!model)
throw new Error('Model cannot be null');
var modelInstanceChanged = this.model_ !== model;
this.model_ = model;
this.modelTrack_.model = model;
this.upperModelTrack_.model = model;
// Set up a reasonable viewport.
if (modelInstanceChanged)
this.viewport_.setWhenPossible(this.setInitialViewport_.bind(this));
tv.b.setPropertyAndDispatchChange(this, 'model', model);
},
get hasVisibleContent() {
return this.modelTrack_.hasVisibleContent ||
this.upperModelTrack_.hasVisibleContent;
},
setInitialViewport_: function() {
// We need the canvas size to be up-to-date at this point. We maybe in
// here before the raf fires, so the size may have not been updated since
// the canvas was resized.
this.modelTrackContainer_.updateCanvasSizeIfNeeded_();
var w = this.modelTrackContainer_.canvas.width;
var min;
var range;
if (this.model_.bounds.isEmpty) {
min = 0;
range = 1000;
} else if (this.model_.bounds.range === 0) {
min = this.model_.bounds.min;
range = 1000;
} else {
min = this.model_.bounds.min;
range = this.model_.bounds.range;
}
var boost = range * 0.15;
tempDisplayTransform.set(this.viewport_.currentDisplayTransform);
tempDisplayTransform.xSetWorldBounds(min - boost,
min + range + boost,
w);
this.viewport_.setDisplayTransformImmediately(tempDisplayTransform);
},
/**
* @param {Filter} filter The filter to use for finding matches.
* @param {Selection} selection The selection to add matches to.
* @return {Task} which performs the filtering.
*/
addAllObjectsMatchingFilterToSelectionAsTask: function(filter, selection) {
return this.modelTrack_.addAllObjectsMatchingFilterToSelectionAsTask(
filter, selection);
this.upperModelTrack_.addAllObjectsMatchingFilterToSelection(
filter, selection);
},
/**
* @return {Element} The element whose focused state determines
* whether to respond to keyboard inputs.
* Defaults to the parent element.
*/
get focusElement() {
if (this.focusElement_)
return this.focusElement_;
return this.parentElement;
},
/**
* Sets the element whose focus state will determine whether
* to respond to keyboard input.
*/
set focusElement(value) {
this.focusElement_ = value;
},
get listenToKeys_() {
if (!this.viewport_.isAttachedToDocumentOrInTestMode)
return false;
if (document.activeElement instanceof TracingFindControl)
return false;
if (document.activeElement instanceof TracingScriptingControl)
return false;
if (!this.focusElement_)
return true;
if (this.focusElement.tabIndex >= 0) {
if (document.activeElement == this.focusElement)
return true;
return tv.b.ui.elementIsChildOf(document.activeElement,
this.focusElement);
}
return true;
},
onMouseMove_: function(e) {
// Zooming requires the delta since the last mousemove so we need to avoid
// tracking it when the zoom interaction is active.
if (this.isZooming_)
return;
this.storeLastMousePos_(e);
},
onTouchStart_: function(e) {
this.storeLastTouchPositions_(e);
this.focusElements_();
},
onTouchMove_: function(e) {
e.preventDefault();
this.onUpdateTransformForTouch_(e);
},
onTouchEnd_: function(e) {
this.storeLastTouchPositions_(e);
this.focusElements_();
},
onKeypress_: function(e) {
var vp = this.viewport_;
if (!this.listenToKeys_)
return;
if (document.activeElement.nodeName == 'INPUT')
return;
var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
var curMouseV, curCenterW;
switch (e.keyCode) {
case 119: // w
case 44: // ,
this.zoomBy_(1.5, true);
break;
case 115: // s
case 111: // o
this.zoomBy_(1 / 1.5, true);
break;
case 103: // g
this.onGridToggle_(true);
break;
case 71: // G
this.onGridToggle_(false);
break;
case 87: // W
case 60: // <
this.zoomBy_(10, true);
break;
case 83: // S
case 79: // O
this.zoomBy_(1 / 10, true);
break;
case 97: // a
this.queueSmoothPan_(viewWidth * 0.3, 0);
break;
case 100: // d
case 101: // e
this.queueSmoothPan_(viewWidth * -0.3, 0);
break;
case 65: // A
this.queueSmoothPan_(viewWidth * 0.5, 0);
break;
case 68: // D
this.queueSmoothPan_(viewWidth * -0.5, 0);
break;
case 48: // 0
this.setInitialViewport_();
break;
case 102: // f
this.zoomToSelection();
break;
case 'm'.charCodeAt(0):
this.setCurrentSelectionAsInterestRange_();
break;
case 104: // h
this.toggleHighDetails_();
break;
}
},
// Not all keys send a keypress.
onKeydown_: function(e) {
if (!this.listenToKeys_)
return;
var sel;
var vp = this.viewport_;
var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
switch (e.keyCode) {
case 37: // left arrow
sel = this.selection.getShiftedSelection(
this.viewport, -1);
if (sel) {
this.setSelectionAndClearHighlight(sel);
this.panToSelection();
e.preventDefault();
} else {
this.queueSmoothPan_(viewWidth * 0.3, 0);
}
break;
case 39: // right arrow
sel = this.selection.getShiftedSelection(
this.viewport, 1);
if (sel) {
this.setSelectionAndClearHighlight(sel);
this.panToSelection();
e.preventDefault();
} else {
this.queueSmoothPan_(-viewWidth * 0.3, 0);
}
break;
case 9: // TAB
if (this.focusElement.tabIndex == -1) {
if (e.shiftKey)
this.selectPrevious_(e);
else
this.selectNext_(e);
e.preventDefault();
}
break;
}
},
onKeyup_: function(e) {
if (!this.listenToKeys_)
return;
if (!e.shiftKey) {
if (this.dragBeginEvent_) {
this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
this.dragBoxXEnd_, this.dragBoxYEnd_);
}
}
},
onDblClick_: function(e) {
if (this.mouseModeSelector_.mode !==
tv.b.ui.MOUSE_SELECTOR_MODE.SELECTION)
return;
if (!this.selection.length || !this.selection[0].title)
return;
var selection = new Selection();
var filter = new tv.c.ExactTitleFilter(this.selection[0].title);
this.modelTrack_.addAllObjectsMatchingFilterToSelection(filter,
selection);
this.setSelectionAndClearHighlight(selection);
},
onMouseWheel_: function(e) {
if (!e.altKey)
return;
var delta = e.wheelDelta / 120;
var zoomScale = Math.pow(1.5, delta);
this.zoomBy_(zoomScale);
e.preventDefault();
},
queueSmoothPan_: function(viewDeltaX, deltaY) {
var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld(
viewDeltaX);
var animation = new tv.c.TimelineDisplayTransformPanAnimation(
deltaX, deltaY);
this.viewport_.queueDisplayTransformAnimation(animation);
},
/**
* Zoom in or out on the timeline by the given scale factor.
* @param {Number} scale The scale factor to apply. If <1, zooms out.
* @param {boolean} Whether to change the zoom level smoothly.
*/
zoomBy_: function(scale, smooth) {
if (scale <= 0) {
return;
}
smooth = !!smooth;
var vp = this.viewport_;
var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
var pixelRatio = window.devicePixelRatio || 1;
var goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio;
var goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld(
goalFocalPointXView);
if (smooth) {
var animation = new tv.c.TimelineDisplayTransformZoomToAnimation(
goalFocalPointXWorld, goalFocalPointXView,
vp.currentDisplayTransform.panY,
scale);
vp.queueDisplayTransformAnimation(animation);
} else {
tempDisplayTransform.set(vp.currentDisplayTransform);
tempDisplayTransform.scaleX = tempDisplayTransform.scaleX * scale;
tempDisplayTransform.xPanWorldPosToViewPos(
goalFocalPointXWorld, goalFocalPointXView, viewWidth);
vp.setDisplayTransformImmediately(tempDisplayTransform);
}
},
/**
* Zoom into the current selection.
*/
zoomToSelection: function() {
if (!this.selectionOfInterest.length)
return;
var bounds = this.selectionOfInterest.bounds;
if (!bounds.range)
return;
var worldCenter = bounds.center;
var viewCenter = this.modelTrackContainer_.canvas.width / 2;
var adjustedWorldRange = bounds.range * 1.25;
var newScale = this.modelTrackContainer_.canvas.width /
adjustedWorldRange;
var zoomInRatio = newScale /
this.viewport_.currentDisplayTransform.scaleX;
var animation = new tv.c.TimelineDisplayTransformZoomToAnimation(
worldCenter, viewCenter,
this.viewport_.currentDisplayTransform.panY,
zoomInRatio);
this.viewport_.queueDisplayTransformAnimation(animation);
},
/**
* Pan the view so the current selection becomes visible.
*/
panToSelection: function() {
if (!this.selectionOfInterest.length)
return;
var bounds = this.selectionOfInterest.bounds;
var worldCenter = bounds.center;
var viewWidth = this.modelTrackContainer_.canvas.width;
var dt = this.viewport_.currentDisplayTransform;
if (false && !bounds.range) {
if (dt.xWorldToView(bounds.center) < 0 ||
dt.xWorldToView(bounds.center) > viewWidth) {
tempDisplayTransform.set(dt);
tempDisplayTransform.xPanWorldPosToViewPos(
worldCenter, 'center', viewWidth);
var deltaX = tempDisplayTransform.panX - dt.panX;
var animation = new tv.c.TimelineDisplayTransformPanAnimation(
deltaX, 0);
this.viewport_.queueDisplayTransformAnimation(animation);
}
return;
}
tempDisplayTransform.set(dt);
tempDisplayTransform.xPanWorldBoundsIntoView(
bounds.min,
bounds.max,
viewWidth);
var deltaX = tempDisplayTransform.panX - dt.panX;
var animation = new tv.c.TimelineDisplayTransformPanAnimation(
deltaX, 0);
this.viewport_.queueDisplayTransformAnimation(animation);
},
navToPosition: function(uiState, showNavLine) {
var location = uiState.location;
var scaleX = uiState.scaleX;
var track = location.getContainingTrack(this.viewport_);
var worldCenter = location.xWorld;
var viewCenter = this.modelTrackContainer_.canvas.width / 5;
var zoomInRatio = scaleX /
this.viewport_.currentDisplayTransform.scaleX;
// Vertically scroll so track is in view.
track.scrollIntoViewIfNeeded();
// Perform zoom and panX animation.
var animation = new tv.c.TimelineDisplayTransformZoomToAnimation(
worldCenter, viewCenter,
this.viewport_.currentDisplayTransform.panY,
zoomInRatio);
this.viewport_.queueDisplayTransformAnimation(animation);
if (!showNavLine)
return;
// Add an X Marker Annotation at the specified timestamp.
if (this.xNavStringMarker_)
this.model.removeAnnotation(this.xNavStringMarker_);
this.xNavStringMarker_ =
new tv.c.trace_model.XMarkerAnnotation(worldCenter);
this.model.addAnnotation(this.xNavStringMarker_);
},
removeXNavStringMarker: function() {
if (!this.xNavStringMarker_)
return;
this.model.removeAnnotation(this.xNavStringMarker_);
},
setCurrentSelectionAsInterestRange_: function() {
var selectionBounds = this.selection.bounds;
if (selectionBounds.empty) {
this.viewport_.interestRange.reset();
return;
}
if (this.viewport_.interestRange.min == selectionBounds.min &&
this.viewport_.interestRange.max == selectionBounds.max)
this.viewport_.interestRange.reset();
else
this.viewport_.interestRange.set(selectionBounds);
},
toggleHighDetails_: function() {
this.viewport_.highDetails = !this.viewport_.highDetails;
},
/**
* Sets the selected events and changes the SelectionState of the events to
* SELECTED.
* @param {Selection} selection A Selection of the new selected events.
*/
set selection(selection) {
this.setSelectionAndHighlight(selection, this.highlight_);
},
get selection() {
return this.selection_;
},
/**
* Sets the highlighted events and changes the SelectionState of the events
* to HIGHLIGHTED. All other events are set to DIMMED, except SELECTED
* ones.
* @param {Selection} selection A Selection of the new selected events.
*/
set highlight(highlight) {
this.setSelectionAndHighlight(this.selection_, highlight);
},
get highlight() {
return this.highlight_;
},
/**
* Getter for events of interest, primarily SELECTED and secondarily
* HIGHLIGHTED events.
*/
get selectionOfInterest() {
if (!this.selection_.length && this.highlight_.length)
return this.highlight_;
return this.selection_;
},
/**
* Sets the selected events, changes the SelectionState of the events to
* SELECTED and clears the highlighted events.
* @param {Selection} selection A Selection of the new selected events.
*/
setSelectionAndClearHighlight: function(selection) {
this.setSelectionAndHighlight(selection, null);
},
/**
* Sets the highlighted events, changes the SelectionState of the events to
* HIGHLIGHTED and clears the selected events. All other events are set to
* DIMMED.
* @param {Selection} highlight A Selection of the new highlighted events.
*/
setHighlightAndClearSelection: function(highlight) {
this.setSelectionAndHighlight(null, highlight);
},
/**
* Sets both selected and highlighted events. If an event is both it will be
* set to SELECTED. All other events are set to DIMMED.
* @param {Selection} selection A Selection of the new selected events.
* @param {Selection} highlight A Selection of the new highlighted events.
*/
setSelectionAndHighlight: function(selection, highlight) {
if (selection === this.selection_ && highlight === this.highlight_)
return;
if ((selection !== null && !(selection instanceof Selection)) ||
(highlight !== null && !(highlight instanceof Selection))) {
throw new Error('Expected Selection');
}
if (highlight && highlight.length) {
// Set all events to DIMMED. This needs to be done before clearing the
// old highlight, so that the old events are still available. This is
// also necessary when the highlight doesn't change, because it might
// have overlapping events with selection.
this.resetEventsTo_(SelectionState.DIMMED);
// Switch the highlight.
if (highlight !== this.highlight_)
this.highlight_ = highlight;
// Set HIGHLIGHTED on the events of the new highlight.
this.setSelectionState_(highlight, SelectionState.HIGHLIGHTED);
} else {
// If no highlight is active the SelectionState needs to be cleared.
// Note that this also clears old SELECTED events, so it doesn't need
// to be called again when setting the selection.
this.resetEventsTo_(SelectionState.NONE);
this.highlight_ = new Selection();
}
if (selection && selection.length) {
// Switch the selection
if (selection !== this.selection_)
this.selection_ = selection;
// Set SELECTED on the events of the new highlight.
this.setSelectionState_(selection, SelectionState.SELECTED);
} else
this.selection_ = new Selection();
tv.b.dispatchSimpleEvent(this, 'selectionChange');
this.showHintText_('Press \'m\' to mark current selection');
if (this.selectionOfInterest.length) {
var track = this.viewport_.trackForEvent(this.selectionOfInterest[0]);
if (track)
track.scrollIntoViewIfNeeded();
}
this.viewport_.dispatchChangeEvent(); // Triggers a redraw.
},
/**
* Sets a new SelectionState on all events in the selection.
* @param {Selection} selection The affected selection.
* @param {SelectionState} selectionState The new selection state.
*/
setSelectionState_: function(selection, selectionState) {
for (var i = 0; i < selection.length; i++)
selection[i].selectionState = selectionState;
},
/**
* Resets all events to the provided SelectionState. When the SelectionState
* changes from or to DIMMED all events in the model need to get updated.
* @param {SelectionState} selectionState The SelectionState to reset to.
*/
resetEventsTo_: function(selectionState) {
var dimmed = this.highlight_.length;
var resetAll = (dimmed && selectionState !== SelectionState.DIMMED) ||
(!dimmed && selectionState === SelectionState.DIMMED);
if (resetAll) {
this.model.iterateAllEvents(
function(event) { event.selectionState = selectionState; });
} else {
this.setSelectionState_(this.selection_, selectionState);
this.setSelectionState_(this.highlight_, selectionState);
}
},
hideDragBox_: function() {
this.dragBox_.style.left = '-1000px';
this.dragBox_.style.top = '-1000px';
this.dragBox_.style.width = 0;
this.dragBox_.style.height = 0;
},
setDragBoxPosition_: function(xStart, yStart, xEnd, yEnd) {
var loY = Math.min(yStart, yEnd);
var hiY = Math.max(yStart, yEnd);
var loX = Math.min(xStart, xEnd);
var hiX = Math.max(xStart, xEnd);
var modelTrackRect = this.modelTrack_.getBoundingClientRect();
var dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY};
dragRect.right = dragRect.left + dragRect.width;
dragRect.bottom = dragRect.top + dragRect.height;
var modelTrackContainerRect =
this.modelTrackContainer_.getBoundingClientRect();
var clipRect = {
left: modelTrackContainerRect.left,
top: modelTrackContainerRect.top,
right: modelTrackContainerRect.right,
bottom: modelTrackContainerRect.bottom
};
var headingWidth = window.getComputedStyle(
this.querySelector('heading')).width;
var trackTitleWidth = parseInt(headingWidth);
clipRect.left = clipRect.left + trackTitleWidth;
var finalDragBox = intersectRect_(clipRect, dragRect);
this.dragBox_.style.left = finalDragBox.left + 'px';
this.dragBox_.style.width = finalDragBox.width + 'px';
this.dragBox_.style.top = finalDragBox.top + 'px';
this.dragBox_.style.height = finalDragBox.height + 'px';
var pixelRatio = window.devicePixelRatio || 1;
var canv = this.modelTrackContainer_.canvas;
var dt = this.viewport_.currentDisplayTransform;
var loWX = dt.xViewToWorld(
(loX - canv.offsetLeft) * pixelRatio);
var hiWX = dt.xViewToWorld(
(hiX - canv.offsetLeft) * pixelRatio);
var roundedDuration = Math.round((hiWX - loWX) * 100) / 100;
this.dragBox_.textContent = roundedDuration + 'ms';
var e = new tv.b.Event('selectionChanging');
e.loWX = loWX;
e.hiWX = hiWX;
this.dispatchEvent(e);
},
onGridToggle_: function(left) {
var tb = left ? this.selection.bounds.min : this.selection.bounds.max;
// Toggle the grid off if the grid is on, the marker position is the same
// and the same element is selected (same timebase).
if (this.viewport_.gridEnabled &&
this.viewport_.gridSide === left &&
this.viewport_.gridInitialTimebase === tb) {
this.viewport_.gridside = undefined;
this.viewport_.gridEnabled = false;
this.viewport_.gridInitialTimebase = undefined;
return;
}
// Shift the timebase left until its just left of model_.bounds.min.
var numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) /
this.viewport_.gridStep_);
this.viewport_.gridEnabled = true;
this.viewport_.gridSide = left;
this.viewport_.gridInitialTimebase = tb;
this.viewport_.gridTimebase = tb -
(numIntervalsSinceStart + 1) * this.viewport_.gridStep_;
},
storeLastMousePos_: function(e) {
this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
},
storeLastTouchPositions_: function(e) {
this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e);
},
extractRelativeMousePosition_: function(e) {
var canv = this.modelTrackContainer_.canvas;
return {
x: e.clientX - canv.offsetLeft,
y: e.clientY - canv.offsetTop
};
},
extractRelativeTouchPositions_: function(e) {
var canv = this.modelTrackContainer_.canvas;
var touches = [];
for (var i = 0; i < e.touches.length; ++i) {
touches.push({
x: e.touches[i].clientX - canv.offsetLeft,
y: e.touches[i].clientY - canv.offsetTop
});
}
return touches;
},
storeInitialMouseDownPos_: function(e) {
var position = this.extractRelativeMousePosition_(e);
this.mouseViewPosAtMouseDown_.x = position.x;
this.mouseViewPosAtMouseDown_.y = position.y;
},
focusElements_: function() {
if (document.activeElement)
document.activeElement.blur();
if (this.focusElement.tabIndex >= 0)
this.focusElement.focus();
},
storeInitialInteractionPositionsAndFocus_: function(e) {
this.storeInitialMouseDownPos_(e);
this.storeLastMousePos_(e);
this.focusElements_();
},
onBeginPanScan_: function(e) {
var vp = this.viewport_;
this.viewportDisplayTransformAtMouseDown_ =
vp.currentDisplayTransform.clone();
this.isPanningAndScanning_ = true;
this.storeInitialInteractionPositionsAndFocus_(e);
e.preventDefault();
},
onUpdatePanScan_: function(e) {
if (!this.isPanningAndScanning_)
return;
var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
var pixelRatio = window.devicePixelRatio || 1;
var xDeltaView = pixelRatio * (this.lastMouseViewPos_.x -
this.mouseViewPosAtMouseDown_.x);
var yDelta = this.lastMouseViewPos_.y -
this.mouseViewPosAtMouseDown_.y;
tempDisplayTransform.set(this.viewportDisplayTransformAtMouseDown_);
tempDisplayTransform.incrementPanXInViewUnits(xDeltaView);
tempDisplayTransform.panY -= yDelta;
this.viewport_.setDisplayTransformImmediately(tempDisplayTransform);
e.preventDefault();
e.stopPropagation();
this.storeLastMousePos_(e);
},
onEndPanScan_: function(e) {
this.isPanningAndScanning_ = false;
this.storeLastMousePos_(e);
if (!e.isClick)
e.preventDefault();
},
onBeginSelection_: function(e) {
var canv = this.modelTrackContainer_.canvas;
var rect = this.modelTrack_.getBoundingClientRect();
var canvRect = canv.getBoundingClientRect();
var inside = rect &&
e.clientX >= rect.left &&
e.clientX < rect.right &&
e.clientY >= rect.top &&
e.clientY < rect.bottom &&
e.clientX >= canvRect.left &&
e.clientX < canvRect.right;
if (!inside)
return;
this.dragBeginEvent_ = e;
this.storeInitialInteractionPositionsAndFocus_(e);
e.preventDefault();
},
onUpdateSelection_: function(e) {
if (!this.dragBeginEvent_)
return;
// Update the drag box
this.dragBoxXStart_ = this.dragBeginEvent_.clientX;
this.dragBoxXEnd_ = e.clientX;
this.dragBoxYStart_ = this.dragBeginEvent_.clientY;
this.dragBoxYEnd_ = e.clientY;
this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
this.dragBoxXEnd_, this.dragBoxYEnd_);
},
onEndSelection_: function(e) {
e.preventDefault();
if (!this.dragBeginEvent_)
return;
// Stop the dragging.
this.hideDragBox_();
var eDown = this.dragBeginEvent_;
this.dragBeginEvent_ = null;
// Figure out extents of the drag.
var loY = Math.min(eDown.clientY, e.clientY);
var hiY = Math.max(eDown.clientY, e.clientY);
var loX = Math.min(eDown.clientX, e.clientX);
var hiX = Math.max(eDown.clientX, e.clientX);
// Convert to worldspace.
var canv = this.modelTrackContainer_.canvas;
var worldOffset = canv.getBoundingClientRect().left;
var loVX = loX - worldOffset;
var hiVX = hiX - worldOffset;
// Figure out what has been selected.
var selection = new Selection();
this.modelTrack_.addIntersectingItemsInRangeToSelection(
loVX, hiVX, loY, hiY, selection);
// Activate the new selection.
var selection_change_event = new tv.c.RequestSelectionChangeEvent();
selection_change_event.selection = selection;
this.dispatchEvent(selection_change_event);
},
onBeginZoom_: function(e) {
this.isZooming_ = true;
this.storeInitialInteractionPositionsAndFocus_(e);
e.preventDefault();
},
onUpdateZoom_: function(e) {
if (!this.isZooming_)
return;
var newPosition = this.extractRelativeMousePosition_(e);
var zoomScaleValue = 1 + (this.lastMouseViewPos_.y -
newPosition.y) * 0.01;
this.zoomBy_(zoomScaleValue, false);
this.storeLastMousePos_(e);
},
onEndZoom_: function(e) {
this.isZooming_ = false;
if (!e.isClick)
e.preventDefault();
},
computeTouchCenter_: function(positions) {
var xSum = 0;
var ySum = 0;
for (var i = 0; i < positions.length; ++i) {
xSum += positions[i].x;
ySum += positions[i].y;
}
return {
x: xSum / positions.length,
y: ySum / positions.length
};
},
computeTouchSpan_: function(positions) {
var xMin = Number.MAX_VALUE;
var yMin = Number.MAX_VALUE;
var xMax = Number.MIN_VALUE;
var yMax = Number.MIN_VALUE;
for (var i = 0; i < positions.length; ++i) {
xMin = Math.min(xMin, positions[i].x);
yMin = Math.min(yMin, positions[i].y);
xMax = Math.max(xMax, positions[i].x);
yMax = Math.max(yMax, positions[i].y);
}
return Math.sqrt((xMin - xMax) * (xMin - xMax) +
(yMin - yMax) * (yMin - yMax));
},
onUpdateTransformForTouch_: function(e) {
var newPositions = this.extractRelativeTouchPositions_(e);
var currentPositions = this.lastTouchViewPositions_;
var newCenter = this.computeTouchCenter_(newPositions);
var currentCenter = this.computeTouchCenter_(currentPositions);
var newSpan = this.computeTouchSpan_(newPositions);
var currentSpan = this.computeTouchSpan_(currentPositions);
var vp = this.viewport_;
var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
var pixelRatio = window.devicePixelRatio || 1;
var xDelta = pixelRatio * (newCenter.x - currentCenter.x);
var yDelta = newCenter.y - currentCenter.y;
var zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1;
var viewFocus = pixelRatio * newCenter.x;
var worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus);
tempDisplayTransform.set(vp.currentDisplayTransform);
tempDisplayTransform.scaleX *= zoomScaleValue;
tempDisplayTransform.xPanWorldPosToViewPos(
worldFocus, viewFocus, viewWidth);
tempDisplayTransform.incrementPanXInViewUnits(xDelta);
tempDisplayTransform.panY -= yDelta;
vp.setDisplayTransformImmediately(tempDisplayTransform);
this.storeLastTouchPositions_(e);
},
initHintText_: function() {
this.hintTextBox_ = this.ownerDocument.createElement('div');
this.hintTextBox_.className = 'hint-text';
this.hintTextBox_.style.display = 'none';
this.appendChild(this.hintTextBox_);
this.pendingHintTextClearTimeout_ = undefined;
},
showHintText_: function(text) {
if (this.pendingHintTextClearTimeout_) {
window.clearTimeout(this.pendingHintTextClearTimeout_);
this.pendingHintTextClearTimeout_ = undefined;
}
this.pendingHintTextClearTimeout_ = setTimeout(
this.hideHintText_.bind(this), 1000);
this.hintTextBox_.textContent = text;
this.hintTextBox_.style.display = '';
},
hideHintText_: function() {
this.pendingHintTextClearTimeout_ = undefined;
this.hintTextBox_.style.display = 'none';
}
};
return {
TimelineTrackView: TimelineTrackView
};
});
</script>