Merge sync and async code paths for getting context menus
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / QuickConsole.js
1 /*
2  * Copyright (C) 2013-2016 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 WI.QuickConsole = class QuickConsole extends WI.View
27 {
28     constructor(element)
29     {
30         super(element);
31
32         this._toggleOrFocusKeyboardShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Escape, this._toggleOrFocus.bind(this));
33         this._toggleOrFocusKeyboardShortcut.implicitlyPreventsDefault = false;
34
35         this._mainExecutionContextPathComponent = this._createExecutionContextPathComponent(WI.mainTarget.executionContext);
36
37         this._otherExecutionContextPathComponents = [];
38         this._frameToPathComponent = new Map;
39         this._targetToPathComponent = new Map;
40
41         this._restoreSelectedExecutionContextForFrame = false;
42
43         this.element.classList.add("quick-console");
44         this.element.addEventListener("mousedown", this._handleMouseDown.bind(this));
45
46         this.prompt = new WI.ConsolePrompt(null, "text/javascript");
47         this.prompt.element.classList.add("text-prompt");
48         this.addSubview(this.prompt);
49
50         // FIXME: CodeMirror 4 has a default "Esc" key handler that always prevents default.
51         // Our keyboard shortcut above will respect the default prevented and ignore the event
52         // and not toggle the console. Install our own Escape key handler that will trigger
53         // when the ConsolePrompt is empty, to restore toggling behavior. A better solution
54         // would be for CodeMirror's event handler to pass if it doesn't do anything.
55         this.prompt.escapeKeyHandlerWhenEmpty = function() { WI.toggleSplitConsole(); };
56
57         this._navigationBar = new WI.QuickConsoleNavigationBar;
58         this.addSubview(this._navigationBar);
59
60         this._executionContextSelectorItem = new WI.HierarchicalPathNavigationItem;
61         this._executionContextSelectorItem.showSelectorArrows = true;
62         this._navigationBar.addNavigationItem(this._executionContextSelectorItem);
63
64         this._executionContextSelectorDivider = new WI.DividerNavigationItem;
65         this._navigationBar.addNavigationItem(this._executionContextSelectorDivider);
66
67         this._rebuildExecutionContextPathComponents();
68
69         WI.Frame.addEventListener(WI.Frame.Event.PageExecutionContextChanged, this._framePageExecutionContextsChanged, this);
70         WI.Frame.addEventListener(WI.Frame.Event.ExecutionContextsCleared, this._frameExecutionContextsCleared, this);
71
72         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this._debuggerActiveCallFrameDidChange, this);
73
74         WI.runtimeManager.addEventListener(WI.RuntimeManager.Event.ActiveExecutionContextChanged, this._activeExecutionContextChanged, this);
75
76         WI.targetManager.addEventListener(WI.TargetManager.Event.TargetAdded, this._targetAdded, this);
77         WI.targetManager.addEventListener(WI.TargetManager.Event.TargetRemoved, this._targetRemoved, this);
78
79         WI.consoleDrawer.addEventListener(WI.ConsoleDrawer.Event.CollapsedStateChanged, this._updateStyles, this);
80
81         WI.TabBrowser.addEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, this._updateStyles, this);
82     }
83
84     // Public
85
86     get navigationBar()
87     {
88         return this._navigationBar;
89     }
90
91     get selectedExecutionContext()
92     {
93         return WI.runtimeManager.activeExecutionContext;
94     }
95
96     set selectedExecutionContext(executionContext)
97     {
98         WI.runtimeManager.activeExecutionContext = executionContext;
99     }
100
101     closed()
102     {
103         WI.Frame.removeEventListener(null, null, this);
104         WI.debuggerManager.removeEventListener(null, null, this);
105         WI.runtimeManager.removeEventListener(null, null, this);
106         WI.targetManager.removeEventListener(null, null, this);
107         WI.consoleDrawer.removeEventListener(null, null, this);
108         WI.TabBrowser.removeEventListener(null, null, this);
109
110         super.closed();
111     }
112
113     // Protected
114
115     layout()
116     {
117         // A hard maximum size of 33% of the window.
118         let maximumAllowedHeight = Math.round(window.innerHeight * 0.33);
119         this.prompt.element.style.maxHeight = maximumAllowedHeight + "px";
120     }
121
122     // Private
123
124     _handleMouseDown(event)
125     {
126         if (event.target !== this.element)
127             return;
128
129         event.preventDefault();
130         this.prompt.focus();
131     }
132
133     _executionContextPathComponentsToDisplay()
134     {
135         // If we are in the debugger the console will use the active call frame, don't show the selector.
136         if (WI.debuggerManager.activeCallFrame)
137             return [];
138
139         // If there is only the Main ExecutionContext, don't show the selector.
140         if (!this._otherExecutionContextPathComponents.length)
141             return [];
142
143         if (this.selectedExecutionContext === WI.mainTarget.executionContext)
144             return [this._mainExecutionContextPathComponent];
145
146         return this._otherExecutionContextPathComponents.filter((component) => component.representedObject === this.selectedExecutionContext);
147     }
148
149     _rebuildExecutionContextPathComponents()
150     {
151         let components = this._executionContextPathComponentsToDisplay();
152         let isEmpty = !components.length;
153
154         this._executionContextSelectorItem.components = components;
155
156         this._executionContextSelectorItem.hidden = isEmpty;
157         this._executionContextSelectorDivider.hidden = isEmpty;
158     }
159
160     _framePageExecutionContextsChanged(event)
161     {
162         let frame = event.target;
163
164         let shouldAutomaticallySelect = this._restoreSelectedExecutionContextForFrame === frame;
165
166         let newExecutionContextPathComponent = this._insertExecutionContextPathComponentForFrame(frame, shouldAutomaticallySelect);
167
168         if (shouldAutomaticallySelect) {
169             this._restoreSelectedExecutionContextForFrame = null;
170             this.selectedExecutionContext = newExecutionContextPathComponent.representedObject;
171         }
172     }
173
174     _frameExecutionContextsCleared(event)
175     {
176         let frame = event.target;
177
178         // If this frame is navigating and it is selected in the UI we want to reselect its new item after navigation.
179         if (event.data.committingProvisionalLoad && !this._restoreSelectedExecutionContextForFrame) {
180             let executionContextPathComponent = this._frameToPathComponent.get(frame);
181             if (executionContextPathComponent && executionContextPathComponent.representedObject === this.selectedExecutionContext) {
182                 this._restoreSelectedExecutionContextForFrame = frame;
183                 // As a fail safe, if the frame never gets an execution context, clear the restore value.
184                 setTimeout(() => { this._restoreSelectedExecutionContextForFrame = false; }, 10);
185             }
186         }
187
188         this._removeExecutionContextPathComponentForFrame(frame);
189     }
190
191     _activeExecutionContextChanged(event)
192     {
193         this._rebuildExecutionContextPathComponents();
194     }
195
196     _createExecutionContextPathComponent(executionContext, preferredName)
197     {
198         console.assert(executionContext instanceof WI.ExecutionContext);
199
200         let pathComponent = new WI.HierarchicalPathComponent(preferredName || executionContext.name, "execution-context", executionContext, true, true);
201         pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
202         pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);
203         pathComponent.truncatedDisplayNameLength = 50;
204         return pathComponent;
205     }
206
207     _createExecutionContextPathComponentFromFrame(frame)
208     {
209         let preferredName = frame.name ? frame.name + " \u2014 " + frame.mainResource.displayName : frame.mainResource.displayName;
210         return this._createExecutionContextPathComponent(frame.pageExecutionContext, preferredName);
211     }
212
213     _compareExecutionContextPathComponents(a, b)
214     {
215         let aExecutionContext = a.representedObject;
216         let bExecutionContext = b.representedObject;
217
218         // "Targets" (workers) at the top.
219         let aNonMainTarget = aExecutionContext.target !== WI.mainTarget;
220         let bNonMainTarget = bExecutionContext.target !== WI.mainTarget;
221         if (aNonMainTarget && !bNonMainTarget)
222             return -1;
223         if (bNonMainTarget && !aNonMainTarget)
224             return 1;
225         if (aNonMainTarget && bNonMainTarget)
226             return a.displayName.extendedLocaleCompare(b.displayName);
227
228         // "Main Frame" follows.
229         if (aExecutionContext === WI.mainTarget.executionContext)
230             return -1;
231         if (bExecutionContext === WI.mainTarget.executionContext)
232             return 1;
233
234         // Only Frame contexts remain.
235         console.assert(aExecutionContext.frame);
236         console.assert(bExecutionContext.frame);
237
238         // Frames with a name above frames without a name.
239         if (aExecutionContext.frame.name && !bExecutionContext.frame.name)
240             return -1;
241         if (!aExecutionContext.frame.name && bExecutionContext.frame.name)
242             return 1;
243
244         return a.displayName.extendedLocaleCompare(b.displayName);
245     }
246
247     _insertOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild)
248     {
249         let index = insertionIndexForObjectInListSortedByFunction(executionContextPathComponent, this._otherExecutionContextPathComponents, this._compareExecutionContextPathComponents);
250
251         let prev = index > 0 ? this._otherExecutionContextPathComponents[index - 1] : this._mainExecutionContextPathComponent;
252         let next = this._otherExecutionContextPathComponents[index] || null;
253         if (prev) {
254             prev.nextSibling = executionContextPathComponent;
255             executionContextPathComponent.previousSibling = prev;
256         }
257         if (next) {
258             next.previousSibling = executionContextPathComponent;
259             executionContextPathComponent.nextSibling = next;
260         }
261
262         this._otherExecutionContextPathComponents.splice(index, 0, executionContextPathComponent);
263
264         if (!skipRebuild)
265             this._rebuildExecutionContextPathComponents();
266     }
267
268     _removeOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild)
269     {
270         executionContextPathComponent.removeEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
271         executionContextPathComponent.removeEventListener(WI.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);
272
273         let prev = executionContextPathComponent.previousSibling;
274         let next = executionContextPathComponent.nextSibling;
275         if (prev)
276             prev.nextSibling = next;
277         if (next)
278             next.previousSibling = prev;
279
280         this._otherExecutionContextPathComponents.remove(executionContextPathComponent, true);
281
282         if (!skipRebuild)
283             this._rebuildExecutionContextPathComponents();
284     }
285
286     _insertExecutionContextPathComponentForFrame(frame, skipRebuild)
287     {
288         if (frame.isMainFrame())
289             return this._mainExecutionContextPathComponent;
290
291         let executionContextPathComponent = this._createExecutionContextPathComponentFromFrame(frame);
292         this._insertOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild);
293         this._frameToPathComponent.set(frame, executionContextPathComponent);
294
295         return executionContextPathComponent;
296     }
297
298     _removeExecutionContextPathComponentForFrame(frame, skipRebuild)
299     {
300         if (frame.isMainFrame())
301             return;
302
303         let executionContextPathComponent = this._frameToPathComponent.take(frame);
304         this._removeOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild);
305     }
306
307     _targetAdded(event)
308     {
309         let target = event.data.target;
310         console.assert(target.type === WI.Target.Type.Worker);
311         let preferredName = WI.UIString("Worker \u2014 %s").format(target.displayName);
312         let executionContextPathComponent = this._createExecutionContextPathComponent(target.executionContext, preferredName);
313
314         this._targetToPathComponent.set(target, executionContextPathComponent);
315         this._insertOtherExecutionContextPathComponent(executionContextPathComponent);
316     }
317
318     _targetRemoved(event)
319     {
320         let target = event.data.target;
321         let executionContextPathComponent = this._targetToPathComponent.take(target);
322         this._removeOtherExecutionContextPathComponent(executionContextPathComponent);
323
324         if (this.selectedExecutionContext === executionContextPathComponent.representedObject)
325             this.selectedExecutionContext = WI.mainTarget.executionContext;
326     }
327
328     _pathComponentSelected(event)
329     {
330         let executionContext = event.data.pathComponent.representedObject;
331         this.selectedExecutionContext = executionContext;
332     }
333
334     _pathComponentClicked(event)
335     {
336         this.prompt.focus();
337     }
338
339     _debuggerActiveCallFrameDidChange(event)
340     {
341         this._rebuildExecutionContextPathComponents();
342     }
343
344     _toggleOrFocus(event)
345     {
346         if (this.prompt.focused) {
347             WI.toggleSplitConsole();
348             event.preventDefault();
349         } else if (!WI.isEditingAnyField() && !WI.isEventTargetAnEditableField(event)) {
350             this.prompt.focus();
351             event.preventDefault();
352         }
353     }
354
355     _updateStyles()
356     {
357         this.element.classList.toggle("showing-log", WI.isShowingConsoleTab() || WI.isShowingSplitConsole());
358     }
359 };