diff options
Diffstat (limited to 'devtools/client/debugger/views/stack-frames-view.js')
-rw-r--r-- | devtools/client/debugger/views/stack-frames-view.js | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/devtools/client/debugger/views/stack-frames-view.js b/devtools/client/debugger/views/stack-frames-view.js new file mode 100644 index 000000000..244f97b3d --- /dev/null +++ b/devtools/client/debugger/views/stack-frames-view.js @@ -0,0 +1,283 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* import-globals-from ../debugger-controller.js */ +/* import-globals-from ../debugger-view.js */ +/* import-globals-from ../utils.js */ +/* globals document, window */ +"use strict"; + +/** + * Functions handling the stackframes UI. + */ +function StackFramesView(DebuggerController, DebuggerView) { + dumpn("StackFramesView was instantiated"); + + this.StackFrames = DebuggerController.StackFrames; + this.DebuggerView = DebuggerView; + + this._onStackframeRemoved = this._onStackframeRemoved.bind(this); + this._onSelect = this._onSelect.bind(this); + this._onScroll = this._onScroll.bind(this); + this._afterScroll = this._afterScroll.bind(this); + this._getStackAsString = this._getStackAsString.bind(this); +} + +StackFramesView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the StackFramesView"); + + this._popupset = document.getElementById("debuggerPopupset"); + + this.widget = new BreadcrumbsWidget(document.getElementById("stackframes")); + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("scroll", this._onScroll, true); + this.widget.setAttribute("context", "stackFramesContextMenu"); + window.addEventListener("resize", this._onScroll, true); + + this.autoFocusOnFirstItem = false; + this.autoFocusOnSelection = false; + + // This view's contents are also mirrored in a different container. + this._mirror = this.DebuggerView.StackFramesClassicList; + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the StackFramesView"); + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("scroll", this._onScroll, true); + window.removeEventListener("resize", this._onScroll, true); + }, + + /** + * Adds a frame in this stackframes container. + * + * @param string aTitle + * The frame title (function name). + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @param boolean aIsBlackBoxed + * Whether or not the frame is black boxed. + */ + addFrame: function (aFrame, aLine, aColumn, aDepth, aIsBlackBoxed) { + let { source } = aFrame; + + // The source may not exist in the source listing yet because it's + // an unnamed eval source, which we hide, so we need to add it + if (!DebuggerView.Sources.getItemByValue(source.actor)) { + DebuggerView.Sources.addSource(source, { force: true }); + } + + let location = DebuggerView.Sources.getDisplayURL(source); + let title = StackFrameUtils.getFrameTitle(aFrame); + + // Blackboxed stack frames are collapsed into a single entry in + // the view. By convention, only the first frame is displayed. + if (aIsBlackBoxed) { + if (this._prevBlackBoxedUrl == location) { + return; + } + this._prevBlackBoxedUrl = location; + } else { + this._prevBlackBoxedUrl = null; + } + + // Create the element node for the stack frame item. + let frameView = this._createFrameView( + title, location, aLine, aDepth, aIsBlackBoxed + ); + + // Append a stack frame item to this container. + this.push([frameView], { + index: 0, /* specifies on which position should the item be appended */ + attachment: { + title: title, + url: location, + line: aLine, + depth: aDepth, + column: aColumn + }, + // Make sure that when the stack frame item is removed, the corresponding + // mirrored item in the classic list is also removed. + finalize: this._onStackframeRemoved + }); + + // Mirror this newly inserted item inside the "Call Stack" tab. + this._mirror.addFrame(title, location, aLine, aDepth); + }, + + _getStackAsString: function () { + return [...this].map(frameItem => { + const { attachment: { title, url, line, column }} = frameItem; + return title + "@" + url + ":" + line + ":" + column; + }).join("\n"); + }, + + addCopyContextMenu: function () { + let menupopup = document.createElement("menupopup"); + let menuitem = document.createElement("menuitem"); + + menupopup.id = "stackFramesContextMenu"; + menuitem.id = "copyStackMenuItem"; + + menuitem.setAttribute("label", "Copy"); + menuitem.addEventListener("command", () => { + let stack = this._getStackAsString(); + clipboardHelper.copyString(stack); + }, false); + menupopup.appendChild(menuitem); + this._popupset.appendChild(menupopup); + }, + + /** + * Selects the frame at the specified depth in this container. + * @param number aDepth + */ + set selectedDepth(aDepth) { + this.selectedItem = aItem => aItem.attachment.depth == aDepth; + }, + + /** + * Gets the currently selected stack frame's depth in this container. + * This will essentially be the opposite of |selectedIndex|, which deals + * with the position in the view, where the last item added is actually + * the bottommost, not topmost. + * @return number + */ + get selectedDepth() { + return this.selectedItem.attachment.depth; + }, + + /** + * Specifies if the active thread has more frames that need to be loaded. + */ + dirty: false, + + /** + * Customization function for creating an item's UI. + * + * @param string aTitle + * The frame title to be displayed in the list. + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @param boolean aIsBlackBoxed + * Whether or not the frame is black boxed. + * @return nsIDOMNode + * The stack frame view. + */ + _createFrameView: function (aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) { + let container = document.createElement("hbox"); + container.id = "stackframe-" + aDepth; + container.className = "dbg-stackframe"; + + let frameDetails = SourceUtils.trimUrlLength( + SourceUtils.getSourceLabel(aUrl), + STACK_FRAMES_SOURCE_URL_MAX_LENGTH, + STACK_FRAMES_SOURCE_URL_TRIM_SECTION); + + if (aIsBlackBoxed) { + container.classList.add("dbg-stackframe-black-boxed"); + } else { + let frameTitleNode = document.createElement("label"); + frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag"; + frameTitleNode.setAttribute("value", aTitle); + container.appendChild(frameTitleNode); + + frameDetails += SEARCH_LINE_FLAG + aLine; + } + + let frameDetailsNode = document.createElement("label"); + frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id"; + frameDetailsNode.setAttribute("value", frameDetails); + container.appendChild(frameDetailsNode); + + return container; + }, + + /** + * Function called each time a stack frame item is removed. + * + * @param object aItem + * The corresponding item. + */ + _onStackframeRemoved: function (aItem) { + dumpn("Finalizing stackframe item: " + aItem.stringify()); + + // Remove the mirrored item in the classic list. + let depth = aItem.attachment.depth; + this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth)); + + // Forget the previously blackboxed stack frame url. + this._prevBlackBoxedUrl = null; + }, + + /** + * The select listener for the stackframes container. + */ + _onSelect: function (e) { + let stackframeItem = this.selectedItem; + if (stackframeItem) { + // The container is not empty and an actual item was selected. + let depth = stackframeItem.attachment.depth; + + // Mirror the selected item in the classic list. + this.suppressSelectionEvents = true; + this._mirror.selectedItem = e => e.attachment.depth == depth; + this.suppressSelectionEvents = false; + + DebuggerController.StackFrames.selectFrame(depth); + } + }, + + /** + * The scroll listener for the stackframes container. + */ + _onScroll: function () { + // Update the stackframes container only if we have to. + if (!this.dirty) { + return; + } + // Allow requests to settle down first. + setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll); + }, + + /** + * Requests the addition of more frames from the controller. + */ + _afterScroll: function () { + let scrollPosition = this.widget.getAttribute("scrollPosition"); + let scrollWidth = this.widget.getAttribute("scrollWidth"); + + // If the stackframes container scrolled almost to the end, with only + // 1/10 of a breadcrumb remaining, load more content. + if (scrollPosition - scrollWidth / 10 < 1) { + this.ensureIndexIsVisible(CALL_STACK_PAGE_SIZE - 1); + this.dirty = false; + + // Loads more stack frames from the debugger server cache. + DebuggerController.StackFrames.addMoreFrames(); + } + }, + + _mirror: null, + _prevBlackBoxedUrl: null +}); + +DebuggerView.StackFrames = new StackFramesView(DebuggerController, DebuggerView); |