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