/* * 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"), "Images/Pencil.svg", 16, 16); 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.TextNavigationItem("enabled-timelines", WI.UIString("Enabled Timelines", "Enabled Timelines @ Timelines Tab", "Label for column showing the list of enabled 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: [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" };