Web Inspector: Inspector frequently restores wrong view when opened (often Timelines...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / FrameTreeElement.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.FrameTreeElement = function(frame, representedObject)
27 {
28     console.assert(frame instanceof WebInspector.Frame);
29
30     WebInspector.ResourceTreeElement.call(this, frame.mainResource, representedObject || frame);
31
32     this._frame = frame;
33     this._newChildQueue = [];
34
35     this._updateExpandedSetting();
36
37     frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
38     frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
39     frame.addEventListener(WebInspector.Frame.Event.ResourceWasRemoved, this._resourceWasRemoved, this);
40     frame.addEventListener(WebInspector.Frame.Event.ChildFrameWasAdded, this._childFrameWasAdded, this);
41     frame.addEventListener(WebInspector.Frame.Event.ChildFrameWasRemoved, this._childFrameWasRemoved, this);
42
43     frame.domTree.addEventListener(WebInspector.DOMTree.Event.ContentFlowWasAdded, this._childContentFlowWasAdded, this);
44     frame.domTree.addEventListener(WebInspector.DOMTree.Event.ContentFlowWasRemoved, this._childContentFlowWasRemoved, this);
45     frame.domTree.addEventListener(WebInspector.DOMTree.Event.RootDOMNodeInvalidated, this._rootDOMNodeInvalidated, this);
46
47     if (this._frame.isMainFrame()) {
48         this._downloadingPage = false;
49         WebInspector.notifications.addEventListener(WebInspector.Notification.PageArchiveStarted, this._pageArchiveStarted, this);
50         WebInspector.notifications.addEventListener(WebInspector.Notification.PageArchiveEnded, this._pageArchiveEnded, this);
51     }
52
53     this._updateParentStatus();
54     this.shouldRefreshChildren = true;
55 };
56
57 WebInspector.FrameTreeElement.MediumChildCountThreshold = 5;
58 WebInspector.FrameTreeElement.LargeChildCountThreshold = 15;
59 WebInspector.FrameTreeElement.NumberOfMediumCategoriesThreshold = 2;
60 WebInspector.FrameTreeElement.NewChildQueueUpdateInterval = 500;
61
62 WebInspector.FrameTreeElement.prototype = {
63     constructor: WebInspector.FrameTreeElement,
64
65     // Public
66
67     get frame()
68     {
69         return this._frame;
70     },
71
72     descendantResourceTreeElementTypeDidChange: function(resourceTreeElement, oldType)
73     {
74         // Called by descendant ResourceTreeElements.
75
76         // Add the tree element again, which will move it to the new location
77         // based on sorting and possible folder changes.
78         this._addTreeElement(resourceTreeElement);
79     },
80
81     descendantResourceTreeElementMainTitleDidChange: function(resourceTreeElement, oldMainTitle)
82     {
83         // Called by descendant ResourceTreeElements.
84
85         // Add the tree element again, which will move it to the new location
86         // based on sorting and possible folder changes.
87         this._addTreeElement(resourceTreeElement);
88     },
89
90     // Overrides from SourceCodeTreeElement.
91
92     updateSourceMapResources: function()
93     {
94         // Frames handle their own SourceMapResources.
95
96         if (!this.treeOutline || !this.treeOutline.includeSourceMapResourceChildren)
97             return;
98
99         if (!this._frame)
100             return;
101
102         this._updateParentStatus();
103
104         if (this.resource && this.resource.sourceMaps.length)
105             this.shouldRefreshChildren = true;
106     },
107
108     onattach: function()
109     {
110         // Frames handle their own SourceMapResources.
111
112         WebInspector.GeneralTreeElement.prototype.onattach.call(this);
113     },
114
115     // Called from ResourceTreeElement.
116
117     updateStatusForMainFrame: function()
118     {
119         function loadedImages()
120         {
121             if (!this._reloadButton || !this._downloadButton)
122                 return;
123
124             var fragment = document.createDocumentFragment("div");
125             fragment.appendChild(this._downloadButton.element);
126             fragment.appendChild(this._reloadButton.element);
127             this.status = fragment;
128
129             delete this._loadingMainFrameButtons;
130         }
131
132         if (this._reloadButton && this._downloadButton) {
133             loadedImages.call(this);
134             return;
135         }
136
137         if (!this._loadingMainFrameButtons) {
138             this._loadingMainFrameButtons = true;
139
140             var tooltip = WebInspector.UIString("Reload page (%s)\nReload ignoring cache (%s)").format(WebInspector._reloadPageKeyboardShortcut.displayName, WebInspector._reloadPageIgnoringCacheKeyboardShortcut.displayName);
141             wrappedSVGDocument(platformImagePath("Reload.svg"), null, tooltip, function(element) {
142                 this._reloadButton = new WebInspector.TreeElementStatusButton(element);
143                 this._reloadButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._reloadPageClicked, this);
144                 loadedImages.call(this);
145             }.bind(this));
146
147             wrappedSVGDocument(platformImagePath("DownloadArrow.svg"), null, WebInspector.UIString("Download Web Archive"), function(element) {
148                 this._downloadButton = new WebInspector.TreeElementStatusButton(element);
149                 this._downloadButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._downloadButtonClicked, this);
150                 this._updateDownloadButton();
151                 loadedImages.call(this);
152             }.bind(this));
153         }
154     },
155
156     // Overrides from TreeElement (Private).
157
158     onpopulate: function()
159     {
160         if (this.children.length && !this.shouldRefreshChildren)
161             return;
162
163         this.shouldRefreshChildren = false;
164
165         this.removeChildren();
166         this._clearNewChildQueue();
167
168         if (this._shouldGroupIntoFolders() && !this._groupedIntoFolders)
169             this._groupedIntoFolders = true;
170
171         for (var i = 0; i < this._frame.childFrames.length; ++i)
172             this._addTreeElementForRepresentedObject(this._frame.childFrames[i]);
173
174         for (var i = 0; i < this._frame.resources.length; ++i)
175             this._addTreeElementForRepresentedObject(this._frame.resources[i]);
176
177         var sourceMaps = this.resource && this.resource.sourceMaps;
178         for (var i = 0; i < sourceMaps.length; ++i) {
179             var sourceMap = sourceMaps[i];
180             for (var j = 0; j < sourceMap.resources.length; ++j)
181             this._addTreeElementForRepresentedObject(sourceMap.resources[j]);
182         }
183
184         var flowMap = this._frame.domTree.flowMap;
185         for (var flowKey in flowMap)
186             this._addTreeElementForRepresentedObject(flowMap[flowKey]);
187     },
188
189     onexpand: function()
190     {
191         this._expandedSetting.value = true;
192         this._frame.domTree.requestContentFlowList();
193     },
194
195     oncollapse: function()
196     {
197         // Only store the setting if we have children, since setting hasChildren to false will cause a collapse,
198         // and we only care about user triggered collapses.
199         if (this.hasChildren)
200             this._expandedSetting.value = false;
201     },
202
203     removeChildren: function()
204     {
205         TreeElement.prototype.removeChildren.call(this);
206
207         if (this._framesFolderTreeElement)
208             this._framesFolderTreeElement.removeChildren();
209
210         for (var type in this._resourceFoldersTypeMap)
211             this._resourceFoldersTypeMap[type].removeChildren();
212
213         delete this._resourceFoldersTypeMap;
214         delete this._framesFolderTreeElement;
215     },
216
217     // Private
218
219     _updateExpandedSetting: function()
220     {
221         this._expandedSetting = new WebInspector.Setting("frame-expanded-" + this._frame.url.hash, this._frame.isMainFrame() ? true : false);
222         if (this._expandedSetting.value)
223             this.expand();
224         else
225             this.collapse();
226     },
227
228     _updateParentStatus: function()
229     {
230         this.hasChildren = (this._frame.resources.length || this._frame.childFrames.length || (this.resource && this.resource.sourceMaps.length));
231         if (!this.hasChildren)
232             this.removeChildren();
233     },
234
235     _mainResourceDidChange: function(event)
236     {
237         this._updateResource(this._frame.mainResource);
238         this._updateParentStatus();
239
240         this._groupedIntoFolders = false;
241
242         this._clearNewChildQueue();
243
244         this.removeChildren();
245
246         // Change the expanded setting since the frame URL has changed. Do this before setting shouldRefreshChildren, since
247         // shouldRefreshChildren will call onpopulate if expanded is true.
248         this._updateExpandedSetting();
249
250         if (this._frame.isMainFrame())
251             this._updateDownloadButton();
252
253         this.shouldRefreshChildren = true;
254     },
255
256     _resourceWasAdded: function(event)
257     {
258         this._addRepresentedObjectToNewChildQueue(event.data.resource);
259     },
260
261     _resourceWasRemoved: function(event)
262     {
263         this._removeChildForRepresentedObject(event.data.resource);
264     },
265
266     _childFrameWasAdded: function(event)
267     {
268         this._addRepresentedObjectToNewChildQueue(event.data.childFrame);
269     },
270
271     _childFrameWasRemoved: function(event)
272     {
273         this._removeChildForRepresentedObject(event.data.childFrame);
274     },
275
276     _childContentFlowWasAdded: function(event)
277     {
278         this._addRepresentedObjectToNewChildQueue(event.data.flow);
279     },
280
281     _childContentFlowWasRemoved: function(event)
282     {
283         this._removeChildForRepresentedObject(event.data.flow);
284     },
285
286     _rootDOMNodeInvalidated: function() {
287         if (this.expanded)
288             this._frame.domTree.requestContentFlowList();
289     },
290
291     _addRepresentedObjectToNewChildQueue: function(representedObject)
292     {
293         // This queue reduces flashing as resources load and change folders when their type becomes known.
294
295         this._newChildQueue.push(representedObject);
296         if (!this._newChildQueueTimeoutIdentifier)
297             this._newChildQueueTimeoutIdentifier = setTimeout(this._populateFromNewChildQueue.bind(this), WebInspector.FrameTreeElement.NewChildQueueUpdateInterval);
298     },
299
300     _removeRepresentedObjectFromNewChildQueue: function(representedObject)
301     {
302         this._newChildQueue.remove(representedObject);
303     },
304
305     _populateFromNewChildQueue: function()
306     {
307         if (!this.children.length) {
308             this._updateParentStatus();
309             this.shouldRefreshChildren = true;
310             return;
311         }
312
313         for (var i = 0; i < this._newChildQueue.length; ++i)
314             this._addChildForRepresentedObject(this._newChildQueue[i]);
315
316         this._newChildQueue = [];
317         this._newChildQueueTimeoutIdentifier = null;
318     },
319
320     _clearNewChildQueue: function()
321     {
322         this._newChildQueue = [];
323         if (this._newChildQueueTimeoutIdentifier) {
324             clearTimeout(this._newChildQueueTimeoutIdentifier);
325             this._newChildQueueTimeoutIdentifier = null;
326         }
327     },
328
329     _addChildForRepresentedObject: function(representedObject)
330     {
331         console.assert(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow);
332         if (!(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow))
333             return;
334
335         this._updateParentStatus();
336
337         if (!this.treeOutline) {
338             // Just mark as needing to update to avoid doing work that might not be needed.
339             this.shouldRefreshChildren = true;
340             return;
341         }
342
343         if (this._shouldGroupIntoFolders() && !this._groupedIntoFolders) {
344             // Mark as needing a refresh to rebuild the tree into folders.
345             this._groupedIntoFolders = true;
346             this.shouldRefreshChildren = true;
347             return;
348         }
349
350         this._addTreeElementForRepresentedObject(representedObject);
351     },
352
353     _removeChildForRepresentedObject: function(representedObject)
354     {
355         console.assert(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow);
356         if (!(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow))
357             return;
358
359         this._removeRepresentedObjectFromNewChildQueue(representedObject);
360
361         this._updateParentStatus();
362
363         if (!this.treeOutline) {
364             // Just mark as needing to update to avoid doing work that might not be needed.
365             this.shouldRefreshChildren = true;
366             return;
367         }
368
369         // Find the tree element for the frame by using getCachedTreeElement
370         // to only get the item if it has been created already.
371         var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
372         if (!childTreeElement || !childTreeElement.parent)
373             return;
374
375         this._removeTreeElement(childTreeElement);
376     },
377
378     _addTreeElementForRepresentedObject: function(representedObject)
379     {
380         var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
381         if (!childTreeElement) {
382             if (representedObject instanceof WebInspector.SourceMapResource)
383                 childTreeElement = new WebInspector.SourceMapResourceTreeElement(representedObject);
384             else if (representedObject instanceof WebInspector.Resource)
385                 childTreeElement = new WebInspector.ResourceTreeElement(representedObject);
386             else if (representedObject instanceof WebInspector.Frame)
387                 childTreeElement = new WebInspector.FrameTreeElement(representedObject);
388             else if (representedObject instanceof WebInspector.ContentFlow)
389                 childTreeElement = new WebInspector.ContentFlowTreeElement(representedObject);
390         }
391
392         this._addTreeElement(childTreeElement);
393     },
394
395     _addTreeElement: function(childTreeElement)
396     {
397         console.assert(childTreeElement);
398         if (!childTreeElement)
399             return;
400
401         var wasSelected = childTreeElement.selected;
402
403         this._removeTreeElement(childTreeElement, true, true);
404
405         var parentTreeElement = this._parentTreeElementForRepresentedObject(childTreeElement.representedObject);
406         if (parentTreeElement !== this && !parentTreeElement.parent)
407             this._insertFolderTreeElement(parentTreeElement);
408
409         this._insertResourceTreeElement(parentTreeElement, childTreeElement);
410
411         if (wasSelected)
412             childTreeElement.revealAndSelect(true, false, true, true);
413     },
414
415     _compareTreeElementsByMainTitle: function(a, b)
416     {
417         return a.mainTitle.localeCompare(b.mainTitle);
418     },
419
420     _insertFolderTreeElement: function(folderTreeElement)
421     {
422         console.assert(this._groupedIntoFolders);
423         console.assert(!folderTreeElement.parent);
424         this.insertChild(folderTreeElement, insertionIndexForObjectInListSortedByFunction(folderTreeElement, this.children, this._compareTreeElementsByMainTitle));
425     },
426
427     _compareResourceTreeElements: function(a, b)
428     {
429         if (a === b)
430             return 0;
431
432         var aIsResource = a instanceof WebInspector.ResourceTreeElement;
433         var bIsResource = b instanceof WebInspector.ResourceTreeElement;
434
435         if (aIsResource && bIsResource)
436             return WebInspector.ResourceTreeElement.compareResourceTreeElements(a, b);
437
438         if (!aIsResource && !bIsResource) {
439             // When both components are not resources then just compare the titles.
440             return a.mainTitle.localeCompare(b.mainTitle);
441         }
442
443         // Non-resources should appear before the resources.
444         // FIXME: There should be a better way to group the elements by their type.
445         return aIsResource ? 1 : -1;
446     },
447
448     _insertResourceTreeElement: function(parentTreeElement, childTreeElement)
449     {
450         console.assert(!childTreeElement.parent);
451         parentTreeElement.insertChild(childTreeElement, insertionIndexForObjectInListSortedByFunction(childTreeElement, parentTreeElement.children, this._compareResourceTreeElements));
452     },
453
454     _removeTreeElement: function(childTreeElement, suppressOnDeselect, suppressSelectSibling)
455     {
456         var oldParent = childTreeElement.parent;
457         if (!oldParent)
458             return;
459
460         oldParent.removeChild(childTreeElement, suppressOnDeselect, suppressSelectSibling);
461
462         if (oldParent === this)
463             return;
464
465         console.assert(oldParent instanceof WebInspector.FolderTreeElement);
466         if (!(oldParent instanceof WebInspector.FolderTreeElement))
467             return;
468
469         // Remove the old parent folder if it is now empty.
470         if (!oldParent.children.length)
471             oldParent.parent.removeChild(oldParent);
472     },
473
474     _folderNameForResourceType: function(type)
475     {
476         return WebInspector.Resource.Type.displayName(type, true);
477     },
478
479     _parentTreeElementForRepresentedObject: function(representedObject)
480     {
481         if (!this._groupedIntoFolders)
482             return this;
483
484         function createFolderTreeElement(type, displayName)
485         {
486             var folderTreeElement = new WebInspector.FolderTreeElement(displayName);
487             folderTreeElement._expandedSetting = new WebInspector.Setting(type + "-folder-expanded-" + this._frame.url.hash, false);
488             if (folderTreeElement._expandedSetting.value)
489                 folderTreeElement.expand();
490             folderTreeElement.onexpand = this._folderTreeElementExpandedStateChange.bind(this);
491             folderTreeElement.oncollapse = this._folderTreeElementExpandedStateChange.bind(this);
492             return folderTreeElement;
493         }
494
495         if (representedObject instanceof WebInspector.Frame) {
496             if (!this._framesFolderTreeElement)
497                 this._framesFolderTreeElement = createFolderTreeElement.call(this, "frames", WebInspector.UIString("Frames"));
498             return this._framesFolderTreeElement;
499         }
500
501         if (representedObject instanceof WebInspector.ContentFlow) {
502             if (!this._flowsFolderTreeElement)
503                 this._flowsFolderTreeElement = createFolderTreeElement.call(this, "flows", WebInspector.UIString("Flows"));
504             return this._flowsFolderTreeElement;
505         }
506
507         if (representedObject instanceof WebInspector.Resource) {
508             var folderName = this._folderNameForResourceType(representedObject.type);
509             if (!folderName)
510                 return this;
511
512             if (!this._resourceFoldersTypeMap)
513                 this._resourceFoldersTypeMap = {};
514             if (!this._resourceFoldersTypeMap[representedObject.type])
515                 this._resourceFoldersTypeMap[representedObject.type] = createFolderTreeElement.call(this, representedObject.type, folderName);
516             return this._resourceFoldersTypeMap[representedObject.type];
517         }
518
519         console.error("Unknown representedObject: ", representedObject);
520         return this;
521     },
522
523     _folderTreeElementExpandedStateChange: function(folderTreeElement)
524     {
525         console.assert(folderTreeElement._expandedSetting);
526         folderTreeElement._expandedSetting.value = folderTreeElement.expanded;
527     },
528
529     _shouldGroupIntoFolders: function()
530     {
531         // Already grouped into folders, keep it that way.
532         if (this._groupedIntoFolders)
533             return true;
534
535         // Resources and Frames are grouped into folders if one of two thresholds are met:
536         // 1) Once the number of medium categories passes NumberOfMediumCategoriesThreshold.
537         // 2) When there is a category that passes LargeChildCountThreshold and there are
538         //    any resources in another category.
539
540         // Folders are avoided when there is only one category or most categories are small.
541
542         var numberOfSmallCategories = 0;
543         var numberOfMediumCategories = 0;
544         var foundLargeCategory = false;
545         var frame = this._frame;
546
547         function pushResourceType(type) {
548             // There are some other properties on WebInspector.Resource.Type that we need to skip, like private data and functions
549             if (type.charAt(0) === "_")
550                 return false;
551
552             // Only care about the values that are strings, not functions, etc.
553             var typeValue = WebInspector.Resource.Type[type];
554             if (typeof typeValue !== "string")
555                 return false;
556
557             return pushCategory(frame.resourcesWithType(typeValue).length);
558         }
559
560         function pushCategory(resourceCount)
561         {
562             if (!resourceCount)
563                 return false;
564
565             // If this type has any resources and there is a known large category, make folders.
566             if (foundLargeCategory)
567                 return true;
568
569             // If there are lots of this resource type, then count it as a large category.
570             if (resourceCount >= WebInspector.FrameTreeElement.LargeChildCountThreshold) {
571                 // If we already have other resources in other small or medium categories, make folders.
572                 if (numberOfSmallCategories || numberOfMediumCategories)
573                     return true;
574
575                 foundLargeCategory = true;
576                 return false;
577             }
578
579             // Check if this is a medium category.
580             if (resourceCount >= WebInspector.FrameTreeElement.MediumChildCountThreshold) {
581                 // If this is the medium category that puts us over the maximum allowed, make folders.
582                 return ++numberOfMediumCategories >= WebInspector.FrameTreeElement.NumberOfMediumCategoriesThreshold;
583             }
584
585             // This is a small category.
586             ++numberOfSmallCategories;
587             return false;
588         }
589
590         // Iterate over all the available resource types.
591         return pushCategory(frame.childFrames.length) || pushCategory(frame.domTree.flowsCount) || Object.keys(WebInspector.Resource.Type).some(pushResourceType);
592     },
593
594     _reloadPageClicked: function(event)
595     {
596         // Ignore cache when the shift key is pressed.
597         PageAgent.reload(event.data.shiftKey);
598     },
599
600     _downloadButtonClicked: function(event)
601     {
602         WebInspector.archiveMainFrame();
603     },
604
605     _updateDownloadButton: function()
606     {
607         console.assert(this._frame.isMainFrame());
608         if (!this._downloadButton)
609             return;
610
611         if (!PageAgent.archive) {
612             this._downloadButton.hidden = true;
613             return;
614         }
615
616         if (this._downloadingPage) {
617             this._downloadButton.enabled = false;
618             return;
619         }
620
621         this._downloadButton.enabled = WebInspector.canArchiveMainFrame();
622     },
623
624     _pageArchiveStarted: function(event)
625     {
626         this._downloadingPage = true;
627         this._updateDownloadButton();
628     },
629
630     _pageArchiveEnded: function(event)
631     {
632         this._downloadingPage = false;
633         this._updateDownloadButton();
634     }
635 };
636
637 WebInspector.FrameTreeElement.prototype.__proto__ = WebInspector.ResourceTreeElement.prototype;