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