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.TimelineDataGrid = function(treeOutline, columns, delegate, editCallback, deleteCallback)
27{
28    WebInspector.DataGrid.call(this, columns, editCallback, deleteCallback);
29
30    this._treeOutlineDataGridSynchronizer = new WebInspector.TreeOutlineDataGridSynchronizer(treeOutline, this, delegate);
31
32    this.element.classList.add(WebInspector.TimelineDataGrid.StyleClassName);
33
34    this._filterableColumns = [];
35
36    // Check if any of the cells can be filtered.
37    for (var [identifier, column] of this.columns) {
38        var scopeBar = columns["scopeBar"];
39        if (!scopeBar)
40            continue;
41        this._filterableColumns.push(identifier);
42        scopeBar.columnIdenfifier = identifier;
43        scopeBar.addEventListener(WebInspector.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this);
44    }
45
46    if (this._filterableColumns.length > 1) {
47        console.error("Creating a TimelineDataGrid with more than one filterable column is not yet supported.");
48        return;
49    }
50
51    if (this._filterableColumns.length) {
52        var items = [new WebInspector.FlexibleSpaceNavigationItem, this.columns.get(this._filterableColumns[0])["scopeBar"], new WebInspector.FlexibleSpaceNavigationItem];
53        this._navigationBar = new WebInspector.NavigationBar(null, items);
54        var container = this.element.appendChild(document.createElement("div"));
55        container.className = "navigation-bar-container";
56        container.appendChild(this._navigationBar.element);
57    }
58
59    this.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridSelectedNodeChanged, this);
60    this.addEventListener(WebInspector.DataGrid.Event.SortChanged, this._sort, this);
61
62    window.addEventListener("resize", this._windowResized.bind(this));
63}
64
65WebInspector.TimelineDataGrid.StyleClassName = "timeline";
66WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
67WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;
68
69WebInspector.TimelineDataGrid.Event = {
70    FiltersDidChange: "timelinedatagrid-filters-did-change"
71};
72
73WebInspector.TimelineDataGrid.createColumnScopeBar = function(prefix, dictionary)
74{
75    prefix = prefix + "-timeline-data-grid-";
76
77    var keys = Object.keys(dictionary).filter(function(key) {
78        return typeof dictionary[key] === "string" || dictionary[key] instanceof String;
79    });
80
81    var scopeBarItems = keys.map(function(key) {
82        var value = dictionary[key];
83        var id = prefix + value;
84        var label = dictionary.displayName(value, true);
85        var item = new WebInspector.ScopeBarItem(id, label);
86        item.value = value;
87        return item;
88    });
89
90    scopeBarItems.unshift(new WebInspector.ScopeBarItem(prefix + "type-all", WebInspector.UIString("All"), true));
91
92    return new WebInspector.ScopeBar(prefix + "scope-bar", scopeBarItems, scopeBarItems[0]);
93};
94
95WebInspector.TimelineDataGrid.prototype = {
96    constructor: WebInspector.TimelineDataGrid,
97    __proto__: WebInspector.DataGrid.prototype,
98
99    // Public
100
101    reset: function()
102    {
103        // May be overridden by subclasses. If so, they should call the superclass.
104
105        this._hidePopover();
106    },
107
108    shown: function()
109    {
110        // May be overridden by subclasses. If so, they should call the superclass.
111
112        this._treeOutlineDataGridSynchronizer.synchronize();
113    },
114
115    hidden: function()
116    {
117        // May be overridden by subclasses. If so, they should call the superclass.
118
119        this._hidePopover();
120    },
121
122    treeElementForDataGridNode: function(dataGridNode)
123    {
124        return this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
125    },
126
127    dataGridNodeForTreeElement: function(treeElement)
128    {
129        return this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);
130    },
131
132    callFramePopoverAnchorElement: function()
133    {
134        // Implemented by subclasses.
135        return null;
136    },
137
138    updateLayout: function()
139    {
140        WebInspector.DataGrid.prototype.updateLayout.call(this);
141
142        if (this._navigationBar)
143            this._navigationBar.updateLayout();
144    },
145
146    treeElementMatchesActiveScopeFilters: function(treeElement)
147    {
148        var dataGridNode = this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);
149        console.assert(dataGridNode);
150
151        for (var identifier of this._filterableColumns) {
152            var scopeBar = this.columns.get(identifier)["scopeBar"];
153            if (!scopeBar || scopeBar.defaultItem.selected)
154                continue;
155
156            var value = dataGridNode.data[identifier];
157            var matchesFilter = scopeBar.selectedItems.some(function(scopeBarItem) {
158                return scopeBarItem.value === value;
159            });
160
161            if (!matchesFilter)
162                return false;
163        }
164
165        return true;
166    },
167
168    addRowInSortOrder: function(treeElement, dataGridNode, parentElement)
169    {
170        this._treeOutlineDataGridSynchronizer.associate(treeElement, dataGridNode);
171
172        parentElement = parentElement || this._treeOutlineDataGridSynchronizer.treeOutline;
173        parentNode = parentElement.root ? this : this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(parentElement);
174
175        console.assert(parentNode);
176
177        if (this.sortColumnIdentifier) {
178            var insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentNode.children, this._sortComparator.bind(this));
179
180            // Insert into the parent, which will cause the synchronizer to insert into the data grid.
181            parentElement.insertChild(treeElement, insertionIndex);
182        } else {
183            // Append to the parent, which will cause the synchronizer to append to the data grid.
184            parentElement.appendChild(treeElement);
185        }
186    },
187
188    shouldIgnoreSelectionEvent: function()
189    {
190        return this._ignoreSelectionEvent || false;
191    },
192
193    // Protected
194
195    dataGridNodeNeedsRefresh: function(dataGridNode)
196    {
197        if (!this._dirtyDataGridNodes)
198            this._dirtyDataGridNodes = new Set;
199        this._dirtyDataGridNodes.add(dataGridNode);
200
201        if (this._scheduledDataGridNodeRefreshIdentifier)
202            return;
203
204        this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this));
205    },
206
207    // Private
208
209    _refreshDirtyDataGridNodes: function()
210    {
211        if (this._scheduledDataGridNodeRefreshIdentifier) {
212            cancelAnimationFrame(this._scheduledDataGridNodeRefreshIdentifier);
213            delete this._scheduledDataGridNodeRefreshIdentifier;
214        }
215
216        if (!this._dirtyDataGridNodes)
217            return;
218
219        var selectedNode = this.selectedNode;
220        var sortComparator = this._sortComparator.bind(this);
221        var treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
222
223        this._treeOutlineDataGridSynchronizer.enabled = false;
224
225        for (var dataGridNode of this._dirtyDataGridNodes) {
226            dataGridNode.refresh();
227
228            if (!this.sortColumnIdentifier)
229                continue;
230
231            if (dataGridNode === selectedNode)
232                this._ignoreSelectionEvent = true;
233
234            var treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
235            console.assert(treeElement);
236
237            treeOutline.removeChild(treeElement);
238            this.removeChild(dataGridNode);
239
240            var insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
241            treeOutline.insertChild(treeElement, insertionIndex);
242            this.insertChild(dataGridNode, insertionIndex);
243
244            // Adding the tree element back to the tree outline subjects it to filters.
245            // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
246            dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
247
248            if (dataGridNode === selectedNode) {
249                selectedNode.revealAndSelect();
250                delete this._ignoreSelectionEvent;
251            }
252        }
253
254        this._treeOutlineDataGridSynchronizer.enabled = true;
255
256        delete this._dirtyDataGridNodes;
257    },
258
259    _sort: function()
260    {
261        var sortColumnIdentifier = this.sortColumnIdentifier;
262        if (!sortColumnIdentifier)
263            return;
264
265        var selectedNode = this.selectedNode;
266        this._ignoreSelectionEvent = true;
267
268        this._treeOutlineDataGridSynchronizer.enabled = false;
269
270        var treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
271        if (treeOutline.selectedTreeElement)
272            treeOutline.selectedTreeElement.deselect(true);
273
274        // Collect parent nodes that need their children sorted. So this in two phases since
275        // traverseNextNode would get confused if we sort the tree while traversing it.
276        var parentDataGridNodes = [this];
277        var currentDataGridNode = this.children[0];
278        while (currentDataGridNode) {
279            if (currentDataGridNode.children.length)
280                parentDataGridNodes.push(currentDataGridNode);
281            currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
282        }
283
284        // Sort the children of collected parent nodes.
285        for (var parentDataGridNode of parentDataGridNodes) {
286            var parentTreeElement = parentDataGridNode === this ? treeOutline : this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(parentDataGridNode);
287            console.assert(parentTreeElement);
288
289            var childDataGridNodes = parentDataGridNode.children.slice();
290
291            parentDataGridNode.removeChildren();
292            parentTreeElement.removeChildren();
293
294            childDataGridNodes.sort(this._sortComparator.bind(this));
295
296            for (var dataGridNode of childDataGridNodes) {
297                var treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
298                console.assert(treeElement);
299
300                parentTreeElement.appendChild(treeElement);
301                parentDataGridNode.appendChild(dataGridNode);
302
303                // Adding the tree element back to the tree outline subjects it to filters.
304                // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
305                dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
306            }
307        }
308
309        this._treeOutlineDataGridSynchronizer.enabled = true;
310
311        if (selectedNode)
312            selectedNode.revealAndSelect();
313
314        delete this._ignoreSelectionEvent;
315    },
316
317    _sortComparator: function(node1, node2)
318    {
319        var sortColumnIdentifier = this.sortColumnIdentifier;
320        if (!sortColumnIdentifier)
321            return 0;
322
323        var sortDirection = this.sortOrder === WebInspector.DataGrid.SortOrder.Ascending ? 1 : -1;
324
325        var value1 = node1.data[sortColumnIdentifier];
326        var value2 = node2.data[sortColumnIdentifier];
327
328        if (typeof value1 === "number" && typeof value2 === "number") {
329            if (isNaN(value1) && isNaN(value2))
330                return 0;
331            if (isNaN(value1))
332                return -sortDirection;
333            if (isNaN(value2))
334                return sortDirection;
335            return (value1 - value2) * sortDirection;
336        }
337
338        if (typeof value1 === "string" && typeof value2 === "string")
339            return value1.localeCompare(value2) * sortDirection;
340
341        if (value1 instanceof WebInspector.CallFrame || value2 instanceof WebInspector.CallFrame) {
342            // Sort by function name if available, then fall back to the source code object.
343            value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
344            value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
345        }
346
347        if (value1 instanceof WebInspector.SourceCode || value2 instanceof WebInspector.SourceCode) {
348            value1 = value1 ? value1.displayName || "" : "";
349            value2 = value2 ? value2.displayName || "" : "";
350        }
351
352        // For everything else (mostly booleans).
353        return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
354    },
355
356    _scopeBarSelectedItemsDidChange: function(event)
357    {
358        var columnIdentifier = event.target.columnIdenfifier;
359        this.dispatchEventToListeners(WebInspector.TimelineDataGrid.Event.FiltersDidChange, {columnIdentifier: columnIdentifier});
360    },
361
362    _dataGridSelectedNodeChanged: function(event)
363    {
364        if (!this.selectedNode) {
365            this._hidePopover();
366            return;
367        }
368
369        var record = this.selectedNode.record;
370        if (!record || !record.callFrames || !record.callFrames.length) {
371            this._hidePopover();
372            return;
373        }
374
375        this._showPopoverForSelectedNodeSoon();
376    },
377
378    _windowResized: function(event)
379    {
380        if (this._popover && this._popover.visible)
381            this._updatePopoverForSelectedNode(false);
382    },
383
384    _showPopoverForSelectedNodeSoon: function()
385    {
386        if (this._showPopoverTimeout)
387            return;
388
389        function delayedWork()
390        {
391            if (!this._popover)
392                this._popover = new WebInspector.Popover;
393
394            this._updatePopoverForSelectedNode(true);
395        }
396
397        this._showPopoverTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout);
398    },
399
400    _hidePopover: function()
401    {
402        if (this._showPopoverTimeout) {
403            clearTimeout(this._showPopoverTimeout);
404            delete this._showPopoverTimeout;
405        }
406
407        if (this._popover)
408            this._popover.dismiss();
409
410        function delayedWork()
411        {
412            if (this._popoverCallStackTreeOutline)
413                this._popoverCallStackTreeOutline.removeChildren();
414        }
415
416        if (this._hidePopoverContentClearTimeout)
417            clearTimeout(this._hidePopoverContentClearTimeout);
418        this._hidePopoverContentClearTimeout = setTimeout(delayedWork.bind(this), WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
419    },
420
421    _updatePopoverForSelectedNode: function(updateContent)
422    {
423        if (!this._popover || !this.selectedNode)
424            return;
425
426        var targetPopoverElement = this.callFramePopoverAnchorElement();
427        console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
428        if (!targetPopoverElement)
429            return;
430
431        var targetFrame = WebInspector.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
432
433        // The element might be hidden if it does not have a width and height.
434        if (!targetFrame.size.width && !targetFrame.size.height)
435            return;
436
437        if (this._hidePopoverContentClearTimeout) {
438            clearTimeout(this._hidePopoverContentClearTimeout);
439            delete this._hidePopoverContentClearTimeout;
440        }
441
442        if (updateContent)
443            this._popover.content = this._createPopoverContent();
444
445        this._popover.present(targetFrame.pad(2), [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_X]);
446    },
447
448    _createPopoverContent: function()
449    {
450        if (!this._popoverCallStackTreeOutline) {
451            var contentElement = document.createElement("ol");
452            contentElement.classList.add("timeline-data-grid-tree-outline");
453            this._popoverCallStackTreeOutline = new TreeOutline(contentElement);
454            this._popoverCallStackTreeOutline.onselect = this._popoverCallStackTreeElementSelected.bind(this);
455        } else
456            this._popoverCallStackTreeOutline.removeChildren();
457
458        var callFrames = this.selectedNode.record.callFrames;
459        for (var i = 0 ; i < callFrames.length; ++i) {
460            var callFrameTreeElement = new WebInspector.CallFrameTreeElement(callFrames[i]);
461            this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
462        }
463
464        var content = document.createElement("div");
465        content.className = "timeline-data-grid-popover";
466        content.appendChild(this._popoverCallStackTreeOutline.element);
467        return content;
468    },
469
470    _popoverCallStackTreeElementSelected: function(treeElement, selectedByUser)
471    {
472        this._popover.dismiss();
473
474        console.assert(treeElement instanceof WebInspector.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
475        var callFrame = treeElement.callFrame;
476        if (!callFrame.sourceCodeLocation)
477            return;
478
479        WebInspector.resourceSidebarPanel.showSourceCodeLocation(callFrame.sourceCodeLocation);
480    }
481};
482