| <!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/settings.html"> |
| <link rel="import" href="/tracing/base/task.html"> |
| <link rel="import" href="/tracing/core/filter.html"> |
| <link rel="import" href="/tracing/model/event.html"> |
| <link rel="import" href="/tracing/model/event_set.html"> |
| <link rel="import" href="/tracing/model/x_marker_annotation.html"> |
| <link rel="import" href="/tracing/ui/base/hotkey_controller.html"> |
| <link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> |
| <link rel="import" href="/tracing/ui/base/timing_tool.html"> |
| <link rel="import" href="/tracing/ui/base/ui.html"> |
| <link rel="import" href="/tracing/ui/timeline_display_transform_animations.html"> |
| <link rel="import" href="/tracing/ui/timeline_viewport.html"> |
| <link rel="import" href="/tracing/ui/tracks/drawing_container.html"> |
| <link rel="import" href="/tracing/ui/tracks/model_track.html"> |
| <link rel="import" href="/tracing/ui/tracks/ruler_track.html"> |
| <link rel="import" href="/tracing/value/unit.html"> |
| |
| <!-- |
| 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 |
| --> |
| <polymer-element name='tr-ui-timeline-track-view'> |
| <template> |
| <style> |
| :host { |
| -webkit-box-orient: vertical; |
| display: -webkit-box; |
| position: relative; |
| } |
| |
| :host ::content * { |
| -webkit-user-select: none; |
| cursor: default; |
| } |
| |
| #drag_box { |
| background-color: rgba(0, 0, 255, 0.25); |
| border: 1px solid rgb(0, 0, 96); |
| font-size: 75%; |
| position: fixed; |
| } |
| |
| #hint_text { |
| position: absolute; |
| bottom: 6px; |
| right: 6px; |
| font-size: 8pt; |
| } |
| </style> |
| <content></content> |
| |
| <div id='drag_box'></div> |
| <div id='hint_text'></div> |
| |
| <tv-ui-b-hotkey-controller id='hotkey_controller'> |
| </tv-ui-b-hotkey-controller> |
| </template> |
| |
| <script> |
| 'use strict'; |
| |
| Polymer({ |
| ready: function() { |
| this.displayTransform_ = new tr.ui.TimelineDisplayTransform(); |
| this.model_ = undefined; |
| |
| this.timelineView_ = undefined; |
| |
| this.viewport_ = new tr.ui.TimelineViewport(this); |
| this.viewportDisplayTransformAtMouseDown_ = undefined; |
| this.brushingStateController_ = undefined; |
| |
| this.rulerTrackContainer_ = |
| new tr.ui.tracks.DrawingContainer(this.viewport_); |
| this.appendChild(this.rulerTrackContainer_); |
| this.rulerTrackContainer_.invalidate(); |
| |
| this.rulerTrack_ = new tr.ui.tracks.RulerTrack(this.viewport_); |
| this.rulerTrackContainer_.appendChild(this.rulerTrack_); |
| |
| this.upperModelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_); |
| this.upperModelTrack_.upperMode = true; |
| this.rulerTrackContainer_.appendChild(this.upperModelTrack_); |
| |
| this.modelTrackContainer_ = |
| new tr.ui.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.ui.tracks.ModelTrack(this.viewport_); |
| this.modelTrackContainer_.appendChild(this.modelTrack_); |
| |
| this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this); |
| |
| this.initMouseModeSelector(); |
| |
| this.hideDragBox_(); |
| |
| this.initHintText_(); |
| |
| this.onSelectionChanged_ = this.onSelectionChanged_.bind(this); |
| |
| this.onDblClick_ = this.onDblClick_.bind(this); |
| this.addEventListener('dblclick', this.onDblClick_); |
| |
| this.onMouseWheel_ = this.onMouseWheel_.bind(this); |
| this.addEventListener('mousewheel', this.onMouseWheel_); |
| |
| this.onMouseDown_ = this.onMouseDown_.bind(this); |
| this.addEventListener('mousedown', this.onMouseDown_); |
| |
| this.onMouseMove_ = this.onMouseMove_.bind(this); |
| this.addEventListener('mousemove', this.onMouseMove_); |
| |
| this.onTouchStart_ = this.onTouchStart_.bind(this); |
| this.addEventListener('touchstart', this.onTouchStart_); |
| |
| this.onTouchMove_ = this.onTouchMove_.bind(this); |
| this.addEventListener('touchmove', this.onTouchMove_); |
| |
| this.onTouchEnd_ = this.onTouchEnd_.bind(this); |
| this.addEventListener('touchend', this.onTouchEnd_); |
| |
| |
| this.addHotKeys_(); |
| |
| this.mouseViewPosAtMouseDown_ = {x: 0, y: 0}; |
| this.lastMouseViewPos_ = {x: 0, y: 0}; |
| |
| this.lastTouchViewPositions_ = []; |
| |
| this.alert_ = undefined; |
| |
| this.isPanningAndScanning_ = false; |
| this.isZooming_ = false; |
| }, |
| |
| initMouseModeSelector: function() { |
| this.mouseModeSelector_ = document.createElement( |
| 'tr-ui-b-mouse-mode-selector'); |
| this.mouseModeSelector_.targetElement = 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.ui.b.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_.setModifierForAlternateMode( |
| m.SELECTION, tr.ui.b.MODIFIER.SHIFT); |
| this.mouseModeSelector_.setModifierForAlternateMode( |
| m.PANSCAN, tr.ui.b.MODIFIER.SPACE); |
| }, |
| |
| get brushingStateController() { |
| return this.brushingStateController_; |
| }, |
| |
| set brushingStateController(brushingStateController) { |
| if (this.brushingStateController_) { |
| this.brushingStateController_.removeEventListener('change', |
| this.onSelectionChanged_); |
| } |
| this.brushingStateController_ = brushingStateController; |
| if (this.brushingStateController_) { |
| this.brushingStateController_.addEventListener('change', |
| this.onSelectionChanged_); |
| } |
| }, |
| |
| set timelineView(view) { |
| this.timelineView_ = view; |
| }, |
| |
| 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(); |
| |
| this.viewport_.detach(); |
| }, |
| |
| get viewport() { |
| return this.viewport_; |
| }, |
| |
| get model() { |
| return this.model_; |
| }, |
| |
| set model(model) { |
| if (!model) |
| throw new Error('Model cannot be undefined'); |
| |
| 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)); |
| }, |
| |
| 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; |
| this.displayTransform_.set(this.viewport_.currentDisplayTransform); |
| this.displayTransform_.xSetWorldBounds( |
| min - boost, min + range + boost, w); |
| this.viewport_.setDisplayTransformImmediately(this.displayTransform_); |
| }, |
| |
| /** |
| * @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; |
| }, |
| |
| 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_(); |
| }, |
| |
| addHotKeys_: function() { |
| this.addKeyDownHotKeys_(); |
| this.addKeyPressHotKeys_(); |
| }, |
| |
| addKeyPressHotKeys_: function() { |
| var addBinding = function(dict) { |
| dict.eventType = 'keypress'; |
| dict.useCapture = false; |
| dict.thisArg = this; |
| var binding = new tr.ui.b.HotKey(dict); |
| this.$.hotkey_controller.addHotKey(binding); |
| }.bind(this); |
| |
| addBinding({ |
| keyCodes: ['w'.charCodeAt(0), ','.charCodeAt(0)], |
| callback: function(e) { |
| this.zoomBy_(1.5, true); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCodes: ['s'.charCodeAt(0), 'o'.charCodeAt(0)], |
| callback: function(e) { |
| this.zoomBy_(1 / 1.5, true); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'g'.charCodeAt(0), |
| callback: function(e) { |
| this.onGridToggle_(true); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'G'.charCodeAt(0), |
| callback: function(e) { |
| this.onGridToggle_(false); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCodes: ['W'.charCodeAt(0), '<'.charCodeAt(0)], |
| callback: function(e) { |
| this.zoomBy_(10, true); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCodes: ['S'.charCodeAt(0), 'O'.charCodeAt(0)], |
| callback: function(e) { |
| this.zoomBy_(1 / 10, true); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'a'.charCodeAt(0), |
| callback: function(e) { |
| this.queueSmoothPan_(this.viewWidth_ * 0.3, 0); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCodes: ['d'.charCodeAt(0), 'e'.charCodeAt(0)], |
| callback: function(e) { |
| this.queueSmoothPan_(this.viewWidth_ * -0.3, 0); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'A'.charCodeAt(0), |
| callback: function(e) { |
| this.queueSmoothPan_(viewWidth * 0.5, 0); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'D'.charCodeAt(0), |
| callback: function(e) { |
| this.queueSmoothPan_(viewWidth * -0.5, 0); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: '0'.charCodeAt(0), |
| callback: function(e) { |
| this.setInitialViewport_(); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'f'.charCodeAt(0), |
| callback: function(e) { |
| this.zoomToSelection(); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'm'.charCodeAt(0), |
| callback: function(e) { |
| this.setCurrentSelectionAsInterestRange_(); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 'h'.charCodeAt(0), |
| callback: function(e) { |
| this.toggleHighDetails_(); |
| e.stopPropagation(); |
| } |
| }); |
| }, |
| |
| get viewWidth_() { |
| return this.modelTrackContainer_.canvas.clientWidth; |
| }, |
| |
| addKeyDownHotKeys_: function() { |
| var addBinding = function(dict) { |
| dict.eventType = 'keydown'; |
| dict.useCapture = false; |
| dict.thisArg = this; |
| var binding = new tr.ui.b.HotKey(dict); |
| this.$.hotkey_controller.addHotKey(binding); |
| }.bind(this); |
| |
| addBinding({ |
| keyCode: 37, // Left arrow. |
| callback: function(e) { |
| var curSel = this.brushingStateController_.selection; |
| var sel = this.viewport.getShiftedSelection(curSel, -1); |
| |
| if (sel) { |
| this.brushingStateController.changeSelectionFromTimeline(sel); |
| this.panToSelection(); |
| } else { |
| this.queueSmoothPan_(this.viewWidth_ * 0.3, 0); |
| } |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| }); |
| |
| addBinding({ |
| keyCode: 39, // Right arrow. |
| callback: function(e) { |
| var curSel = this.brushingStateController_.selection; |
| var sel = this.viewport.getShiftedSelection(curSel, 1); |
| if (sel) { |
| this.brushingStateController.changeSelectionFromTimeline(sel); |
| this.panToSelection(); |
| } else { |
| this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0); |
| } |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| }); |
| }, |
| |
| onDblClick_: function(e) { |
| if (this.mouseModeSelector_.mode !== |
| tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) |
| return; |
| |
| var curSelection = this.brushingStateController_.selection; |
| if (!curSelection.length || !curSelection[0].title) |
| return; |
| |
| var selection = new tr.model.EventSet(); |
| var filter = new tr.c.ExactTitleFilter(curSelection[0].title); |
| this.modelTrack_.addAllEventsMatchingFilterToSelection(filter, |
| selection); |
| |
| this.brushingStateController.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.ui.b.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.ui.b.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.model.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.ui.b.UIState(loc, |
| this.viewport_.currentDisplayTransform.scaleX); |
| this.timelineView_.setFindCtlText( |
| state.toUserFriendlyString(this.viewport_)); |
| }.bind(this), |
| undefined, // Mouse up handler. |
| function onKeyUpDuringDrag() { |
| if (this.dragBeginEvent_) { |
| this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, |
| this.dragBoxXEnd_, this.dragBoxYEnd_); |
| } |
| }.bind(this)); |
| }, |
| |
| queueSmoothPan_: function(viewDeltaX, deltaY) { |
| var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld( |
| viewDeltaX); |
| var animation = new tr.ui.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 pixelRatio = window.devicePixelRatio || 1; |
| |
| var goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio; |
| var goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld( |
| goalFocalPointXView); |
| if (smooth) { |
| var animation = new tr.ui.TimelineDisplayTransformZoomToAnimation( |
| goalFocalPointXWorld, goalFocalPointXView, |
| vp.currentDisplayTransform.panY, |
| scale); |
| vp.queueDisplayTransformAnimation(animation); |
| } else { |
| this.displayTransform_.set(vp.currentDisplayTransform); |
| this.displayTransform_.scaleX *= scale; |
| this.displayTransform_.xPanWorldPosToViewPos( |
| goalFocalPointXWorld, goalFocalPointXView, this.viewWidth_); |
| vp.setDisplayTransformImmediately(this.displayTransform_); |
| } |
| }, |
| |
| /** |
| * Zoom into the current selection. |
| */ |
| zoomToSelection: function() { |
| if (!this.brushingStateController.selectionOfInterest.length) |
| return; |
| |
| var bounds = this.brushingStateController.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.ui.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.brushingStateController.selectionOfInterest.length) |
| return; |
| |
| var bounds = this.brushingStateController.selectionOfInterest.bounds; |
| var worldCenter = bounds.center; |
| var viewWidth = this.viewWidth_; |
| |
| var dt = this.viewport_.currentDisplayTransform; |
| if (false && !bounds.range) { |
| if (dt.xWorldToView(bounds.center) < 0 || |
| dt.xWorldToView(bounds.center) > viewWidth) { |
| this.displayTransform_.set(dt); |
| this.displayTransform_.xPanWorldPosToViewPos( |
| worldCenter, 'center', viewWidth); |
| var deltaX = this.displayTransform_.panX - dt.panX; |
| var animation = new tr.ui.TimelineDisplayTransformPanAnimation( |
| deltaX, 0); |
| this.viewport_.queueDisplayTransformAnimation(animation); |
| } |
| return; |
| } |
| |
| this.displayTransform_.set(dt); |
| this.displayTransform_.xPanWorldBoundsIntoView( |
| bounds.min, |
| bounds.max, |
| viewWidth); |
| var deltaX = this.displayTransform_.panX - dt.panX; |
| var animation = new tr.ui.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.ui.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.brushingStateController_.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.$.drag_box.style.left = '-1000px'; |
| this.$.drag_box.style.top = '-1000px'; |
| this.$.drag_box.style.width = 0; |
| this.$.drag_box.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('tr-ui-heading')).width; |
| var trackTitleWidth = parseInt(headingWidth); |
| clipRect.left = clipRect.left + trackTitleWidth; |
| |
| var intersectRect_ = function(r1, r2) { |
| if (r2.left > r1.right || r2.right < r1.left || |
| r2.top > r1.bottom || r2.bottom < r1.top) |
| return false; |
| |
| var results = {}; |
| 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; |
| }; |
| |
| // TODO(dsinclair): intersectRect_ can return false (which should actually |
| // be undefined) but we use finalDragBox without checking the return value |
| // which could potentially blowup. Fix this ..... |
| var finalDragBox = intersectRect_(clipRect, dragRect); |
| |
| this.$.drag_box.style.left = finalDragBox.left + 'px'; |
| this.$.drag_box.style.width = finalDragBox.width + 'px'; |
| this.$.drag_box.style.top = finalDragBox.top + 'px'; |
| this.$.drag_box.style.height = finalDragBox.height + 'px'; |
| this.$.drag_box.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.$.drag_box.textContent = |
| tr.v.Unit.byName.timeDurationInMs.format(hiWX - loWX); |
| |
| var e = new tr.b.Event('selectionChanging'); |
| e.loWX = loWX; |
| e.hiWX = hiWX; |
| this.dispatchEvent(e); |
| }, |
| |
| onGridToggle_: function(left) { |
| var selection = this.brushingStateController_.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() { |
| this.$.hotkey_controller.childRequestsGeneralFocus(this); |
| }, |
| |
| 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.viewWidth_; |
| |
| var pixelRatio = window.devicePixelRatio || 1; |
| var xDeltaView = pixelRatio * (this.lastMouseViewPos_.x - |
| this.mouseViewPosAtMouseDown_.x); |
| |
| var yDelta = this.lastMouseViewPos_.y - |
| this.mouseViewPosAtMouseDown_.y; |
| |
| this.displayTransform_.set(this.viewportDisplayTransformAtMouseDown_); |
| this.displayTransform_.incrementPanXInViewUnits(xDeltaView); |
| this.displayTransform_.panY -= yDelta; |
| this.viewport_.setDisplayTransformImmediately(this.displayTransform_); |
| |
| 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_ = undefined; |
| |
| // 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 tr.model.EventSet(); |
| if (eDown.appendSelection) { |
| var previousSelection = this.brushingStateController_.selection; |
| if (previousSelection !== undefined) |
| selection.addEventSet(previousSelection); |
| } |
| this.modelTrack_.addIntersectingEventsInRangeToSelection( |
| loVX, hiVX, loY, hiY, selection); |
| |
| // Activate the new selection. |
| this.brushingStateController_.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.viewWidth_; |
| 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); |
| |
| this.displayTransform_.set(vp.currentDisplayTransform); |
| this.displayTransform_.scaleX *= zoomScaleValue; |
| this.displayTransform_.xPanWorldPosToViewPos( |
| worldFocus, viewFocus, viewWidth); |
| this.displayTransform_.incrementPanXInViewUnits(xDelta); |
| this.displayTransform_.panY -= yDelta; |
| vp.setDisplayTransformImmediately(this.displayTransform_); |
| this.storeLastTouchPositions_(e); |
| }, |
| |
| initHintText_: function() { |
| this.$.hint_text.style.display = 'none'; |
| |
| 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.$.hint_text.textContent = text; |
| this.$.hint_text.style.display = ''; |
| }, |
| |
| hideHintText_: function() { |
| this.pendingHintTextClearTimeout_ = undefined; |
| this.$.hint_text.style.display = 'none'; |
| } |
| }); |
| </script> |
| </polymer-element> |