| /* |
| * Copyright (C) 2013, 2015-2016 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.TimelineOverview = class TimelineOverview extends WI.View |
| { |
| constructor(timelineRecording) |
| { |
| super(); |
| |
| console.assert(timelineRecording instanceof WI.TimelineRecording); |
| |
| this._timelinesViewModeSettings = this._createViewModeSettings(WI.TimelineOverview.ViewMode.Timelines, WI.TimelineOverview.MinimumDurationPerPixel, WI.TimelineOverview.MaximumDurationPerPixel, 0.01, 0, 15); |
| this._instrumentTypes = WI.TimelineManager.availableTimelineTypes(); |
| |
| let minimumDurationPerPixel = 1 / WI.TimelineRecordFrame.MaximumWidthPixels; |
| let maximumDurationPerPixel = 1 / WI.TimelineRecordFrame.MinimumWidthPixels; |
| this._renderingFramesViewModeSettings = this._createViewModeSettings(WI.TimelineOverview.ViewMode.RenderingFrames, minimumDurationPerPixel, maximumDurationPerPixel, minimumDurationPerPixel, 0, 100); |
| |
| this._recording = timelineRecording; |
| this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this); |
| this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this); |
| this._recording.addEventListener(WI.TimelineRecording.Event.MarkerAdded, this._markerAdded, this); |
| this._recording.addEventListener(WI.TimelineRecording.Event.Reset, this._recordingReset, this); |
| |
| this.element.classList.add("timeline-overview"); |
| this._updateWheelAndGestureHandlers(); |
| |
| this._graphsContainerView = new WI.View; |
| this._graphsContainerView.element.classList.add("graphs-container"); |
| this._graphsContainerView.element.addEventListener("click", this._handleGraphsContainerClick.bind(this)); |
| this.addSubview(this._graphsContainerView); |
| |
| this._selectedTimelineRecord = null; |
| this._overviewGraphsByTypeMap = new Map; |
| |
| this._editInstrumentsButton = new WI.ActivateButtonNavigationItem("toggle-edit-instruments", WI.UIString("Edit configuration"), WI.UIString("Save configuration")); |
| this._editInstrumentsButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleEditingInstruments, this); |
| this._editingInstruments = false; |
| this._updateEditInstrumentsButton(); |
| |
| let instrumentsNavigationBar = new WI.NavigationBar; |
| instrumentsNavigationBar.element.classList.add("timelines"); |
| instrumentsNavigationBar.addNavigationItem(new WI.FlexibleSpaceNavigationItem); |
| instrumentsNavigationBar.addNavigationItem(this._editInstrumentsButton); |
| this.addSubview(instrumentsNavigationBar); |
| |
| this._timelinesTreeOutline = new WI.TreeOutline; |
| this._timelinesTreeOutline.element.classList.add("timelines"); |
| this._timelinesTreeOutline.disclosureButtons = false; |
| this._timelinesTreeOutline.large = true; |
| this._timelinesTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._timelinesTreeSelectionDidChange, this); |
| this.element.appendChild(this._timelinesTreeOutline.element); |
| |
| this._treeElementsByTypeMap = new Map; |
| |
| this._timelineRuler = new WI.TimelineRuler; |
| this._timelineRuler.allowsClippedLabels = true; |
| this._timelineRuler.allowsTimeRangeSelection = true; |
| this._timelineRuler.element.addEventListener("mousedown", this._timelineRulerMouseDown.bind(this)); |
| this._timelineRuler.element.addEventListener("click", this._timelineRulerMouseClicked.bind(this)); |
| this._timelineRuler.addEventListener(WI.TimelineRuler.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this); |
| this.addSubview(this._timelineRuler); |
| |
| this._currentTimeMarker = new WI.TimelineMarker(0, WI.TimelineMarker.Type.CurrentTime); |
| this._timelineRuler.addMarker(this._currentTimeMarker); |
| |
| this._scrollContainerElement = document.createElement("div"); |
| this._scrollContainerElement.classList.add("scroll-container"); |
| this._scrollContainerElement.addEventListener("scroll", this._handleScrollEvent.bind(this)); |
| this.element.appendChild(this._scrollContainerElement); |
| |
| this._scrollWidthSizer = document.createElement("div"); |
| this._scrollWidthSizer.classList.add("scroll-width-sizer"); |
| this._scrollContainerElement.appendChild(this._scrollWidthSizer); |
| |
| this._startTime = 0; |
| this._currentTime = 0; |
| this._revealCurrentTime = false; |
| this._endTime = 0; |
| this._pixelAlignDuration = false; |
| this._mouseWheelDelta = 0; |
| this._cachedScrollContainerWidth = NaN; |
| this._timelineRulerSelectionChanged = false; |
| this._viewMode = WI.TimelineOverview.ViewMode.Timelines; |
| this._selectedTimeline = null; |
| |
| for (let instrument of this._recording.instruments) |
| this._instrumentAdded(instrument); |
| |
| if (!WI.timelineManager.isCapturingPageReload()) |
| this._resetSelection(); |
| |
| this._viewModeDidChange(); |
| |
| WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this); |
| WI.timelineManager.addEventListener(WI.TimelineManager.Event.RecordingImported, this._recordingImported, this); |
| } |
| |
| // Import / Export |
| |
| exportData() |
| { |
| let json = { |
| secondsPerPixel: this.secondsPerPixel, |
| scrollStartTime: this.scrollStartTime, |
| selectionStartTime: this.selectionStartTime, |
| selectionDuration: this.selectionDuration, |
| }; |
| |
| if (this._selectedTimeline) |
| json.selectedTimelineType = this._selectedTimeline.type; |
| |
| return json; |
| } |
| |
| // Public |
| |
| get selectedTimeline() |
| { |
| return this._selectedTimeline; |
| } |
| |
| set selectedTimeline(x) |
| { |
| if (this._editingInstruments) |
| return; |
| |
| if (this._selectedTimeline === x) |
| return; |
| |
| this._selectedTimeline = x; |
| if (this._selectedTimeline) { |
| let treeElement = this._treeElementsByTypeMap.get(this._selectedTimeline.type); |
| console.assert(treeElement, "Missing tree element for timeline", this._selectedTimeline); |
| |
| let omitFocus = true; |
| let wasSelectedByUser = false; |
| treeElement.select(omitFocus, wasSelectedByUser); |
| } else if (this._timelinesTreeOutline.selectedTreeElement) |
| this._timelinesTreeOutline.selectedTreeElement.deselect(); |
| } |
| |
| get editingInstruments() |
| { |
| return this._editingInstruments; |
| } |
| |
| get viewMode() |
| { |
| return this._viewMode; |
| } |
| |
| set viewMode(x) |
| { |
| if (this._viewMode === x) |
| return; |
| |
| this._viewMode = x; |
| this._viewModeDidChange(); |
| } |
| |
| get startTime() |
| { |
| return this._startTime; |
| } |
| |
| set startTime(x) |
| { |
| x = x || 0; |
| |
| if (this._startTime === x) |
| return; |
| |
| if (this._viewMode !== WI.TimelineOverview.ViewMode.RenderingFrames) { |
| let selectionOffset = this.selectionStartTime - this._startTime; |
| this.selectionStartTime = selectionOffset + x; |
| } |
| |
| this._startTime = x; |
| |
| this.needsLayout(); |
| } |
| |
| get currentTime() |
| { |
| return this._currentTime; |
| } |
| |
| set currentTime(x) |
| { |
| x = x || 0; |
| |
| if (this._currentTime === x) |
| return; |
| |
| this._currentTime = x; |
| this._revealCurrentTime = true; |
| |
| this.needsLayout(); |
| } |
| |
| get secondsPerPixel() |
| { |
| return this._currentSettings.durationPerPixelSetting.value; |
| } |
| |
| set secondsPerPixel(x) |
| { |
| x = Math.min(this._currentSettings.maximumDurationPerPixel, Math.max(this._currentSettings.minimumDurationPerPixel, x)); |
| |
| if (this.secondsPerPixel === x) |
| return; |
| |
| if (this._pixelAlignDuration) { |
| x = 1 / Math.round(1 / x); |
| if (this.secondsPerPixel === x) |
| return; |
| } |
| |
| this._currentSettings.durationPerPixelSetting.value = x; |
| |
| this.needsLayout(); |
| } |
| |
| get pixelAlignDuration() |
| { |
| return this._pixelAlignDuration; |
| } |
| |
| set pixelAlignDuration(x) |
| { |
| if (this._pixelAlignDuration === x) |
| return; |
| |
| this._mouseWheelDelta = 0; |
| this._pixelAlignDuration = x; |
| if (this._pixelAlignDuration) |
| this.secondsPerPixel = 1 / Math.round(1 / this.secondsPerPixel); |
| } |
| |
| get endTime() |
| { |
| return this._endTime; |
| } |
| |
| set endTime(x) |
| { |
| x = x || 0; |
| |
| if (this._endTime === x) |
| return; |
| |
| this._endTime = x; |
| |
| this.needsLayout(); |
| } |
| |
| get scrollStartTime() |
| { |
| return this._currentSettings.scrollStartTime; |
| } |
| |
| set scrollStartTime(x) |
| { |
| x = x || 0; |
| |
| if (this.scrollStartTime === x) |
| return; |
| |
| this._currentSettings.scrollStartTime = x; |
| |
| this.needsLayout(); |
| } |
| |
| get scrollContainerWidth() |
| { |
| return this._cachedScrollContainerWidth; |
| } |
| |
| get visibleDuration() |
| { |
| if (isNaN(this._cachedScrollContainerWidth)) { |
| this._cachedScrollContainerWidth = this._scrollContainerElement.offsetWidth; |
| if (!this._cachedScrollContainerWidth) |
| this._cachedScrollContainerWidth = NaN; |
| } |
| |
| return this._cachedScrollContainerWidth * this.secondsPerPixel; |
| } |
| |
| get selectionStartTime() |
| { |
| return this._timelineRuler.selectionStartTime; |
| } |
| |
| set selectionStartTime(x) |
| { |
| x = x || 0; |
| |
| if (this._timelineRuler.selectionStartTime === x) |
| return; |
| |
| let selectionDuration = this.selectionDuration; |
| this._timelineRuler.selectionStartTime = x; |
| this._timelineRuler.selectionEndTime = x + selectionDuration; |
| } |
| |
| get selectionDuration() |
| { |
| return this._timelineRuler.selectionEndTime - this._timelineRuler.selectionStartTime; |
| } |
| |
| set selectionDuration(x) |
| { |
| x = Math.max(this._timelineRuler.minimumSelectionDuration, x); |
| |
| this._timelineRuler.selectionEndTime = this._timelineRuler.selectionStartTime + x; |
| } |
| |
| get height() |
| { |
| let height = 0; |
| for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { |
| if (!overviewGraph.hidden) |
| height += overviewGraph.height; |
| } |
| return height; |
| } |
| |
| attached() |
| { |
| super.attached(); |
| |
| for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) { |
| if (this._canShowTimelineType(type)) |
| overviewGraph.hidden = false; |
| } |
| |
| this.needsLayout(WI.View.LayoutReason.Resize); |
| } |
| |
| detached() |
| { |
| for (let overviewGraph of this._overviewGraphsByTypeMap.values()) |
| overviewGraph.hidden = true; |
| |
| this.hideScanner(); |
| |
| super.detached(); |
| } |
| |
| closed() |
| { |
| WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this); |
| WI.timelineManager.removeEventListener(WI.TimelineManager.Event.RecordingImported, this._recordingImported, this); |
| |
| super.closed(); |
| } |
| |
| reset() |
| { |
| this._selectedTimelineRecord = null; |
| for (let overviewGraph of this._overviewGraphsByTypeMap.values()) |
| overviewGraph.reset(); |
| |
| this._mouseWheelDelta = 0; |
| |
| this._resetSelection(); |
| } |
| |
| revealMarker(marker) |
| { |
| this.scrollStartTime = marker.time - (this.visibleDuration / 2); |
| } |
| |
| recordWasFiltered(timeline, record, filtered) |
| { |
| let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type); |
| console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type); |
| if (!overviewGraph) |
| return; |
| |
| console.assert(!overviewGraph.hidden, "Record filtered in hidden overview graph", record); |
| |
| overviewGraph.recordWasFiltered(record, filtered); |
| } |
| |
| selectRecord(timeline, record) |
| { |
| let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type); |
| console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type); |
| if (!overviewGraph) |
| return; |
| |
| console.assert(!overviewGraph.hidden, "Record selected in hidden overview graph", record); |
| |
| overviewGraph.selectedRecord = record; |
| } |
| |
| showScanner(time) |
| { |
| this._timelineRuler.showScanner(time); |
| } |
| |
| hideScanner() |
| { |
| this._timelineRuler.hideScanner(); |
| } |
| |
| updateLayoutIfNeeded(layoutReason) |
| { |
| if (this.layoutPending) { |
| super.updateLayoutIfNeeded(layoutReason); |
| return; |
| } |
| |
| this._timelineRuler.updateLayoutIfNeeded(layoutReason); |
| |
| for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { |
| if (!overviewGraph.hidden) |
| overviewGraph.updateLayoutIfNeeded(layoutReason); |
| } |
| } |
| |
| discontinuitiesInTimeRange(startTime, endTime) |
| { |
| return this._recording.discontinuitiesInTimeRange(startTime, endTime); |
| } |
| |
| // Protected |
| |
| get timelineRuler() |
| { |
| return this._timelineRuler; |
| } |
| |
| layout() |
| { |
| let startTime = this._startTime; |
| let endTime = this._endTime; |
| let currentTime = this._currentTime; |
| if (this._viewMode === WI.TimelineOverview.ViewMode.RenderingFrames) { |
| let renderingFramesTimeline = this._recording.timelines.get(WI.TimelineRecord.Type.RenderingFrame); |
| console.assert(renderingFramesTimeline, "Recoring missing rendering frames timeline"); |
| |
| startTime = 0; |
| endTime = renderingFramesTimeline.records.length; |
| currentTime = endTime; |
| } |
| |
| // Calculate the required width based on the duration and seconds per pixel. |
| let duration = endTime - startTime; |
| let newWidth = Math.ceil(duration / this.secondsPerPixel); |
| |
| // Update all relevant elements to the new required width. |
| this._updateElementWidth(this._scrollWidthSizer, newWidth); |
| |
| this._currentTimeMarker.time = currentTime; |
| |
| if (this._revealCurrentTime) { |
| this.revealMarker(this._currentTimeMarker); |
| this._revealCurrentTime = false; |
| } |
| |
| const visibleDuration = this.visibleDuration; |
| |
| // Clamp the scroll start time to match what the scroll bar would allow. |
| let scrollStartTime = Math.min(this.scrollStartTime, endTime - visibleDuration); |
| scrollStartTime = Math.max(startTime, scrollStartTime); |
| |
| this._timelineRuler.zeroTime = startTime; |
| this._timelineRuler.startTime = scrollStartTime; |
| this._timelineRuler.secondsPerPixel = this.secondsPerPixel; |
| |
| if (!this._dontUpdateScrollLeft) { |
| this._ignoreNextScrollEvent = true; |
| let scrollLeft = Math.ceil((scrollStartTime - startTime) / this.secondsPerPixel); |
| if (scrollLeft) |
| this._scrollContainerElement.scrollLeft = scrollLeft; |
| } |
| |
| for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { |
| if (overviewGraph.hidden) |
| continue; |
| |
| overviewGraph.zeroTime = startTime; |
| overviewGraph.startTime = scrollStartTime; |
| overviewGraph.currentTime = currentTime; |
| overviewGraph.endTime = scrollStartTime + visibleDuration; |
| } |
| } |
| |
| sizeDidChange() |
| { |
| this._cachedScrollContainerWidth = NaN; |
| } |
| |
| // Private |
| |
| _updateElementWidth(element, newWidth) |
| { |
| var currentWidth = parseInt(element.style.width); |
| if (currentWidth !== newWidth) |
| element.style.width = newWidth + "px"; |
| } |
| |
| _handleScrollEvent(event) |
| { |
| if (this._ignoreNextScrollEvent) { |
| this._ignoreNextScrollEvent = false; |
| return; |
| } |
| |
| this._dontUpdateScrollLeft = true; |
| |
| let scrollOffset = this._scrollContainerElement.scrollLeft; |
| if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) |
| this.scrollStartTime = this._startTime - (scrollOffset * this.secondsPerPixel); |
| else |
| this.scrollStartTime = this._startTime + (scrollOffset * this.secondsPerPixel); |
| |
| // Force layout so we can update with the scroll position synchronously. |
| this.updateLayoutIfNeeded(); |
| |
| this._dontUpdateScrollLeft = false; |
| |
| this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1); |
| } |
| |
| _handleWheelEvent(event) |
| { |
| // Ignore cloned events that come our way, we already handled the original. |
| if (event.__cloned) |
| return; |
| |
| // Ignore wheel events while handing gestures. |
| if (this._handlingGesture) |
| return; |
| |
| // Require twice the vertical delta to overcome horizontal scrolling. This prevents most |
| // cases of inadvertent zooming for slightly diagonal scrolls. |
| if (Math.abs(event.deltaX) >= Math.abs(event.deltaY) * 0.5) { |
| // Clone the event to dispatch it on the scroll container. Mark it as cloned so we don't get into a loop. |
| let newWheelEvent = new event.constructor(event.type, event); |
| newWheelEvent.__cloned = true; |
| |
| this._scrollContainerElement.dispatchEvent(newWheelEvent); |
| return; |
| } |
| |
| // Remember the mouse position in time. |
| let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft; |
| let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel); |
| let deviceDirection = event.webkitDirectionInvertedFromDevice ? 1 : -1; |
| let delta = event.deltaY * (this.secondsPerPixel / WI.TimelineOverview.ScrollDeltaDenominator) * deviceDirection; |
| |
| // Reset accumulated wheel delta when direction changes. |
| if (this._pixelAlignDuration && (delta < 0 && this._mouseWheelDelta >= 0 || delta >= 0 && this._mouseWheelDelta < 0)) |
| this._mouseWheelDelta = 0; |
| |
| let previousDurationPerPixel = this.secondsPerPixel; |
| this._mouseWheelDelta += delta; |
| this.secondsPerPixel += this._mouseWheelDelta; |
| |
| if (this.secondsPerPixel === this._currentSettings.minimumDurationPerPixel && delta < 0 || this.secondsPerPixel === this._currentSettings.maximumDurationPerPixel && delta >= 0) |
| this._mouseWheelDelta = 0; |
| else |
| this._mouseWheelDelta = previousDurationPerPixel + this._mouseWheelDelta - this.secondsPerPixel; |
| |
| // Center the zoom around the mouse based on the remembered mouse position time. |
| this.scrollStartTime = mousePositionTime - (mouseOffset * this.secondsPerPixel); |
| |
| this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleGestureStart(event) |
| { |
| if (this._handlingGesture) { |
| // FIXME: <https://webkit.org/b/151068> [Mac] Unexpected gesturestart events when already handling gesture |
| return; |
| } |
| |
| let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft; |
| let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel); |
| |
| this._handlingGesture = true; |
| this._gestureStartStartTime = mousePositionTime; |
| this._gestureStartDurationPerPixel = this.secondsPerPixel; |
| |
| this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleGestureChange(event) |
| { |
| // Cap zooming out at 5x. |
| let scale = Math.max(1 / 5, event.scale); |
| |
| let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft; |
| let newSecondsPerPixel = this._gestureStartDurationPerPixel / scale; |
| |
| this.secondsPerPixel = newSecondsPerPixel; |
| this.scrollStartTime = this._gestureStartStartTime - (mouseOffset * this.secondsPerPixel); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleGestureEnd(event) |
| { |
| this._handlingGesture = false; |
| this._gestureStartStartTime = NaN; |
| this._gestureStartDurationPerPixel = NaN; |
| } |
| |
| _instrumentAdded(instrumentOrEvent) |
| { |
| let instrument = instrumentOrEvent instanceof WI.Instrument ? instrumentOrEvent : instrumentOrEvent.data.instrument; |
| console.assert(instrument instanceof WI.Instrument, instrument); |
| |
| let timeline = this._recording.timelineForInstrument(instrument); |
| console.assert(!this._overviewGraphsByTypeMap.has(timeline.type), timeline); |
| console.assert(!this._treeElementsByTypeMap.has(timeline.type), timeline); |
| |
| let treeElement = new WI.TimelineTreeElement(timeline); |
| let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this)); |
| this._timelinesTreeOutline.insertChild(treeElement, insertionIndex); |
| this._treeElementsByTypeMap.set(timeline.type, treeElement); |
| |
| let overviewGraph = WI.TimelineOverviewGraph.createForTimeline(timeline, this); |
| overviewGraph.addEventListener(WI.TimelineOverviewGraph.Event.RecordSelected, this._handleOverviewGraphRecordSelected, this); |
| this._overviewGraphsByTypeMap.set(timeline.type, overviewGraph); |
| this._graphsContainerView.insertSubviewBefore(overviewGraph, this._graphsContainerView.subviews[insertionIndex]); |
| |
| treeElement.element.style.height = overviewGraph.height + "px"; |
| |
| if (!this._canShowTimelineType(timeline.type)) { |
| overviewGraph.hidden = true; |
| treeElement.hidden = true; |
| } |
| |
| this.needsLayout(); |
| } |
| |
| _instrumentRemoved(event) |
| { |
| let instrument = event.data.instrument; |
| console.assert(instrument instanceof WI.Instrument, instrument); |
| |
| let timeline = this._recording.timelineForInstrument(instrument); |
| let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type); |
| console.assert(overviewGraph, "Missing overview graph for timeline type", timeline.type); |
| |
| let treeElement = this._treeElementsByTypeMap.get(timeline.type); |
| let shouldSuppressOnDeselect = false; |
| let shouldSuppressSelectSibling = true; |
| this._timelinesTreeOutline.removeChild(treeElement, shouldSuppressOnDeselect, shouldSuppressSelectSibling); |
| |
| overviewGraph.removeEventListener(WI.TimelineOverviewGraph.Event.RecordSelected, this._handleOverviewGraphRecordSelected, this); |
| this._graphsContainerView.removeSubview(overviewGraph); |
| |
| this._overviewGraphsByTypeMap.delete(timeline.type); |
| this._treeElementsByTypeMap.delete(timeline.type); |
| } |
| |
| _markerAdded(event) |
| { |
| this._timelineRuler.addMarker(event.data.marker); |
| } |
| |
| _handleGraphsContainerClick(event) |
| { |
| // Set when a WI.TimelineRecordBar receives the "click" first and is about to be selected. |
| if (event.__timelineRecordClickEventHandled) |
| return; |
| |
| this._recordSelected(null, null); |
| } |
| |
| _timelineRulerMouseDown(event) |
| { |
| this._timelineRulerSelectionChanged = false; |
| } |
| |
| _timelineRulerMouseClicked(event) |
| { |
| if (this._timelineRulerSelectionChanged) |
| return; |
| |
| for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { |
| if (overviewGraph.hidden) |
| continue; |
| |
| let graphRect = overviewGraph.element.getBoundingClientRect(); |
| if (!(event.pageX >= graphRect.left && event.pageX <= graphRect.right && event.pageY >= graphRect.top && event.pageY <= graphRect.bottom)) |
| continue; |
| |
| // Clone the event to dispatch it on the overview graph element. |
| let newClickEvent = new event.constructor(event.type, event); |
| overviewGraph.element.dispatchEvent(newClickEvent); |
| return; |
| } |
| } |
| |
| _timeRangeSelectionChanged(event) |
| { |
| this._timelineRulerSelectionChanged = true; |
| |
| let startTime = this._viewMode === WI.TimelineOverview.ViewMode.Timelines ? this._startTime : 0; |
| this._currentSettings.selectionStartValueSetting.value = this.selectionStartTime - startTime; |
| this._currentSettings.selectionDurationSetting.value = this.selectionDuration; |
| |
| this.dispatchEventToListeners(WI.TimelineOverview.Event.TimeRangeSelectionChanged); |
| } |
| |
| _handleOverviewGraphRecordSelected(event) |
| { |
| let {record, recordBar} = event.data; |
| |
| // Ignore deselection events, as they are handled by the newly selected record's timeline. |
| if (!record) |
| return; |
| |
| this._recordSelected(record, recordBar); |
| } |
| |
| _recordSelected(record, recordBar) |
| { |
| if (record === this._selectedTimelineRecord) |
| return; |
| |
| if (this._selectedTimelineRecord && (!record || this._selectedTimelineRecord.type !== record.type)) { |
| let timelineOverviewGraph = this._overviewGraphsByTypeMap.get(this._selectedTimelineRecord.type); |
| console.assert(timelineOverviewGraph); |
| if (timelineOverviewGraph) |
| timelineOverviewGraph.selectedRecord = null; |
| } |
| |
| this._selectedTimelineRecord = record; |
| |
| if (this._selectedTimelineRecord) { |
| let firstRecord = this._selectedTimelineRecord; |
| let lastRecord = this._selectedTimelineRecord; |
| if (recordBar) { |
| firstRecord = recordBar.records[0]; |
| lastRecord = recordBar.records.lastValue; |
| } |
| |
| let startTime = firstRecord instanceof WI.RenderingFrameTimelineRecord ? firstRecord.frameIndex : firstRecord.startTime; |
| let endTime = lastRecord instanceof WI.RenderingFrameTimelineRecord ? lastRecord.frameIndex : lastRecord.endTime; |
| if (startTime < this.selectionStartTime || (endTime > this.selectionStartTime + this.selectionDuration) || firstRecord instanceof WI.CPUTimelineRecord) { |
| let selectionPadding = this.secondsPerPixel * 10; |
| this.selectionStartTime = startTime - selectionPadding; |
| this.selectionDuration = endTime - startTime + (selectionPadding * 2); |
| } |
| } |
| |
| this.dispatchEventToListeners(WI.TimelineOverview.Event.RecordSelected, {record: this._selectedTimelineRecord}); |
| } |
| |
| _resetSelection() |
| { |
| function reset(settings) |
| { |
| settings.durationPerPixelSetting.reset(); |
| settings.selectionStartValueSetting.reset(); |
| settings.selectionDurationSetting.reset(); |
| } |
| |
| reset(this._timelinesViewModeSettings); |
| if (this._renderingFramesViewModeSettings) |
| reset(this._renderingFramesViewModeSettings); |
| |
| this.secondsPerPixel = this._currentSettings.durationPerPixelSetting.value; |
| this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value; |
| this.selectionDuration = this._currentSettings.selectionDurationSetting.value; |
| } |
| |
| _recordingReset(event) |
| { |
| this._timelineRuler.clearMarkers(); |
| |
| this._timelineRuler.addMarker(this._currentTimeMarker); |
| } |
| |
| _canShowTimelineType(type) |
| { |
| let timelineViewMode = WI.TimelineOverview.ViewMode.Timelines; |
| if (type === WI.TimelineRecord.Type.RenderingFrame) |
| timelineViewMode = WI.TimelineOverview.ViewMode.RenderingFrames; |
| |
| return timelineViewMode === this._viewMode; |
| } |
| |
| _viewModeDidChange() |
| { |
| this._stopEditingInstruments(); |
| |
| let startTime = 0; |
| let isRenderingFramesMode = this._viewMode === WI.TimelineOverview.ViewMode.RenderingFrames; |
| if (isRenderingFramesMode) { |
| this._timelineRuler.minimumSelectionDuration = 1; |
| this._timelineRuler.snapInterval = 1; |
| this._timelineRuler.formatLabelCallback = (value) => value.maxDecimals(0).toLocaleString(); |
| } else { |
| this._timelineRuler.minimumSelectionDuration = 0.01; |
| this._timelineRuler.snapInterval = NaN; |
| this._timelineRuler.formatLabelCallback = null; |
| |
| startTime = this._startTime; |
| } |
| |
| this.pixelAlignDuration = isRenderingFramesMode; |
| this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value + startTime; |
| this.selectionDuration = this._currentSettings.selectionDurationSetting.value; |
| |
| for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) { |
| let treeElement = this._treeElementsByTypeMap.get(type); |
| console.assert(treeElement, "Missing tree element for timeline type", type); |
| |
| let hidden = !this._canShowTimelineType(type); |
| treeElement.hidden = hidden; |
| overviewGraph.hidden = hidden; |
| } |
| |
| this.element.classList.toggle("frames", isRenderingFramesMode); |
| |
| if (this.didInitialLayout) |
| this.updateLayout(WI.View.LayoutReason.Resize); |
| } |
| |
| _createViewModeSettings(viewMode, minimumDurationPerPixel, maximumDurationPerPixel, durationPerPixel, selectionStartValue, selectionDuration) |
| { |
| durationPerPixel = Math.min(maximumDurationPerPixel, Math.max(minimumDurationPerPixel, durationPerPixel)); |
| |
| let durationPerPixelSetting = new WI.Setting(viewMode + "-duration-per-pixel", durationPerPixel); |
| let selectionStartValueSetting = new WI.Setting(viewMode + "-selection-start-value", selectionStartValue); |
| let selectionDurationSetting = new WI.Setting(viewMode + "-selection-duration", selectionDuration); |
| |
| return { |
| scrollStartTime: 0, |
| minimumDurationPerPixel, |
| maximumDurationPerPixel, |
| durationPerPixelSetting, |
| selectionStartValueSetting, |
| selectionDurationSetting |
| }; |
| } |
| |
| get _currentSettings() |
| { |
| return this._viewMode === WI.TimelineOverview.ViewMode.Timelines ? this._timelinesViewModeSettings : this._renderingFramesViewModeSettings; |
| } |
| |
| _timelinesTreeSelectionDidChange(event) |
| { |
| let timeline = null; |
| let selectedTreeElement = this._timelinesTreeOutline.selectedTreeElement; |
| if (selectedTreeElement) { |
| timeline = selectedTreeElement.representedObject; |
| console.assert(timeline instanceof WI.Timeline, timeline); |
| console.assert(this._recording.timelines.get(timeline.type) === timeline, timeline); |
| |
| for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) |
| overviewGraph.selected = type === timeline.type; |
| } |
| |
| this._selectedTimeline = timeline; |
| this.dispatchEventToListeners(WI.TimelineOverview.Event.TimelineSelected); |
| } |
| |
| _toggleEditingInstruments(event) |
| { |
| if (this._editingInstruments) |
| this._stopEditingInstruments(); |
| else |
| this._startEditingInstruments(); |
| } |
| |
| _editingInstrumentsDidChange() |
| { |
| this.element.classList.toggle(WI.TimelineOverview.EditInstrumentsStyleClassName, this._editingInstruments); |
| this._timelineRuler.enabled = !this._editingInstruments; |
| |
| this._updateWheelAndGestureHandlers(); |
| this._updateEditInstrumentsButton(); |
| |
| this.dispatchEventToListeners(WI.TimelineOverview.Event.EditingInstrumentsDidChange); |
| } |
| |
| _updateEditInstrumentsButton() |
| { |
| let newLabel = this._editingInstruments ? WI.UIString("Done") : WI.UIString("Edit"); |
| this._editInstrumentsButton.label = newLabel; |
| this._editInstrumentsButton.activated = this._editingInstruments; |
| this._editInstrumentsButton.enabled = !WI.timelineManager.isCapturing(); |
| } |
| |
| _updateWheelAndGestureHandlers() |
| { |
| if (this._editingInstruments) { |
| this.element.removeEventListener("wheel", this._handleWheelEventListener); |
| this.element.removeEventListener("gesturestart", this._handleGestureStartEventListener); |
| this.element.removeEventListener("gesturechange", this._handleGestureChangeEventListener); |
| this.element.removeEventListener("gestureend", this._handleGestureEndEventListener); |
| this._handleWheelEventListener = null; |
| this._handleGestureStartEventListener = null; |
| this._handleGestureChangeEventListener = null; |
| this._handleGestureEndEventListener = null; |
| } else { |
| this._handleWheelEventListener = this._handleWheelEvent.bind(this); |
| this._handleGestureStartEventListener = this._handleGestureStart.bind(this); |
| this._handleGestureChangeEventListener = this._handleGestureChange.bind(this); |
| this._handleGestureEndEventListener = this._handleGestureEnd.bind(this); |
| this.element.addEventListener("wheel", this._handleWheelEventListener); |
| this.element.addEventListener("gesturestart", this._handleGestureStartEventListener); |
| this.element.addEventListener("gesturechange", this._handleGestureChangeEventListener); |
| this.element.addEventListener("gestureend", this._handleGestureEndEventListener); |
| } |
| } |
| |
| _startEditingInstruments() |
| { |
| console.assert(this._viewMode === WI.TimelineOverview.ViewMode.Timelines); |
| |
| if (this._editingInstruments) |
| return; |
| |
| this._editingInstruments = true; |
| |
| for (let type of this._instrumentTypes) { |
| let treeElement = this._treeElementsByTypeMap.get(type); |
| if (!treeElement) { |
| let timeline = this._recording.timelines.get(type); |
| console.assert(timeline, "Missing timeline for type " + type); |
| |
| const placeholder = true; |
| treeElement = new WI.TimelineTreeElement(timeline, placeholder); |
| |
| let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this)); |
| this._timelinesTreeOutline.insertChild(treeElement, insertionIndex); |
| |
| let placeholderGraph = new WI.View; |
| placeholderGraph.element.classList.add("timeline-overview-graph"); |
| treeElement[WI.TimelineOverview.PlaceholderOverviewGraph] = placeholderGraph; |
| this._graphsContainerView.insertSubviewBefore(placeholderGraph, this._graphsContainerView.subviews[insertionIndex]); |
| } |
| |
| treeElement.editing = true; |
| treeElement.addEventListener(WI.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this); |
| } |
| |
| this._editingInstrumentsDidChange(); |
| } |
| |
| _stopEditingInstruments() |
| { |
| if (!this._editingInstruments) |
| return; |
| |
| this._editingInstruments = false; |
| |
| let instruments = this._recording.instruments; |
| for (let treeElement of this._treeElementsByTypeMap.values()) { |
| if (treeElement.status.checked) { |
| treeElement.editing = false; |
| treeElement.removeEventListener(WI.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this); |
| continue; |
| } |
| |
| let timelineInstrument = instruments.find((instrument) => instrument.timelineRecordType === treeElement.representedObject.type); |
| this._recording.removeInstrument(timelineInstrument); |
| } |
| |
| let placeholderTreeElements = this._timelinesTreeOutline.children.filter((treeElement) => treeElement.placeholder); |
| for (let treeElement of placeholderTreeElements) { |
| this._timelinesTreeOutline.removeChild(treeElement); |
| |
| let placeholderGraph = treeElement[WI.TimelineOverview.PlaceholderOverviewGraph]; |
| console.assert(placeholderGraph); |
| this._graphsContainerView.removeSubview(placeholderGraph); |
| |
| if (treeElement.status.checked) { |
| let instrument = WI.Instrument.createForTimelineType(treeElement.representedObject.type); |
| this._recording.addInstrument(instrument); |
| } |
| } |
| |
| let instrumentTypes = instruments.map((instrument) => instrument.timelineRecordType); |
| WI.timelineManager.enabledTimelineTypes = instrumentTypes; |
| |
| this._editingInstrumentsDidChange(); |
| } |
| |
| _handleTimelineCapturingStateChanged(event) |
| { |
| switch (WI.timelineManager.capturingState) { |
| case WI.TimelineManager.CapturingState.Active: |
| this._editInstrumentsButton.enabled = false; |
| this._stopEditingInstruments(); |
| break; |
| |
| case WI.TimelineManager.CapturingState.Inactive: |
| this._editInstrumentsButton.enabled = true; |
| break; |
| } |
| } |
| |
| _recordingImported(event) |
| { |
| let {overviewData} = event.data; |
| |
| if (overviewData.secondsPerPixel !== undefined) |
| this.secondsPerPixel = overviewData.secondsPerPixel; |
| if (overviewData.scrollStartTime !== undefined) |
| this.scrollStartTime = overviewData.scrollStartTime; |
| if (overviewData.selectionStartTime !== undefined) |
| this.selectionStartTime = overviewData.selectionStartTime; |
| if (overviewData.selectionDuration !== undefined) { |
| if (overviewData.selectionDuration === Number.MAX_VALUE) |
| this._timelineRuler.selectEntireRange(); |
| else |
| this.selectionDuration = overviewData.selectionDuration; |
| } |
| if (overviewData.selectedTimelineType !== undefined) { |
| let timeline = this._recording.timelineForRecordType(overviewData.selectedTimelineType); |
| if (timeline) |
| this.selectedTimeline = timeline; |
| } |
| } |
| |
| _compareTimelineTreeElements(a, b) |
| { |
| let aTimelineType = a.representedObject.type; |
| let bTimelineType = b.representedObject.type; |
| |
| // Always sort the Rendering Frames timeline last. |
| if (aTimelineType === WI.TimelineRecord.Type.RenderingFrame) |
| return 1; |
| if (bTimelineType === WI.TimelineRecord.Type.RenderingFrame) |
| return -1; |
| |
| if (a.placeholder !== b.placeholder) |
| return a.placeholder ? 1 : -1; |
| |
| let aTimelineIndex = this._instrumentTypes.indexOf(aTimelineType); |
| let bTimelineIndex = this._instrumentTypes.indexOf(bTimelineType); |
| return aTimelineIndex - bTimelineIndex; |
| } |
| |
| _timelineTreeElementEnabledDidChange(event) |
| { |
| let enabled = this._timelinesTreeOutline.children.some((treeElement) => { |
| let timelineType = treeElement.representedObject.type; |
| return this._canShowTimelineType(timelineType) && treeElement.status.checked; |
| }); |
| |
| this._editInstrumentsButton.enabled = enabled; |
| } |
| }; |
| |
| WI.TimelineOverview.PlaceholderOverviewGraph = Symbol("placeholder-overview-graph"); |
| |
| WI.TimelineOverview.ScrollDeltaDenominator = 500; |
| WI.TimelineOverview.EditInstrumentsStyleClassName = "edit-instruments"; |
| WI.TimelineOverview.MinimumDurationPerPixel = 0.0001; |
| WI.TimelineOverview.MaximumDurationPerPixel = 60; |
| |
| WI.TimelineOverview.ViewMode = { |
| Timelines: "timeline-overview-view-mode-timelines", |
| RenderingFrames: "timeline-overview-view-mode-rendering-frames" |
| }; |
| |
| WI.TimelineOverview.Event = { |
| EditingInstrumentsDidChange: "editing-instruments-did-change", |
| RecordSelected: "timeline-overview-record-selected", |
| TimelineSelected: "timeline-overview-timeline-selected", |
| TimeRangeSelectionChanged: "timeline-overview-time-range-selection-changed" |
| }; |