| <!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/tracks/drawing_container.html"> |
| <link rel="import" href="/core/tracks/model_track.html"> |
| <link rel="import" href="/core/tracks/ruler_track.html"> |
| <link rel="import" href="/model/event.html"> |
| <link rel="import" href="/model/x_marker_annotation.html"> |
| |
| <script> |
| 'use strict'; |
| |
| /** |
| * @fileoverview Interactive visualizaiton of Model objects |
| * based loosely on gantt charts. Each thread in the Model 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 |
| * |
| */ |
| tr.exportTo('tr.c', function() { |
| var Selection = tr.c.Selection; |
| var SelectionState = tr.model.SelectionState; |
| var Viewport = tr.c.TimelineViewport; |
| |
| var tempDisplayTransform = new tr.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 Model 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 = tr.b.ui.define('div'); |
| |
| TimelineTrackView.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| |
| model_: null, |
| |
| decorate: function(timelineView) { |
| |
| this.classList.add('timeline-track-view'); |
| |
| this.timelineView_ = timelineView; |
| |
| this.viewport_ = new Viewport(this); |
| this.viewportDisplayTransformAtMouseDown_ = null; |
| this.selectionController_ = undefined; |
| |
| this.rulerTrackContainer_ = |
| new tr.c.tracks.DrawingContainer(this.viewport_); |
| this.appendChild(this.rulerTrackContainer_); |
| this.rulerTrackContainer_.invalidate(); |
| |
| this.rulerTrack_ = new tr.c.tracks.RulerTrack(this.viewport_); |
| this.rulerTrackContainer_.appendChild(this.rulerTrack_); |
| |
| this.upperModelTrack_ = new tr.c.tracks.ModelTrack(this.viewport_); |
| this.upperModelTrack_.upperMode = true; |
| this.rulerTrackContainer_.appendChild(this.upperModelTrack_); |
| |
| this.modelTrackContainer_ = |
| new tr.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 tr.c.tracks.ModelTrack(this.viewport_); |
| this.modelTrackContainer_.appendChild(this.modelTrack_); |
| |
| this.timingTool_ = new tr.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.onSelectionChanged_ = this.onSelectionChanged_.bind(this); |
| |
| 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.bindEventListener_(this, 'mousedown', this.onMouseDown_, 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.alert_ = undefined; |
| |
| 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 tr.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 = tr.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, tr.b.ui.MODIFIER.SHIFT); |
| this.mouseModeSelector_.setModifierForAlternateMode( |
| m.PANSCAN, tr.b.ui.MODIFIER.SPACE); |
| this.mouseModeSelector_.setModifierForAlternateMode( |
| m.ZOOM, tr.b.ui.MODIFIER.CMD_OR_CTRL); |
| }, |
| |
| get selectionController() { |
| return this.selectionController_; |
| }, |
| |
| set selectionController(selectionController) { |
| if (this.selectionController_) { |
| this.selectionController_.removeEventListener('change', |
| this.onSelectionChanged_); |
| } |
| this.selectionController_ = selectionController; |
| if (this.selectionController_) { |
| this.selectionController_.addEventListener('change', |
| this.onSelectionChanged_); |
| } |
| }, |
| |
| onSelectionChanged_: function() { |
| this.showHintText_('Press \'m\' to mark current selection'); |
| this.viewport_.dispatchChangeEvent(); |
| }, |
| |
| set selection(selection) { |
| throw new Error('DO NOT CALL THIS'); |
| }, |
| |
| set highlight(highlight) { |
| throw new Error('DO NOT CALL THIS'); |
| }, |
| |
| 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)); |
| |
| tr.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. |
| */ |
| addAllEventsMatchingFilterToSelectionAsTask: function(filter, selection) { |
| var modelTrack = this.modelTrack_; |
| var firstT = modelTrack.addAllEventsMatchingFilterToSelectionAsTask( |
| filter, selection); |
| var lastT = firstT.after(function() { |
| this.upperModelTrack_.addAllEventsMatchingFilterToSelection( |
| filter, selection); |
| |
| }, this); |
| return firstT; |
| }, |
| |
| /** |
| * @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 tr.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.selectionController_.selection.getShiftedSelection( |
| this.viewport, -1); |
| |
| if (sel) { |
| this.selectionController.changeSelectionFromTimeline(sel); |
| this.panToSelection(); |
| e.preventDefault(); |
| } else { |
| this.queueSmoothPan_(viewWidth * 0.3, 0); |
| } |
| break; |
| case 39: // right arrow |
| sel = this.selectionController_.selection.getShiftedSelection( |
| this.viewport, 1); |
| if (sel) { |
| this.selectionController.changeSelectionFromTimeline(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 !== |
| tr.b.ui.MOUSE_SELECTOR_MODE.SELECTION) |
| return; |
| |
| var curSelection = this.selectionController_.selection; |
| if (!curSelection.length || !curSelection[0].title) |
| return; |
| |
| var selection = new Selection(); |
| var filter = new tr.c.ExactTitleFilter(curSelection[0].title); |
| this.modelTrack_.addAllEventsMatchingFilterToSelection(filter, |
| selection); |
| |
| this.selectionController.changeSelectionFromTimeline(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(); |
| }, |
| |
| onMouseDown_: function(e) { |
| if (this.mouseModeSelector_.mode !== |
| tr.b.ui.MOUSE_SELECTOR_MODE.SELECTION) |
| return; |
| // Mouse down must start on ruler track for crosshair guide lines to draw. |
| if (e.target !== this.rulerTrack_) |
| return; |
| |
| // Make sure we don't start a selection drag event here. |
| this.dragBeginEvent_ = undefined; |
| |
| // Remove nav string marker if it exists, since we're clearing the |
| // find control box. |
| if (this.xNavStringMarker_) { |
| this.model.removeAnnotation(this.xNavStringMarker_); |
| this.xNavStringMarker_ = undefined; |
| } |
| |
| var dt = this.viewport_.currentDisplayTransform; |
| tr.b.ui.trackMouseMovesUntilMouseUp(function(e) { // Mouse move handler. |
| // If mouse event is on ruler, don't do anything. |
| if (e.target === this.rulerTrack_) |
| return; |
| |
| var relativePosition = this.extractRelativeMousePosition_(e); |
| var loc = tr.c.Location.fromViewCoordinates( |
| this.viewport_, relativePosition.x, relativePosition.y); |
| // Not all points on the timeline represents a valid location. |
| // ex. process header tracks, letter dot tracks. |
| if (!loc) |
| return; |
| |
| if (this.guideLineAnnotation_ === undefined) { |
| this.guideLineAnnotation_ = |
| new tr.model.XMarkerAnnotation(loc.xWorld); |
| this.model.addAnnotation(this.guideLineAnnotation_); |
| } else { |
| this.guideLineAnnotation_.timestamp = loc.xWorld; |
| this.modelTrackContainer_.invalidate(); |
| } |
| |
| // Set the findcontrol's text to nav string of current state. |
| var state = new tr.c.UIState(loc, |
| this.viewport_.currentDisplayTransform.scaleX); |
| this.timelineView_.setFindCtlText( |
| state.toUserFriendlyString(this.viewport_)); |
| }.bind(this)); |
| }, |
| |
| queueSmoothPan_: function(viewDeltaX, deltaY) { |
| var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld( |
| viewDeltaX); |
| var animation = new tr.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 tr.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.selectionController.selectionOfInterest.length) |
| return; |
| |
| var bounds = this.selectionController.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 tr.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.selectionController.selectionOfInterest.length) |
| return; |
| |
| var bounds = this.selectionController.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 tr.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 tr.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 tr.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 tr.model.XMarkerAnnotation(worldCenter); |
| this.model.addAnnotation(this.xNavStringMarker_); |
| }, |
| |
| setCurrentSelectionAsInterestRange_: function() { |
| var selectionBounds = this.selectionController_.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; |
| }, |
| |
| 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'; |
| this.dragBox_.style.whiteSpace = 'nowrap'; |
| |
| 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); |
| |
| this.dragBox_.textContent = tr.b.units.tsString((hiWX - loWX)); |
| |
| var e = new tr.b.Event('selectionChanging'); |
| e.loWX = loWX; |
| e.hiWX = hiWX; |
| this.dispatchEvent(e); |
| }, |
| |
| onGridToggle_: function(left) { |
| var selection = this.selectionController_.selection; |
| var tb = left ? selection.bounds.min : 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_.addIntersectingEventsInRangeToSelection( |
| loVX, hiVX, loY, hiY, selection); |
| |
| // Activate the new selection. |
| this.selectionController_.changeSelectionFromTimeline(selection); |
| }, |
| |
| 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> |