blob: f4d0c1cb74cc5af366db2714c448f7204f3a1ddd [file] [log] [blame]
/*
* Copyright (C) 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.BreakpointPopoverController = class BreakpointPopoverController extends WI.Object
{
constructor()
{
super();
this._breakpoint = null;
this._popover = null;
this._popoverContentElement = null;
}
// Public
appendContextMenuItems(contextMenu, breakpoint, breakpointDisplayElement)
{
console.assert(document.body.contains(breakpointDisplayElement), "Breakpoint popover display element must be in the DOM.");
const editBreakpoint = () => {
console.assert(!this._popover, "Breakpoint popover already exists.");
if (this._popover)
return;
this._createPopoverContent(breakpoint);
this._popover = new WI.Popover(this);
this._popover.content = this._popoverContentElement;
let bounds = WI.Rect.rectFromClientRect(breakpointDisplayElement.getBoundingClientRect());
bounds.origin.x -= 1; // Move the anchor left one pixel so it looks more centered.
this._popover.present(bounds.pad(2), [WI.RectEdge.MAX_Y]);
};
const removeBreakpoint = () => {
WI.debuggerManager.removeBreakpoint(breakpoint);
};
const toggleBreakpoint = () => {
breakpoint.disabled = !breakpoint.disabled;
};
const toggleAutoContinue = () => {
breakpoint.autoContinue = !breakpoint.autoContinue;
};
const revealOriginalSourceCodeLocation = () => {
const options = {
ignoreNetworkTab: true,
ignoreSearchTab: true,
};
WI.showOriginalOrFormattedSourceCodeLocation(breakpoint.sourceCodeLocation, options);
};
if (WI.debuggerManager.isBreakpointEditable(breakpoint))
contextMenu.appendItem(WI.UIString("Edit Breakpoint\u2026"), editBreakpoint);
if (breakpoint.autoContinue && !breakpoint.disabled) {
contextMenu.appendItem(WI.UIString("Disable Breakpoint"), toggleBreakpoint);
contextMenu.appendItem(WI.UIString("Cancel Automatic Continue"), toggleAutoContinue);
} else if (!breakpoint.disabled)
contextMenu.appendItem(WI.UIString("Disable Breakpoint"), toggleBreakpoint);
else
contextMenu.appendItem(WI.UIString("Enable Breakpoint"), toggleBreakpoint);
if (!breakpoint.autoContinue && !breakpoint.disabled && breakpoint.actions.length)
contextMenu.appendItem(WI.UIString("Set to Automatically Continue"), toggleAutoContinue);
if (WI.debuggerManager.isBreakpointRemovable(breakpoint))
contextMenu.appendItem(WI.UIString("Delete Breakpoint"), removeBreakpoint);
if (breakpoint._sourceCodeLocation.hasMappedLocation()) {
contextMenu.appendSeparator();
contextMenu.appendItem(WI.UIString("Reveal in Original Resource"), revealOriginalSourceCodeLocation);
}
}
// CodeMirrorCompletionController delegate
completionControllerShouldAllowEscapeCompletion()
{
return false;
}
// Private
_createPopoverContent(breakpoint)
{
console.assert(!this._popoverContentElement, "Popover content element already exists.");
if (this._popoverContentElement)
return;
this._breakpoint = breakpoint;
this._popoverContentElement = document.createElement("div");
this._popoverContentElement.className = "edit-breakpoint-popover-content";
let checkboxElement = document.createElement("input");
checkboxElement.type = "checkbox";
checkboxElement.checked = !this._breakpoint.disabled;
checkboxElement.addEventListener("change", this._popoverToggleEnabledCheckboxChanged.bind(this));
let checkboxLabel = document.createElement("label");
checkboxLabel.className = "toggle";
checkboxLabel.appendChild(checkboxElement);
checkboxLabel.append(this._breakpoint.sourceCodeLocation.displayLocationString());
let table = document.createElement("table");
let conditionRow = table.appendChild(document.createElement("tr"));
let conditionHeader = conditionRow.appendChild(document.createElement("th"));
let conditionData = conditionRow.appendChild(document.createElement("td"));
let conditionLabel = conditionHeader.appendChild(document.createElement("label"));
conditionLabel.textContent = WI.UIString("Condition");
let conditionEditorElement = conditionData.appendChild(document.createElement("div"));
conditionEditorElement.classList.add("edit-breakpoint-popover-condition", WI.SyntaxHighlightedStyleClassName);
this._conditionCodeMirror = WI.CodeMirrorEditor.create(conditionEditorElement, {
extraKeys: {Tab: false},
lineWrapping: false,
mode: "text/javascript",
matchBrackets: true,
placeholder: WI.UIString("Conditional expression"),
scrollbarStyle: null,
value: this._breakpoint.condition || "",
});
let conditionCodeMirrorInputField = this._conditionCodeMirror.getInputField();
conditionCodeMirrorInputField.id = "codemirror-condition-input-field";
conditionLabel.setAttribute("for", conditionCodeMirrorInputField.id);
this._conditionCodeMirrorEscapeOrEnterKeyHandler = this._conditionCodeMirrorEscapeOrEnterKey.bind(this);
this._conditionCodeMirror.addKeyMap({
"Esc": this._conditionCodeMirrorEscapeOrEnterKeyHandler,
"Enter": this._conditionCodeMirrorEscapeOrEnterKeyHandler,
});
this._conditionCodeMirror.on("change", this._conditionCodeMirrorChanged.bind(this));
this._conditionCodeMirror.on("beforeChange", this._conditionCodeMirrorBeforeChange.bind(this));
let completionController = new WI.CodeMirrorCompletionController(this._conditionCodeMirror, this);
completionController.addExtendedCompletionProvider("javascript", WI.javaScriptRuntimeCompletionProvider);
// CodeMirror needs a refresh after the popover displays, to layout, otherwise it doesn't appear.
setTimeout(() => {
this._conditionCodeMirror.refresh();
this._conditionCodeMirror.focus();
}, 0);
// COMPATIBILITY (iOS 9): Legacy backends don't support breakpoint ignore count.
// Since support can't be tested directly, check for Runtime.run (iOS 9.3) or Console.heapSnapshot (iOS 10.0+).
// FIXME: Use explicit version checking once https://webkit.org/b/148680 is fixed.
if (InspectorBackend.hasCommand("Console.heapSnapshot") || InspectorBackend.hasCommand("Runtime.run")) {
let ignoreCountRow = table.appendChild(document.createElement("tr"));
let ignoreCountHeader = ignoreCountRow.appendChild(document.createElement("th"));
let ignoreCountLabel = ignoreCountHeader.appendChild(document.createElement("label"));
let ignoreCountData = ignoreCountRow.appendChild(document.createElement("td"));
this._ignoreCountInput = ignoreCountData.appendChild(document.createElement("input"));
this._ignoreCountInput.id = "edit-breakpoint-popover-ignore";
this._ignoreCountInput.type = "number";
this._ignoreCountInput.min = 0;
this._ignoreCountInput.value = 0;
this._ignoreCountInput.addEventListener("change", this._popoverIgnoreInputChanged.bind(this));
ignoreCountLabel.setAttribute("for", this._ignoreCountInput.id);
ignoreCountLabel.textContent = WI.UIString("Ignore");
this._ignoreCountText = ignoreCountData.appendChild(document.createElement("span"));
this._updateIgnoreCountText();
}
let actionRow = table.appendChild(document.createElement("tr"));
let actionHeader = actionRow.appendChild(document.createElement("th"));
let actionData = this._actionsContainer = actionRow.appendChild(document.createElement("td"));
let actionLabel = actionHeader.appendChild(document.createElement("label"));
actionLabel.textContent = WI.UIString("Action");
if (!this._breakpoint.actions.length)
this._popoverActionsCreateAddActionButton();
else {
this._popoverContentElement.classList.add(WI.BreakpointPopoverController.WidePopoverClassName);
for (let i = 0; i < this._breakpoint.actions.length; ++i) {
let breakpointActionView = new WI.BreakpointActionView(this._breakpoint.actions[i], this, true);
this._popoverActionsInsertBreakpointActionView(breakpointActionView, i);
}
}
let optionsRow = this._popoverOptionsRowElement = table.appendChild(document.createElement("tr"));
if (!this._breakpoint.actions.length)
optionsRow.classList.add(WI.BreakpointPopoverController.HiddenStyleClassName);
let optionsHeader = optionsRow.appendChild(document.createElement("th"));
let optionsData = optionsRow.appendChild(document.createElement("td"));
let optionsLabel = optionsHeader.appendChild(document.createElement("label"));
let optionsCheckbox = this._popoverOptionsCheckboxElement = optionsData.appendChild(document.createElement("input"));
let optionsCheckboxLabel = optionsData.appendChild(document.createElement("label"));
optionsCheckbox.id = "edit-breakpoint-popover-auto-continue";
optionsCheckbox.type = "checkbox";
optionsCheckbox.checked = this._breakpoint.autoContinue;
optionsCheckbox.addEventListener("change", this._popoverToggleAutoContinueCheckboxChanged.bind(this));
optionsLabel.textContent = WI.UIString("Options");
optionsCheckboxLabel.setAttribute("for", optionsCheckbox.id);
optionsCheckboxLabel.textContent = WI.UIString("Automatically continue after evaluating");
this._popoverContentElement.appendChild(checkboxLabel);
this._popoverContentElement.appendChild(table);
}
_popoverToggleEnabledCheckboxChanged(event)
{
this._breakpoint.disabled = !event.target.checked;
}
_conditionCodeMirrorChanged(codeMirror, change)
{
this._breakpoint.condition = (codeMirror.getValue() || "").trim();
}
_conditionCodeMirrorBeforeChange(codeMirror, change)
{
if (change.update) {
let newText = change.text.join("").replace(/\n/g, "");
change.update(change.from, change.to, [newText]);
}
return true;
}
_conditionCodeMirrorEscapeOrEnterKey()
{
if (!this._popover)
return;
this._popover.dismiss();
}
_popoverIgnoreInputChanged(event)
{
let ignoreCount = 0;
if (event.target.value) {
ignoreCount = parseInt(event.target.value, 10);
if (isNaN(ignoreCount) || ignoreCount < 0)
ignoreCount = 0;
}
this._ignoreCountInput.value = ignoreCount;
this._breakpoint.ignoreCount = ignoreCount;
this._updateIgnoreCountText();
}
_popoverToggleAutoContinueCheckboxChanged(event)
{
this._breakpoint.autoContinue = event.target.checked;
}
_popoverActionsCreateAddActionButton()
{
this._popoverContentElement.classList.remove(WI.BreakpointPopoverController.WidePopoverClassName);
this._actionsContainer.removeChildren();
let addActionButton = this._actionsContainer.appendChild(document.createElement("button"));
addActionButton.textContent = WI.UIString("Add Action");
addActionButton.addEventListener("click", this._popoverActionsAddActionButtonClicked.bind(this));
}
_popoverActionsAddActionButtonClicked(event)
{
this._popoverContentElement.classList.add(WI.BreakpointPopoverController.WidePopoverClassName);
this._actionsContainer.removeChildren();
let newAction = this._breakpoint.createAction(WI.BreakpointAction.Type.Log);
let newBreakpointActionView = new WI.BreakpointActionView(newAction, this);
this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, -1);
this._popoverOptionsRowElement.classList.remove(WI.BreakpointPopoverController.HiddenStyleClassName);
this._popover.update();
}
_popoverActionsInsertBreakpointActionView(breakpointActionView, index)
{
if (index === -1)
this._actionsContainer.appendChild(breakpointActionView.element);
else {
let nextElement = this._actionsContainer.children[index + 1] || null;
this._actionsContainer.insertBefore(breakpointActionView.element, nextElement);
}
}
_updateIgnoreCountText()
{
if (this._breakpoint.ignoreCount === 1)
this._ignoreCountText.textContent = WI.UIString("time before stopping");
else
this._ignoreCountText.textContent = WI.UIString("times before stopping");
}
breakpointActionViewAppendActionView(breakpointActionView, newAction)
{
let newBreakpointActionView = new WI.BreakpointActionView(newAction, this);
let index = 0;
let children = this._actionsContainer.children;
for (let i = 0; children.length; ++i) {
if (children[i] === breakpointActionView.element) {
index = i;
break;
}
}
this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, index);
this._popoverOptionsRowElement.classList.remove(WI.BreakpointPopoverController.HiddenStyleClassName);
this._popover.update();
}
breakpointActionViewRemoveActionView(breakpointActionView)
{
breakpointActionView.element.remove();
if (!this._actionsContainer.children.length) {
this._popoverActionsCreateAddActionButton();
this._popoverOptionsRowElement.classList.add(WI.BreakpointPopoverController.HiddenStyleClassName);
this._popoverOptionsCheckboxElement.checked = false;
}
this._popover.update();
}
breakpointActionViewResized(breakpointActionView)
{
this._popover.update();
}
willDismissPopover(popover)
{
console.assert(this._popover === popover);
this._popoverContentElement = null;
this._popoverOptionsRowElement = null;
this._popoverOptionsCheckboxElement = null;
this._actionsContainer = null;
this._popover = null;
}
didDismissPopover(popover)
{
// Remove Evaluate and Probe actions that have no data.
let emptyActions = this._breakpoint.actions.filter(function(action) {
if (action.type !== WI.BreakpointAction.Type.Evaluate && action.type !== WI.BreakpointAction.Type.Probe)
return false;
return !(action.data && action.data.trim());
});
for (let action of emptyActions)
this._breakpoint.removeAction(action);
this._breakpoint = null;
}
};
WI.BreakpointPopoverController.WidePopoverClassName = "wide";
WI.BreakpointPopoverController.HiddenStyleClassName = "hidden";