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