Web Inspector: Reduce synchronous view layouts
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ContentBrowser.js
1 /*
2  * Copyright (C) 2013, 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.ContentBrowser = class ContentBrowser extends WebInspector.View
27 {
28     constructor(element, delegate, disableBackForward)
29     {
30         super(element);
31
32         this.element.classList.add("content-browser");
33
34         this._navigationBar = new WebInspector.NavigationBar;
35         this.addSubview(this._navigationBar);
36
37         this._contentViewContainer = new WebInspector.ContentViewContainer;
38         this._contentViewContainer.addEventListener(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange, this._currentContentViewDidChange, this);
39         this.addSubview(this._contentViewContainer);
40
41         this._findBanner = new WebInspector.FindBanner(this);
42         this._findBanner.addEventListener(WebInspector.FindBanner.Event.DidShow, this._findBannerDidShow, this);
43         this._findBanner.addEventListener(WebInspector.FindBanner.Event.DidHide, this._findBannerDidHide, this);
44
45         if (!disableBackForward) {
46             this._backKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Control, WebInspector.KeyboardShortcut.Key.Left, this._backButtonClicked.bind(this), this.element);
47             this._forwardKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Control, WebInspector.KeyboardShortcut.Key.Right, this._forwardButtonClicked.bind(this), this.element);
48
49             this._backButtonNavigationItem = new WebInspector.ButtonNavigationItem("back", WebInspector.UIString("Back (%s)").format(this._backKeyboardShortcut.displayName), "Images/BackForwardArrows.svg#back-arrow-mask", 8, 13);
50             this._backButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._backButtonClicked, this);
51             this._backButtonNavigationItem.enabled = false;
52             this._navigationBar.addNavigationItem(this._backButtonNavigationItem);
53
54             this._forwardButtonNavigationItem = new WebInspector.ButtonNavigationItem("forward", WebInspector.UIString("Forward (%s)").format(this._forwardKeyboardShortcut.displayName), "Images/BackForwardArrows.svg#forward-arrow-mask", 8, 13);
55             this._forwardButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._forwardButtonClicked, this);
56             this._forwardButtonNavigationItem.enabled = false;
57             this._navigationBar.addNavigationItem(this._forwardButtonNavigationItem);
58
59             this._navigationBar.addNavigationItem(new WebInspector.DividerNavigationItem);
60         }
61
62         this._hierarchicalPathNavigationItem = new WebInspector.HierarchicalPathNavigationItem;
63         this._hierarchicalPathNavigationItem.addEventListener(WebInspector.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, this._hierarchicalPathComponentWasSelected, this);
64         this._navigationBar.addNavigationItem(this._hierarchicalPathNavigationItem);
65
66         this._contentViewSelectionPathNavigationItem = new WebInspector.HierarchicalPathNavigationItem;
67
68         this._dividingFlexibleSpaceNavigationItem = new WebInspector.FlexibleSpaceNavigationItem;
69         this._navigationBar.addNavigationItem(this._dividingFlexibleSpaceNavigationItem);
70
71         WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
72         WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
73         WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.NumberOfSearchResultsDidChange, this._contentViewNumberOfSearchResultsDidChange, this);
74         WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.NavigationItemsDidChange, this._contentViewNavigationItemsDidChange, this);
75
76         this._delegate = delegate || null;
77
78         this._currentContentViewNavigationItems = [];
79     }
80
81     // Public
82
83     get navigationBar()
84     {
85         return this._navigationBar;
86     }
87
88     get contentViewContainer()
89     {
90         return this._contentViewContainer;
91     }
92
93     get delegate()
94     {
95         return this._delegate;
96     }
97
98     set delegate(newDelegate)
99     {
100         this._delegate = newDelegate || null;
101     }
102
103     get currentContentView()
104     {
105         return this._contentViewContainer.currentContentView;
106     }
107
108     get currentRepresentedObjects()
109     {
110         var representedObjects = [];
111
112         var lastComponent = this._hierarchicalPathNavigationItem.lastComponent;
113         if (lastComponent && lastComponent.representedObject)
114             representedObjects.push(lastComponent.representedObject);
115
116         lastComponent = this._contentViewSelectionPathNavigationItem.lastComponent;
117         if (lastComponent && lastComponent.representedObject)
118             representedObjects.push(lastComponent.representedObject);
119
120         var currentContentView = this.currentContentView;
121         if (currentContentView) {
122             var supplementalRepresentedObjects = currentContentView.supplementalRepresentedObjects;
123             if (supplementalRepresentedObjects && supplementalRepresentedObjects.length)
124                 representedObjects = representedObjects.concat(supplementalRepresentedObjects);
125         }
126
127         return representedObjects;
128     }
129
130     showContentViewForRepresentedObject(representedObject, cookie, extraArguments)
131     {
132         var contentView = this.contentViewForRepresentedObject(representedObject, false, extraArguments);
133         return this._contentViewContainer.showContentView(contentView, cookie);
134     }
135
136     showContentView(contentView, cookie)
137     {
138         return this._contentViewContainer.showContentView(contentView, cookie);
139     }
140
141     contentViewForRepresentedObject(representedObject, onlyExisting, extraArguments)
142     {
143         return this._contentViewContainer.contentViewForRepresentedObject(representedObject, onlyExisting, extraArguments);
144     }
145
146     updateHierarchicalPathForCurrentContentView()
147     {
148         var currentContentView = this.currentContentView;
149         this._updateHierarchicalPathNavigationItem(currentContentView ? currentContentView.representedObject : null);
150     }
151
152     canGoBack()
153     {
154         var currentContentView = this.currentContentView;
155         if (currentContentView && currentContentView.canGoBack())
156             return true;
157         return this._contentViewContainer.canGoBack();
158     }
159
160     canGoForward()
161     {
162         var currentContentView = this.currentContentView;
163         if (currentContentView && currentContentView.canGoForward())
164             return true;
165         return this._contentViewContainer.canGoForward();
166     }
167
168     goBack()
169     {
170         var currentContentView = this.currentContentView;
171         if (currentContentView && currentContentView.canGoBack()) {
172             currentContentView.goBack();
173             this._updateBackForwardButtons();
174             return;
175         }
176
177         this._contentViewContainer.goBack();
178
179         // The _updateBackForwardButtons function is called by _currentContentViewDidChange,
180         // so it does not need to be called here.
181     }
182
183     goForward()
184     {
185         var currentContentView = this.currentContentView;
186         if (currentContentView && currentContentView.canGoForward()) {
187             currentContentView.goForward();
188             this._updateBackForwardButtons();
189             return;
190         }
191
192         this._contentViewContainer.goForward();
193
194         // The _updateBackForwardButtons function is called by _currentContentViewDidChange,
195         // so it does not need to be called here.
196     }
197
198     handleFindEvent(event)
199     {
200         var currentContentView = this.currentContentView;
201         if (!currentContentView || !currentContentView.supportsSearch)
202             return;
203
204         // LogContentView has custom search handling.
205         if (typeof currentContentView.handleFindEvent === "function") {
206             currentContentView.handleFindEvent(event);
207             return;
208         }
209
210         this._findBanner.show();
211     }
212
213     findBannerPerformSearch(findBanner, query)
214     {
215         var currentContentView = this.currentContentView;
216         if (!currentContentView || !currentContentView.supportsSearch)
217             return;
218
219         currentContentView.performSearch(query);
220     }
221
222     findBannerSearchCleared(findBanner)
223     {
224         var currentContentView = this.currentContentView;
225         if (!currentContentView || !currentContentView.supportsSearch)
226             return;
227
228         currentContentView.searchCleared();
229     }
230
231     findBannerSearchQueryForSelection(findBanner)
232     {
233         var currentContentView = this.currentContentView;
234         if (!currentContentView || !currentContentView.supportsSearch)
235             return null;
236
237         return currentContentView.searchQueryWithSelection();
238     }
239
240     findBannerRevealPreviousResult(findBanner)
241     {
242         var currentContentView = this.currentContentView;
243         if (!currentContentView || !currentContentView.supportsSearch)
244             return;
245
246         currentContentView.revealPreviousSearchResult(!findBanner.showing);
247     }
248
249     findBannerRevealNextResult(findBanner)
250     {
251         var currentContentView = this.currentContentView;
252         if (!currentContentView || !currentContentView.supportsSearch)
253             return;
254
255         currentContentView.revealNextSearchResult(!findBanner.showing);
256     }
257
258     shown()
259     {
260         this._contentViewContainer.shown();
261
262         this._findBanner.enableKeyboardShortcuts();
263     }
264
265     hidden()
266     {
267         this._contentViewContainer.hidden();
268
269         this._findBanner.disableKeyboardShortcuts();
270     }
271
272     // Private
273
274     _backButtonClicked(event)
275     {
276         this.goBack();
277     }
278
279     _forwardButtonClicked(event)
280     {
281         this.goForward();
282     }
283
284     _findBannerDidShow(event)
285     {
286         var currentContentView = this.currentContentView;
287         if (!currentContentView || !currentContentView.supportsSearch)
288             return;
289
290         currentContentView.automaticallyRevealFirstSearchResult = true;
291     }
292
293     _findBannerDidHide(event)
294     {
295         var currentContentView = this.currentContentView;
296         if (!currentContentView || !currentContentView.supportsSearch)
297             return;
298
299         currentContentView.automaticallyRevealFirstSearchResult = false;
300     }
301
302     _contentViewNumberOfSearchResultsDidChange(event)
303     {
304         if (event.target !== this.currentContentView)
305             return;
306
307         this._findBanner.numberOfResults = this.currentContentView.numberOfSearchResults;
308     }
309
310     _updateHierarchicalPathNavigationItem(representedObject)
311     {
312         if (!this.delegate || typeof this.delegate.contentBrowserTreeElementForRepresentedObject !== "function")
313             return;
314
315         var treeElement = representedObject ? this.delegate.contentBrowserTreeElementForRepresentedObject(this, representedObject) : null;
316         var pathComponents = [];
317
318         while (treeElement && !treeElement.root) {
319             var pathComponent = new WebInspector.GeneralTreeElementPathComponent(treeElement);
320             pathComponents.unshift(pathComponent);
321             treeElement = treeElement.parent;
322         }
323
324         this._hierarchicalPathNavigationItem.components = pathComponents;
325     }
326
327     _updateContentViewSelectionPathNavigationItem(contentView)
328     {
329         var selectionPathComponents = contentView ? contentView.selectionPathComponents || [] : [];
330         this._contentViewSelectionPathNavigationItem.components = selectionPathComponents;
331
332         if (!selectionPathComponents.length) {
333             this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = false;
334             this._navigationBar.removeNavigationItem(this._contentViewSelectionPathNavigationItem);
335             return;
336         }
337
338         // Insert the _contentViewSelectionPathNavigationItem after the _hierarchicalPathNavigationItem, if needed.
339         if (!this._navigationBar.navigationItems.includes(this._contentViewSelectionPathNavigationItem)) {
340             var hierarchicalPathItemIndex = this._navigationBar.navigationItems.indexOf(this._hierarchicalPathNavigationItem);
341             console.assert(hierarchicalPathItemIndex !== -1);
342             this._navigationBar.insertNavigationItem(this._contentViewSelectionPathNavigationItem, hierarchicalPathItemIndex + 1);
343             this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = true;
344         }
345     }
346
347     _updateBackForwardButtons()
348     {
349         if (!this._backButtonNavigationItem || !this._forwardButtonNavigationItem)
350             return;
351
352         this._backButtonNavigationItem.enabled = this.canGoBack();
353         this._forwardButtonNavigationItem.enabled = this.canGoForward();
354     }
355
356     _updateContentViewNavigationItems()
357     {
358         var navigationBar = this.navigationBar;
359
360         // First, we remove the navigation items added by the previous content view.
361         this._currentContentViewNavigationItems.forEach(function(navigationItem) {
362             navigationBar.removeNavigationItem(navigationItem);
363         });
364
365         var currentContentView = this.currentContentView;
366         if (!currentContentView) {
367             this._currentContentViewNavigationItems = [];
368             return;
369         }
370
371         var insertionIndex = navigationBar.navigationItems.indexOf(this._dividingFlexibleSpaceNavigationItem) + 1;
372         console.assert(insertionIndex >= 0);
373
374         // Keep track of items we'll be adding to the navigation bar.
375         var newNavigationItems = [];
376
377         // Go through each of the items of the new content view and add a divider before them.
378         currentContentView.navigationItems.forEach(function(navigationItem, index) {
379             // Add dividers before items unless it's the first item and not a button.
380             if (index !== 0 || navigationItem instanceof WebInspector.ButtonNavigationItem) {
381                 var divider = new WebInspector.DividerNavigationItem;
382                 navigationBar.insertNavigationItem(divider, insertionIndex++);
383                 newNavigationItems.push(divider);
384             }
385             navigationBar.insertNavigationItem(navigationItem, insertionIndex++);
386             newNavigationItems.push(navigationItem);
387         });
388
389         // Remember the navigation items we inserted so we can remove them
390         // for the next content view.
391         this._currentContentViewNavigationItems = newNavigationItems;
392     }
393
394     _updateFindBanner(currentContentView)
395     {
396         if (!currentContentView) {
397             this._findBanner.targetElement = null;
398             this._findBanner.numberOfResults = null;
399             return;
400         }
401
402         this._findBanner.targetElement = currentContentView.element;
403         this._findBanner.numberOfResults = currentContentView.hasPerformedSearch ? currentContentView.numberOfSearchResults : null;
404
405         if (currentContentView.supportsSearch && this._findBanner.searchQuery) {
406             currentContentView.automaticallyRevealFirstSearchResult = this._findBanner.showing;
407             currentContentView.performSearch(this._findBanner.searchQuery);
408         }
409     }
410
411     _dispatchCurrentRepresentedObjectsDidChangeEventSoon()
412     {
413         if (this._currentRepresentedObjectsDidChangeTimeout)
414             return;
415         this._currentRepresentedObjectsDidChangeTimeout = setTimeout(this._dispatchCurrentRepresentedObjectsDidChangeEvent.bind(this), 0);
416     }
417
418     _dispatchCurrentRepresentedObjectsDidChangeEvent()
419     {
420         if (this._currentRepresentedObjectsDidChangeTimeout) {
421             clearTimeout(this._currentRepresentedObjectsDidChangeTimeout);
422             delete this._currentRepresentedObjectsDidChangeTimeout;
423         }
424
425         this.dispatchEventToListeners(WebInspector.ContentBrowser.Event.CurrentRepresentedObjectsDidChange);
426     }
427
428     _contentViewSelectionPathComponentDidChange(event)
429     {
430         if (event.target !== this.currentContentView)
431             return;
432
433         this._updateContentViewSelectionPathNavigationItem(event.target);
434         this._updateBackForwardButtons();
435
436         this._updateContentViewNavigationItems();
437
438         this._navigationBar.needsLayout();
439
440         this._dispatchCurrentRepresentedObjectsDidChangeEventSoon();
441     }
442
443     _contentViewSupplementalRepresentedObjectsDidChange(event)
444     {
445         if (event.target !== this.currentContentView)
446             return;
447
448         this._dispatchCurrentRepresentedObjectsDidChangeEventSoon();
449     }
450
451     _currentContentViewDidChange(event)
452     {
453         var currentContentView = this.currentContentView;
454
455         this._updateHierarchicalPathNavigationItem(currentContentView ? currentContentView.representedObject : null);
456         this._updateContentViewSelectionPathNavigationItem(currentContentView);
457         this._updateBackForwardButtons();
458
459         this._updateContentViewNavigationItems();
460         this._updateFindBanner(currentContentView);
461
462         this._navigationBar.needsLayout();
463
464         this.dispatchEventToListeners(WebInspector.ContentBrowser.Event.CurrentContentViewDidChange);
465
466         this._dispatchCurrentRepresentedObjectsDidChangeEvent();
467     }
468
469     _contentViewNavigationItemsDidChange(event)
470     {
471         if (event.target !== this.currentContentView)
472             return;
473
474         this._updateContentViewNavigationItems();
475         this._navigationBar.needsLayout();
476     }
477
478     _hierarchicalPathComponentWasSelected(event)
479     {
480         console.assert(event.data.pathComponent instanceof WebInspector.GeneralTreeElementPathComponent);
481
482         var treeElement = event.data.pathComponent.generalTreeElement;
483         var originalTreeElement = treeElement;
484
485         // Some tree elements (like folders) are not viewable. Find the first descendant that is viewable.
486         while (treeElement && !WebInspector.ContentView.isViewable(treeElement.representedObject))
487             treeElement = treeElement.traverseNextTreeElement(false, originalTreeElement, false);
488
489         if (!treeElement)
490             return;
491
492         treeElement.revealAndSelect();
493     }
494 };
495
496 WebInspector.ContentBrowser.Event = {
497     CurrentRepresentedObjectsDidChange: "content-browser-current-represented-objects-did-change",
498     CurrentContentViewDidChange: "content-browser-current-content-view-did-change"
499 };