4ff010725ff181639bc7674dfa8759ff2547bc22
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / FolderizedTreeElement.js
1 /*
2  * Copyright (C) 2014 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.FolderizedTreeElement = function(classNames, title, subtitle, representedObject, hasChildren)
27 {
28     WebInspector.GeneralTreeElement.call(this, classNames, title, subtitle, representedObject, hasChildren);
29
30     this.shouldRefreshChildren = true;
31
32     this._folderSettingsKey = "";
33     this._folderTypeMap = new Map;
34     this._folderizeSettingsMap = new Map;
35     this._groupedIntoFolders = false;
36     this._clearNewChildQueue();
37 };
38
39 WebInspector.FolderizedTreeElement.MediumChildCountThreshold = 5;
40 WebInspector.FolderizedTreeElement.LargeChildCountThreshold = 15;
41 WebInspector.FolderizedTreeElement.NumberOfMediumCategoriesThreshold = 2;
42 WebInspector.FolderizedTreeElement.NewChildQueueUpdateInterval = 500;
43
44 WebInspector.FolderizedTreeElement.prototype = {
45     constructor: WebInspector.FolderizedTreeElement,
46     __proto__: WebInspector.GeneralTreeElement.prototype,
47
48     // Public
49
50     get groupedIntoFolders()
51     {
52         return this._groupedIntoFolders;
53     },
54
55     set folderSettingsKey(x)
56     {
57         this._folderSettingsKey = x;
58     },
59
60     registerFolderizeSettings: function(type, folderDisplayName, validateRepresentedObjectCallback, countChildrenCallback, treeElementConstructor)
61     {
62         console.assert(type);
63         console.assert(folderDisplayName);
64         console.assert(typeof validateRepresentedObjectCallback === "function");
65         console.assert(typeof countChildrenCallback === "function");
66         console.assert(typeof treeElementConstructor === "function");
67
68         var settings = {
69             type,
70             folderDisplayName,
71             validateRepresentedObjectCallback,
72             countChildrenCallback,
73             treeElementConstructor
74         };
75
76         this._folderizeSettingsMap.set(type, settings);
77     },
78
79     // Overrides from TreeElement (Private).
80
81     removeChildren: function()
82     {
83         TreeElement.prototype.removeChildren.call(this);
84
85         this._clearNewChildQueue();
86
87         for (var folder of this._folderTypeMap.values())
88             folder.removeChildren();
89
90         this._folderTypeMap.clear();
91
92         this._groupedIntoFolders = false;
93     },
94
95     // Protected
96
97     addChildForRepresentedObject: function(representedObject)
98     {
99         var settings = this._settingsForRepresentedObject(representedObject);
100         console.assert(settings);
101         if (!settings) {
102             console.error("No settings for represented object", representedObject);
103             return;
104         }
105
106         var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
107         if (!childTreeElement)
108             childTreeElement = new settings.treeElementConstructor(representedObject);
109
110         this._addTreeElement(childTreeElement);
111     },
112
113     addRepresentedObjectToNewChildQueue: function(representedObject)
114     {
115         // This queue reduces flashing as resources load and change folders when their type becomes known.
116
117         this._newChildQueue.push(representedObject);
118         if (!this._newChildQueueTimeoutIdentifier)
119             this._newChildQueueTimeoutIdentifier = setTimeout(this._populateFromNewChildQueue.bind(this), WebInspector.FolderizedTreeElement.NewChildQueueUpdateInterval);
120     },
121
122     removeChildForRepresentedObject: function(representedObject)
123     {
124         this._removeRepresentedObjectFromNewChildQueue(representedObject);
125         this.updateParentStatus();
126
127         if (!this.treeOutline) {
128             // Just mark as needing to update to avoid doing work that might not be needed.
129             this.shouldRefreshChildren = true;
130             return;
131         }
132
133         // Find the tree element for the frame by using getCachedTreeElement
134         // to only get the item if it has been created already.
135         var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
136         if (!childTreeElement || !childTreeElement.parent)
137             return;
138
139         this._removeTreeElement(childTreeElement);
140     },
141
142     compareChildTreeElements: function(a, b)
143     {
144         return this._compareTreeElementsByMainTitle(a, b);
145     },
146
147     updateParentStatus: function()
148     {
149         var hasChildren = false;
150         for (var settings of this._folderizeSettingsMap.values()) {
151             if (settings.countChildrenCallback()) {
152                 hasChildren = true;
153                 break;
154             }
155         }
156
157         this.hasChildren = hasChildren;
158         if (!this.hasChildren)
159             this.removeChildren();
160     },
161
162     prepareToPopulate: function()
163     {
164         if (!this._groupedIntoFolders && this._shouldGroupIntoFolders())
165             this._groupedIntoFolders = true;
166     },
167
168     // Private
169
170     _clearNewChildQueue: function()
171     {
172         this._newChildQueue = [];
173         if (this._newChildQueueTimeoutIdentifier) {
174             clearTimeout(this._newChildQueueTimeoutIdentifier);
175             this._newChildQueueTimeoutIdentifier = null;
176         }
177     },
178
179     _populateFromNewChildQueue: function()
180     {
181         if (!this.children.length) {
182             this.updateParentStatus();
183             this.shouldRefreshChildren = true;
184             return;
185         }
186
187         this.prepareToPopulate();
188
189         for (var i = 0; i < this._newChildQueue.length; ++i)
190             this.addChildForRepresentedObject(this._newChildQueue[i]);
191
192         this._clearNewChildQueue();
193     },
194
195     _removeRepresentedObjectFromNewChildQueue: function(representedObject)
196     {
197         this._newChildQueue.remove(representedObject);
198     },
199
200     _addTreeElement: function(childTreeElement)
201     {
202         console.assert(childTreeElement);
203         if (!childTreeElement)
204             return;
205
206         var wasSelected = childTreeElement.selected;
207
208         this._removeTreeElement(childTreeElement, true, true);
209
210         var parentTreeElement = this._parentTreeElementForRepresentedObject(childTreeElement.representedObject);
211         if (parentTreeElement !== this && !parentTreeElement.parent)
212             this._insertFolderTreeElement(parentTreeElement);
213
214         this._insertChildTreeElement(parentTreeElement, childTreeElement);
215
216         if (wasSelected)
217             childTreeElement.revealAndSelect(true, false, true, true);
218     },
219
220     _compareTreeElementsByMainTitle: function(a, b)
221     {
222         return a.mainTitle.localeCompare(b.mainTitle);
223     },
224
225     _insertFolderTreeElement: function(folderTreeElement)
226     {
227         console.assert(this._groupedIntoFolders);
228         console.assert(!folderTreeElement.parent);
229         this.insertChild(folderTreeElement, insertionIndexForObjectInListSortedByFunction(folderTreeElement, this.children, this._compareTreeElementsByMainTitle));
230     },
231
232     _insertChildTreeElement: function(parentTreeElement, childTreeElement)
233     {
234         console.assert(!childTreeElement.parent);
235         parentTreeElement.insertChild(childTreeElement, insertionIndexForObjectInListSortedByFunction(childTreeElement, parentTreeElement.children, this.compareChildTreeElements.bind(this)));
236     },
237
238     _removeTreeElement: function(childTreeElement, suppressOnDeselect, suppressSelectSibling)
239     {
240         var oldParent = childTreeElement.parent;
241         if (!oldParent)
242             return;
243
244         oldParent.removeChild(childTreeElement, suppressOnDeselect, suppressSelectSibling);
245
246         if (oldParent === this)
247             return;
248
249         console.assert(oldParent instanceof WebInspector.FolderTreeElement);
250         if (!(oldParent instanceof WebInspector.FolderTreeElement))
251             return;
252
253         // Remove the old parent folder if it is now empty.
254         if (!oldParent.children.length)
255             oldParent.parent.removeChild(oldParent);
256     },
257
258     _parentTreeElementForRepresentedObject: function(representedObject)
259     {
260         if (!this._groupedIntoFolders)
261             return this;
262
263         console.assert(this._folderSettingsKey !== "");
264
265         function createFolderTreeElement(type, displayName)
266         {
267             var folderTreeElement = new WebInspector.FolderTreeElement(displayName);
268             folderTreeElement.__expandedSetting = new WebInspector.Setting(type + "-folder-expanded-" + this._folderSettingsKey, false);
269             if (folderTreeElement.__expandedSetting.value)
270                 folderTreeElement.expand();
271             folderTreeElement.onexpand = this._folderTreeElementExpandedStateChange.bind(this);
272             folderTreeElement.oncollapse = this._folderTreeElementExpandedStateChange.bind(this);
273             return folderTreeElement;
274         }
275
276         var settings = this._settingsForRepresentedObject(representedObject);
277         if (!settings) {
278             console.error("Unknown representedObject", representedObject);
279             return this;
280         }
281
282         var folder = this._folderTypeMap.get(settings.type);
283         if (folder)
284             return folder;
285
286         folder = createFolderTreeElement.call(this, settings.type, settings.folderDisplayName);
287         this._folderTypeMap.set(settings.type, folder);
288         return folder;
289     },
290
291     _folderTreeElementExpandedStateChange: function(folderTreeElement)
292     {
293         console.assert(folderTreeElement.__expandedSetting);
294         folderTreeElement.__expandedSetting.value = folderTreeElement.expanded;
295     },
296
297     _settingsForRepresentedObject: function(representedObject)
298     {
299         for (var settings of this._folderizeSettingsMap.values()) {
300             if (settings.validateRepresentedObjectCallback(representedObject))
301                 return settings;
302         }
303         return null;
304     },
305
306     _shouldGroupIntoFolders: function()
307     {
308         // Already grouped into folders, keep it that way.
309         if (this._groupedIntoFolders)
310             return true;
311
312         // Child objects are grouped into folders if one of two thresholds are met:
313         // 1) Once the number of medium categories passes NumberOfMediumCategoriesThreshold.
314         // 2) When there is a category that passes LargeChildCountThreshold and there are
315         //    any child objects in another category.
316
317         // Folders are avoided when there is only one category or most categories are small.
318
319         var numberOfSmallCategories = 0;
320         var numberOfMediumCategories = 0;
321         var foundLargeCategory = false;
322
323         function pushCategory(childCount)
324         {
325             if (!childCount)
326                 return false;
327
328             // If this type has any resources and there is a known large category, make folders.
329             if (foundLargeCategory)
330                 return true;
331
332             // If there are lots of this resource type, then count it as a large category.
333             if (childCount >= WebInspector.FolderizedTreeElement.LargeChildCountThreshold) {
334                 // If we already have other resources in other small or medium categories, make folders.
335                 if (numberOfSmallCategories || numberOfMediumCategories)
336                     return true;
337
338                 foundLargeCategory = true;
339                 return false;
340             }
341
342             // Check if this is a medium category.
343             if (childCount >= WebInspector.FolderizedTreeElement.MediumChildCountThreshold) {
344                 // If this is the medium category that puts us over the maximum allowed, make folders.
345                 return ++numberOfMediumCategories >= WebInspector.FolderizedTreeElement.NumberOfMediumCategoriesThreshold;
346             }
347
348             // This is a small category.
349             ++numberOfSmallCategories;
350             return false;
351         }
352
353         // Iterate over all the available child object types.
354         for (var settings of this._folderizeSettingsMap.values()) {
355             if (pushCategory(settings.countChildrenCallback()))
356                 return true;
357         }
358         return false;
359     }
360 };