Changeset 200067 in webkit
- Timestamp:
- Apr 25, 2016 6:00:17 PM (8 years ago)
- Location:
- trunk/Source/WebInspectorUI
- Files:
-
- 15 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Source/WebInspectorUI/ChangeLog
r200065 r200067 1 2016-04-25 Matt Baker <mattbaker@apple.com> 2 3 Web Inspector: hook up grid row filtering in the new Timelines UI 4 https://bugs.webkit.org/show_bug.cgi?id=154924 5 <rdar://problem/24934607> 6 7 Reviewed by Timothy Hatcher. 8 9 Re-implement timeline data grid filtering that previously existed in the 10 navigation sidebar. This patch adds support for filter text, scope bars, 11 and filtering based on ruler selection. 12 13 Multi-column filter support is now part of DataGrid. The grid checks compares 14 filter text against cell data of type string. DataGridNode subclasses may 15 provide custom string data for columns that format complex objects (such 16 as SourceCodeLocations). Cells containing data of type number are not 17 considered for filtering at this time. 18 19 * UserInterface/Views/DataGrid.js: 20 (WebInspector.DataGrid): 21 (WebInspector.DataGrid.prototype.set filterText): 22 (WebInspector.DataGrid.prototype.get filterDelegate): 23 (WebInspector.DataGrid.prototype.set filterDelegate): 24 (WebInspector.DataGrid.prototype.filterDidChange): 25 Called internally by the grid whenever the filter text or delegate changes. 26 Also called by clients that implement a filter delegate, to inform the 27 grid that a custom filter has changed. 28 29 (WebInspector.DataGrid.prototype.hasCustomFilters): 30 (WebInspector.DataGrid.prototype.matchNodeAgainstCustomFilters): 31 Calls the filter delegate, if it exists, and provides a hook for 32 subclasses to provide custom filtering. 33 34 (WebInspector.DataGrid.prototype._applyFiltersToNode.matchTextFilter): 35 (WebInspector.DataGrid.prototype._applyFiltersToNode.makeVisible): 36 (WebInspector.DataGrid.prototype._applyFiltersToNode): 37 Filters data grid nodes and fires filter events as needed. 38 (WebInspector.DataGrid.prototype._hasFilterDelegate): 39 Helper function. 40 (WebInspector.DataGrid.prototype._updateVisibleRows): 41 Exclude hidden nodes from revealed rows. 42 (WebInspector.DataGrid.prototype._updateFilter): 43 Filtering entry point, called on an animation frame. Updates visible 44 rows if any node was filtered/unfiltered. 45 46 (WebInspector.DataGridNode): 47 (WebInspector.DataGridNode.prototype.get filterableData): 48 Gets an array of filterable strings for the node. 49 (WebInspector.DataGridNode.prototype.refresh): 50 Resets cached filterable strings. 51 (WebInspector.DataGridNode.prototype.filterableDataForColumn): 52 Can be overridden by subclasses to provide filterable text for complex 53 cell data, like as objects formatted as document fragments. 54 55 * UserInterface/Views/LayoutTimelineDataGridNode.js: 56 (WebInspector.LayoutTimelineDataGridNode.prototype.get data): 57 58 * UserInterface/Views/LayoutTimelineView.js: 59 (WebInspector.LayoutTimelineView): 60 Register grid and remove logic that has been moved to the base class. 61 (WebInspector.LayoutTimelineView.prototype.filterDidChange): 62 Update highlight after grid filter change. 63 (WebInspector.LayoutTimelineView.prototype._dataGridSelectedNodeChanged): 64 Update highlight when selection changes. 65 (WebInspector.LayoutTimelineView.prototype.matchTreeElementAgainstCustomFilters): Deleted. 66 (WebInspector.LayoutTimelineView.prototype.treeElementDeselected): Deleted. 67 (WebInspector.LayoutTimelineView.prototype._dataGridFiltersDidChange): Deleted. 68 (WebInspector.LayoutTimelineView.prototype._dataGridNodeSelected): Deleted. 69 No longer needed. 70 71 * UserInterface/Views/NetworkTimelineView.js: 72 (WebInspector.NetworkTimelineView): 73 Register grid and remove logic that has been moved to the base class. 74 (WebInspector.NetworkTimelineView.prototype.matchTreeElementAgainstCustomFilters): Deleted. 75 (WebInspector.NetworkTimelineView.prototype._dataGridFiltersDidChange): Deleted. 76 (WebInspector.NetworkTimelineView.prototype._dataGridNodeSelected): Deleted. 77 No longer needed. 78 79 * UserInterface/Views/OverviewTimelineView.js: 80 (WebInspector.OverviewTimelineView): 81 Register grid and remove logic that has been moved to the base class. 82 (WebInspector.OverviewTimelineView.prototype._dataGridNodeSelected): Deleted. 83 No longer needed. 84 85 * UserInterface/Views/RenderingFrameTimelineView.js: 86 (WebInspector.RenderingFrameTimelineView): 87 Register grid and remove logic that has been moved to the base class. 88 (WebInspector.RenderingFrameTimelineView.prototype.get filterStartTime): 89 (WebInspector.RenderingFrameTimelineView.prototype.get filterEndTime): 90 Convert selection indices into filter start and end times. 91 (WebInspector.RenderingFrameTimelineView.prototype.matchDataGridNodeAgainstCustomFilters): 92 Perform custom filtering on rendering frame duration. 93 (WebInspector.RenderingFrameTimelineView.prototype._scopeBarSelectionDidChange): 94 Inform grid of custom filter change. 95 (WebInspector.RenderingFrameTimelineView.prototype.matchTreeElementAgainstCustomFilters): Deleted. 96 (WebInspector.RenderingFrameTimelineView.prototype._dataGridNodeSelected): Deleted. 97 No longer needed. 98 99 * UserInterface/Views/ResourceTimelineDataGridNode.js: 100 (WebInspector.ResourceTimelineDataGridNode.prototype.filterableDataForColumn): 101 Use URL string for filtering "name" column. 102 103 * UserInterface/Views/ScriptClusterTimelineView.js: 104 (WebInspector.ScriptClusterTimelineView.prototype.updateFilter): 105 Forwarding for TimelineView API. 106 (WebInspector.ScriptClusterTimelineView.prototype.matchDataGridNodeAgainstCustomFilters): 107 (WebInspector.ScriptClusterTimelineView.prototype.matchTreeElementAgainstCustomFilters): Deleted. 108 Renamed to matchDataGridNodeAgainstCustomFilters. 109 (WebInspector.ScriptClusterTimelineView.prototype._scriptClusterViewCurrentContentViewDidChange): Deleted. 110 Removed FIXME comment. Updating TimelineView times is sufficient to trigger filtering. 111 112 * UserInterface/Views/ScriptDetailsTimelineView.js: 113 (WebInspector.ScriptDetailsTimelineView): 114 Register grid and remove logic that has been moved to the base class. 115 (WebInspector.ScriptDetailsTimelineView.prototype._dataGridFiltersDidChange): Deleted. 116 (WebInspector.ScriptDetailsTimelineView.prototype._dataGridNodeSelected): Deleted. 117 No longer needed. 118 119 * UserInterface/Views/ScriptTimelineDataGridNode.js: 120 (WebInspector.ScriptTimelineDataGridNode.prototype.filterableDataForColumn): 121 Use main title and subtitle strings for filtering "name" column. 122 (WebInspector.ScriptTimelineDataGridNode.prototype._createNameCellDocumentFragment): 123 (WebInspector.ScriptTimelineDataGridNode.prototype._subtitle): 124 Break out for use in filterableDataForColumn. 125 126 * UserInterface/Views/TimelineDataGrid.js: 127 (WebInspector.TimelineDataGrid): 128 Cleanup variable names. 129 (WebInspector.TimelineDataGrid.prototype.hasCustomFilters): 130 Always true because filtering on ruler selection always occurs. 131 (WebInspector.TimelineDataGrid.prototype.matchNodeAgainstCustomFilters): 132 Match nodes against scope bar filters. 133 (WebInspector.TimelineDataGrid.prototype._scopeBarSelectedItemsDidChange): 134 Inform grid of custom filter change. 135 (WebInspector.TimelineDataGrid.prototype.treeElementMatchesActiveScopeFilters): Deleted. 136 Re-implemented as _nodeMatchesActiveScopeFilters. 137 (WebInspector.TimelineDataGrid.prototype._updateScopeBarForcedVisibility): Deleted. 138 Old UI. No longer needed. 139 140 * UserInterface/Views/TimelineDataGridNode.js: 141 (WebInspector.TimelineDataGridNode.prototype.filterableDataForColumn): 142 Filter strings for SourceCodeLocation and CallFrame objects. 143 144 * UserInterface/Views/TimelineRecordingContentView.js: 145 (WebInspector.TimelineRecordingContentView): 146 Listen for FilterBar changes and TimelineView record filtering. 147 (WebInspector.TimelineRecordingContentView.prototype._filterDidChange): 148 Update grid filters when filter bar changes. 149 (WebInspector.TimelineRecordingContentView.prototype._recordWasFiltered): 150 Update overview when records are filtered/unfiltered. 151 (WebInspector.TimelineRecordingContentView.prototype.filterDidChange): Deleted. 152 (WebInspector.TimelineRecordingContentView.prototype.recordWasFiltered): Deleted. 153 (WebInspector.TimelineRecordingContentView.prototype.matchTreeElementAgainstCustomFilters.checkTimeBounds): Deleted. 154 (WebInspector.TimelineRecordingContentView.prototype.matchTreeElementAgainstCustomFilters): Deleted. 155 Re-implemented in DataGrid. 156 (WebInspector.TimelineRecordingContentView.prototype._updateTimes): Deleted. 157 FIXME comment removed. Filtering occurs when TimelineView times are updated. 158 (WebInspector.TimelineRecordingContentView.prototype._timeRangeSelectionChanged): Deleted. 159 160 * UserInterface/Views/TimelineView.js: 161 (WebInspector.TimelineView): 162 (WebInspector.TimelineView.prototype.get navigationItems): 163 Used by TimelineRecordingContentView to add scope bar items to the 164 lower content browser's navigation bar. 165 166 (WebInspector.TimelineView.prototype.set startTime): 167 (WebInspector.TimelineView.prototype.set endTime): 168 (WebInspector.TimelineView.prototype.set currentTime): 169 Update grid filter when recording times change. 170 (WebInspector.TimelineView.prototype.get filterStartTime): 171 (WebInspector.TimelineView.prototype.get filterEndTime): 172 Let subclasses (RenderingFrameTimelineView) provide filter start/end times. 173 (WebInspector.TimelineView.prototype.setupDataGrid): 174 Register the grid used by the TimelineView subclass, allowing the base 175 class to hook into common event listeners and provide boilerplate functionality. 176 177 (WebInspector.TimelineView.prototype.updateFilter): 178 For data grid views, updates grid filters and sets new filter text. 179 (WebInspector.TimelineView.prototype.matchDataGridNodeAgainstCustomFilters): 180 (WebInspector.TimelineView.prototype.dataGridMatchNodeAgainstCustomFilters.checkTimeBounds): 181 (WebInspector.TimelineView.prototype.dataGridMatchNodeAgainstCustomFilters): 182 DataGrid filter delegate. Lets subclasses apply custom filters first, 183 then filters based on ruler selection if needed. 184 185 (WebInspector.TimelineView.prototype.filterDidChange): 186 Hook for subclasses to respond to filter changes. 187 (WebInspector.TimelineView.prototype._filterTimesDidChange.delayedWork): 188 (WebInspector.TimelineView.prototype._filterTimesDidChange): 189 Helper function for coalescing ruler selection updates into a single 190 filter update. 191 192 (WebInspector.TimelineView.prototype.matchTreeElementAgainstCustomFilters): Deleted. 193 (WebInspector.TimelineView.prototype.filterUpdated): Deleted. 194 No longer needed. 195 1 196 2016-04-25 Joseph Pecoraro <pecoraro@apple.com> 2 197 -
trunk/Source/WebInspectorUI/UserInterface/Views/DataGrid.js
r199972 r200067 54 54 this._columnWidthsInitialized = false; 55 55 56 this._filterText = ""; 57 this._filterDelegate = null; 58 56 59 this.element.className = "data-grid"; 57 60 this.element.tabIndex = 0; … … 282 285 } 283 286 287 set filterText(x) 288 { 289 if (this._filterText === x) 290 return; 291 292 this._filterText = x; 293 this.filterDidChange(); 294 } 295 296 get filterDelegate() { return this._filterDelegate; } 297 298 set filterDelegate(delegate) 299 { 300 this._filterDelegate = delegate; 301 this.filterDidChange(); 302 } 303 304 filterDidChange() 305 { 306 if (this._scheduledFilterUpdateIdentifier) 307 return; 308 309 this._scheduledFilterUpdateIdentifier = requestAnimationFrame(this._updateFilter.bind(this)); 310 } 311 312 hasCustomFilters() 313 { 314 return this._hasFilterDelegate(); 315 } 316 317 matchNodeAgainstCustomFilters(node) 318 { 319 if (!this._hasFilterDelegate()) 320 return true; 321 return this._filterDelegate.dataGridMatchNodeAgainstCustomFilters(node); 322 } 323 324 _applyFiltersToNode(node) 325 { 326 if (!this._textFilterRegex && !this.hasCustomFilters()) { 327 // No filters, so make everything visible. 328 node.hidden = false; 329 330 // If the node was expanded during filtering, collapse it again. 331 if (node.expanded && node[WebInspector.DataGrid.WasExpandedDuringFilteringSymbol]) { 332 node[WebInspector.DataGrid.WasExpandedDuringFilteringSymbol] = false; 333 node.collapse(); 334 } 335 336 return; 337 } 338 339 let filterableData = node.filterableData || []; 340 let flags = {expandNode: false}; 341 let filterRegex = this._textFilterRegex; 342 343 function matchTextFilter() 344 { 345 if (!filterableData.length || !filterRegex) 346 return true; 347 348 if (filterableData.some((value) => filterRegex.test(value))) { 349 flags.expandNode = true; 350 return true; 351 } 352 353 return false; 354 } 355 356 function makeVisible() 357 { 358 // Make this element visible. 359 node.hidden = false; 360 361 // Make the ancestors visible and expand them. 362 let currentAncestor = node.parent; 363 while (currentAncestor && !currentAncestor.root) { 364 currentAncestor.hidden = false; 365 366 // Only expand if the built-in filters matched, not custom filters. 367 if (flags.expandNode && !currentAncestor.expanded) { 368 currentAncestor[WebInspector.DataGrid.WasExpandedDuringFilteringSymbol] = true; 369 currentAncestor.expand(); 370 } 371 372 currentAncestor = currentAncestor.parent; 373 } 374 } 375 376 if (matchTextFilter() && this.matchNodeAgainstCustomFilters(node)) { 377 // Make the node visible since it matches. 378 makeVisible(); 379 380 // If the node didn't match a built-in filter and was expanded earlier during filtering, collapse it again. 381 if (!flags.expandNode && node.expanded && node[WebInspector.DataGrid.WasExpandedDuringFilteringSymbol]) { 382 node[WebInspector.DataGrid.WasExpandedDuringFilteringSymbol] = false; 383 node.collapse(); 384 } 385 386 return; 387 } 388 389 // Make the node invisible since it does not match. 390 node.hidden = true; 391 } 392 284 393 _updateSortedColumn(oldSortColumnIdentifier) 285 394 { … … 300 409 301 410 this.dispatchEventToListeners(WebInspector.DataGrid.Event.SortChanged); 411 } 412 413 _hasFilterDelegate() 414 { 415 return this._filterDelegate && typeof this._filterDelegate.dataGridMatchNodeAgainstCustomFilters === "function"; 302 416 } 303 417 … … 868 982 let updateOffsetThreshold = rowHeight * 5; 869 983 870 let revealedRows = this._rows.filter((row) => row.revealed );984 let revealedRows = this._rows.filter((row) => row.revealed && !row.hidden); 871 985 872 986 let scrollTop = this._scrollContainerElement.scrollTop; … … 1570 1684 this._currentResizer = null; 1571 1685 } 1686 1687 _updateFilter() 1688 { 1689 if (this._scheduledFilterUpdateIdentifier) { 1690 cancelAnimationFrame(this._scheduledFilterUpdateIdentifier); 1691 this._scheduledFilterUpdateIdentifier = undefined; 1692 } 1693 1694 if (!this._rows.length) 1695 return; 1696 1697 this._textFilterRegex = simpleGlobStringToRegExp(this._filterText, "i"); 1698 1699 // Don't populate if we don't have any active filters. 1700 // We only need to populate when a filter needs to reveal. 1701 let dontPopulate = !this._textFilterRegex && !this.hasCustomFilters(); 1702 1703 let filterDidModifyNode = false; 1704 let currentNode = this._rows[0]; 1705 while (currentNode && !currentNode.root) { 1706 const currentNodeWasHidden = currentNode.hidden; 1707 this._applyFiltersToNode(currentNode); 1708 if (currentNodeWasHidden !== currentNode.hidden) { 1709 this.dispatchEventToListeners(WebInspector.DataGrid.Event.NodeWasFiltered, {node: currentNode}); 1710 filterDidModifyNode = true; 1711 } 1712 1713 currentNode = currentNode.traverseNextNode(false, null, dontPopulate); 1714 } 1715 1716 if (!filterDidModifyNode) 1717 return; 1718 1719 this._updateVisibleRows(); 1720 this.dispatchEventToListeners(WebInspector.DataGrid.Event.FilterDidChange); 1721 } 1572 1722 }; 1573 1723 … … 1576 1726 SelectedNodeChanged: "datagrid-selected-node-changed", 1577 1727 ExpandedNode: "datagrid-expanded-node", 1578 CollapsedNode: "datagrid-collapsed-node" 1728 CollapsedNode: "datagrid-collapsed-node", 1729 FilterDidChange: "datagrid-filter-did-change", 1730 NodeWasFiltered: "datagrid-node-was-filtered" 1579 1731 }; 1580 1732 … … 1593 1745 WebInspector.DataGrid.PreviousColumnOrdinalSymbol = Symbol("previous-column-ordinal"); 1594 1746 WebInspector.DataGrid.NextColumnOrdinalSymbol = Symbol("next-column-ordinal"); 1747 WebInspector.DataGrid.WasExpandedDuringFilteringSymbol = Symbol("was-expanded-during-filtering"); 1595 1748 1596 1749 WebInspector.DataGrid.ColumnResizePadding = 10; … … 1727 1880 } 1728 1881 1882 get filterableData() 1883 { 1884 if (this._cachedFilterableData) 1885 return this._cachedFilterableData; 1886 1887 this._cachedFilterableData = []; 1888 1889 for (let column of this.dataGrid.columns.values()) { 1890 let value = this.filterableDataForColumn(column.columnIdentifier); 1891 if (!value) 1892 continue; 1893 1894 if (!(value instanceof Array)) 1895 value = [value]; 1896 1897 if (!value.length) 1898 continue; 1899 1900 this._cachedFilterableData = this._cachedFilterableData.concat(value); 1901 } 1902 1903 return this._cachedFilterableData; 1904 } 1905 1729 1906 get revealed() 1730 1907 { … … 1857 2034 } 1858 2035 2036 this._cachedFilterableData = null; 1859 2037 this._needsRefresh = false; 1860 2038 … … 2255 2433 return null; 2256 2434 } 2435 2436 // Protected 2437 2438 filterableDataForColumn(columnIdentifier) 2439 { 2440 let value = this.data[columnIdentifier]; 2441 return typeof value === "string" ? value : null; 2442 } 2257 2443 }; 2258 2444 -
trunk/Source/WebInspectorUI/UserInterface/Views/LayoutTimelineDataGridNode.js
r199635 r200067 45 45 if (!this._cachedData) { 46 46 this._cachedData = { 47 type: this._record.eventType, 47 48 name: this.displayName(), 48 49 width: this._record.width, -
trunk/Source/WebInspectorUI/UserInterface/Views/LayoutTimelineView.js
r199634 r200067 32 32 console.assert(timeline.type === WebInspector.TimelineRecord.Type.Layout, timeline); 33 33 34 let columns = { name: {}, location: {}, width: {}, height: {}, startTime: {}, totalTime: {}};34 let columns = {type: {}, name: {}, location: {}, width: {}, height: {}, startTime: {}, totalTime: {}}; 35 35 36 36 columns.name.title = WebInspector.UIString("Type"); … … 43 43 } 44 44 45 columns.name.scopeBar = WebInspector.TimelineDataGrid.createColumnScopeBar("layout", typeToLabelMap); 45 columns.type.scopeBar = WebInspector.TimelineDataGrid.createColumnScopeBar("layout", typeToLabelMap); 46 columns.type.hidden = true; 47 46 48 columns.name.disclosure = true; 47 49 columns.name.icon = true; 48 50 49 this._scopeBar = columns. name.scopeBar;51 this._scopeBar = columns.type.scopeBar; 50 52 51 53 columns.location.title = WebInspector.UIString("Initiator"); … … 70 72 71 73 this._dataGrid = new WebInspector.LayoutTimelineDataGrid(columns); 72 this._dataGrid.addEventListener(WebInspector.TimelineDataGrid.Event.FiltersDidChange, this._dataGridFiltersDidChange, this); 73 this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridNodeSelected, this); 74 this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridSelectedNodeChanged, this); 75 76 this.setupDataGrid(this._dataGrid); 74 77 75 78 this._dataGrid.sortColumnIdentifierSetting = new WebInspector.Setting("layout-timeline-view-sort", "startTime"); … … 142 145 } 143 146 144 filterDidChange()145 {146 super.filterDidChange();147 148 this._updateHighlight();149 }150 151 matchTreeElementAgainstCustomFilters(treeElement)152 {153 return this._dataGrid.treeElementMatchesActiveScopeFilters(treeElement);154 }155 156 147 reset() 157 148 { … … 175 166 } 176 167 177 treeElementDeselected(treeElement)178 { 179 super. treeElementDeselected(treeElement);168 filterDidChange() 169 { 170 super.filterDidChange(); 180 171 181 172 this._updateHighlight(); … … 247 238 } 248 239 249 _dataGridFiltersDidChange(event)250 {251 // FIXME: <https://webkit.org/b/154924> Web Inspector: hook up grid row filtering in the new Timelines UI252 }253 254 _dataGridNodeSelected(event)255 {256 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);257 }258 259 240 _updateHighlight() 260 241 { … … 328 309 this._updateHighlight(); 329 310 } 311 312 _dataGridSelectedNodeChanged(event) 313 { 314 this._updateHighlight(); 315 } 330 316 }; -
trunk/Source/WebInspectorUI/UserInterface/Views/NetworkTimelineView.js
r199974 r200067 89 89 90 90 this._dataGrid = new WebInspector.TimelineDataGrid(columns); 91 this._dataGrid.addEventListener(WebInspector.TimelineDataGrid.Event.FiltersDidChange, this._dataGridFiltersDidChange, this);92 this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridNodeSelected, this);93 91 this._dataGrid.sortDelegate = this; 94 92 this._dataGrid.sortColumnIdentifierSetting = new WebInspector.Setting("network-timeline-view-sort", "requestSent"); 95 93 this._dataGrid.sortOrderSetting = new WebInspector.Setting("network-timeline-view-sort-order", WebInspector.DataGrid.SortOrder.Ascending); 96 94 95 this.setupDataGrid(this._dataGrid); 96 97 97 this.element.classList.add("network"); 98 98 this.addSubview(this._dataGrid); … … 138 138 139 139 this._dataGrid.closed(); 140 }141 142 matchTreeElementAgainstCustomFilters(treeElement)143 {144 return this._dataGrid.treeElementMatchesActiveScopeFilters(treeElement);145 140 } 146 141 … … 236 231 this.needsLayout(); 237 232 } 238 239 _dataGridFiltersDidChange(event)240 {241 // FIXME: <https://webkit.org/b/154924> Web Inspector: hook up grid row filtering in the new Timelines UI242 }243 244 _dataGridNodeSelected(event)245 {246 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);247 }248 233 }; -
trunk/Source/WebInspectorUI/UserInterface/Views/OverviewTimelineView.js
r199241 r200067 46 46 47 47 this._dataGrid = new WebInspector.DataGrid(columns); 48 this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridNodeSelected, this); 48 49 this.setupDataGrid(this._dataGrid); 49 50 50 51 this._currentTimeMarker = new WebInspector.TimelineMarker(0, WebInspector.TimelineMarker.Type.CurrentTime); … … 295 296 } 296 297 297 _dataGridNodeSelected(event)298 {299 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);300 }301 302 298 _recordingReset(event) 303 299 { -
trunk/Source/WebInspectorUI/UserInterface/Views/RenderingFrameTimelineView.js
r199634 r200067 78 78 79 79 this._dataGrid = new WebInspector.TimelineDataGrid(columns); 80 this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridNodeSelected, this);81 80 this._dataGrid.sortColumnIdentifierSetting = new WebInspector.Setting("rendering-frame-timeline-view-sort", "startTime"); 82 81 this._dataGrid.sortOrderSetting = new WebInspector.Setting("rendering-frame-timeline-view-sort-order", WebInspector.DataGrid.SortOrder.Ascending); 82 83 this.setupDataGrid(this._dataGrid); 83 84 84 85 this.element.classList.add("rendering-frame"); … … 152 153 } 153 154 154 matchTreeElementAgainstCustomFilters(treeElement) 155 { 156 console.assert(this._scopeBar.selectedItems.length === 1); 157 var selectedScopeBarItem = this._scopeBar.selectedItems[0]; 158 if (!selectedScopeBarItem || selectedScopeBarItem.id === WebInspector.RenderingFrameTimelineView.DurationFilter.All) 159 return true; 160 161 while (treeElement && !(treeElement.record instanceof WebInspector.RenderingFrameTimelineRecord)) 162 treeElement = treeElement.parent; 163 164 console.assert(treeElement, "Cannot apply duration filter: no RenderingFrameTimelineRecord found."); 165 if (!treeElement) 166 return false; 167 168 var minimumDuration = selectedScopeBarItem.id === WebInspector.RenderingFrameTimelineView.DurationFilter.OverOneMillisecond ? 0.001 : 0.015; 169 return treeElement.record.duration > minimumDuration; 155 get filterStartTime() 156 { 157 let records = this.representedObject.records; 158 let startIndex = this.startTime; 159 if (startIndex >= records.length) 160 return Infinity; 161 162 return records[startIndex].startTime; 163 } 164 165 get filterEndTime() 166 { 167 let records = this.representedObject.records; 168 let endIndex = this.endTime - 1; 169 if (endIndex >= records.length) 170 return Infinity; 171 172 return records[endIndex].endTime; 170 173 } 171 174 … … 194 197 return new WebInspector.ProfileNodeDataGridNode(treeElement.profileNode, this.zeroTime, this.startTime, this.endTime); 195 198 return null; 199 } 200 201 matchDataGridNodeAgainstCustomFilters(node) 202 { 203 if (!super.matchDataGridNodeAgainstCustomFilters(node)) 204 return false; 205 206 console.assert(node instanceof WebInspector.TimelineDataGridNode); 207 console.assert(this._scopeBar.selectedItems.length === 1); 208 let selectedScopeBarItem = this._scopeBar.selectedItems[0]; 209 if (!selectedScopeBarItem || selectedScopeBarItem.id === WebInspector.RenderingFrameTimelineView.DurationFilter.All) 210 return true; 211 212 while (node && !(node.record instanceof WebInspector.RenderingFrameTimelineRecord)) 213 node = node.parent; 214 215 console.assert(node, "Cannot apply duration filter: no RenderingFrameTimelineRecord found."); 216 if (!node) 217 return false; 218 219 let minimumDuration = selectedScopeBarItem.id === WebInspector.RenderingFrameTimelineView.DurationFilter.OverOneMillisecond ? 0.001 : 0.015; 220 return node.record.duration > minimumDuration; 196 221 } 197 222 … … 265 290 } 266 291 267 _dataGridNodeSelected(event) 268 { 269 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange); 270 } 271 272 _scopeBarSelectionDidChange(event) 273 { 274 // FIXME: <https://webkit.org/b/154924> Web Inspector: hook up grid row filtering in the new Timelines UI 292 _scopeBarSelectionDidChange() 293 { 294 this.filterDidChange(); 275 295 } 276 296 }; -
trunk/Source/WebInspectorUI/UserInterface/Views/ResourceTimelineDataGridNode.js
r197909 r200067 147 147 } 148 148 149 // Protected 150 151 filterableDataForColumn(columnIdentifier) 152 { 153 if (columnIdentifier === "name") 154 return this._resource.url; 155 return super.filterableDataForColumn(columnIdentifier); 156 } 157 149 158 // Private 150 159 -
trunk/Source/WebInspectorUI/UserInterface/Views/ScriptClusterTimelineView.js
r198601 r200067 72 72 get navigationSidebarTreeOutline() { return this._contentViewContainer.currentContentView.navigationSidebarTreeOutline; } 73 73 reset() { return this._contentViewContainer.currentContentView.reset(); } 74 updateFilter(filters) { return this._contentViewContainer.currentContentView.updateFilter(filters); } 74 75 filterDidChange() { return this._contentViewContainer.currentContentView.filterDidChange(); } 75 match TreeElementAgainstCustomFilters(treeElement) { return this._contentViewContainer.currentContentView.matchTreeElementAgainstCustomFilters(treeElement); }76 matchDataGridNodeAgainstCustomFilters(node) { return this._contentViewContainer.currentContentView.matchDataGridNodeAgainstCustomFilters(node); } 76 77 77 78 // Public … … 197 198 currentContentView.endTime = previousContentView.endTime; 198 199 currentContentView.currentTime = previousContentView.currentTime; 199 200 // FIXME: <https://webkit.org/b/154924> Web Inspector: hook up grid row filtering in the new Timelines UI201 200 } 202 201 }; -
trunk/Source/WebInspectorUI/UserInterface/Views/ScriptDetailsTimelineView.js
r199974 r200067 73 73 74 74 this._dataGrid = new WebInspector.ScriptTimelineDataGrid(columns); 75 this._dataGrid.addEventListener(WebInspector.TimelineDataGrid.Event.FiltersDidChange, this._dataGridFiltersDidChange, this);76 this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridNodeSelected, this);77 75 this._dataGrid.sortDelegate = this; 78 76 this._dataGrid.sortColumnIdentifierSetting = new WebInspector.Setting("script-timeline-view-sort", "startTime"); 79 77 this._dataGrid.sortOrderSetting = new WebInspector.Setting("script-timeline-view-sort-order", WebInspector.DataGrid.SortOrder.Ascending); 78 79 this.setupDataGrid(this._dataGrid); 80 80 81 81 this.element.classList.add("script"); … … 237 237 this.needsLayout(); 238 238 } 239 240 _dataGridFiltersDidChange(event)241 {242 // FIXME: <https://webkit.org/b/154924> Web Inspector: hook up grid row filtering in the new Timelines UI243 }244 245 _dataGridNodeSelected(event)246 {247 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);248 }249 239 }; -
trunk/Source/WebInspectorUI/UserInterface/Views/ScriptTimelineDataGridNode.js
r199974 r200067 157 157 } 158 158 159 // Protected 160 161 filterableDataForColumn(columnIdentifier) 162 { 163 if (columnIdentifier === "name") 164 return [this.displayName(), this.subtitle]; 165 166 return super.filterableDataForColumn(columnIdentifier); 167 } 168 159 169 // Private 160 170 -
trunk/Source/WebInspectorUI/UserInterface/Views/TimelineDataGrid.js
r199974 r200067 26 26 WebInspector.TimelineDataGrid = class TimelineDataGrid extends WebInspector.DataGrid 27 27 { 28 constructor(columns, treeOutline, delegate, editCallback, deleteCallback)28 constructor(columns, treeOutline, synchronizerDelegate, editCallback, deleteCallback) 29 29 { 30 30 super(columns, editCallback, deleteCallback); 31 31 32 32 if (treeOutline) 33 this._treeOutlineDataGridSynchronizer = new WebInspector.TreeOutlineDataGridSynchronizer(treeOutline, this, delegate);33 this._treeOutlineDataGridSynchronizer = new WebInspector.TreeOutlineDataGridSynchronizer(treeOutline, this, synchronizerDelegate); 34 34 35 35 this.element.classList.add("timeline"); 36 36 37 this._filterableColumns = [];38 37 this._sortDelegate = null; 38 this._scopeBarColumns = []; 39 39 40 40 // Check if any of the cells can be filtered. … … 45 45 continue; 46 46 47 this._ filterableColumns.push(identifier);47 this._scopeBarColumns.push(identifier); 48 48 scopeBar.columnIdentifier = identifier; 49 49 scopeBar.addEventListener(WebInspector.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this); 50 50 } 51 51 52 if (this._ filterableColumns.length > 1) {52 if (this._scopeBarColumns.length > 1) { 53 53 console.error("Creating a TimelineDataGrid with more than one filterable column is not yet supported."); 54 54 return; … … 150 150 } 151 151 152 treeElementMatchesActiveScopeFilters(treeElement)153 {154 if (!this._treeOutlineDataGridSynchronizer)155 return false;156 157 var dataGridNode = this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);158 console.assert(dataGridNode);159 160 for (var identifier of this._filterableColumns) {161 var scopeBar = this.columns.get(identifier).scopeBar;162 if (!scopeBar || scopeBar.defaultItem.selected)163 continue;164 165 var value = dataGridNode.data[identifier];166 var matchesFilter = scopeBar.selectedItems.some(function(scopeBarItem) {167 return scopeBarItem.value === value;168 });169 170 if (!matchesFilter)171 return false;172 }173 174 return true;175 }176 177 152 addRowInSortOrder(treeElement, dataGridNode, parentTreeElementOrDataGridNode) 178 153 { … … 234 209 235 210 this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this)); 211 } 212 213 hasCustomFilters() 214 { 215 return true; 216 } 217 218 matchNodeAgainstCustomFilters(node) 219 { 220 if (!super.matchNodeAgainstCustomFilters(node)) 221 return false; 222 223 for (let identifier of this._scopeBarColumns) { 224 let scopeBar = this.columns.get(identifier).scopeBar; 225 if (!scopeBar || scopeBar.defaultItem.selected) 226 continue; 227 228 let value = node.data[identifier]; 229 if (!scopeBar.selectedItems.some((scopeBarItem) => scopeBarItem.value === value)) 230 return false; 231 } 232 233 return true; 236 234 } 237 235 … … 421 419 } 422 420 423 _updateScopeBarForcedVisibility()424 {425 for (var identifier of this._filterableColumns) {426 var scopeBar = this.columns.get(identifier).scopeBar;427 if (scopeBar) {428 this.element.classList.toggle(WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName, scopeBar.hasNonDefaultItemSelected());429 break;430 }431 }432 }433 434 421 _scopeBarSelectedItemsDidChange(event) 435 422 { 436 this._updateScopeBarForcedVisibility(); 437 438 var columnIdentifier = event.target.columnIdentifier; 439 this.dispatchEventToListeners(WebInspector.TimelineDataGrid.Event.FiltersDidChange, {columnIdentifier}); 423 this.filterDidChange(); 440 424 } 441 425 … … 565 549 }; 566 550 551 WebInspector.TimelineDataGrid.WasExpandedDuringFilteringSymbol = Symbol("was-expanded-during-filtering"); 552 567 553 WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter"; 568 554 WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout = 250; 569 555 WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500; 570 571 WebInspector.TimelineDataGrid.Event = {572 FiltersDidChange: "timelinedatagrid-filters-did-change"573 };574 -
trunk/Source/WebInspectorUI/UserInterface/Views/TimelineDataGridNode.js
r197909 r200067 365 365 return true; 366 366 } 367 368 filterableDataForColumn(columnIdentifier) 369 { 370 let value = this.data[columnIdentifier]; 371 if (value instanceof WebInspector.SourceCodeLocation) 372 return value.displayLocationString(); 373 374 if (value instanceof WebInspector.CallFrame) 375 return [value.functionName, value.sourceCodeLocation.displayLocationString()]; 376 377 return super.filterableDataForColumn(columnIdentifier); 378 } 367 379 }; -
trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js
r199972 r200067 54 54 this._filterBarNavigationItem = new WebInspector.FilterBarNavigationItem; 55 55 this._filterBarNavigationItem.filterBar.placeholder = WebInspector.UIString("Filter Records"); 56 this._filterBarNavigationItem.filterBar.addEventListener(WebInspector.FilterBar.Event.FilterDidChange, this._filterDidChange, this); 56 57 this._timelineContentBrowser.navigationBar.addNavigationItem(this._filterBarNavigationItem); 57 58 this.addSubview(this._timelineContentBrowser); … … 86 87 WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this); 87 88 89 WebInspector.TimelineView.addEventListener(WebInspector.TimelineView.Event.RecordWasFiltered, this._recordWasFiltered, this); 90 88 91 for (let instrument of this._recording.instruments) 89 92 this._instrumentAdded(instrument); … … 214 217 { 215 218 this._timelineContentBrowser.goForward(); 216 }217 218 filterDidChange()219 {220 if (!this.currentTimelineView)221 return;222 223 this.currentTimelineView.filterDidChange();224 }225 226 recordWasFiltered(record, filtered)227 {228 if (!this.currentTimelineView)229 return;230 231 this._timelineOverview.recordWasFiltered(this.currentTimelineView.representedObject, record, filtered);232 }233 234 matchTreeElementAgainstCustomFilters(treeElement)235 {236 if (this.currentTimelineView && !this.currentTimelineView.matchTreeElementAgainstCustomFilters(treeElement))237 return false;238 239 let startTime = this._timelineOverview.selectionStartTime;240 let endTime = startTime + this._timelineOverview.selectionDuration;241 let currentTime = this._currentTime || this._recording.startTime;242 243 if (this._timelineOverview.viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames) {244 console.assert(this._renderingFrameTimeline);245 246 if (this._renderingFrameTimeline && this._renderingFrameTimeline.records.length) {247 let records = this._renderingFrameTimeline.records;248 let startIndex = this._timelineOverview.timelineRuler.snapInterval ? startTime : Math.floor(startTime);249 if (startIndex >= records.length)250 return false;251 252 let endIndex = this._timelineOverview.timelineRuler.snapInterval ? endTime - 1: Math.floor(endTime);253 endIndex = Math.min(endIndex, records.length - 1);254 console.assert(startIndex <= endIndex, startIndex);255 256 startTime = records[startIndex].startTime;257 endTime = records[endIndex].endTime;258 }259 }260 261 function checkTimeBounds(itemStartTime, itemEndTime)262 {263 itemStartTime = itemStartTime || currentTime;264 itemEndTime = itemEndTime || currentTime;265 266 return startTime <= itemEndTime && itemStartTime <= endTime;267 }268 269 if (treeElement instanceof WebInspector.ResourceTreeElement) {270 var resource = treeElement.resource;271 return checkTimeBounds(resource.requestSentTimestamp, resource.finishedOrFailedTimestamp);272 }273 274 if (treeElement instanceof WebInspector.SourceCodeTimelineTreeElement) {275 var sourceCodeTimeline = treeElement.sourceCodeTimeline;276 277 // Do a quick check of the timeline bounds before we check each record.278 if (!checkTimeBounds(sourceCodeTimeline.startTime, sourceCodeTimeline.endTime))279 return false;280 281 for (var record of sourceCodeTimeline.records) {282 if (checkTimeBounds(record.startTime, record.endTime))283 return true;284 }285 286 return false;287 }288 289 if (treeElement instanceof WebInspector.ProfileNodeTreeElement) {290 var profileNode = treeElement.profileNode;291 if (checkTimeBounds(profileNode.startTime, profileNode.endTime))292 return true;293 294 return false;295 }296 297 if (treeElement instanceof WebInspector.TimelineRecordTreeElement) {298 var record = treeElement.record;299 return checkTimeBounds(record.startTime, record.endTime);300 }301 302 console.error("Unknown TreeElement, can't filter by time.");303 return true;304 219 } 305 220 … … 470 385 if (this._timelineOverview.timelineRuler.entireRangeSelected) 471 386 this._updateTimelineViewSelection(this._overviewTimelineView); 472 473 // Filter records on new recording times.474 // FIXME: <https://webkit.org/b/154924> Web Inspector: hook up grid row filtering in the new Timelines UI475 387 476 388 // Force a layout now since we are already in an animation frame and don't need to delay it until the next. … … 700 612 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange); 701 613 } 702 703 // Delay until the next frame to stay in sync with the current timeline view's time-based layout changes.704 requestAnimationFrame(function() {705 var selectedTreeElement = this.currentTimelineView && this.currentTimelineView.navigationSidebarTreeOutline ? this.currentTimelineView.navigationSidebarTreeOutline.selectedTreeElement : null;706 var selectionWasHidden = selectedTreeElement && selectedTreeElement.hidden;707 708 // Filter records on new timeline selection.709 // FIXME: <https://webkit.org/b/154924> Web Inspector: hook up grid row filtering in the new Timelines UI710 711 if (selectedTreeElement && selectedTreeElement.hidden !== selectionWasHidden)712 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);713 }.bind(this));714 614 } 715 615 … … 797 697 this._updateTimelineOverviewHeight(); 798 698 } 699 700 _filterDidChange() 701 { 702 if (!this.currentTimelineView) 703 return; 704 705 this.currentTimelineView.updateFilter(this._filterBarNavigationItem.filterBar.filters); 706 } 707 708 _recordWasFiltered(event) 709 { 710 if (event.target !== this.currentTimelineView) 711 return; 712 713 let record = event.data.record; 714 let filtered = event.data.filtered; 715 this._timelineOverview.recordWasFiltered(this.currentTimelineView.representedObject, record, filtered); 716 } 799 717 }; -
trunk/Source/WebInspectorUI/UserInterface/Views/TimelineView.js
r199972 r200067 44 44 // Public 45 45 46 get navigationItems() 47 { 48 return this._scopeBar ? [this._scopeBar] : []; 49 } 50 46 51 get navigationSidebarTreeOutlineScopeBar() 47 52 { … … 86 91 this._startTime = x; 87 92 93 this._filterTimesDidChange(); 88 94 this.needsLayout(); 89 95 } … … 103 109 this._endTime = x; 104 110 111 this._filterTimesDidChange(); 105 112 this.needsLayout(); 106 113 } … … 129 136 } 130 137 131 if (checkIfLayoutIsNeeded.call(this, oldCurrentTime) || checkIfLayoutIsNeeded.call(this, this._currentTime)) 138 if (checkIfLayoutIsNeeded.call(this, oldCurrentTime) || checkIfLayoutIsNeeded.call(this, this._currentTime)) { 139 this._filterTimesDidChange(); 132 140 this.needsLayout(); 133 } 134 135 reset() 136 { 137 // Implemented by sub-classes if needed. 138 } 139 140 filterDidChange() 141 { 142 // Implemented by sub-classes if needed. 141 } 142 } 143 144 get filterStartTime() 145 { 146 // Implemented by sub-classes if needed. 147 return this.startTime; 148 } 149 150 get filterEndTime() 151 { 152 // Implemented by sub-classes if needed. 153 return this.endTime; 154 } 155 156 setupDataGrid(dataGrid) 157 { 158 console.assert(!this._timelineDataGrid); 159 160 this._timelineDataGrid = dataGrid; 161 this._timelineDataGrid.filterDelegate = this; 162 this._timelineDataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, () => { 163 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange); 164 }); 165 166 this._timelineDataGrid.addEventListener(WebInspector.DataGrid.Event.NodeWasFiltered, (event) => { 167 let node = event.data.node; 168 if (!(node instanceof WebInspector.TimelineDataGridNode)) 169 return; 170 171 this.dispatchEventToListeners(WebInspector.TimelineView.Event.RecordWasFiltered, {record: node.record, filtered: node.hidden}); 172 }); 173 174 this._timelineDataGrid.addEventListener(WebInspector.DataGrid.Event.FilterDidChange, (event) => { 175 this.filterDidChange(); 176 }); 143 177 } 144 178 … … 167 201 } 168 202 169 matchTreeElementAgainstCustomFilters(treeElement) 203 reset() 204 { 205 // Implemented by sub-classes if needed. 206 } 207 208 updateFilter(filters) 209 { 210 if (!this._timelineDataGrid) 211 return; 212 213 this._timelineDataGrid.filterText = filters ? filters.text : ""; 214 } 215 216 matchDataGridNodeAgainstCustomFilters(node) 170 217 { 171 218 // Implemented by sub-classes if needed. 172 219 return true; 173 }174 175 filterUpdated()176 {177 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);178 220 } 179 221 … … 187 229 } 188 230 231 // DataGrid filter delegate 232 233 dataGridMatchNodeAgainstCustomFilters(node) 234 { 235 console.assert(node); 236 if (!this.matchDataGridNodeAgainstCustomFilters(node)) 237 return false; 238 239 let startTime = this.filterStartTime; 240 let endTime = this.filterEndTime; 241 let currentTime = this.currentTime; 242 243 function checkTimeBounds(itemStartTime, itemEndTime) 244 { 245 itemStartTime = itemStartTime || currentTime; 246 itemEndTime = itemEndTime || currentTime; 247 248 return startTime <= itemEndTime && itemStartTime <= endTime; 249 } 250 251 if (node instanceof WebInspector.ResourceTimelineDataGridNode) { 252 let resource = node.resource; 253 return checkTimeBounds(resource.requestSentTimestamp, resource.finishedOrFailedTimestamp); 254 } 255 256 if (node instanceof WebInspector.SourceCodeTimelineTimelineDataGridNode) { 257 let sourceCodeTimeline = node.sourceCodeTimeline; 258 259 // Do a quick check of the timeline bounds before we check each record. 260 if (!checkTimeBounds(sourceCodeTimeline.startTime, sourceCodeTimeline.endTime)) 261 return false; 262 263 for (let record of sourceCodeTimeline.records) { 264 if (checkTimeBounds(record.startTime, record.endTime)) 265 return true; 266 } 267 268 return false; 269 } 270 271 if (node instanceof WebInspector.ProfileNodeDataGridNode) { 272 let profileNode = node.profileNode; 273 if (checkTimeBounds(profileNode.startTime, profileNode.endTime)) 274 return true; 275 276 return false; 277 } 278 279 if (node instanceof WebInspector.TimelineDataGridNode) { 280 let record = node.record; 281 return checkTimeBounds(record.startTime, record.endTime); 282 } 283 284 console.error("Unknown DataGridNode, can't filter by time."); 285 return true; 286 } 287 189 288 // Protected 190 289 … … 193 292 // Implemented by sub-classes if needed. 194 293 } 294 295 filterDidChange() 296 { 297 // Implemented by sub-classes if needed. 298 } 299 300 // Private 301 302 _filterTimesDidChange() 303 { 304 if (!this._timelineDataGrid || this._updateFilterTimeout) 305 return; 306 307 function delayedWork() 308 { 309 this._updateFilterTimeout = undefined; 310 this._timelineDataGrid.filterDidChange(); 311 } 312 313 this._updateFilterTimeout = setTimeout(delayedWork.bind(this), 0); 314 } 195 315 }; 316 317 WebInspector.TimelineView.Event = { 318 RecordWasFiltered: "record-was-filtered" 319 };
Note: See TracChangeset
for help on using the changeset viewer.