1/*
2 * Copyright (C) 2013 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
26WebInspector.OverviewTimelineView = function(recording)
27{
28    WebInspector.TimelineView.call(this);
29
30    this._recording = recording;
31
32    this.navigationSidebarTreeOutline.onselect = this._treeElementSelected.bind(this);
33    this.navigationSidebarTreeOutline.ondeselect = this._treeElementDeselected.bind(this);
34
35    var columns = {"graph": {width: "100%"}};
36
37    this._dataGrid = new WebInspector.DataGrid(columns);
38    this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridNodeSelected, this);
39    this._dataGrid.element.classList.add("no-header");
40
41    this._treeOutlineDataGridSynchronizer = new WebInspector.TreeOutlineDataGridSynchronizer(this._contentTreeOutline, this._dataGrid);
42
43    this._timelineRuler = new WebInspector.TimelineRuler;
44    this._timelineRuler.allowsClippedLabels = true;
45    this.element.appendChild(this._timelineRuler.element);
46
47    this._currentTimeMarker = new WebInspector.TimelineMarker(0, WebInspector.TimelineMarker.Type.CurrentTime);
48    this._timelineRuler.addMarker(this._currentTimeMarker);
49
50    this.element.classList.add(WebInspector.OverviewTimelineView.StyleClassName);
51    this.element.appendChild(this._dataGrid.element);
52
53    this._networkTimeline = recording.timelines.get(WebInspector.TimelineRecord.Type.Network);
54    this._networkTimeline.addEventListener(WebInspector.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
55
56    recording.addEventListener(WebInspector.TimelineRecording.Event.SourceCodeTimelineAdded, this._sourceCodeTimelineAdded, this);
57
58    this._pendingRepresentedObjects = [];
59};
60
61WebInspector.OverviewTimelineView.StyleClassName = "overview";
62
63WebInspector.OverviewTimelineView.prototype = {
64    constructor: WebInspector.OverviewTimelineView,
65    __proto__: WebInspector.TimelineView.prototype,
66
67    // Public
68
69    get navigationSidebarTreeOutlineLabel()
70    {
71        return WebInspector.UIString("Timeline Events");
72    },
73
74    get secondsPerPixel()
75    {
76        return this._timelineRuler.secondsPerPixel;
77    },
78
79    shown: function()
80    {
81        WebInspector.TimelineView.prototype.shown.call(this);
82
83        this._treeOutlineDataGridSynchronizer.synchronize();
84    },
85
86    updateLayout: function()
87    {
88        WebInspector.TimelineView.prototype.updateLayout.call(this);
89
90        var oldZeroTime = this._timelineRuler.zeroTime;
91        var oldStartTime = this._timelineRuler.startTime;
92        var oldEndTime = this._timelineRuler.endTime;
93        var oldCurrentTime = this._currentTimeMarker.time;
94
95        this._timelineRuler.zeroTime = this.zeroTime;
96        this._timelineRuler.startTime = this.startTime;
97        this._timelineRuler.endTime = this.endTime;
98        this._currentTimeMarker.time = this.currentTime;
99
100        // The TimelineDataGridNode graphs are positioned with percentages, so they auto resize with the view.
101        // We only need to refresh the graphs when the any of the times change.
102        if (this.zeroTime !== oldZeroTime || this.startTime !== oldStartTime || this.endTime !== oldEndTime || this.currentTime !== oldCurrentTime) {
103            var dataGridNode = this._dataGrid.children[0];
104            while (dataGridNode) {
105                dataGridNode.refreshGraph();
106                dataGridNode = dataGridNode.traverseNextNode(true, null, true);
107            }
108        }
109
110        if (this.currentTime !== oldCurrentTime) {
111            var selectedTreeElement = this.navigationSidebarTreeOutline.selectedTreeElement;
112            var selectionWasHidden = selectedTreeElement && selectedTreeElement.hidden;
113
114            // Check the filters again since the current time change might have revealed this node. Start and end time changes are handled by TimelineContentView.
115            WebInspector.timelineSidebarPanel.updateFilter();
116
117            if (selectedTreeElement && selectedTreeElement.hidden !== selectionWasHidden)
118                this.dispatchEventToListeners(WebInspector.TimelineView.Event.SelectionPathComponentsDidChange);
119        }
120
121        this._timelineRuler.updateLayout();
122
123        this._processPendingRepresentedObjects();
124    },
125
126    get selectionPathComponents()
127    {
128        var dataGridNode = this._dataGrid.selectedNode;
129        if (!dataGridNode)
130            return null;
131
132        var pathComponents = [];
133
134        while (dataGridNode && !dataGridNode.root) {
135            var treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
136            console.assert(treeElement);
137            if (!treeElement)
138                break;
139
140            if (treeElement.hidden)
141                return null;
142
143            var pathComponent = new WebInspector.GeneralTreeElementPathComponent(treeElement);
144            pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this.treeElementPathComponentSelected, this);
145            pathComponents.unshift(pathComponent);
146            dataGridNode = dataGridNode.parent;
147        }
148
149        return pathComponents;
150    },
151
152    // Protected
153
154    treeElementPathComponentSelected: function(event)
155    {
156        var dataGridNode = this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(event.data.pathComponent.generalTreeElement);
157        if (!dataGridNode)
158            return;
159        dataGridNode.revealAndSelect();
160    },
161
162    // Private
163
164    _compareTreeElementsByDetails: function(a, b)
165    {
166        if (a instanceof WebInspector.SourceCodeTimelineTreeElement && b instanceof WebInspector.ResourceTreeElement)
167            return -1;
168
169        if (a instanceof WebInspector.ResourceTreeElement && b instanceof WebInspector.SourceCodeTimelineTreeElement)
170            return 1;
171
172        if (a instanceof WebInspector.SourceCodeTimelineTreeElement && b instanceof WebInspector.SourceCodeTimelineTreeElement) {
173            aTimeline = a.sourceCodeTimeline;
174            bTimeline = b.sourceCodeTimeline;
175
176            if (!aTimeline.sourceCodeLocation && !bTimeline.sourceCodeLocation) {
177                if (aTimeline.recordType !== bTimeline.recordType)
178                    return aTimeline.recordType.localeCompare(bTimeline.recordType);
179
180                return a.mainTitle.localeCompare(b.mainTitle);
181            }
182
183            if (!aTimeline.sourceCodeLocation || !bTimeline.sourceCodeLocation)
184                return !!aTimeline.sourceCodeLocation - !!bTimeline.sourceCodeLocation;
185
186            if (aTimeline.sourceCodeLocation.lineNumber !== bTimeline.sourceCodeLocation.lineNumber)
187                return aTimeline.sourceCodeLocation.lineNumber - bTimeline.sourceCodeLocation.lineNumber;
188
189            return aTimeline.sourceCodeLocation.columnNumber - bTimeline.sourceCodeLocation.columnNumber;
190        }
191
192        // Fallback to comparing by start time for ResourceTreeElement or anything else.
193        return this._compareTreeElementsByStartTime(a, b);
194    },
195
196    _compareTreeElementsByStartTime: function(a, b)
197    {
198        function getStartTime(treeElement)
199        {
200            if (treeElement instanceof WebInspector.ResourceTreeElement)
201                return treeElement.resource.firstTimestamp;
202            if (treeElement instanceof WebInspector.SourceCodeTimelineTreeElement)
203                return treeElement.sourceCodeTimeline.startTime;
204
205            console.error("Unknown tree element.");
206            return 0;
207        }
208
209        var result = getStartTime(a) - getStartTime(b);
210        if (result)
211            return result;
212
213        // Fallback to comparing titles.
214        return a.mainTitle.localeCompare(b.mainTitle);
215    },
216
217    _insertTreeElement: function(treeElement, parentTreeElement)
218    {
219        console.assert(treeElement);
220        console.assert(!treeElement.parent);
221        console.assert(parentTreeElement);
222
223        parentTreeElement.insertChild(treeElement, insertionIndexForObjectInListSortedByFunction(treeElement, parentTreeElement.children, this._compareTreeElementsByStartTime.bind(this)));
224    },
225
226    _addResourceToTreeIfNeeded: function(resource)
227    {
228        console.assert(resource);
229        if (!resource)
230            return null;
231
232        var treeElement = this.navigationSidebarTreeOutline.findTreeElement(resource);
233        if (treeElement)
234            return treeElement;
235
236        var parentFrame = resource.parentFrame;
237        if (!parentFrame)
238            return;
239
240        var expandedByDefault = false;
241        if (parentFrame.mainResource === resource || parentFrame.provisionalMainResource === resource) {
242            parentFrame = parentFrame.parentFrame;
243            expandedByDefault = !parentFrame; // Main frame expands by default.
244        }
245
246        var resourceTreeElement = new WebInspector.ResourceTreeElement(resource);
247        if (expandedByDefault)
248            resourceTreeElement.expand();
249
250        var resourceTimelineRecord = this._networkTimeline ? this._networkTimeline.recordForResource(resource) : null;
251        if (!resourceTimelineRecord)
252            resourceTimelineRecord = new WebInspector.ResourceTimelineRecord(resource);
253
254        var resourceDataGridNode = new WebInspector.ResourceTimelineDataGridNode(resourceTimelineRecord, true, this);
255        this._treeOutlineDataGridSynchronizer.associate(resourceTreeElement, resourceDataGridNode);
256
257        var parentTreeElement = this.navigationSidebarTreeOutline;
258        if (parentFrame) {
259            // Find the parent main resource, adding it if needed, to append this resource as a child.
260            var parentResource = parentFrame.provisionalMainResource || parentFrame.mainResource;
261
262            parentTreeElement = this._addResourceToTreeIfNeeded(parentResource);
263            console.assert(parentTreeElement);
264            if (!parentTreeElement)
265                return;
266        }
267
268        this._insertTreeElement(resourceTreeElement, parentTreeElement);
269
270        return resourceTreeElement;
271    },
272
273    _addSourceCodeTimeline: function(sourceCodeTimeline)
274    {
275        var parentTreeElement = sourceCodeTimeline.sourceCodeLocation ? this._addResourceToTreeIfNeeded(sourceCodeTimeline.sourceCode) : this.navigationSidebarTreeOutline;
276        console.assert(parentTreeElement);
277        if (!parentTreeElement)
278            return;
279
280        var sourceCodeTimelineTreeElement = new WebInspector.SourceCodeTimelineTreeElement(sourceCodeTimeline);
281        var sourceCodeTimelineDataGridNode = new WebInspector.SourceCodeTimelineTimelineDataGridNode(sourceCodeTimeline, this);
282
283        this._treeOutlineDataGridSynchronizer.associate(sourceCodeTimelineTreeElement, sourceCodeTimelineDataGridNode);
284        this._insertTreeElement(sourceCodeTimelineTreeElement, parentTreeElement);
285    },
286
287    _processPendingRepresentedObjects: function()
288    {
289        if (!this._pendingRepresentedObjects || !this._pendingRepresentedObjects.length)
290            return;
291
292        for (var representedObject of this._pendingRepresentedObjects) {
293            if (representedObject instanceof WebInspector.Resource)
294                this._addResourceToTreeIfNeeded(representedObject);
295            else if (representedObject instanceof WebInspector.SourceCodeTimeline)
296                this._addSourceCodeTimeline(representedObject);
297            else
298                console.error("Unknown represented object");
299        }
300
301        this._pendingRepresentedObjects = [];
302    },
303
304    _networkTimelineRecordAdded: function(event)
305    {
306        var resourceTimelineRecord = event.data.record;
307        console.assert(resourceTimelineRecord instanceof WebInspector.ResourceTimelineRecord);
308
309        this._pendingRepresentedObjects.push(resourceTimelineRecord.resource);
310
311        this.needsLayout();
312
313        // We don't expect to have any source code timelines yet. Those should be added with _sourceCodeTimelineAdded.
314        console.assert(!this._recording.sourceCodeTimelinesForSourceCode(resourceTimelineRecord.resource).length);
315    },
316
317    _sourceCodeTimelineAdded: function(event)
318    {
319        var sourceCodeTimeline = event.data.sourceCodeTimeline;
320        console.assert(sourceCodeTimeline);
321        if (!sourceCodeTimeline)
322            return;
323
324        this._pendingRepresentedObjects.push(sourceCodeTimeline);
325
326        this.needsLayout();
327    },
328
329    _dataGridNodeSelected: function(event)
330    {
331        this.dispatchEventToListeners(WebInspector.TimelineView.Event.SelectionPathComponentsDidChange);
332    },
333
334    _treeElementDeselected: function(treeElement)
335    {
336        if (treeElement.status)
337            treeElement.status = "";
338    },
339
340    _treeElementSelected: function(treeElement, selectedByUser)
341    {
342        if (!WebInspector.timelineSidebarPanel.canShowDifferentContentView())
343            return;
344
345        if (treeElement instanceof WebInspector.FolderTreeElement)
346            return;
347
348        if (treeElement instanceof WebInspector.ResourceTreeElement || treeElement instanceof WebInspector.ScriptTreeElement) {
349            WebInspector.resourceSidebarPanel.showSourceCode(treeElement.representedObject);
350            this._updateTreeElementWithCloseButton(treeElement);
351            return;
352        }
353
354        if (!(treeElement instanceof WebInspector.SourceCodeTimelineTreeElement)) {
355            console.error("Unknown tree element selected.");
356            return;
357        }
358
359        if (!treeElement.sourceCodeTimeline.sourceCodeLocation) {
360            WebInspector.timelineSidebarPanel.showTimelineOverview();
361            return;
362        }
363
364        WebInspector.resourceSidebarPanel.showOriginalOrFormattedSourceCodeLocation(treeElement.sourceCodeTimeline.sourceCodeLocation);
365        this._updateTreeElementWithCloseButton(treeElement);
366    },
367
368    _updateTreeElementWithCloseButton: function(treeElement)
369    {
370        if (this._closeStatusButton) {
371            treeElement.status = this._closeStatusButton.element;
372            return;
373        }
374
375        wrappedSVGDocument(platformImagePath("Close.svg"), null, WebInspector.UIString("Close resource view"), function(element) {
376            this._closeStatusButton = new WebInspector.TreeElementStatusButton(element);
377            this._closeStatusButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._closeStatusButtonClicked, this);
378            if (treeElement === this.navigationSidebarTreeOutline.selectedTreeElement)
379                this._updateTreeElementWithCloseButton(treeElement);
380        }.bind(this));
381    },
382
383    _closeStatusButtonClicked: function(event)
384    {
385        this.navigationSidebarTreeOutline.selectedTreeElement.deselect();
386        WebInspector.timelineSidebarPanel.showTimelineOverview();
387    }
388};
389