Web Inspector: Update glyphs to be more like Xcode 6
[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         for (var i = 0; i < this._newChildQueue.length; ++i)
308             this._addChildForRepresentedObject(this._newChildQueue[i]);
309
310         this._newChildQueue = [];
311         this._newChildQueueTimeoutIdentifier = null;
312     },
313
314     _clearNewChildQueue: function()
315     {
316         this._newChildQueue = [];
317         if (this._newChildQueueTimeoutIdentifier) {
318             clearTimeout(this._newChildQueueTimeoutIdentifier);
319             this._newChildQueueTimeoutIdentifier = null;
320         }
321     },
322
323     _addChildForRepresentedObject: function(representedObject)
324     {
325         console.assert(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow);
326         if (!(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow))
327             return;
328
329         this._updateParentStatus();
330
331         if (!this.treeOutline) {
332             // Just mark as needing to update to avoid doing work that might not be needed.
333             this.shouldRefreshChildren = true;
334             return;
335         }
336
337         if (this._shouldGroupIntoFolders() && !this._groupedIntoFolders) {
338             // Mark as needing a refresh to rebuild the tree into folders.
339             this._groupedIntoFolders = true;
340             this.shouldRefreshChildren = true;
341             return;
342         }
343
344         this._addTreeElementForRepresentedObject(representedObject);
345     },
346
347     _removeChildForRepresentedObject: function(representedObject)
348     {
349         console.assert(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow);
350         if (!(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow))
351             return;
352
353         this._removeRepresentedObjectFromNewChildQueue(representedObject);
354
355         this._updateParentStatus();
356
357         if (!this.treeOutline) {
358             // Just mark as needing to update to avoid doing work that might not be needed.
359             this.shouldRefreshChildren = true;
360             return;
361         }
362
363         // Find the tree element for the frame by using getCachedTreeElement
364         // to only get the item if it has been created already.
365         var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
366         if (!childTreeElement || !childTreeElement.parent)
367             return;
368
369         this._removeTreeElement(childTreeElement);
370     },
371
372     _addTreeElementForRepresentedObject: function(representedObject)
373     {
374         var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
375         if (!childTreeElement) {
376             if (representedObject instanceof WebInspector.SourceMapResource)
377                 childTreeElement = new WebInspector.SourceMapResourceTreeElement(representedObject);
378             else if (representedObject instanceof WebInspector.Resource)
379                 childTreeElement = new WebInspector.ResourceTreeElement(representedObject);
380             else if (representedObject instanceof WebInspector.Frame)
381                 childTreeElement = new WebInspector.FrameTreeElement(representedObject);
382             else if (representedObject instanceof WebInspector.ContentFlow)
383                 childTreeElement = new WebInspector.ContentFlowTreeElement(representedObject);
384         }
385
386         this._addTreeElement(childTreeElement);
387     },
388
389     _addTreeElement: function(childTreeElement)
390     {
391         console.assert(childTreeElement);
392         if (!childTreeElement)
393             return;
394
395         var wasSelected = childTreeElement.selected;
396
397         this._removeTreeElement(childTreeElement, true, true);
398
399         var parentTreeElement = this._parentTreeElementForRepresentedObject(childTreeElement.representedObject);
400         if (parentTreeElement !== this && !parentTreeElement.parent)
401             this._insertFolderTreeElement(parentTreeElement);
402
403         this._insertResourceTreeElement(parentTreeElement, childTreeElement);
404
405         if (wasSelected)
406             childTreeElement.revealAndSelect(true, false, true, true);
407     },
408
409     _compareTreeElementsByMainTitle: function(a, b)
410     {
411         return a.mainTitle.localeCompare(b.mainTitle);
412     },
413
414     _insertFolderTreeElement: function(folderTreeElement)
415     {
416         console.assert(this._groupedIntoFolders);
417         console.assert(!folderTreeElement.parent);
418         this.insertChild(folderTreeElement, insertionIndexForObjectInListSortedByFunction(folderTreeElement, this.children, this._compareTreeElementsByMainTitle));
419     },
420
421     _compareResourceTreeElements: function(a, b)
422     {
423         if (a === b)
424             return 0;
425
426         var aIsResource = a instanceof WebInspector.ResourceTreeElement;
427         var bIsResource = b instanceof WebInspector.ResourceTreeElement;
428
429         if (aIsResource && bIsResource)
430             return WebInspector.ResourceTreeElement.compareResourceTreeElements(a, b);
431
432         if (!aIsResource && !bIsResource) {
433             // When both components are not resources then just compare the titles.
434             return a.mainTitle.localeCompare(b.mainTitle);
435         }
436
437         // Non-resources should appear before the resources.
438         // FIXME: There should be a better way to group the elements by their type.
439         return aIsResource ? 1 : -1;
440     },
441
442     _insertResourceTreeElement: function(parentTreeElement, childTreeElement)
443     {
444         console.assert(!childTreeElement.parent);
445         parentTreeElement.insertChild(childTreeElement, insertionIndexForObjectInListSortedByFunction(childTreeElement, parentTreeElement.children, this._compareResourceTreeElements));
446     },
447
448     _removeTreeElement: function(childTreeElement, suppressOnDeselect, suppressSelectSibling)
449     {
450         var oldParent = childTreeElement.parent;
451         if (!oldParent)
452             return;
453
454         oldParent.removeChild(childTreeElement, suppressOnDeselect, suppressSelectSibling);
455
456         if (oldParent === this)
457             return;
458
459         console.assert(oldParent instanceof WebInspector.FolderTreeElement);
460         if (!(oldParent instanceof WebInspector.FolderTreeElement))
461             return;
462
463         // Remove the old parent folder if it is now empty.
464         if (!oldParent.children.length)
465             oldParent.parent.removeChild(oldParent);
466     },
467
468     _folderNameForResourceType: function(type)
469     {
470         return WebInspector.Resource.Type.displayName(type, true);
471     },
472
473     _parentTreeElementForRepresentedObject: function(representedObject)
474     {
475         if (!this._groupedIntoFolders)
476             return this;
477
478         function createFolderTreeElement(type, displayName)
479         {
480             var folderTreeElement = new WebInspector.FolderTreeElement(displayName);
481             folderTreeElement._expandedSetting = new WebInspector.Setting(type + "-folder-expanded-" + this._frame.url.hash, false);
482             if (folderTreeElement._expandedSetting.value)
483                 folderTreeElement.expand();
484             folderTreeElement.onexpand = this._folderTreeElementExpandedStateChange.bind(this);
485             folderTreeElement.oncollapse = this._folderTreeElementExpandedStateChange.bind(this);
486             return folderTreeElement;
487         }
488
489         if (representedObject instanceof WebInspector.Frame) {
490             if (!this._framesFolderTreeElement)
491                 this._framesFolderTreeElement = createFolderTreeElement.call(this, "frames", WebInspector.UIString("Frames"));
492             return this._framesFolderTreeElement;
493         }
494
495         if (representedObject instanceof WebInspector.ContentFlow) {
496             if (!this._flowsFolderTreeElement)
497                 this._flowsFolderTreeElement = createFolderTreeElement.call(this, "flows", WebInspector.UIString("Flows"));
498             return this._flowsFolderTreeElement;
499         }
500
501         if (representedObject instanceof WebInspector.Resource) {
502             var folderName = this._folderNameForResourceType(representedObject.type);
503             if (!folderName)
504                 return this;
505
506             if (!this._resourceFoldersTypeMap)
507                 this._resourceFoldersTypeMap = {};
508             if (!this._resourceFoldersTypeMap[representedObject.type])
509                 this._resourceFoldersTypeMap[representedObject.type] = createFolderTreeElement.call(this, representedObject.type, folderName);
510             return this._resourceFoldersTypeMap[representedObject.type];
511         }
512
513         console.error("Unknown representedObject: ", representedObject);
514         return this;
515     },
516
517     _folderTreeElementExpandedStateChange: function(folderTreeElement)
518     {
519         console.assert(folderTreeElement._expandedSetting);
520         folderTreeElement._expandedSetting.value = folderTreeElement.expanded;
521     },
522
523     _shouldGroupIntoFolders: function()
524     {
525         // Already grouped into folders, keep it that way.
526         if (this._groupedIntoFolders)
527             return true;
528
529         // Resources and Frames are grouped into folders if one of two thresholds are met:
530         // 1) Once the number of medium categories passes NumberOfMediumCategoriesThreshold.
531         // 2) When there is a category that passes LargeChildCountThreshold and there are
532         //    any resources in another category.
533
534         // Folders are avoided when there is only one category or most categories are small.
535
536         var numberOfSmallCategories = 0;
537         var numberOfMediumCategories = 0;
538         var foundLargeCategory = false;
539         var frame = this._frame;
540
541         function pushResourceType(type) {
542             // There are some other properties on WebInspector.Resource.Type that we need to skip, like private data and functions
543             if (type.charAt(0) === "_")
544                 return false;
545
546             // Only care about the values that are strings, not functions, etc.
547             var typeValue = WebInspector.Resource.Type[type];
548             if (typeof typeValue !== "string")
549                 return false;
550
551             return pushCategory(frame.resourcesWithType(typeValue).length);
552         }
553
554         function pushCategory(resourceCount)
555         {
556             if (!resourceCount)
557                 return false;
558
559             // If this type has any resources and there is a known large category, make folders.
560             if (foundLargeCategory)
561                 return true;
562
563             // If there are lots of this resource type, then count it as a large category.
564             if (resourceCount >= WebInspector.FrameTreeElement.LargeChildCountThreshold) {
565                 // If we already have other resources in other small or medium categories, make folders.
566                 if (numberOfSmallCategories || numberOfMediumCategories)
567                     return true;
568
569                 foundLargeCategory = true;
570                 return false;
571             }
572
573             // Check if this is a medium category.
574             if (resourceCount >= WebInspector.FrameTreeElement.MediumChildCountThreshold) {
575                 // If this is the medium category that puts us over the maximum allowed, make folders.
576                 return ++numberOfMediumCategories >= WebInspector.FrameTreeElement.NumberOfMediumCategoriesThreshold;
577             }
578
579             // This is a small category.
580             ++numberOfSmallCategories;
581             return false;
582         }
583
584         // Iterate over all the available resource types.
585         return pushCategory(frame.childFrames.length) || pushCategory(frame.domTree.flowsCount) || Object.keys(WebInspector.Resource.Type).some(pushResourceType);
586     },
587
588     _reloadPageClicked: function(event)
589     {
590         // Ignore cache when the shift key is pressed.
591         PageAgent.reload(event.data.shiftKey);
592     },
593
594     _downloadButtonClicked: function(event)
595     {
596         WebInspector.archiveMainFrame();
597     },
598
599     _updateDownloadButton: function()
600     {
601         console.assert(this._frame.isMainFrame());
602         if (!this._downloadButton)
603             return;
604
605         if (!PageAgent.archive) {
606             this._downloadButton.hidden = true;
607             return;
608         }
609
610         if (this._downloadingPage) {
611             this._downloadButton.enabled = false;
612             return;
613         }
614
615         this._downloadButton.enabled = WebInspector.canArchiveMainFrame();
616     },
617
618     _pageArchiveStarted: function(event)
619     {
620         this._downloadingPage = true;
621         this._updateDownloadButton();
622     },
623
624     _pageArchiveEnded: function(event)
625     {
626         this._downloadingPage = false;
627         this._updateDownloadButton();
628     }
629 };
630
631 WebInspector.FrameTreeElement.prototype.__proto__ = WebInspector.ResourceTreeElement.prototype;