| <!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) { |
| 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); |
| |
| // 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> |