1e1efbc8f5e0788a2d38f48a3ea441bd4bcd85bc
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineOverview.js
1 /*
2  * Copyright (C) 2013, 2015-2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.TimelineOverview = class TimelineOverview extends WebInspector.View
27 {
28     constructor(timelineRecording, delegate)
29     {
30         super();
31
32         console.assert(timelineRecording instanceof WebInspector.TimelineRecording);
33
34         this._timelinesViewModeSettings = this._createViewModeSettings(WebInspector.TimelineOverview.ViewMode.Timelines, 0.0001, 60, 0.01, 0, 15);
35         this._instrumentTypes = WebInspector.TimelineManager.availableTimelineTypes();
36
37         if (WebInspector.FPSInstrument.supported()) {
38             let minimumDurationPerPixel = 1 / WebInspector.TimelineRecordFrame.MaximumWidthPixels;
39             let maximumDurationPerPixel = 1 / WebInspector.TimelineRecordFrame.MinimumWidthPixels;
40             this._renderingFramesViewModeSettings = this._createViewModeSettings(WebInspector.TimelineOverview.ViewMode.RenderingFrames, minimumDurationPerPixel, maximumDurationPerPixel, minimumDurationPerPixel, 0, 100);
41         }
42
43         this._recording = timelineRecording;
44         this._recording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
45         this._recording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);
46         this._recording.addEventListener(WebInspector.TimelineRecording.Event.MarkerAdded, this._markerAdded, this);
47         this._recording.addEventListener(WebInspector.TimelineRecording.Event.Reset, this._recordingReset, this);
48
49         this._delegate = delegate;
50
51         this.element.classList.add("timeline-overview");
52         this._updateWheelAndGestureHandlers();
53
54         this._graphsContainerView = new WebInspector.View;
55         this._graphsContainerView.element.classList.add("graphs-container");
56         this.addSubview(this._graphsContainerView);
57
58         this._overviewGraphsByTypeMap = new Map;
59
60         this._editInstrumentsButton = new WebInspector.ActivateButtonNavigationItem("toggle-edit-instruments", WebInspector.UIString("Edit configuration"), WebInspector.UIString("Save configuration"));
61         this._editInstrumentsButton.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._toggleEditingInstruments, this);
62         this._editingInstruments = false;
63         this._updateEditInstrumentsButton();
64
65         let instrumentsNavigationBar = new WebInspector.NavigationBar;
66         instrumentsNavigationBar.element.classList.add("timelines");
67         instrumentsNavigationBar.addNavigationItem(new WebInspector.FlexibleSpaceNavigationItem);
68         instrumentsNavigationBar.addNavigationItem(this._editInstrumentsButton);
69         this.addSubview(instrumentsNavigationBar);
70
71         this._timelinesTreeOutline = new WebInspector.TreeOutline;
72         this._timelinesTreeOutline.element.classList.add("timelines");
73         this._timelinesTreeOutline.disclosureButtons = false;
74         this._timelinesTreeOutline.large = true;
75         this._timelinesTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._timelinesTreeSelectionDidChange, this);
76         this.element.appendChild(this._timelinesTreeOutline.element);
77
78         this._treeElementsByTypeMap = new Map;
79
80         this._timelineRuler = new WebInspector.TimelineRuler;
81         this._timelineRuler.allowsClippedLabels = true;
82         this._timelineRuler.allowsTimeRangeSelection = true;
83         this._timelineRuler.element.addEventListener("mousedown", this._timelineRulerMouseDown.bind(this));
84         this._timelineRuler.element.addEventListener("click", this._timelineRulerMouseClicked.bind(this));
85         this._timelineRuler.addEventListener(WebInspector.TimelineRuler.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
86         this.addSubview(this._timelineRuler);
87
88         this._currentTimeMarker = new WebInspector.TimelineMarker(0, WebInspector.TimelineMarker.Type.CurrentTime);
89         this._timelineRuler.addMarker(this._currentTimeMarker);
90
91         this._scrollContainerElement = document.createElement("div");
92         this._scrollContainerElement.classList.add("scroll-container");
93         this._scrollContainerElement.addEventListener("scroll", this._handleScrollEvent.bind(this));
94         this.element.appendChild(this._scrollContainerElement);
95
96         this._scrollWidthSizer = document.createElement("div");
97         this._scrollWidthSizer.classList.add("scroll-width-sizer");
98         this._scrollContainerElement.appendChild(this._scrollWidthSizer);
99
100         this._startTime = 0;
101         this._currentTime = 0;
102         this._revealCurrentTime = false;
103         this._endTime = 0;
104         this._pixelAlignDuration = false;
105         this._mouseWheelDelta = 0;
106         this._cachedScrollContainerWidth = NaN;
107         this._timelineRulerSelectionChanged = false;
108         this._viewMode = WebInspector.TimelineOverview.ViewMode.Timelines;
109         this._selectedTimeline = null;;
110
111         for (let instrument of this._recording.instruments)
112             this._instrumentAdded(instrument);
113
114         if (!WebInspector.timelineManager.isCapturingPageReload())
115             this._resetSelection();
116
117         this._viewModeDidChange();
118
119         WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
120         WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
121     }
122
123     // Public
124
125     get selectedTimeline()
126     {
127         return this._selectedTimeline;
128     }
129
130     set selectedTimeline(x)
131     {
132         if (this._editingInstruments)
133             return;
134
135         if (this._selectedTimeline === x)
136             return;
137
138         this._selectedTimeline = x;
139         if (this._selectedTimeline) {
140             let treeElement = this._treeElementsByTypeMap.get(this._selectedTimeline.type);
141             console.assert(treeElement, "Missing tree element for timeline", this._selectedTimeline);
142
143             let omitFocus = true;
144             let wasSelectedByUser = false;
145             treeElement.select(omitFocus, wasSelectedByUser);
146         } else if (this._timelinesTreeOutline.selectedTreeElement)
147             this._timelinesTreeOutline.selectedTreeElement.deselect();
148     }
149
150     get editingInstruments()
151     {
152         return this._editingInstruments;
153     }
154
155     get viewMode()
156     {
157         return this._viewMode;
158     }
159
160     set viewMode(x)
161     {
162         if (this._editingInstruments)
163             return;
164
165         if (this._viewMode === x)
166             return;
167
168         this._viewMode = x;
169         this._viewModeDidChange();
170     }
171
172     get startTime()
173     {
174         return this._startTime;
175     }
176
177     set startTime(x)
178     {
179         x = x || 0;
180
181         if (this._startTime === x)
182             return;
183
184         if (this._viewMode !== WebInspector.TimelineOverview.ViewMode.RenderingFrames) {
185             let selectionOffset = this.selectionStartTime - this._startTime;
186             this.selectionStartTime = selectionOffset + x;
187         }
188
189         this._startTime = x;
190
191         this.needsLayout();
192     }
193
194     get currentTime()
195     {
196         return this._currentTime;
197     }
198
199     set currentTime(x)
200     {
201         x = x || 0;
202
203         if (this._currentTime === x)
204             return;
205
206         this._currentTime = x;
207         this._revealCurrentTime = true;
208
209         this.needsLayout();
210     }
211
212     get secondsPerPixel()
213     {
214         return this._currentSettings.durationPerPixelSetting.value;
215     }
216
217     set secondsPerPixel(x)
218     {
219         x = Math.min(this._currentSettings.maximumDurationPerPixel, Math.max(this._currentSettings.minimumDurationPerPixel, x));
220
221         if (this.secondsPerPixel === x)
222             return;
223
224         if (this._pixelAlignDuration) {
225             x = 1 / Math.round(1 / x);
226             if (this.secondsPerPixel === x)
227                 return;
228         }
229
230         this._currentSettings.durationPerPixelSetting.value = x;
231
232         this.needsLayout();
233     }
234
235     get pixelAlignDuration()
236     {
237         return this._pixelAlignDuration;
238     }
239
240     set pixelAlignDuration(x)
241     {
242         if (this._pixelAlignDuration === x)
243             return;
244
245         this._mouseWheelDelta = 0;
246         this._pixelAlignDuration = x;
247         if (this._pixelAlignDuration)
248             this.secondsPerPixel = 1 / Math.round(1 / this.secondsPerPixel);
249     }
250
251     get endTime()
252     {
253         return this._endTime;
254     }
255
256     set endTime(x)
257     {
258         x = x || 0;
259
260         if (this._endTime === x)
261             return;
262
263         this._endTime = x;
264
265         this.needsLayout();
266     }
267
268     get scrollStartTime()
269     {
270         return this._currentSettings.scrollStartTime;
271     }
272
273     set scrollStartTime(x)
274     {
275         x = x || 0;
276
277         if (this.scrollStartTime === x)
278             return;
279
280         this._currentSettings.scrollStartTime = x;
281
282         this.needsLayout();
283     }
284
285     get scrollContainerWidth()
286     {
287         return this._cachedScrollContainerWidth;
288     }
289
290     get visibleDuration()
291     {
292         if (isNaN(this._cachedScrollContainerWidth)) {
293             this._cachedScrollContainerWidth = this._scrollContainerElement.offsetWidth;
294             if (!this._cachedScrollContainerWidth)
295                 this._cachedScrollContainerWidth = NaN;
296         }
297
298         return this._cachedScrollContainerWidth * this.secondsPerPixel;
299     }
300
301     get selectionStartTime()
302     {
303         return this._timelineRuler.selectionStartTime;
304     }
305
306     set selectionStartTime(x)
307     {
308         x = x || 0;
309
310         if (this._timelineRuler.selectionStartTime === x)
311             return;
312
313         let selectionDuration = this.selectionDuration;
314         this._timelineRuler.selectionStartTime = x;
315         this._timelineRuler.selectionEndTime = x + selectionDuration;
316     }
317
318     get selectionDuration()
319     {
320         return this._timelineRuler.selectionEndTime - this._timelineRuler.selectionStartTime;
321     }
322
323     set selectionDuration(x)
324     {
325         x = Math.max(this._timelineRuler.minimumSelectionDuration, x);
326
327         this._timelineRuler.selectionEndTime = this._timelineRuler.selectionStartTime + x;
328     }
329
330     get height()
331     {
332         let height = 0;
333         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
334             if (overviewGraph.visible)
335                 height += overviewGraph.height;
336         }
337         return height;
338     }
339
340     get visible()
341     {
342         return this._visible;
343     }
344
345     shown()
346     {
347         this._visible = true;
348
349         for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) {
350             if (this._canShowTimelineType(type))
351                 overviewGraph.shown();
352         }
353
354         this.updateLayout(WebInspector.View.LayoutReason.Resize);
355     }
356
357     hidden()
358     {
359         this._visible = false;
360
361         for (let overviewGraph of this._overviewGraphsByTypeMap.values())
362             overviewGraph.hidden();
363     }
364
365     reset()
366     {
367         for (let overviewGraph of this._overviewGraphsByTypeMap.values())
368             overviewGraph.reset();
369
370         this._mouseWheelDelta = 0;
371
372         this._resetSelection();
373     }
374
375     revealMarker(marker)
376     {
377         this.scrollStartTime = marker.time - (this.visibleDuration / 2);
378     }
379
380     recordWasFiltered(timeline, record, filtered)
381     {
382         let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
383         console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type);
384         if (!overviewGraph)
385             return;
386
387         console.assert(overviewGraph.visible, "Record filtered in hidden overview graph", record);
388
389         overviewGraph.recordWasFiltered(record, filtered);
390     }
391
392     selectRecord(timeline, record)
393     {
394         let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
395         console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type);
396         if (!overviewGraph)
397             return;
398
399         console.assert(overviewGraph.visible, "Record selected in hidden overview graph", record);
400
401         overviewGraph.selectedRecord = record;
402     }
403
404     userSelectedRecord(record)
405     {
406         if (this._delegate && this._delegate.timelineOverviewUserSelectedRecord)
407             this._delegate.timelineOverviewUserSelectedRecord(this, record);
408     }
409
410     updateLayoutIfNeeded()
411     {
412         if (this.layoutPending) {
413             super.updateLayoutIfNeeded();
414             return;
415         }
416
417         this._timelineRuler.updateLayoutIfNeeded();
418
419         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
420             if (overviewGraph.visible)
421                 overviewGraph.updateLayoutIfNeeded();
422         }
423     }
424
425     // Protected
426
427     get timelineRuler()
428     {
429         return this._timelineRuler;
430     }
431
432     layout()
433     {
434         let startTime = this._startTime;
435         let endTime = this._endTime;
436         let currentTime = this._currentTime;
437         if (this._viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames) {
438             let renderingFramesTimeline = this._recording.timelines.get(WebInspector.TimelineRecord.Type.RenderingFrame);
439             console.assert(renderingFramesTimeline, "Recoring missing rendering frames timeline");
440
441             startTime = 0;
442             endTime = renderingFramesTimeline.records.length;
443             currentTime = endTime;
444         }
445
446         // Calculate the required width based on the duration and seconds per pixel.
447         let duration = endTime - startTime;
448         let newWidth = Math.ceil(duration / this.secondsPerPixel);
449
450         // Update all relevant elements to the new required width.
451         this._updateElementWidth(this._scrollWidthSizer, newWidth);
452
453         this._currentTimeMarker.time = currentTime;
454
455         if (this._revealCurrentTime) {
456             this.revealMarker(this._currentTimeMarker);
457             this._revealCurrentTime = false;
458         }
459
460         const visibleDuration = this.visibleDuration;
461
462         // Clamp the scroll start time to match what the scroll bar would allow.
463         let scrollStartTime = Math.min(this.scrollStartTime, endTime - visibleDuration);
464         scrollStartTime = Math.max(startTime, scrollStartTime);
465
466         this._timelineRuler.zeroTime = startTime;
467         this._timelineRuler.startTime = scrollStartTime;
468         this._timelineRuler.secondsPerPixel = this.secondsPerPixel;
469
470         if (!this._dontUpdateScrollLeft) {
471             this._ignoreNextScrollEvent = true;
472             let scrollLeft = Math.ceil((scrollStartTime - startTime) / this.secondsPerPixel);
473             if (scrollLeft)
474                 this._scrollContainerElement.scrollLeft = scrollLeft;
475         }
476
477         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
478             if (!overviewGraph.visible)
479                 continue;
480
481             overviewGraph.zeroTime = startTime;
482             overviewGraph.startTime = scrollStartTime;
483             overviewGraph.currentTime = currentTime;
484             overviewGraph.endTime = scrollStartTime + visibleDuration;
485         }
486     }
487
488     sizeDidChange()
489     {
490         this._cachedScrollContainerWidth = NaN;
491     }
492
493     // Private
494
495     _updateElementWidth(element, newWidth)
496     {
497         var currentWidth = parseInt(element.style.width);
498         if (currentWidth !== newWidth)
499             element.style.width = newWidth + "px";
500     }
501
502     _handleScrollEvent(event)
503     {
504         if (this._ignoreNextScrollEvent) {
505             this._ignoreNextScrollEvent = false;
506             return;
507         }
508
509         this._dontUpdateScrollLeft = true;
510
511         let scrollOffset = this._scrollContainerElement.scrollLeft;
512         this.scrollStartTime = this._startTime + (scrollOffset * this.secondsPerPixel);
513
514         // Force layout so we can update with the scroll position synchronously.
515         this.updateLayoutIfNeeded();
516
517         this._dontUpdateScrollLeft = false;
518     }
519
520     _handleWheelEvent(event)
521     {
522         // Ignore cloned events that come our way, we already handled the original.
523         if (event.__cloned)
524             return;
525
526         // Ignore wheel events while handing gestures.
527         if (this._handlingGesture)
528             return;
529
530         // Require twice the vertical delta to overcome horizontal scrolling. This prevents most
531         // cases of inadvertent zooming for slightly diagonal scrolls.
532         if (Math.abs(event.deltaX) >= Math.abs(event.deltaY) * 0.5) {
533             // Clone the event to dispatch it on the scroll container. Mark it as cloned so we don't get into a loop.
534             let newWheelEvent = new event.constructor(event.type, event);
535             newWheelEvent.__cloned = true;
536
537             this._scrollContainerElement.dispatchEvent(newWheelEvent);
538             return;
539         }
540
541         // Remember the mouse position in time.
542         let mouseOffset = event.pageX - this.element.totalOffsetLeft;
543         let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel);
544         let deviceDirection = event.webkitDirectionInvertedFromDevice ? 1 : -1;
545         let delta = event.deltaY * (this.secondsPerPixel / WebInspector.TimelineOverview.ScrollDeltaDenominator) * deviceDirection;
546
547         // Reset accumulated wheel delta when direction changes.
548         if (this._pixelAlignDuration && (delta < 0 && this._mouseWheelDelta >= 0 || delta >= 0 && this._mouseWheelDelta < 0))
549             this._mouseWheelDelta = 0;
550
551         let previousDurationPerPixel = this.secondsPerPixel;
552         this._mouseWheelDelta += delta;
553         this.secondsPerPixel += this._mouseWheelDelta;
554
555         if (this.secondsPerPixel === this._currentSettings.minimumDurationPerPixel && delta < 0 || this.secondsPerPixel === this._currentSettings.maximumDurationPerPixel && delta >= 0)
556             this._mouseWheelDelta = 0;
557         else
558             this._mouseWheelDelta = previousDurationPerPixel + this._mouseWheelDelta - this.secondsPerPixel;
559
560         // Center the zoom around the mouse based on the remembered mouse position time.
561         this.scrollStartTime = mousePositionTime - (mouseOffset * this.secondsPerPixel);
562
563         event.preventDefault();
564         event.stopPropagation();
565     }
566
567     _handleGestureStart(event)
568     {
569         if (this._handlingGesture) {
570             // FIXME: <https://webkit.org/b/151068> [Mac] Unexpected gesturestart events when already handling gesture
571             return;
572         }
573
574         let mouseOffset = event.pageX - this.element.totalOffsetLeft;
575         let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel);
576
577         this._handlingGesture = true;
578         this._gestureStartStartTime = mousePositionTime;
579         this._gestureStartDurationPerPixel = this.secondsPerPixel;
580
581         event.preventDefault();
582         event.stopPropagation();
583     }
584
585     _handleGestureChange(event)
586     {
587         // Cap zooming out at 5x.
588         let scale = Math.max(1/5, event.scale);
589
590         let mouseOffset = event.pageX - this.element.totalOffsetLeft;
591         let newSecondsPerPixel = this._gestureStartDurationPerPixel / scale;
592
593         this.secondsPerPixel = newSecondsPerPixel;
594         this.scrollStartTime = this._gestureStartStartTime - (mouseOffset * this.secondsPerPixel);
595
596         event.preventDefault();
597         event.stopPropagation();
598     }
599
600     _handleGestureEnd(event)
601     {
602         this._handlingGesture = false;
603         this._gestureStartStartTime = NaN;
604         this._gestureStartDurationPerPixel = NaN;
605     }
606
607     _instrumentAdded(instrumentOrEvent)
608     {
609         let instrument = instrumentOrEvent instanceof WebInspector.Instrument ? instrumentOrEvent : instrumentOrEvent.data.instrument;
610         console.assert(instrument instanceof WebInspector.Instrument, instrument);
611
612         let timeline = this._recording.timelineForInstrument(instrument);
613         console.assert(!this._overviewGraphsByTypeMap.has(timeline.type), timeline);
614         console.assert(!this._treeElementsByTypeMap.has(timeline.type), timeline);
615
616         let treeElement = new WebInspector.TimelineTreeElement(timeline);
617         let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this));
618         this._timelinesTreeOutline.insertChild(treeElement, insertionIndex);
619         this._treeElementsByTypeMap.set(timeline.type, treeElement);
620
621         let overviewGraph = WebInspector.TimelineOverviewGraph.createForTimeline(timeline, this);
622         overviewGraph.addEventListener(WebInspector.TimelineOverviewGraph.Event.RecordSelected, this._recordSelected, this);
623         this._overviewGraphsByTypeMap.set(timeline.type, overviewGraph);
624         this._graphsContainerView.insertSubviewBefore(overviewGraph, this._graphsContainerView.subviews[insertionIndex]);
625
626         treeElement.element.style.height = overviewGraph.height + "px";
627
628         if (!this._canShowTimelineType(timeline.type)) {
629             overviewGraph.hidden();
630             treeElement.hidden = true;
631         }
632     }
633
634     _instrumentRemoved(event)
635     {
636         let instrument = event.data.instrument;
637         console.assert(instrument instanceof WebInspector.Instrument, instrument);
638
639         let timeline = this._recording.timelineForInstrument(instrument);
640         let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
641         console.assert(overviewGraph, "Missing overview graph for timeline type", timeline.type);
642
643         let treeElement = this._treeElementsByTypeMap.get(timeline.type);
644         let shouldSuppressOnDeselect = false;
645         let shouldSuppressSelectSibling = true;
646         this._timelinesTreeOutline.removeChild(treeElement, shouldSuppressOnDeselect, shouldSuppressSelectSibling);
647
648         overviewGraph.removeEventListener(WebInspector.TimelineOverviewGraph.Event.RecordSelected, this._recordSelected, this);
649         this._graphsContainerView.removeSubview(overviewGraph);
650
651         this._overviewGraphsByTypeMap.delete(timeline.type);
652         this._treeElementsByTypeMap.delete(timeline.type);
653     }
654
655     _markerAdded(event)
656     {
657         this._timelineRuler.addMarker(event.data.marker);
658     }
659
660     _timelineRulerMouseDown(event)
661     {
662         this._timelineRulerSelectionChanged = false;
663     }
664
665     _timelineRulerMouseClicked(event)
666     {
667         if (this._timelineRulerSelectionChanged)
668             return;
669
670         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
671             let graphRect = overviewGraph.element.getBoundingClientRect();
672             if (!(event.pageX >= graphRect.left && event.pageX <= graphRect.right && event.pageY >= graphRect.top && event.pageY <= graphRect.bottom))
673                 continue;
674
675             // Clone the event to dispatch it on the overview graph element.
676             let newClickEvent = new event.constructor(event.type, event);
677             overviewGraph.element.dispatchEvent(newClickEvent);
678             return;
679         }
680     }
681
682     _timeRangeSelectionChanged(event)
683     {
684         this._timelineRulerSelectionChanged = true;
685
686         let startTime = this._viewMode === WebInspector.TimelineOverview.ViewMode.Timelines ? this._startTime : 0;
687         this._currentSettings.selectionStartValueSetting.value = this.selectionStartTime - startTime;
688         this._currentSettings.selectionDurationSetting.value = this.selectionDuration;
689
690         this.dispatchEventToListeners(WebInspector.TimelineOverview.Event.TimeRangeSelectionChanged);
691     }
692
693     _recordSelected(event)
694     {
695         for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) {
696             if (overviewGraph !== event.target)
697                 continue;
698
699             let timeline = this._recording.timelines.get(type);
700             console.assert(timeline, "Timeline recording missing timeline type", type);
701             this.dispatchEventToListeners(WebInspector.TimelineOverview.Event.RecordSelected, {timeline, record: event.data.record});
702             return;
703         }
704     }
705
706     _resetSelection()
707     {
708         function reset(settings)
709         {
710             settings.durationPerPixelSetting.reset();
711             settings.selectionStartValueSetting.reset();
712             settings.selectionDurationSetting.reset();
713         }
714
715         reset(this._timelinesViewModeSettings);
716         if (this._renderingFramesViewModeSettings)
717             reset(this._renderingFramesViewModeSettings);
718
719         this.secondsPerPixel = this._currentSettings.durationPerPixelSetting.value;
720         this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value;
721         this.selectionDuration = this._currentSettings.selectionDurationSetting.value;
722     }
723
724     _recordingReset(event)
725     {
726         this._timelineRuler.clearMarkers();
727         this._timelineRuler.addMarker(this._currentTimeMarker);
728     }
729
730     _canShowTimelineType(type)
731     {
732         let timelineViewMode = WebInspector.TimelineOverview.ViewMode.Timelines;
733         if (type === WebInspector.TimelineRecord.Type.RenderingFrame)
734             timelineViewMode = WebInspector.TimelineOverview.ViewMode.RenderingFrames;
735
736         return timelineViewMode === this._viewMode;
737     }
738
739     _viewModeDidChange()
740     {
741         let startTime = 0;
742         let isRenderingFramesMode = this._viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames;
743         if (isRenderingFramesMode) {
744             this._timelineRuler.minimumSelectionDuration = 1;
745             this._timelineRuler.snapInterval = 1;
746             this._timelineRuler.formatLabelCallback = (value) => value.toFixed(0);
747         } else {
748             this._timelineRuler.minimumSelectionDuration = 0.01;
749             this._timelineRuler.snapInterval = NaN;
750             this._timelineRuler.formatLabelCallback = null;
751
752             startTime = this._startTime;
753         }
754
755         this.pixelAlignDuration = isRenderingFramesMode;
756         this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value + startTime;
757         this.selectionDuration = this._currentSettings.selectionDurationSetting.value;
758
759         for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) {
760             let treeElement = this._treeElementsByTypeMap.get(type);
761             console.assert(treeElement, "Missing tree element for timeline type", type);
762
763             treeElement.hidden = !this._canShowTimelineType(type);
764             if (treeElement.hidden)
765                 overviewGraph.hidden();
766             else
767                 overviewGraph.shown();
768         }
769
770         this.element.classList.toggle("frames", isRenderingFramesMode);
771
772         this.updateLayout(WebInspector.View.LayoutReason.Resize);
773     }
774
775     _createViewModeSettings(viewMode, minimumDurationPerPixel, maximumDurationPerPixel, durationPerPixel, selectionStartValue, selectionDuration)
776     {
777         durationPerPixel = Math.min(maximumDurationPerPixel, Math.max(minimumDurationPerPixel, durationPerPixel));
778
779         let durationPerPixelSetting = new WebInspector.Setting(viewMode + "-duration-per-pixel", durationPerPixel);
780         let selectionStartValueSetting = new WebInspector.Setting(viewMode + "-selection-start-value", selectionStartValue);
781         let selectionDurationSetting = new WebInspector.Setting(viewMode + "-selection-duration", selectionDuration);
782
783         return {
784             scrollStartTime: 0,
785             minimumDurationPerPixel,
786             maximumDurationPerPixel,
787             durationPerPixelSetting,
788             selectionStartValueSetting,
789             selectionDurationSetting
790         };
791     }
792
793     get _currentSettings()
794     {
795         return this._viewMode === WebInspector.TimelineOverview.ViewMode.Timelines ? this._timelinesViewModeSettings : this._renderingFramesViewModeSettings;
796     }
797
798     _timelinesTreeSelectionDidChange(event)
799     {
800         function updateGraphSelectedState(timeline, selected)
801         {
802             let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
803             console.assert(overviewGraph, "Missing overview graph for timeline", timeline);
804             overviewGraph.selected = selected;
805         }
806
807         let selectedTreeElement = event.data.selectedElement;
808         let deselectedTreeElement = event.data.deselectedElement;
809         let timeline = null;
810         if (selectedTreeElement) {
811             timeline = selectedTreeElement.representedObject;
812             console.assert(timeline instanceof WebInspector.Timeline, timeline);
813             console.assert(this._recording.timelines.get(timeline.type) === timeline, timeline);
814
815             updateGraphSelectedState.call(this, timeline, true);
816         }
817
818         if (deselectedTreeElement)
819             updateGraphSelectedState.call(this, deselectedTreeElement.representedObject, false);
820
821         this._selectedTimeline = timeline;
822         this.dispatchEventToListeners(WebInspector.TimelineOverview.Event.TimelineSelected);
823     }
824
825     _toggleEditingInstruments(event)
826     {
827         if (this._editingInstruments)
828             this._stopEditingInstruments();
829         else
830             this._startEditingInstruments();
831     }
832
833     _editingInstrumentsDidChange()
834     {
835         this.element.classList.toggle(WebInspector.TimelineOverview.EditInstrumentsStyleClassName, this._editingInstruments);
836         this._timelineRuler.enabled = !this._editingInstruments;
837
838         this._updateWheelAndGestureHandlers();
839         this._updateEditInstrumentsButton();
840
841         this.dispatchEventToListeners(WebInspector.TimelineOverview.Event.EditingInstrumentsDidChange);
842     }
843
844     _updateEditInstrumentsButton()
845     {
846         let newLabel = this._editingInstruments ? WebInspector.UIString("Done") : WebInspector.UIString("Edit");
847         this._editInstrumentsButton.label = newLabel;
848         this._editInstrumentsButton.activated = this._editingInstruments;
849         this._editInstrumentsButton.enabled = !WebInspector.timelineManager.isCapturing();
850     }
851
852     _updateWheelAndGestureHandlers()
853     {
854         if (this._editingInstruments) {
855             this.element.removeEventListener("wheel", this._handleWheelEventListener);
856             this.element.removeEventListener("gesturestart", this._handleGestureStartEventListener);
857             this.element.removeEventListener("gesturechange", this._handleGestureChangeEventListener);
858             this.element.removeEventListener("gestureend", this._handleGestureEndEventListener);
859             this._handleWheelEventListener = null;
860             this._handleGestureStartEventListener = null;
861             this._handleGestureChangeEventListener = null;
862             this._handleGestureEndEventListener = null;
863         } else {
864             this._handleWheelEventListener = this._handleWheelEvent.bind(this);
865             this._handleGestureStartEventListener = this._handleGestureStart.bind(this);
866             this._handleGestureChangeEventListener = this._handleGestureChange.bind(this);
867             this._handleGestureEndEventListener = this._handleGestureEnd.bind(this);
868             this.element.addEventListener("wheel", this._handleWheelEventListener);
869             this.element.addEventListener("gesturestart", this._handleGestureStartEventListener);
870             this.element.addEventListener("gesturechange", this._handleGestureChangeEventListener);
871             this.element.addEventListener("gestureend", this._handleGestureEndEventListener);
872         }
873     }
874
875     _startEditingInstruments()
876     {
877         console.assert(this._viewMode === WebInspector.TimelineOverview.ViewMode.Timelines);
878
879         if (this._editingInstruments)
880             return;
881
882         this._editingInstruments = true;
883
884         for (let type of this._instrumentTypes) {
885             let treeElement = this._treeElementsByTypeMap.get(type);
886             if (!treeElement) {
887                 let timeline = this._recording.timelines.get(type);
888                 console.assert(timeline, "Missing timeline for type " + type);
889
890                 const placeholder = true;
891                 treeElement = new WebInspector.TimelineTreeElement(timeline, placeholder);
892
893                 let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this));
894                 this._timelinesTreeOutline.insertChild(treeElement, insertionIndex);
895             }
896
897             treeElement.editing = true;
898             treeElement.addEventListener(WebInspector.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this);
899         }
900
901         this._editingInstrumentsDidChange();
902     }
903
904     _stopEditingInstruments()
905     {
906         if (!this._editingInstruments)
907             return;
908
909         this._editingInstruments = false;
910
911         let instruments = this._recording.instruments;
912         for (let treeElement of this._treeElementsByTypeMap.values()) {
913             if (treeElement.status.checked) {
914                 treeElement.editing = false;
915                 treeElement.removeEventListener(WebInspector.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this);
916                 continue;
917             }
918
919             let timelineInstrument = instruments.find((instrument) => instrument.timelineRecordType === treeElement.representedObject.type);
920             this._recording.removeInstrument(timelineInstrument);
921         }
922
923         let placeholderTreeElements = this._timelinesTreeOutline.children.filter((treeElement) => treeElement.placeholder);
924         for (let treeElement of placeholderTreeElements) {
925             this._timelinesTreeOutline.removeChild(treeElement);
926
927             if (treeElement.status.checked) {
928                 let instrument = WebInspector.Instrument.createForTimelineType(treeElement.representedObject.type);
929                 this._recording.addInstrument(instrument);
930             }
931         }
932
933         let instrumentTypes = instruments.map((instrument) => instrument.timelineRecordType);
934         WebInspector.timelineManager.enabledTimelineTypes = instrumentTypes;
935
936         this._editingInstrumentsDidChange();
937     }
938
939     _capturingStarted()
940     {
941         this._editInstrumentsButton.enabled = false;
942         this._stopEditingInstruments();
943     }
944
945     _capturingStopped()
946     {
947         this._editInstrumentsButton.enabled = true;
948     }
949
950     _compareTimelineTreeElements(a, b)
951     {
952         let aTimelineType = a.representedObject.type;
953         let bTimelineType = b.representedObject.type;
954
955         // Always sort the Rendering Frames timeline last.
956         if (aTimelineType === WebInspector.TimelineRecord.Type.RenderingFrame)
957             return 1;
958         if (bTimelineType === WebInspector.TimelineRecord.Type.RenderingFrame)
959             return -1;
960
961         if (a.placeholder !== b.placeholder)
962             return a.placeholder ? 1 : -1;
963
964         let aTimelineIndex = this._instrumentTypes.indexOf(aTimelineType);
965         let bTimelineIndex = this._instrumentTypes.indexOf(bTimelineType);
966         return aTimelineIndex - bTimelineIndex;
967     }
968
969     _timelineTreeElementEnabledDidChange(event)
970     {
971         let enabled = this._timelinesTreeOutline.children.some((treeElement) => {
972             let timelineType = treeElement.representedObject.type;
973             return this._canShowTimelineType(timelineType) && treeElement.status.checked;
974         });
975
976         this._editInstrumentsButton.enabled = enabled;
977     }
978 };
979
980 WebInspector.TimelineOverview.ScrollDeltaDenominator = 500;
981 WebInspector.TimelineOverview.EditInstrumentsStyleClassName = "edit-instruments";
982
983 WebInspector.TimelineOverview.ViewMode = {
984     Timelines: "timeline-overview-view-mode-timelines",
985     RenderingFrames: "timeline-overview-view-mode-rendering-frames"
986 };
987
988 WebInspector.TimelineOverview.Event = {
989     EditingInstrumentsDidChange: "editing-instruments-did-change",
990     RecordSelected: "timeline-overview-record-selected",
991     TimelineSelected: "timeline-overview-timeline-selected",
992     TimeRangeSelectionChanged: "timeline-overview-time-range-selection-changed"
993 };