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