4a1f930f8debbe7f20fbc7a0f1d50a9b2f258f85
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / NavigationSidebarPanel.js
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
26 WebInspector.NavigationSidebarPanel = function(identifier, displayName, image, keyboardShortcutKey, autoPruneOldTopLevelResourceTreeElements, autoHideToolbarItemWhenEmpty, wantsTopOverflowShadow, element, role, label)
27 {
28     if (keyboardShortcutKey)
29         this._keyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Control, keyboardShortcutKey, this.toggle.bind(this));
30
31     if (this._keyboardShortcut) {
32         var showToolTip = WebInspector.UIString("Show the %s navigation sidebar (%s)").format(displayName, this._keyboardShortcut.displayName);
33         var hideToolTip = WebInspector.UIString("Hide the %s navigation sidebar (%s)").format(displayName, this._keyboardShortcut.displayName);
34     } else {
35         var showToolTip = WebInspector.UIString("Show the %s navigation sidebar").format(displayName);
36         var hideToolTip = WebInspector.UIString("Hide the %s navigation sidebar").format(displayName);
37     }
38
39     WebInspector.SidebarPanel.call(this, identifier, displayName, showToolTip, hideToolTip, image, element, role, label || displayName);
40
41     this.element.classList.add(WebInspector.NavigationSidebarPanel.StyleClassName);
42
43     this._autoHideToolbarItemWhenEmpty = autoHideToolbarItemWhenEmpty || false;
44
45     if (autoHideToolbarItemWhenEmpty)
46         this.toolbarItem.hidden = true;
47
48     this._visibleContentTreeOutlines = new Set;
49
50     this.contentElement.addEventListener("scroll", this._updateContentOverflowShadowVisibility.bind(this));
51
52     this._contentTreeOutline = this.createContentTreeOutline(true);
53
54     this._filterBar = new WebInspector.FilterBar();
55     this._filterBar.addEventListener(WebInspector.FilterBar.Event.TextFilterDidChange, this._textFilterDidChange, this);
56     this.element.appendChild(this._filterBar.element);
57
58     this._bottomOverflowShadowElement = document.createElement("div");
59     this._bottomOverflowShadowElement.className = WebInspector.NavigationSidebarPanel.OverflowShadowElementStyleClassName;
60     this.element.appendChild(this._bottomOverflowShadowElement);
61
62     if (wantsTopOverflowShadow) {
63         this._topOverflowShadowElement = document.createElement("div");
64         this._topOverflowShadowElement.classList.add(WebInspector.NavigationSidebarPanel.OverflowShadowElementStyleClassName);
65         this._topOverflowShadowElement.classList.add(WebInspector.NavigationSidebarPanel.TopOverflowShadowElementStyleClassName);
66         this.element.appendChild(this._topOverflowShadowElement);
67     }
68
69     window.addEventListener("resize", this._updateContentOverflowShadowVisibility.bind(this));
70
71     this._filtersSetting = new WebInspector.Setting(identifier + "-navigation-sidebar-filters", {});
72     this._filterBar.filters = this._filtersSetting.value;
73
74     this._emptyContentPlaceholderElement = document.createElement("div");
75     this._emptyContentPlaceholderElement.className = WebInspector.NavigationSidebarPanel.EmptyContentPlaceholderElementStyleClassName;
76
77     this._emptyContentPlaceholderMessageElement = document.createElement("div");
78     this._emptyContentPlaceholderMessageElement.className = WebInspector.NavigationSidebarPanel.EmptyContentPlaceholderMessageElementStyleClassName;
79     this._emptyContentPlaceholderElement.appendChild(this._emptyContentPlaceholderMessageElement);
80
81     this._generateStyleRulesIfNeeded();
82     this._generateDisclosureTrianglesIfNeeded();
83
84     if (autoPruneOldTopLevelResourceTreeElements) {
85         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._checkForOldResources, this);
86         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ChildFrameWasRemoved, this._checkForOldResources, this);
87         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasRemoved, this._checkForOldResources, this);
88     }
89 };
90
91 WebInspector.NavigationSidebarPanel.StyleClassName = "navigation";
92 WebInspector.NavigationSidebarPanel.OverflowShadowElementStyleClassName = "overflow-shadow";
93 WebInspector.NavigationSidebarPanel.TopOverflowShadowElementStyleClassName = "top";
94 WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementHiddenStyleClassName = "hidden";
95 WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementStyleClassName = "navigation-sidebar-panel-content-tree-outline";
96 WebInspector.NavigationSidebarPanel.HideDisclosureButtonsStyleClassName = "hide-disclosure-buttons";
97 WebInspector.NavigationSidebarPanel.EmptyContentPlaceholderElementStyleClassName = "empty-content-placeholder";
98 WebInspector.NavigationSidebarPanel.EmptyContentPlaceholderMessageElementStyleClassName = "message";
99 WebInspector.NavigationSidebarPanel.DisclosureTriangleOpenCanvasIdentifier = "navigation-sidebar-panel-disclosure-triangle-open";
100 WebInspector.NavigationSidebarPanel.DisclosureTriangleClosedCanvasIdentifier = "navigation-sidebar-panel-disclosure-triangle-closed";
101 WebInspector.NavigationSidebarPanel.DisclosureTriangleNormalCanvasIdentifierSuffix = "-normal";
102 WebInspector.NavigationSidebarPanel.DisclosureTriangleSelectedCanvasIdentifierSuffix = "-selected";
103
104 WebInspector.NavigationSidebarPanel.prototype = {
105     constructor: WebInspector.NavigationSidebarPanel,
106     __proto__: WebInspector.SidebarPanel.prototype,
107
108     // Public
109
110     get contentTreeOutlineElement()
111     {
112         return this._contentTreeOutline.element;
113     },
114
115     get contentTreeOutline()
116     {
117         return this._contentTreeOutline;
118     },
119
120     set contentTreeOutline(newTreeOutline)
121     {
122         console.assert(newTreeOutline);
123         if (!newTreeOutline)
124             return;
125
126         if (this._contentTreeOutline)
127             this._contentTreeOutline.element.classList.add(WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementHiddenStyleClassName);
128
129         this._contentTreeOutline = newTreeOutline;
130         this._contentTreeOutline.element.classList.remove(WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementHiddenStyleClassName);
131
132         this._visibleContentTreeOutlines.delete(this._contentTreeOutline);
133         this._visibleContentTreeOutlines.add(newTreeOutline);
134
135         this._updateFilter();
136     },
137
138     get contentTreeOutlineToAutoPrune()
139     {
140         return this._contentTreeOutline;
141     },
142
143     get visibleContentTreeOutlines()
144     {
145         return this._visibleContentTreeOutlines;
146     },
147
148     get hasSelectedElement()
149     {
150         return !!this._contentTreeOutline.selectedTreeElement;
151     },
152
153     get filterBar()
154     {
155         return this._filterBar;
156     },
157
158     get restoringState()
159     {
160         return this._restoringState;
161     },
162
163     createContentTreeOutline: function(dontHideByDefault, suppressFiltering)
164     {
165         var contentTreeOutlineElement = document.createElement("ol");
166         contentTreeOutlineElement.className = WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementStyleClassName;
167         if (!dontHideByDefault)
168             contentTreeOutlineElement.classList.add(WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementHiddenStyleClassName);
169         this.contentElement.appendChild(contentTreeOutlineElement);
170
171         var contentTreeOutline = new TreeOutline(contentTreeOutlineElement);
172         contentTreeOutline.allowsRepeatSelection = true;
173
174         if (!suppressFiltering) {
175             contentTreeOutline.onadd = this._treeElementAddedOrChanged.bind(this);
176             contentTreeOutline.onchange = this._treeElementAddedOrChanged.bind(this);
177             contentTreeOutline.onexpand = this._treeElementExpandedOrCollapsed.bind(this);
178             contentTreeOutline.oncollapse = this._treeElementExpandedOrCollapsed.bind(this);
179         }
180
181         if (dontHideByDefault)
182             this._visibleContentTreeOutlines.add(contentTreeOutline);
183
184         return contentTreeOutline;
185     },
186
187     treeElementForRepresentedObject: function(representedObject)
188     {
189         return this._contentTreeOutline.getCachedTreeElement(representedObject);
190     },
191
192     showDefaultContentView: function()
193     {
194         // Implemneted by subclasses if needed to show a content view when no existing tree element is selected.
195     },
196
197     showContentViewForCurrentSelection: function()
198     {
199         // Reselect the selected tree element to cause the content view to be shown as well. <rdar://problem/10854727>
200         var selectedTreeElement = this._contentTreeOutline.selectedTreeElement;
201         if (selectedTreeElement)
202             selectedTreeElement.select();
203     },
204
205     saveStateToCookie: function(cookie)
206     {
207         console.assert(cookie);
208
209         // This does not save folder selections, which lack a represented object and content view.
210         var selectedTreeElement = null;
211         this._visibleContentTreeOutlines.forEach(function(outline) {
212             if (outline.selectedTreeElement)
213                 selectedTreeElement = outline.selectedTreeElement;
214         });
215
216         if (!selectedTreeElement)
217             return;
218
219         if (this._isTreeElementWithoutRepresentedObject(selectedTreeElement))
220             return;
221
222         var representedObject = selectedTreeElement.representedObject;
223         cookie[WebInspector.TypeIdentifierCookieKey] = representedObject.constructor.TypeIdentifier;
224
225         if (representedObject.saveIdentityToCookie)
226             representedObject.saveIdentityToCookie(cookie);
227         else
228             console.error("Error: TreeElement.representedObject is missing a saveIdentityToCookie implementation. TreeElement.constructor: ", selectedTreeElement.constructor);
229     },
230
231     // This can be supplemented by subclasses that admit a simpler strategy for static tree elements.
232     restoreStateFromCookie: function(cookie, relaxedMatchDelay)
233     {
234         this._pendingViewStateCookie = cookie;
235         this._restoringState = true;
236
237         // Check if any existing tree elements in any outline match the cookie.
238         this._checkOutlinesForPendingViewStateCookie();
239
240         if (this._finalAttemptToRestoreViewStateTimeout)
241             clearTimeout(this._finalAttemptToRestoreViewStateTimeout);
242
243         function finalAttemptToRestoreViewStateFromCookie()
244         {
245             delete this._finalAttemptToRestoreViewStateTimeout;
246
247             this._checkOutlinesForPendingViewStateCookie(true);
248
249             delete this._pendingViewStateCookie;
250             delete this._restoringState;
251         }
252
253         // If the specific tree element wasn't found, we may need to wait for the resources
254         // to be registered. We try one last time (match type only) after an arbitrary amount of timeout.
255         this._finalAttemptToRestoreViewStateTimeout = setTimeout(finalAttemptToRestoreViewStateFromCookie.bind(this), relaxedMatchDelay);
256     },
257
258     showEmptyContentPlaceholder: function(message, hideToolbarItem)
259     {
260         console.assert(message);
261
262         if (this._emptyContentPlaceholderElement.parentNode && this._emptyContentPlaceholderMessageElement.textContent === message)
263             return;
264
265         this._emptyContentPlaceholderMessageElement.textContent = message;
266         this.element.appendChild(this._emptyContentPlaceholderElement);
267
268         this._hideToolbarItemWhenEmpty = hideToolbarItem || false;
269         this._updateToolbarItemVisibility();
270         this._updateContentOverflowShadowVisibility();
271     },
272
273     hideEmptyContentPlaceholder: function()
274     {
275         if (!this._emptyContentPlaceholderElement.parentNode)
276             return;
277
278         this._emptyContentPlaceholderElement.parentNode.removeChild(this._emptyContentPlaceholderElement);
279
280         this._hideToolbarItemWhenEmpty = false;
281         this._updateToolbarItemVisibility();
282         this._updateContentOverflowShadowVisibility();
283     },
284
285     updateEmptyContentPlaceholder: function(message)
286     {
287         this._updateToolbarItemVisibility();
288
289         if (!this._contentTreeOutline.children.length) {
290             // No tree elements, so no results.
291             this.showEmptyContentPlaceholder(message);
292         } else if (!this._emptyFilterResults) {
293             // There are tree elements, and not all of them are hidden by the filter.
294             this.hideEmptyContentPlaceholder();
295         }
296     },
297
298     updateFilter: function()
299     {
300         this._updateFilter();
301     },
302
303     hasCustomFilters: function()
304     {
305         // Implemented by subclasses if needed.
306         return false;
307     },
308
309     matchTreeElementAgainstCustomFilters: function(treeElement)
310     {
311         // Implemented by subclasses if needed.
312         return true;
313     },
314
315     applyFiltersToTreeElement: function(treeElement)
316     {
317         if (!this._filterBar.hasActiveFilters() && !this.hasCustomFilters()) {
318             // No filters, so make everything visible.
319             treeElement.hidden = false;
320
321             // If this tree element was expanded during filtering, collapse it again.
322             if (treeElement.expanded && treeElement.__wasExpandedDuringFiltering) {
323                 delete treeElement.__wasExpandedDuringFiltering;
324                 treeElement.collapse();
325             }
326
327             return;
328         }
329
330         var filterableData = treeElement.filterableData || {};
331
332         var matchedBuiltInFilters = false;
333
334         var self = this;
335         function matchTextFilter(inputs)
336         {
337             if (!inputs || !self._textFilterRegex)
338                 return true;
339
340             // Convert to a single item array if needed.
341             if (!(inputs instanceof Array))
342                 inputs = [inputs];
343
344             // Loop over all the inputs and try to match them.
345             for (var input of inputs) {
346                 if (!input)
347                     continue;
348                 if (self._textFilterRegex.test(input)) {
349                     matchedBuiltInFilters = true;
350                     return true;
351                 }
352             }
353
354             // No inputs matched.
355             return false;
356         }
357
358         function makeVisible()
359         {
360             // Make this element visible.
361             treeElement.hidden = false;
362
363             // Make the ancestors visible and expand them.
364             var currentAncestor = treeElement.parent;
365             while (currentAncestor && !currentAncestor.root) {
366                 currentAncestor.hidden = false;
367
368                 // Only expand if the built-in filters matched, not custom filters.
369                 if (matchedBuiltInFilters && !currentAncestor.expanded) {
370                     currentAncestor.__wasExpandedDuringFiltering = true;
371                     currentAncestor.expand();
372                 }
373
374                 currentAncestor = currentAncestor.parent;
375             }
376         }
377
378         if (matchTextFilter(filterableData.text) && this.matchTreeElementAgainstCustomFilters(treeElement)) {
379             // Make this element visible since it matches.
380             makeVisible();
381
382             // If this tree element didn't match a built-in filter and was expanded earlier during filtering, collapse it again.
383             if (!matchedBuiltInFilters && treeElement.expanded && treeElement.__wasExpandedDuringFiltering) {
384                 delete treeElement.__wasExpandedDuringFiltering;
385                 treeElement.collapse();
386             }
387
388             return;
389         }
390
391         // Make this element invisible since it does not match.
392         treeElement.hidden = true;
393     },
394
395     show: function()
396     {
397         if (!this.parentSidebar)
398             return;
399
400         WebInspector.SidebarPanel.prototype.show.call(this);
401
402         this.contentTreeOutlineElement.focus();
403     },
404
405     shown: function()
406     {
407         WebInspector.SidebarPanel.prototype.shown.call(this);
408
409         this._updateContentOverflowShadowVisibility();
410
411         // Force the navigation item to be visible. This makes sure it is
412         // always visible when the panel is shown.
413         this.toolbarItem.hidden = false;
414     },
415
416     hidden: function()
417     {
418         WebInspector.SidebarPanel.prototype.hidden.call(this);
419
420         this._updateToolbarItemVisibility();
421     },
422
423     // Private
424     
425     _updateContentOverflowShadowVisibilitySoon: function()
426     {
427         if (this._updateContentOverflowShadowVisibilityIdentifier)
428             return;
429
430         this._updateContentOverflowShadowVisibilityIdentifier = setTimeout(this._updateContentOverflowShadowVisibility.bind(this), 0);
431     },
432
433     _updateContentOverflowShadowVisibility: function()
434     {
435         delete this._updateContentOverflowShadowVisibilityIdentifier;
436
437         var scrollHeight = this.contentElement.scrollHeight;
438         var offsetHeight = this.contentElement.offsetHeight;
439
440         if (scrollHeight < offsetHeight) {
441             if (this._topOverflowShadowElement)
442                 this._topOverflowShadowElement.style.opacity = 0;
443             this._bottomOverflowShadowElement.style.opacity = 0;
444             return;
445         }
446
447         if (WebInspector.Platform.isLegacyMacOS)
448             const edgeThreshold = 10;
449         else
450             const edgeThreshold = 1;
451
452         var scrollTop = this.contentElement.scrollTop;
453
454         var topCoverage = Math.min(scrollTop, edgeThreshold);
455         var bottomCoverage = Math.max(0, (offsetHeight + scrollTop) - (scrollHeight - edgeThreshold));
456
457         if (this._topOverflowShadowElement)
458             this._topOverflowShadowElement.style.opacity = (topCoverage / edgeThreshold).toFixed(1);
459         this._bottomOverflowShadowElement.style.opacity = (1 - (bottomCoverage / edgeThreshold)).toFixed(1);
460     },
461
462     _updateToolbarItemVisibility: function()
463     {
464         // Hide the navigation item if requested or auto-hiding and we are not visible and we are empty.
465         var shouldHide = ((this._hideToolbarItemWhenEmpty || this._autoHideToolbarItemWhenEmpty) && !this.selected && !this._contentTreeOutline.children.length);
466         this.toolbarItem.hidden = shouldHide;
467     },
468
469     _checkForEmptyFilterResults: function()
470     {
471         // No tree elements, so don't touch the empty content placeholder.
472         if (!this._contentTreeOutline.children.length)
473             return;
474
475         // Iterate over all the top level tree elements. If any are visible, return early.
476         var currentTreeElement = this._contentTreeOutline.children[0];
477         while (currentTreeElement) {
478             if (!currentTreeElement.hidden) {
479                 // Not hidden, so hide any empty content message.
480                 this.hideEmptyContentPlaceholder();
481                 this._emptyFilterResults = false;
482                 return;
483             }
484
485             currentTreeElement = currentTreeElement.nextSibling;
486         }
487
488         // All top level tree elements are hidden, so filtering hid everything. Show a message.
489         this.showEmptyContentPlaceholder(WebInspector.UIString("No Filter Results"));
490         this._emptyFilterResults = true;
491     },
492
493     _textFilterDidChange: function()
494     {
495         this._updateFilter();
496     },
497
498     _updateFilter: function()
499     {
500         var selectedTreeElement = this._contentTreeOutline.selectedTreeElement;
501         var selectionWasHidden = selectedTreeElement && selectedTreeElement.hidden;
502
503         var filters = this._filterBar.filters;
504         this._textFilterRegex = simpleGlobStringToRegExp(filters.text, "i");
505         this._filtersSetting.value = filters;
506
507         // Don't populate if we don't have any active filters.
508         // We only need to populate when a filter needs to reveal.
509         var dontPopulate = !this._filterBar.hasActiveFilters();
510
511         // Update the whole tree.
512         var currentTreeElement = this._contentTreeOutline.children[0];
513         while (currentTreeElement && !currentTreeElement.root) {
514             this.applyFiltersToTreeElement(currentTreeElement);
515             currentTreeElement = currentTreeElement.traverseNextTreeElement(false, null, dontPopulate);
516         }
517
518         this._checkForEmptyFilterResults();
519         this._updateContentOverflowShadowVisibility();
520
521         // Filter may have hidden the selected resource in the timeline view, which should now notify its listeners.
522         if (selectedTreeElement && selectedTreeElement.hidden !== selectionWasHidden) {
523             var currentContentView = WebInspector.contentBrowser.currentContentView;
524             if (currentContentView instanceof WebInspector.TimelineRecordingContentView && typeof currentContentView.currentTimelineView.filterUpdated === "function")
525                 currentContentView.currentTimelineView.filterUpdated();
526         }
527     },
528
529     _treeElementAddedOrChanged: function(treeElement)
530     {
531         // Don't populate if we don't have any active filters.
532         // We only need to populate when a filter needs to reveal.
533         var dontPopulate = !this._filterBar.hasActiveFilters();
534
535         // Apply the filters to the tree element and its descendants.
536         var currentTreeElement = treeElement;
537         while (currentTreeElement && !currentTreeElement.root) {
538             this.applyFiltersToTreeElement(currentTreeElement);
539             currentTreeElement = currentTreeElement.traverseNextTreeElement(false, treeElement, dontPopulate);
540         }
541
542         this._checkForEmptyFilterResults();
543         this._updateContentOverflowShadowVisibilitySoon();
544
545         if (this.selected)
546             this._checkElementsForPendingViewStateCookie(treeElement);
547     },
548
549     _treeElementExpandedOrCollapsed: function(treeElement)
550     {
551         this._updateContentOverflowShadowVisibility();
552     },
553
554     _generateStyleRulesIfNeeded: function()
555     {
556         if (WebInspector.NavigationSidebarPanel._styleElement)
557             return;
558
559         WebInspector.NavigationSidebarPanel._styleElement = document.createElement("style");
560
561         const maximumSidebarTreeDepth = 32;
562         const baseLeftPadding = 5; // Matches the padding in NavigationSidebarPanel.css for the item class. Keep in sync.
563         const depthPadding = 10;
564
565         var styleText = "";
566         var childrenSubstring = "";
567         for (var i = 1; i <= maximumSidebarTreeDepth; ++i) {
568             // Keep all the elements at the same depth once the maximum is reached.
569             childrenSubstring += i === maximumSidebarTreeDepth ? " .children" : " > .children";
570             styleText += "." + WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementStyleClassName + childrenSubstring + " > .item { ";
571             styleText += "padding-left: " + (baseLeftPadding + (depthPadding * i)) + "px; }\n";
572         }
573
574         WebInspector.NavigationSidebarPanel._styleElement.textContent = styleText;
575
576         document.head.appendChild(WebInspector.NavigationSidebarPanel._styleElement);
577     },
578
579     _generateDisclosureTrianglesIfNeeded: function()
580     {
581         if (WebInspector.NavigationSidebarPanel._generatedDisclosureTriangles)
582             return;
583
584         // Set this early instead of in _generateDisclosureTriangle because we don't want multiple panels that are
585         // created at the same time to duplicate the work (even though it would be harmless.)
586         WebInspector.NavigationSidebarPanel._generatedDisclosureTriangles = true;
587
588         var specifications = {};
589
590         if (WebInspector.Platform.isLegacyMacOS) {
591             specifications[WebInspector.NavigationSidebarPanel.DisclosureTriangleNormalCanvasIdentifierSuffix] = {
592                 fillColor: [112, 126, 139],
593                 shadowColor: [255, 255, 255, 0.8],
594                 shadowOffsetX: 0,
595                 shadowOffsetY: 1,
596                 shadowBlur: 0
597             };
598
599             specifications[WebInspector.NavigationSidebarPanel.DisclosureTriangleSelectedCanvasIdentifierSuffix] = {
600                 fillColor: [255, 255, 255],
601                 shadowColor: [61, 91, 110, 0.8],
602                 shadowOffsetX: 0,
603                 shadowOffsetY: 1,
604                 shadowBlur: 2
605             };
606         } else {
607             specifications[WebInspector.NavigationSidebarPanel.DisclosureTriangleNormalCanvasIdentifierSuffix] = {
608                 fillColor: [140, 140, 140]
609             };
610
611             specifications[WebInspector.NavigationSidebarPanel.DisclosureTriangleSelectedCanvasIdentifierSuffix] = {
612                 fillColor: [255, 255, 255]
613             };
614         }
615
616         generateColoredImagesForCSS("Images/DisclosureTriangleSmallOpen.svg", specifications, 13, 13, WebInspector.NavigationSidebarPanel.DisclosureTriangleOpenCanvasIdentifier);
617         generateColoredImagesForCSS("Images/DisclosureTriangleSmallClosed.svg", specifications, 13, 13, WebInspector.NavigationSidebarPanel.DisclosureTriangleClosedCanvasIdentifier);
618     },
619
620     _checkForOldResources: function(event)
621     {
622         if (this._checkForOldResourcesTimeoutIdentifier)
623             return;
624
625         function delayedWork()
626         {
627             delete this._checkForOldResourcesTimeoutIdentifier;
628
629             var contentTreeOutline = this.contentTreeOutlineToAutoPrune;
630
631             // Check all the ResourceTreeElements at the top level to make sure their Resource still has a parentFrame in the frame hierarchy.
632             // If the parentFrame is no longer in the frame hierarchy we know it was removed due to a navigation or some other page change and
633             // we should remove the issues for that resource.
634             for (var i = contentTreeOutline.children.length - 1; i >= 0; --i) {
635                 var treeElement = contentTreeOutline.children[i];
636                 if (!(treeElement instanceof WebInspector.ResourceTreeElement))
637                     continue;
638
639                 var resource = treeElement.resource;
640                 if (!resource.parentFrame || resource.parentFrame.isDetached())
641                     contentTreeOutline.removeChildAtIndex(i, true, true);
642             }
643
644             if (typeof this._updateEmptyContentPlaceholder === "function")
645                 this._updateEmptyContentPlaceholder();
646         }
647
648         // Check on a delay to coalesce multiple calls to _checkForOldResources.
649         this._checkForOldResourcesTimeoutIdentifier = setTimeout(delayedWork.bind(this), 0);
650     },
651
652     _isTreeElementWithoutRepresentedObject: function(treeElement)
653     {
654         return treeElement instanceof WebInspector.FolderTreeElement
655             || treeElement instanceof WebInspector.DatabaseHostTreeElement
656             || typeof treeElement.representedObject === "string"
657             || treeElement.representedObject instanceof String;
658     },
659
660     _checkOutlinesForPendingViewStateCookie: function(matchTypeOnly)
661     {
662         if (!this._pendingViewStateCookie)
663             return;
664
665         var visibleTreeElements = [];
666         this._visibleContentTreeOutlines.forEach(function(outline) {
667             var currentTreeElement = outline.hasChildren ? outline.children[0] : null;
668             while (currentTreeElement) {
669                 visibleTreeElements.push(currentTreeElement);
670                 currentTreeElement = currentTreeElement.traverseNextTreeElement(false, null, false);
671             }
672         });
673
674         return this._checkElementsForPendingViewStateCookie(visibleTreeElements, matchTypeOnly);
675     },
676
677     _checkElementsForPendingViewStateCookie: function(treeElements, matchTypeOnly)
678     {
679         if (!this._pendingViewStateCookie)
680             return;
681
682         var cookie = this._pendingViewStateCookie;
683
684         function treeElementMatchesCookie(treeElement)
685         {
686             if (this._isTreeElementWithoutRepresentedObject(treeElement))
687                 return false;
688
689             var representedObject = treeElement.representedObject;
690             if (!representedObject)
691                 return false;
692
693             var typeIdentifier = cookie[WebInspector.TypeIdentifierCookieKey];
694             if (typeIdentifier !== representedObject.constructor.TypeIdentifier)
695                 return false;
696
697             if (matchTypeOnly)
698                 return true;
699
700             var candidateObjectCookie = {};
701             if (representedObject.saveIdentityToCookie)
702                 representedObject.saveIdentityToCookie(candidateObjectCookie);
703
704             var candidateCookieKeys = Object.keys(candidateObjectCookie);
705             return candidateCookieKeys.length && candidateCookieKeys.every(function valuesMatchForKey(key) {
706                 return candidateObjectCookie[key] === cookie[key];
707             });
708         }
709
710         if (!(treeElements instanceof Array))
711             treeElements = [treeElements];
712
713         var matchedElement = null;
714         treeElements.some(function(element) {
715             if (treeElementMatchesCookie.call(this, element)) {
716                 matchedElement = element;
717                 return true;
718             }
719         }, this);
720
721         if (matchedElement) {
722             matchedElement.revealAndSelect();
723
724             delete this._pendingViewStateCookie;
725
726             // Delay clearing the restoringState flag until the next runloop so listeners
727             // checking for it in this runloop still know state was being restored.
728             setTimeout(function() {
729                 delete this._restoringState;
730             }.bind(this));
731
732             if (this._finalAttemptToRestoreViewStateTimeout) {
733                 clearTimeout(this._finalAttemptToRestoreViewStateTimeout);
734                 delete this._finalAttemptToRestoreViewStateTimeout;
735             }
736         }
737     }
738 };