Web Inspector: Edit Breakpoint popover sometimes appears misplaced in top left
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Breakpoint.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.Breakpoint = function(sourceCodeLocationOrInfo, disabled, condition)
27 {
28     WebInspector.Object.call(this);
29
30     if (sourceCodeLocationOrInfo instanceof WebInspector.SourceCodeLocation) {
31         var sourceCode = sourceCodeLocationOrInfo.sourceCode;
32         var url = sourceCode ? sourceCode.url : null;
33         var scriptIdentifier = sourceCode instanceof WebInspector.Script ? sourceCode.id : null;
34         var location = sourceCodeLocationOrInfo;
35     } else if (sourceCodeLocationOrInfo && typeof sourceCodeLocationOrInfo === "object") {
36         var url = sourceCodeLocationOrInfo.url;
37         var lineNumber = sourceCodeLocationOrInfo.lineNumber || 0;
38         var columnNumber = sourceCodeLocationOrInfo.columnNumber || 0;
39         var location = new WebInspector.SourceCodeLocation(null, lineNumber, columnNumber);
40         var autoContinue = sourceCodeLocationOrInfo.autoContinue || false;
41         var actions = sourceCodeLocationOrInfo.actions || [];
42         for (var i = 0; i < actions.length; ++i)
43             actions[i] = new WebInspector.BreakpointAction(this, actions[i]);
44         disabled = sourceCodeLocationOrInfo.disabled;
45         condition = sourceCodeLocationOrInfo.condition;
46     } else
47         console.error("Unexpected type passed to WebInspector.Breakpoint", sourceCodeLocationOrInfo);
48
49     this._id = null;
50     this._url = url || null;
51     this._scriptIdentifier = scriptIdentifier || null;
52     this._disabled = disabled || false;
53     this._condition = condition || "";
54     this._autoContinue = autoContinue || false;
55     this._actions = actions || [];
56     this._resolved = false;
57
58     this._sourceCodeLocation = location;
59     this._sourceCodeLocation.addEventListener(WebInspector.SourceCodeLocation.Event.LocationChanged, this._sourceCodeLocationLocationChanged, this);
60     this._sourceCodeLocation.addEventListener(WebInspector.SourceCodeLocation.Event.DisplayLocationChanged, this._sourceCodeLocationDisplayLocationChanged, this);
61 };
62
63 WebInspector.Object.addConstructorFunctions(WebInspector.Breakpoint);
64
65 WebInspector.Breakpoint.PopoverClassName = "edit-breakpoint-popover-content";
66 WebInspector.Breakpoint.WidePopoverClassName = "wide";
67 WebInspector.Breakpoint.PopoverConditionInputId = "edit-breakpoint-popover-condition";
68 WebInspector.Breakpoint.PopoverOptionsAutoContinueInputId = "edit-breakpoint-popoover-auto-continue";
69
70 WebInspector.Breakpoint.DefaultBreakpointActionType = WebInspector.BreakpointAction.Type.Log;
71
72 WebInspector.Breakpoint.Event = {
73     DisabledStateDidChange: "breakpoint-disabled-state-did-change",
74     ResolvedStateDidChange: "breakpoint-resolved-state-did-change",
75     ConditionDidChange: "breakpoint-condition-did-change",
76     ActionsDidChange: "breakpoint-actions-did-change",
77     AutoContinueDidChange: "breakpoint-auto-continue-did-change",
78     LocationDidChange: "breakpoint-location-did-change",
79     DisplayLocationDidChange: "breakpoint-display-location-did-change",
80 };
81
82 WebInspector.Breakpoint.prototype = {
83     constructor: WebInspector.Breakpoint,
84
85     // Public
86
87     get id()
88     {
89         return this._id;
90     },
91
92     set id(id)
93     {
94         this._id = id || null;
95     },
96
97     get url()
98     {
99         return this._url;
100     },
101
102     get scriptIdentifier()
103     {
104         return this._scriptIdentifier;
105     },
106
107     get sourceCodeLocation()
108     {
109         return this._sourceCodeLocation;
110     },
111
112     get resolved()
113     {
114         return this._resolved && WebInspector.debuggerManager.breakpointsEnabled;
115     },
116
117     set resolved(resolved)
118     {
119         if (this._resolved === resolved)
120             return;
121
122         this._resolved = resolved || false;
123
124         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.ResolvedStateDidChange);
125     },
126
127     get disabled()
128     {
129         return this._disabled;
130     },
131
132     set disabled(disabled)
133     {
134         if (this._disabled === disabled)
135             return;
136
137         this._disabled = disabled || false;
138
139         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.DisabledStateDidChange);
140     },
141
142     get condition()
143     {
144         return this._condition;
145     },
146
147     set condition(condition)
148     {
149         if (this._condition === condition)
150             return;
151
152         this._condition = condition;
153
154         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.ConditionDidChange);
155     },
156
157     get autoContinue()
158     {
159         return this._autoContinue;
160     },
161
162     set autoContinue(cont)
163     {
164         if (this._autoContinue === cont)
165             return;
166
167         this._autoContinue = cont;
168
169         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.AutoContinueDidChange);
170     },
171
172     get actions()
173     {
174         return this._actions;
175     },
176
177     get options()
178     {
179         return {
180             condition: this._condition,
181             actions: this._serializableActions(),
182             autoContinue: this._autoContinue
183         };
184     },
185
186     get info()
187     {
188         // The id, scriptIdentifier and resolved state are tied to the current session, so don't include them for serialization.
189         return {
190             url: this._url,
191             lineNumber: this._sourceCodeLocation.lineNumber,
192             columnNumber: this._sourceCodeLocation.columnNumber,
193             disabled: this._disabled,
194             condition: this._condition,
195             actions: this._serializableActions(),
196             autoContinue: this._autoContinue
197         };
198     },
199
200     appendContextMenuItems: function(contextMenu, breakpointDisplayElement)
201     {
202         console.assert(document.body.contains(breakpointDisplayElement), "breakpoint popover display element must be in the DOM");
203
204         var boundingClientRect = breakpointDisplayElement.getBoundingClientRect();
205
206         function editBreakpoint()
207         {
208             this._showEditBreakpointPopover(boundingClientRect);
209         }
210
211         function removeBreakpoint()
212         {
213             WebInspector.debuggerManager.removeBreakpoint(this);
214         }
215
216         function toggleBreakpoint()
217         {
218             this.disabled = !this.disabled;
219         }
220
221         function revealOriginalSourceCodeLocation()
222         {
223             WebInspector.resourceSidebarPanel.showOriginalOrFormattedSourceCodeLocation(this._sourceCodeLocation);
224         }
225
226         if (WebInspector.debuggerManager.isBreakpointEditable(this))
227             contextMenu.appendItem(WebInspector.UIString("Edit Breakpoint…"), editBreakpoint.bind(this));
228
229         if (this._disabled)
230             contextMenu.appendItem(WebInspector.UIString("Enable Breakpoint"), toggleBreakpoint.bind(this));
231         else
232             contextMenu.appendItem(WebInspector.UIString("Disable Breakpoint"), toggleBreakpoint.bind(this));
233
234         if (WebInspector.debuggerManager.isBreakpointRemovable(this)) {
235             contextMenu.appendSeparator();
236             contextMenu.appendItem(WebInspector.UIString("Delete Breakpoint"), removeBreakpoint.bind(this));
237         }
238
239         if (this._sourceCodeLocation.hasMappedLocation()) {
240             contextMenu.appendSeparator();
241             contextMenu.appendItem(WebInspector.UIString("Reveal in Original Resource"), revealOriginalSourceCodeLocation.bind(this));
242         }
243     },
244
245     createAction: function(type, precedingAction)
246     {
247         var newAction = new WebInspector.BreakpointAction(this, type, null);
248
249         if (!precedingAction)
250             this._actions.push(newAction);
251         else {
252             var index = this._actions.indexOf(precedingAction);
253             console.assert(index !== -1);
254             if (index === -1)
255                 this._actions.push(newAction);
256             else
257                 this._actions.splice(index + 1, 0, newAction);
258         }
259
260         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.ActionsDidChange);
261
262         return newAction;
263     },
264
265     recreateAction: function(type, actionToReplace)
266     {
267         var newAction = new WebInspector.BreakpointAction(this, type, null);
268
269         var index = this._actions.indexOf(actionToReplace);
270         console.assert(index !== -1);
271         if (index === -1)
272             return null;
273
274         this._actions[index] = newAction;
275
276         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.ActionsDidChange);
277
278         return newAction;
279     },
280
281     removeAction: function(action)
282     {
283         var index = this._actions.indexOf(action);
284         console.assert(index !== -1);
285         if (index === -1)
286             return;
287
288         this._actions.splice(index, 1);
289
290         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.ActionsDidChange);
291     },
292
293     // Protected (Called by BreakpointAction)
294
295     breakpointActionDidChange: function(action)
296     {
297         var index = this._actions.indexOf(action);
298         console.assert(index !== -1);
299         if (index === -1)
300             return;
301
302         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.ActionsDidChange);
303     },
304
305     // Private
306
307     _serializableActions: function()
308     {
309         var actions = [];
310         for (var i = 0; i < this._actions.length; ++i)
311             actions.push(this._actions[i].info);
312         return actions;
313     },
314
315     _popoverToggleEnabledCheckboxChanged: function(event)
316     {
317         this.disabled = !event.target.checked;
318     },
319
320     _popoverConditionInputChanged: function(event)
321     {
322         this.condition = event.target.value;
323     },
324
325     _popoverToggleAutoContinueCheckboxChanged: function(event)
326     {
327         this.autoContinue = event.target.checked;
328     },
329
330     _popoverConditionInputKeyDown: function(event)
331     {
332         if (this._keyboardShortcutEsc.matchesEvent(event) || this._keyboardShortcutEnter.matchesEvent(event)) {
333             this._popover.dismiss();
334             event.stopPropagation();
335             event.preventDefault();
336         }
337     },
338
339     _editBreakpointPopoverContentElement: function()
340     {
341         var content = this._popoverContentElement = document.createElement("div");
342         content.className = WebInspector.Breakpoint.PopoverClassName;
343
344         var checkboxElement = document.createElement("input");
345         checkboxElement.type = "checkbox";
346         checkboxElement.checked = !this._disabled;
347         checkboxElement.addEventListener("change", this._popoverToggleEnabledCheckboxChanged.bind(this));
348
349         var checkboxLabel = document.createElement("label");
350         checkboxLabel.className = "toggle";
351         checkboxLabel.appendChild(checkboxElement);
352         checkboxLabel.appendChild(document.createTextNode(this._sourceCodeLocation.displayLocationString()));
353
354         var table = document.createElement("table");
355
356         var conditionRow = table.appendChild(document.createElement("tr"));
357         var conditionHeader = conditionRow.appendChild(document.createElement("th"));
358         var conditionData = conditionRow.appendChild(document.createElement("td"));
359         var conditionLabel = conditionHeader.appendChild(document.createElement("label"));
360         var conditionInput = conditionData.appendChild(document.createElement("input"));
361         conditionInput.id = WebInspector.Breakpoint.PopoverConditionInputId;
362         conditionInput.value = this._condition || "";
363         conditionInput.spellcheck = false;
364         conditionInput.addEventListener("change", this._popoverConditionInputChanged.bind(this));
365         conditionInput.addEventListener("keydown", this._popoverConditionInputKeyDown.bind(this));
366         conditionInput.placeholder = WebInspector.UIString("Conditional expression");
367         conditionLabel.setAttribute("for", conditionInput.id);
368         conditionLabel.textContent = WebInspector.UIString("Condition");
369
370         if (DebuggerAgent.setBreakpoint.supports("options")) {
371             var actionRow = table.appendChild(document.createElement("tr"));
372             var actionHeader = actionRow.appendChild(document.createElement("th"));
373             var actionData = this._actionsContainer = actionRow.appendChild(document.createElement("td"));
374             var actionLabel = actionHeader.appendChild(document.createElement("label"));
375             actionLabel.textContent = WebInspector.UIString("Action");
376
377             if (!this._actions.length)
378                 this._popoverActionsCreateAddActionButton();
379             else {
380                 this._popoverContentElement.classList.add(WebInspector.Breakpoint.WidePopoverClassName);
381                 for (var i = 0; i < this._actions.length; ++i) {
382                     var breakpointActionView = new WebInspector.BreakpointActionView(this._actions[i], this, true);
383                     this._popoverActionsInsertBreakpointActionView(breakpointActionView, i);
384                 }
385             }
386
387             var optionsRow = table.appendChild(document.createElement("tr"));
388             var optionsHeader = optionsRow.appendChild(document.createElement("th"));
389             var optionsData = optionsRow.appendChild(document.createElement("td"));
390             var optionsLabel = optionsHeader.appendChild(document.createElement("label"));
391             var optionsCheckbox = optionsData.appendChild(document.createElement("input"));
392             var optionsCheckboxLabel = optionsData.appendChild(document.createElement("label"));
393             optionsCheckbox.id = WebInspector.Breakpoint.PopoverOptionsAutoContinueInputId;
394             optionsCheckbox.type = "checkbox";
395             optionsCheckbox.checked = this._autoContinue;
396             optionsCheckbox.addEventListener("change", this._popoverToggleAutoContinueCheckboxChanged.bind(this));
397             optionsLabel.textContent = WebInspector.UIString("Options");
398             optionsCheckboxLabel.setAttribute("for", optionsCheckbox.id);
399             optionsCheckboxLabel.textContent = WebInspector.UIString("Automatically continue after evaluating");
400         }
401
402         content.appendChild(checkboxLabel);
403         content.appendChild(table);
404
405         return content;
406     },
407
408     _popoverActionsCreateAddActionButton: function()
409     {
410         this._popoverContentElement.classList.remove(WebInspector.Breakpoint.WidePopoverClassName);
411         this._actionsContainer.removeChildren();
412
413         var addActionButton = this._actionsContainer.appendChild(document.createElement("button"));
414         addActionButton.textContent = WebInspector.UIString("Add Action");
415         addActionButton.addEventListener("click", this._popoverActionsAddActionButtonClicked.bind(this));
416     },
417
418     _popoverActionsAddActionButtonClicked: function(event)
419     {
420         this._popoverContentElement.classList.add(WebInspector.Breakpoint.WidePopoverClassName);
421         this._actionsContainer.removeChildren();
422
423         var newAction = this.createAction(WebInspector.Breakpoint.DefaultBreakpointActionType);
424         var newBreakpointActionView = new WebInspector.BreakpointActionView(newAction, this);
425         this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, -1);
426
427         this._popover.update();
428     },
429
430     _popoverActionsInsertBreakpointActionView: function(breakpointActionView, index)
431     {
432         if (index === -1)
433             this._actionsContainer.appendChild(breakpointActionView.element)
434         else {
435             var nextElement = this._actionsContainer.children[index + 1] || null;
436             this._actionsContainer.insertBefore(breakpointActionView.element, nextElement);
437         }
438     },
439
440     breakpointActionViewAppendActionView: function(breakpointActionView, newAction)
441     {
442         var newBreakpointActionView = new WebInspector.BreakpointActionView(newAction, this);
443
444         var index = 0;
445         var children = this._actionsContainer.children;
446         for (var i = 0; children.length; ++i) {
447             if (children[i] === breakpointActionView.element) {
448                 index = i;
449                 break;
450             }
451         }
452
453         this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, index);
454
455         this._popover.update();
456     },
457
458     breakpointActionViewRemoveActionView: function(breakpointActionView)
459     {
460         breakpointActionView.element.remove();
461
462         if (!this._actionsContainer.children.length)
463             this._popoverActionsCreateAddActionButton();
464
465         this._popover.update();
466     },
467
468     breakpointActionViewResized: function(breakpointActionView)
469     {
470         this._popover.update();
471     },
472
473     willDismissPopover: function(popover)
474     {
475         console.assert(this._popover === popover);
476         delete this._popoverContentElement;
477         delete this._actionsContainer;
478         delete this._popover;
479     },
480
481     _showEditBreakpointPopover: function(boundingClientRect)
482     {
483         const padding = 2;
484         var bounds = WebInspector.Rect.rectFromClientRect(boundingClientRect);
485
486         bounds.origin.x -= 1; // Move the anchor left one pixel so it looks more centered.
487         bounds.origin.x -= padding;
488         bounds.origin.y -= padding;
489         bounds.size.width += padding * 2;
490         bounds.size.height += padding * 2;
491
492         this._popover = this._popover || new WebInspector.Popover(this);
493         this._popover.content = this._editBreakpointPopoverContentElement();
494         this._popover.present(bounds, [WebInspector.RectEdge.MAX_Y]);
495
496         if (!this._keyboardShortcutEsc) {
497             this._keyboardShortcutEsc = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Escape);
498             this._keyboardShortcutEnter = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Enter);
499         }
500
501         document.getElementById(WebInspector.Breakpoint.PopoverConditionInputId).select();
502     },
503
504     _sourceCodeLocationLocationChanged: function(event)
505     {
506         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.LocationDidChange, event.data);
507     },
508
509     _sourceCodeLocationDisplayLocationChanged: function(event)
510     {
511         this.dispatchEventToListeners(WebInspector.Breakpoint.Event.DisplayLocationDidChange, event.data);
512     }
513 };
514
515 WebInspector.Breakpoint.prototype.__proto__ = WebInspector.Object.prototype;