Web Inspector: Make Console's Execution Context picker stand out when it is non-default
[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         this._executionContextSelectorItem.element.classList.toggle("non-default-execution-context", this.selectedExecutionContext !== WI.mainTarget.executionContext);
196     }
197
198     _createExecutionContextPathComponent(executionContext, preferredName)
199     {
200         console.assert(executionContext instanceof WI.ExecutionContext);
201
202         let pathComponent = new WI.HierarchicalPathComponent(preferredName || executionContext.name, "execution-context", executionContext, true, true);
203         pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
204         pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);
205         pathComponent.truncatedDisplayNameLength = 50;
206         return pathComponent;
207     }
208
209     _createExecutionContextPathComponentFromFrame(frame)
210     {
211         let preferredName = frame.name ? frame.name + " \u2014 " + frame.mainResource.displayName : frame.mainResource.displayName;
212         return this._createExecutionContextPathComponent(frame.pageExecutionContext, preferredName);
213     }
214
215     _compareExecutionContextPathComponents(a, b)
216     {
217         let aExecutionContext = a.representedObject;
218         let bExecutionContext = b.representedObject;
219
220         // "Targets" (workers) at the top.
221         let aNonMainTarget = aExecutionContext.target !== WI.mainTarget;
222         let bNonMainTarget = bExecutionContext.target !== WI.mainTarget;
223         if (aNonMainTarget && !bNonMainTarget)
224             return -1;
225         if (bNonMainTarget && !aNonMainTarget)
226             return 1;
227         if (aNonMainTarget && bNonMainTarget)
228             return a.displayName.extendedLocaleCompare(b.displayName);
229
230         // "Main Frame" follows.
231         if (aExecutionContext === WI.mainTarget.executionContext)
232             return -1;
233         if (bExecutionContext === WI.mainTarget.executionContext)
234             return 1;
235
236         // Only Frame contexts remain.
237         console.assert(aExecutionContext.frame);
238         console.assert(bExecutionContext.frame);
239
240         // Frames with a name above frames without a name.
241         if (aExecutionContext.frame.name && !bExecutionContext.frame.name)
242             return -1;
243         if (!aExecutionContext.frame.name && bExecutionContext.frame.name)
244             return 1;
245
246         return a.displayName.extendedLocaleCompare(b.displayName);
247     }
248
249     _insertOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild)
250     {
251         let index = insertionIndexForObjectInListSortedByFunction(executionContextPathComponent, this._otherExecutionContextPathComponents, this._compareExecutionContextPathComponents);
252
253         let prev = index > 0 ? this._otherExecutionContextPathComponents[index - 1] : this._mainExecutionContextPathComponent;
254         let next = this._otherExecutionContextPathComponents[index] || null;
255         if (prev) {
256             prev.nextSibling = executionContextPathComponent;
257             executionContextPathComponent.previousSibling = prev;
258         }
259         if (next) {
260             next.previousSibling = executionContextPathComponent;
261             executionContextPathComponent.nextSibling = next;
262         }
263
264         this._otherExecutionContextPathComponents.splice(index, 0, executionContextPathComponent);
265
266         if (!skipRebuild)
267             this._rebuildExecutionContextPathComponents();
268     }
269
270     _removeOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild)
271     {
272         executionContextPathComponent.removeEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
273         executionContextPathComponent.removeEventListener(WI.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);
274
275         let prev = executionContextPathComponent.previousSibling;
276         let next = executionContextPathComponent.nextSibling;
277         if (prev)
278             prev.nextSibling = next;
279         if (next)
280             next.previousSibling = prev;
281
282         this._otherExecutionContextPathComponents.remove(executionContextPathComponent, true);
283
284         if (!skipRebuild)
285             this._rebuildExecutionContextPathComponents();
286     }
287
288     _insertExecutionContextPathComponentForFrame(frame, skipRebuild)
289     {
290         if (frame.isMainFrame())
291             return this._mainExecutionContextPathComponent;
292
293         let executionContextPathComponent = this._createExecutionContextPathComponentFromFrame(frame);
294         this._insertOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild);
295         this._frameToPathComponent.set(frame, executionContextPathComponent);
296
297         return executionContextPathComponent;
298     }
299
300     _removeExecutionContextPathComponentForFrame(frame, skipRebuild)
301     {
302         if (frame.isMainFrame())
303             return;
304
305         let executionContextPathComponent = this._frameToPathComponent.take(frame);
306         this._removeOtherExecutionContextPathComponent(executionContextPathComponent, skipRebuild);
307     }
308
309     _targetAdded(event)
310     {
311         let target = event.data.target;
312         console.assert(target.type === WI.Target.Type.Worker);
313         let preferredName = WI.UIString("Worker \u2014 %s").format(target.displayName);
314         let executionContextPathComponent = this._createExecutionContextPathComponent(target.executionContext, preferredName);
315
316         this._targetToPathComponent.set(target, executionContextPathComponent);
317         this._insertOtherExecutionContextPathComponent(executionContextPathComponent);
318     }
319
320     _targetRemoved(event)
321     {
322         let target = event.data.target;
323         let executionContextPathComponent = this._targetToPathComponent.take(target);
324         this._removeOtherExecutionContextPathComponent(executionContextPathComponent);
325
326         if (this.selectedExecutionContext === executionContextPathComponent.representedObject)
327             this.selectedExecutionContext = WI.mainTarget.executionContext;
328     }
329
330     _pathComponentSelected(event)
331     {
332         let executionContext = event.data.pathComponent.representedObject;
333         this.selectedExecutionContext = executionContext;
334     }
335
336     _pathComponentClicked(event)
337     {
338         this.prompt.focus();
339     }
340
341     _debuggerActiveCallFrameDidChange(event)
342     {
343         this._rebuildExecutionContextPathComponents();
344     }
345
346     _toggleOrFocus(event)
347     {
348         if (this.prompt.focused) {
349             WI.toggleSplitConsole();
350             event.preventDefault();
351         } else if (!WI.isEditingAnyField() && !WI.isEventTargetAnEditableField(event)) {
352             this.prompt.focus();
353             event.preventDefault();
354         }
355     }
356
357     _updateStyles()
358     {
359         this.element.classList.toggle("showing-log", WI.isShowingConsoleTab() || WI.isShowingSplitConsole());
360     }
361 };