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