/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ "use strict"; const SOURCE_URL_MAX_LENGTH = 64; // chars const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes const PANES_APPEARANCE_DELAY = 50; // ms const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars const GLOBAL_SEARCH_ACTION_DELAY = 150; // ms const SEARCH_GLOBAL_FLAG = "!"; const SEARCH_LINE_FLAG = ":"; const SEARCH_TOKEN_FLAG = "#"; /** * Object defining the debugger view components. */ let DebuggerView = { /** * Initializes the debugger view. * * @param function aCallback * Called after the view finishes initializing. */ initialize: function DV_initialize(aCallback) { dumpn("Initializing the DebuggerView"); this.Toolbar.initialize(); this.Options.initialize(); this.ChromeGlobals.initialize(); this.Sources.initialize(); this.Filtering.initialize(); this.StackFrames.initialize(); this.Breakpoints.initialize(); this.GlobalSearch.initialize(); this.Variables = new VariablesView(document.getElementById("variables")); this.Variables.emptyText = L10N.getStr("emptyVariablesText"); this.Variables.nonEnumVisible = Prefs.nonEnumVisible; this.Variables.eval = DebuggerController.StackFrames.evaluate; this._initializePanes(); this._initializeEditor(aCallback) this._isInitialized = true; }, /** * Destroys the debugger view. * * @param function aCallback * Called after the view finishes destroying. */ destroy: function DV_destroy(aCallback) { dumpn("Destroying the DebuggerView"); this.Toolbar.destroy(); this.Options.destroy(); this.ChromeGlobals.destroy(); this.Sources.destroy(); this.Filtering.destroy(); this.StackFrames.destroy(); this.Breakpoints.destroy(); this.GlobalSearch.destroy(); this._destroyPanes(); this._destroyEditor(); aCallback(); }, /** * Initializes the UI for all the displayed panes. */ _initializePanes: function DV__initializePanes() { dumpn("Initializing the DebuggerView panes"); this._togglePanesButton = document.getElementById("toggle-panes"); this._stackframesAndBreakpoints = document.getElementById("stackframes+breakpoints"); this._variables = document.getElementById("variables"); this._stackframesAndBreakpoints.setAttribute("width", Prefs.stackframesWidth); this._variables.setAttribute("width", Prefs.variablesWidth); this.togglePanes({ visible: false, animated: false, silent: true }); }, /** * Destroys the UI for all the displayed panes. */ _destroyPanes: function DV__initializePanes() { dumpn("Destroying the DebuggerView panes"); Prefs.stackframesWidth = this._stackframesAndBreakpoints.getAttribute("width"); Prefs.variablesWidth = this._variables.getAttribute("width"); this._togglePanesButton = null; this._stackframesAndBreakpoints = null; this._variables = null; }, /** * Initializes the SourceEditor instance. * * @param function aCallback * Called after the editor finishes initializing. */ _initializeEditor: function DV__initializeEditor(aCallback) { dumpn("Initializing the DebuggerView editor"); let placeholder = document.getElementById("editor"); let config = { mode: SourceEditor.MODES.JAVASCRIPT, readOnly: true, showLineNumbers: true, showAnnotationRuler: true, showOverviewRuler: true }; this.editor = new SourceEditor(); this.editor.init(placeholder, config, function() { this._onEditorLoad(); aCallback(); }.bind(this)); }, /** * The load event handler for the source editor, also executing any necessary * post-load operations. */ _onEditorLoad: function DV__onEditorLoad() { dumpn("Finished loading the DebuggerView editor"); DebuggerController.Breakpoints.initialize(); this.editor.focus(); }, /** * Destroys the SourceEditor instance and also executes any necessary * post-unload operations. */ _destroyEditor: function DV__destroyEditor() { dumpn("Destroying the DebuggerView editor"); DebuggerController.Breakpoints.destroy(); this.editor = null; }, /** * Sets the proper editor mode (JS or HTML) according to the specified * content type, or by determining the type from the url. * * @param string aUrl * The script url. * @param string aContentType [optional] * The script content type. */ setEditorMode: function DV_setEditorMode(aUrl, aContentType) { if (!this.editor) { return; } dumpn("Setting the DebuggerView editor mode: " + aUrl + ", content type: " + aContentType); if (aContentType) { if (/javascript/.test(aContentType)) { this.editor.setMode(SourceEditor.MODES.JAVASCRIPT); } else { this.editor.setMode(SourceEditor.MODES.HTML); } } else { // Use JS mode for files with .js and .jsm extensions. if (/\.jsm?$/.test(SourceUtils.trimUrlQuery(aUrl))) { this.editor.setMode(SourceEditor.MODES.JAVASCRIPT); } else { this.editor.setMode(SourceEditor.MODES.HTML); } } }, /** * Load the editor with the specified source text. * * @param object aSource * The source object coming from the active thread. * @param object aOptions [optional] * Additional options for showing the source. Supported options: * - targetLine: place the caret position at the given line number * - debugLine: place the debug location at the given line number * - callback: function called when the source is shown */ setEditorSource: function DV_setEditorSource(aSource, aOptions = {}) { if (!this.editor) { return; } dumpn("Setting the DebuggerView editor source: " + aSource.url + ", loaded: " + aSource.loaded + ", options: " + aOptions.toSource()); // If the source is not loaded, display a placeholder text. if (!aSource.loaded) { this.editor.setMode(SourceEditor.MODES.TEXT); this.editor.setText(L10N.getStr("loadingText")); this.editor.resetUndo(); // Get the source text from the active thread. DebuggerController.SourceScripts.getText(aSource, function(aUrl, aText) { aSource.loaded = true; aSource.text = aText; this.setEditorSource(aSource, aOptions); }.bind(this)); } // If the source is already loaded, display it immediately. else { if (aSource.text.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) { this.setEditorMode(aSource.url, aSource.contentType); } this.editor.setText(aSource.text); this.editor.resetUndo(); this.updateEditor(); DebuggerView.Sources.selectedValue = aSource.url; DebuggerController.Breakpoints.updateEditorBreakpoints(); // Handle any additional options for showing the source. if (aOptions.targetLine) { editor.setCaretPosition(aOptions.targetLine - 1); } if (aOptions.debugLine) { editor.setDebugLocation(aOptions.debugLine - 1); } if (aOptions.callback) { aOptions.callback(aSource); } // Notify that we've shown a source file. window.dispatchEvent("Debugger:SourceShown", aSource); } }, /** * Update the source editor's current caret and debug location based on * a requested url and line. If unspecified, they default to the location * given by the currently active frame in the stack. * * @param string aUrl [optional] * The target source url. * @param number aLine [optional] * The target line number in the source. * @param object aFlags [optional] * An object containing some of the following boolean properties: * - noSwitch: don't switch to the source if not currently selected * - noCaret: don't set the caret location at the specified line * - noDebug: don't set the debug location at the specified line */ updateEditor: function DV_updateEditor(aUrl, aLine, aFlags = {}) { if (!this.editor) { return; } // If the location is not specified, default to the location given by // the currently active frame in the stack. if (!aUrl && !aLine) { let cachedFrames = DebuggerController.activeThread.cachedFrames; let currentFrame = DebuggerController.StackFrames.currentFrame; let frame = cachedFrames[currentFrame]; if (frame) { let { url, line } = frame.where; this.updateEditor(url, line, { noSwitch: true }); } return; } dumpn("Updating the DebuggerView editor: " + aUrl + " @ " + aLine + ", flags: " + aFlags.toSource()); // If the currently displayed source is the requested one, update. if (this.Sources.selectedValue == aUrl) { updateLine(aLine); } // If the requested source exists, display it and update. else if (this.Sources.containsValue(aUrl) && !aFlags.noSwitch) { this.Sources.selectedValue = aUrl; updateLine(aLine); } // Dumb request, invalidate the caret position and debug location. else { updateLine(0); } // Updates the source editor's caret position and debug location. // @param number a Line function updateLine(aLine) { if (!aFlags.noCaret) { DebuggerView.editor.setCaretPosition(aLine - 1); } if (!aFlags.noDebug) { DebuggerView.editor.setDebugLocation(aLine - 1); } } }, /** * Gets the text in the source editor's specified line. * * @param number aLine [optional] * The line to get the text from. * If unspecified, it defaults to the current caret position line. * @return string * The specified line's text. */ getEditorLine: function SS_getEditorLine(aLine) { let line = aLine || this.editor.getCaretPosition().line; let start = this.editor.getLineStart(line); let end = this.editor.getLineEnd(line); return this.editor.getText(start, end); }, /** * Gets the visibility state of the panes. * @return boolean */ get panesHidden() this.stackframesAndBreakpointsHidden && this.variablesHidden, /** * Gets the visibility state of the stackframes and breakpoints pane. * @return boolean */ get stackframesAndBreakpointsHidden() !!this._togglePanesButton.getAttribute("stackframesAndBreakpointsHidden"), /** * Gets the visibility state of the varialbes pane. * @return boolean */ get variablesHidden() !!this._togglePanesButton.getAttribute("variablesHidden"), /** * Sets all the panes hidden or visible. * * @param object aFlags [optional] * An object containing some of the following boolean properties: * - visible: true if the pane should be shown, false for hidden * - animated: true to display an animation on toggle * - silent: true to not update any designated prefs */ togglePanes: function DV__togglePanes(aFlags = {}) { this._toggleStackframesAndBreakpointsPane(aFlags); this._toggleVariablesPane(aFlags); }, /** * Shows the stackframes, breakpoints and variable panes if currently hidden * and the preferences dictate otherwise. */ showPanesIfPreffered: function DV_showPanesIfPreffered() { let self = this; // Try to keep animations as smooth as possible, so wait a few cycles. window.setTimeout(function() { let target; if (Prefs.stackframesPaneVisible && self.stackframesAndBreakpointsHidden) { self._toggleStackframesAndBreakpointsPane({ visible: true, animated: true, silent: true }); target = self._stackframesAndBreakpoints; } if (Prefs.variablesPaneVisible && self.variablesHidden) { self._toggleVariablesPane({ visible: true, animated: true, silent: true }); target = self._variables; } // Displaying the panes may have the effect of triggering scrollbars to // appear in the source editor, which would render the currently // highlighted line to appear behind them in some cases. if (target) { target.addEventListener("transitionend", function onEvent() { target.removeEventListener("transitionend", onEvent, false); self.updateEditor(); }, false); } }, PANES_APPEARANCE_DELAY); }, /** * Sets the stackframes and breakpoints (left) pane hidden or visible. * @see DebuggerView.togglePanes */ _toggleStackframesAndBreakpointsPane: function DV__toggleStackframesAndBreakpointsPane(aFlags) { if (aFlags.animated) { this._stackframesAndBreakpoints.setAttribute("animated", ""); } else { this._stackframesAndBreakpoints.removeAttribute("animated"); } if (aFlags.visible) { this._stackframesAndBreakpoints.style.marginLeft = "0"; this._togglePanesButton.removeAttribute("stackframesAndBreakpointsHidden"); this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("collapsePanes")); } else { let margin = parseInt(this._stackframesAndBreakpoints.getAttribute("width")) + 1; this._stackframesAndBreakpoints.style.marginLeft = -margin + "px"; this._togglePanesButton.setAttribute("stackframesAndBreakpointsHidden", "true"); this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("expandPanes")); } if (!aFlags.silent) { Prefs.stackframesPaneVisible = !!aFlags.visible; } }, /** * Sets the variables (right) pane hidden or visible. * @see DebuggerView.togglePanes */ _toggleVariablesPane: function DV__toggleVariablesPane(aFlags) { if (aFlags.animated) { this._variables.setAttribute("animated", ""); } else { this._variables.removeAttribute("animated"); } if (aFlags.visible) { this._variables.style.marginRight = "0"; this._togglePanesButton.removeAttribute("variablesHidden"); this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("collapsePanes")); } else { let margin = parseInt(this._variables.getAttribute("width")) + 1; this._variables.style.marginRight = -margin + "px"; this._togglePanesButton.setAttribute("variablesHidden", "true"); this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("expandPanes")); } if (!aFlags.silent) { Prefs.variablesPaneVisible = !!aFlags.visible; } }, /** * Handles any initialization on a tab navigation event issued by the client. */ _handleTabNavigation: function DV__handleTabNavigation() { dumpn("Handling tab navigation in the DebuggerView"); this.ChromeGlobals.empty(); this.Sources.empty(); this.Filtering.clearSearch(); this.GlobalSearch.clearView(); this.GlobalSearch.clearCache(); this.StackFrames.empty(); this.Breakpoints.empty(); this.Variables.empty(); SourceUtils.clearLabelsCache(); if (this.editor) { this.editor.setText(""); } }, Toolbar: null, Options: null, ChromeGlobals: null, Sources: null, Filtering: null, StackFrames: null, Breakpoints: null, GlobalSearch: null, Variables: null, _editor: null, _togglePanesButton: null, _stackframesAndBreakpoints: null, _variables: null, _isInitialized: false, }; /** * A generic item used to describe elements present in views like the * ChromeGlobals, Sources, Stackframes, Breakpoints etc. * * @param string aLabel * The label displayed in the container. * @param string aValue * The actual internal value of the item. * @param string aDescription [optional] * An optional description of the item. * @param any aAttachment [optional] * Some attached primitive/object. */ function MenuItem(aLabel, aValue, aDescription, aAttachment) { this._label = aLabel + ""; this._value = aValue + ""; this._description = aDescription + ""; this.attachment = aAttachment; } MenuItem.prototype = { /** * Gets the label set for this item. * @return string */ get label() this._label, /** * Gets the value set for this item. * @return string */ get value() this._value, /** * Gets the description set for this item. * @return string */ get description() this._description, /** * Gets the element associated with this item. * @return nsIDOMNode */ get target() this._target, _label: "", _value: "", _description: "", _target: null, finalize: null, attachment: null }; /** * A generic items container, used for displaying views like the * ChromeGlobals, Sources, Stackframes, Breakpoints etc. * Iterable via "for (let item in menuContainer) { }". * * Language: * - An "item" is an instance (or compatible iterface) of a MenuItem. * - An "element" or "node" is a nsIDOMNode. * * The container node supplied to all instances of this constructor can either * be a element, or any other object interfacing the following * methods: * - function:nsIDOMNode appendItem(aLabel:string, aValue:string) * - function:nsIDOMNode insertItemAt(aIndex:number, aLabel:string, aValue:string) * - function:nsIDOMNode getItemAtIndex(aIndex:number) * - function removeChild(aChild:nsIDOMNode) * - function removeAllItems() * - get:number itemCount() * - get:number selectedIndex() * - set selectedIndex(aIndex:number) * - get:nsIDOMNode selectedItem() * - set selectedItem(aChild:nsIDOMNode) * - function getAttribute(aName:string) * - function setAttribute(aName:string, aValue:string) * - function removeAttribute(aName:string) * - function addEventListener(aName:string, aCallback:function, aBubbleFlag:boolean) * - function removeEventListener(aName:string, aCallback:function, aBubbleFlag:boolean) * * @param nsIDOMNode aContainerNode [optional] * The element associated with the displayed container. Although required, * derived objects may set this value later, upon debugger initialization. */ function MenuContainer(aContainerNode) { this._container = aContainerNode; this._stagedItems = []; this._itemsByLabel = new Map(); this._itemsByValue = new Map(); this._itemsByElement = new Map(); } MenuContainer.prototype = { /** * Prepares an item to be added to this container. This allows for a large * number of items to be batched up before being alphabetically sorted and * added in this menu. * * If the "forced" flag is true, the item will be immediately inserted at the * correct position in this container, so that all the items remain sorted. * This can (possibly) be much slower than batching up multiple items. * * By default, this container assumes that all the items should be displayed * sorted by their label. This can be overridden with the "unsorted" flag. * * Furthermore, this container makes sure that all the items are unique * (two items with the same label or value are not allowed) and non-degenerate * (items with "undefined" or "null" labels/values). This can, as well, be * overridden via the "relaxed" flag. * * @param string aLabel * The label displayed in the container. * @param string aValue * The actual internal value of the item. * @param object aOptions [optional] * Additional options or flags supported by this operation: * - forced: true to force the item to be immediately added * - unsorted: true if the items should not remain sorted * - relaxed: true if this container should allow dupes & degenerates * - description: an optional description of the item * - attachment: some attached primitive/object * @return MenuItem * The item associated with the displayed element if a forced push, * undefined if the item was staged for a later commit. */ push: function DVMC_push(aLabel, aValue, aOptions = {}) { let item = new MenuItem( aLabel, aValue, aOptions.description, aOptions.attachment); // Batch the item to be added later. if (!aOptions.forced) { this._stagedItems.push(item); } // Find the target position in this container and insert the item there. else if (!aOptions.unsorted) { return this._insertItemAt(this._findExpectedIndex(aLabel), item, aOptions); } // Just append the item in this container. else { return this._appendItem(item, aOptions); } }, /** * Flushes all the prepared items into this container. * * @param object aOptions [optional] * Additional options or flags supported by this operation: * - unsorted: true if the items should not be sorted beforehand */ commit: function DVMC_commit(aOptions = {}) { let stagedItems = this._stagedItems; // By default, sort the items before adding them to this container. if (!aOptions.unsorted) { stagedItems.sort(function(a, b) a.label.toLowerCase() > b.label.toLowerCase()); } // Append the prepared items to this container. for (let item of stagedItems) { this._appendItem(item, aOptions); } // Recreate the temporary items list for ulterior pushes. this._stagedItems = []; }, /** * Updates this container to reflect the information provided by the * currently selected item. * * @return boolean * True if a selected item was available, false otherwise. */ refresh: function DVMC_refresh() { let selectedValue = this.selectedValue; if (!selectedValue) { return false; } let entangledLabel = this.getItemByValue(selectedValue).label; this._container.setAttribute("label", entangledLabel); this._container.setAttribute("tooltiptext", selectedValue); return true; }, /** * Immediately removes the specified item from this container. * * @param MenuItem aItem * The item associated with the element to remove. */ remove: function DVMC__remove(aItem) { this._container.removeChild(aItem.target); this._untangleItem(aItem); }, /** * Removes all items from this container. */ empty: function DVMC_empty() { this._preferredValue = this.selectedValue; this._container.selectedIndex = -1; this._container.setAttribute("label", this._emptyLabel); this._container.removeAttribute("tooltiptext"); this._container.removeAllItems(); for (let [_, item] of this._itemsByElement) { this._untangleItem(item); } this._itemsByLabel = new Map(); this._itemsByValue = new Map(); this._itemsByElement = new Map(); this._stagedItems = []; }, /** * Does not remove any item in this container. Instead, it overrides the * current label to signal that it is unavailable and removes the tooltip. */ setUnavailable: function DVMC_setUnavailable() { this._container.setAttribute("label", this._unavailableLabel); this._container.removeAttribute("tooltiptext"); }, /** * Checks whether an item with the specified label is among the elements * shown in this container. * * @param string aLabel * The item's label. * @return boolean * True if the label is known, false otherwise. */ containsLabel: function DVMC_containsLabel(aLabel) { return this._itemsByLabel.has(aLabel) || this._stagedItems.some(function(o) o.label == aLabel); }, /** * Checks whether an item with the specified value is among the elements * shown in this container. * * @param string aValue * The item's value. * @return boolean * True if the value is known, false otherwise. */ containsValue: function DVMC_containsValue(aValue) { return this._itemsByValue.has(aValue) || this._stagedItems.some(function(o) o.value == aValue); }, /** * Checks whether an item with the specified trimmed value is among the * elements shown in this container. * * @param string aValue * The item's value. * @param function aTrim [optional] * A custom trimming function. * @return boolean * True if the trimmed value is known, false otherwise. */ containsTrimmedValue: function DVMC_containsTrimmedValue(aValue, aTrim = SourceUtils.trimUrlQuery) { let trimmedValue = aTrim(aValue); for (let [value] of this._itemsByValue) { if (aTrim(value) == trimmedValue) { return true; } } return this._stagedItems.some(function(o) aTrim(o.value) == trimmedValue); }, /** * Gets the preferred selected value to be displayed in this container. * @return string */ get preferredValue() this._preferredValue, /** * Retrieves the selected element's index in this container. * @return number */ get selectedIndex() this._container.selectedIndex, /** * Retrieves the item associated with the selected element. * @return MenuItem */ get selectedItem() this._container.selectedItem ? this._itemsByElement.get(this._container.selectedItem) : null, /** * Retrieves the label of the selected element. * @return string */ get selectedLabel() this._container.selectedItem ? this._itemsByElement.get(this._container.selectedItem).label : null, /** * Retrieves the value of the selected element. * @return string */ get selectedValue() this._container.selectedItem ? this._itemsByElement.get(this._container.selectedItem).value : null, /** * Selects the element at the specified index in this container. * @param number aIndex */ set selectedIndex(aIndex) this._container.selectedIndex = aIndex, /** * Selects the element with the entangled item in this container. * @param MenuItem aItem */ set selectedItem(aItem) this._container.selectedItem = aItem.target, /** * Selects the element with the specified label in this container. * @param string aLabel */ set selectedLabel(aLabel) { let item = this._itemsByLabel.get(aValue); if (item) { this._container.selectedItem = item.target; } }, /** * Selects the element with the specified value in this container. * @param string aValue */ set selectedValue(aValue) { let item = this._itemsByValue.get(aValue); if (item) { this._container.selectedItem = item.target; } }, /** * Gets the item in the container having the specified label. * * @param string aLabel * The label used to identify the element. * @return MenuItem * The matched item, or null if nothing is found. */ getItemByLabel: function DVMC_getItemByLabel(aLabel) { return this._itemsByLabel.get(aLabel); }, /** * Gets the item in the container having the specified value. * * @param string aValue * The value used to identify the element. * @return MenuItem * The matched item, or null if nothing is found. */ getItemByValue: function DVMC_getItemByValue(aValue) { return this._itemsByValue.get(aValue); }, /** * Gets the item in the container associated with the specified element. * * @param nsIDOMNode aElement * The element used to identify the item. * @return MenuItem * The matched item, or null if nothing is found. */ getItemForElement: function DVMC_getItemForElement(aElement) { while (aElement) { let item = this._itemsByElement.get(aElement); if (item) { return item; } aElement = aElement.parentNode; } return null; }, /** * Returns the list of labels in this container. * @return array */ get labels() { let labels = []; for (let [label] of this._itemsByLabel) { labels.push(label); } return labels; }, /** * Returns the list of values in this container. * @return array */ get values() { let values = []; for (let [value] of this._itemsByValue) { values.push(value); } return values; }, /** * Gets the total visible (non-hidden) items in this container. * @return number */ get visibleItems() { let count = 0; for (let [element] of this._itemsByElement) { count += element.hidden ? 0 : 1; } return count; }, /** * Specifies the required conditions for an item to be considered unique. * Possible values: * - 1: label AND value are different from all other items * - 2: label OR value are different from all other items * - 3: only label is required to be different * - 4: only value is required to be different */ uniquenessQualifier: 1, /** * Checks if an item is unique in this container. * * @param MenuItem aItem * An object containing a label and a value property. * @return boolean * True if the element is unique, false otherwise. */ isUnique: function DVMC_isUnique(aItem) { switch (this.uniquenessQualifier) { case 1: return !this._itemsByLabel.has(aItem.label) && !this._itemsByValue.has(aItem.value); case 2: return !this._itemsByLabel.has(aItem.label) || !this._itemsByValue.has(aItem.value); case 3: return !this._itemsByLabel.has(aItem.label); case 4: return !this._itemsByValue.has(aItem.value); } return false; }, /** * Checks if an item's label and value are eligible for this container. * * @param MenuItem aItem * An object containing a label and a value property. * @return boolean * True if the element is eligible, false otherwise. */ isEligible: function DVMC_isEligible(aItem) { return this.isUnique(aItem) && aItem.label != "undefined" && aItem.label != "null" && aItem.value != "undefined" && aItem.value != "null"; }, /** * Finds the expected item index in this container based on its label. * * @param string aLabel * The label used to identify the element. * @return number * The expected item index. */ _findExpectedIndex: function DVMC__findExpectedIndex(aLabel) { let container = this._container; let itemCount = container.itemCount; for (let i = 0; i < itemCount; i++) { if (this.getItemForElement(container.getItemAtIndex(i)).label > aLabel) { return i; } } return itemCount; }, /** * Immediately appends an item in this container. * * @param MenuItem aItem * An object containing a label and a value property. * @param object aOptions [optional] * Additional options or flags supported by this operation: * - relaxed: true if this container should allow dupes & degenerates * @return MenuItem * The item associated with the displayed element, null if rejected. */ _appendItem: function DVMC__appendItem(aItem, aOptions = {}) { if (!aOptions.relaxed && !this.isEligible(aItem)) { return null; } return this._entangleItem(aItem, this._container.appendItem( aItem.label, aItem.value, "", aOptions.attachment)); }, /** * Immediately inserts an item in this container at the specified index. * * @param number aIndex * The position in the container intended for this item. * @param MenuItem aItem * An object containing a label and a value property. * @param object aOptions [optional] * Additional options or flags supported by this operation: * - relaxed: true if this container should allow dupes & degenerates * @return MenuItem * The item associated with the displayed element, null if rejected. */ _insertItemAt: function DVMC__insertItemAt(aIndex, aItem, aOptions) { if (!aOptions.relaxed && !this.isEligible(aItem)) { return null; } return this._entangleItem(aItem, this._container.insertItemAt( aIndex, aItem.label, aItem.value, "", aOptions.attachment)); }, /** * Entangles an item (model) with a displayed node element (view). * * @param MenuItem aItem * The item describing the element. * @param nsIDOMNode aElement * The element displaying the item. * @return MenuItem * The same item. */ _entangleItem: function DVMC__entangleItem(aItem, aElement) { this._itemsByLabel.set(aItem.label, aItem); this._itemsByValue.set(aItem.value, aItem); this._itemsByElement.set(aElement, aItem); aItem._target = aElement; return aItem; }, /** * Untangles an item (model) from a displayed node element (view). * * @param MenuItem aItem * The item describing the element. * @return MenuItem * The same item. */ _untangleItem: function DVMC__untangleItem(aItem) { if (aItem.finalize instanceof Function) { aItem.finalize(aItem); } this._itemsByLabel.delete(aItem.label); this._itemsByValue.delete(aItem.value); this._itemsByElement.delete(aItem.target); aItem._target = null; return aItem; }, /** * A generator-iterator over all the items in this container. */ __iterator__: function DVMC_iterator() { for (let [_, item] of this._itemsByElement) { yield item; } }, _container: null, _stagedItems: null, _itemsByLabel: null, _itemsByValue: null, _itemsByElement: null, _preferredValue: null, _emptyLabel: "", _unavailableLabel: "" }; /** * A stacked list of items, compatible with MenuContainer instances, used for * displaying views like the StackFrames, Breakpoints etc. * * Custom methods introduced by this view, not necessary for a MenuContainer: * set emptyText(aValue:string) * set itemType(aType:string) * set itemFactory(aCallback:function) * * TODO: Use this in #796135 - "Provide some obvious UI for scripts filtering". * * @param nsIDOMNode aAssociatedNode * The element associated with the displayed container. */ function StackList(aAssociatedNode) { this._parent = aAssociatedNode; this._appendEmptyNotice(); // Create an internal list container. this._list = document.createElement("vbox"); this._parent.appendChild(this._list); } StackList.prototype = { /** * Immediately appends an item in this container. * * @param string aLabel * The label displayed in the container. * @param string aValue * The actual internal value of the item. * @param string aDescription [optional] * An optional description of the item. * @param any aAttachment [optional] * Some attached primitive/object. * @return nsIDOMNode * The element associated with the displayed item. */ appendItem: function DVSL_appendItem(aLabel, aValue, aDescription, aAttachment) { return this.insertItemAt( Number.MAX_VALUE, aLabel, aValue, aDescription, aAttachment); }, /** * Immediately inserts an item in this container at the specified index. * * @param number aIndex * The position in the container intended for this item. * @param string aLabel * The label displayed in the container. * @param string aValue * The actual internal value of the item. * @param string aDescription [optional] * An optional description of the item. * @param any aAttachment [optional] * Some attached primitive/object. * @return nsIDOMNode * The element associated with the displayed item. */ insertItemAt: function DVSL_insertItemAt(aIndex, aLabel, aValue, aDescription, aAttachment) { let list = this._list; let childNodes = list.childNodes; let element = document.createElement(this.itemType); this._createItemView(element, aLabel, aValue, aAttachment); this._removeEmptyNotice(); return list.insertBefore(element, childNodes[aIndex]); }, /** * Returns the child node in this container situated at the specified index. * * @param number aIndex * The position in the container intended for this item. * @return nsIDOMNode * The element associated with the displayed item. */ getItemAtIndex: function DVSL_getItemAtIndex(aIndex) { return this._list.childNodes[aIndex]; }, /** * Immediately removes the specified child node from this container. * * @param nsIDOMNode aChild * The element associated with the displayed item. */ removeChild: function DVSL__removeChild(aChild) { this._list.removeChild(aChild); if (!this.itemCount) { this._appendEmptyNotice(); } }, /** * Immediately removes all of the child nodes from this container. */ removeAllItems: function DVSL_removeAllItems() { let parent = this._parent; let list = this._list; let firstChild; while (firstChild = list.firstChild) { list.removeChild(firstChild); } parent.scrollTop = 0; parent.scrollLeft = 0; this._selectedItem = null; this._selectedIndex = -1; this._appendEmptyNotice(); }, /** * Gets the number of child nodes present in this container. * @return number */ get itemCount() this._list.childNodes.length, /** * Gets the index of the selected child node in this container. * @return number */ get selectedIndex() this._selectedIndex, /** * Sets the index of the selected child node in this container. * Only one child node may be selected at a time. * @param number aIndex */ set selectedIndex(aIndex) this.selectedItem = this._list.childNodes[aIndex], /** * Gets the currently selected child node in this container. * @return nsIDOMNode */ get selectedItem() this._selectedItem, /** * Sets the currently selected child node in this container. * @param nsIDOMNode aChild */ set selectedItem(aChild) { let childNodes = this._list.childNodes; if (!aChild) { this._selectedItem = null; this._selectedIndex = -1; } for (let node of childNodes) { if (node == aChild) { node.classList.add("selected"); this._selectedIndex = Array.indexOf(childNodes, node); this._selectedItem = node; } else { node.classList.remove("selected"); } } }, /** * Applies an attribute to this container. * * @param string aName * The name of the attribute to set. * @return string * The attribute value. */ getAttribute: function DVSL_setAttribute(aName) { return this._parent.getAttribute(aName); }, /** * Applies an attribute to this container. * * @param string aName * The name of the attribute to set. * @param any aValue * The supplied attribute value. */ setAttribute: function DVSL_setAttribute(aName, aValue) { this._parent.setAttribute(aName, aValue); }, /** * Removes an attribute applied to this container. * * @param string aName * The name of the attribute to remove. */ removeAttribute: function DVSL_removeAttribute(aName) { this._parent.removeAttribute(aName); }, /** * Adds an event listener to this container. * * @param string aName * The name of the listener to set. * @param function aCallback * The function to be called when the event is triggered. * @param boolean aBubbleFlag * True if the event should bubble. */ addEventListener: function DVSL_addEventListener(aName, aCallback, aBubbleFlag) { this._parent.addEventListener(aName, aCallback, aBubbleFlag); }, /** * Removes an event listener added to this container. * * @param string aName * The name of the listener to remove. * @param function aCallback * The function called when the event was triggered. * @param boolean aBubbleFlag * True if the event was bubbling. */ removeEventListener: function DVSL_removeEventListener(aName, aCallback, aBubbleFlag) { this._parent.removeEventListener(aName, aCallback, aBubbleFlag); }, /** * Sets the text displayed in this container when there are no available items. * @param string aValue */ set emptyText(aValue) { if (this._emptyTextNode) { this._emptyTextNode.setAttribute("value", aValue); } this._emptyTextValue = aValue; }, /** * Overrides the item's element type (e.g. "vbox" or "hbox"). * @param string aType */ itemType: "hbox", /** * Overrides the customization function for creating an item's UI. * @param function aCallback */ set itemFactory(aCallback) this._createItemView = aCallback, /** * Customization function for creating an item's UI for this container. * * @param nsIDOMNode aElementNode * The element associated with the displayed item. * @param string aLabel * The item's label. * @param string aValue * The item's value. */ _createItemView: function DVSL__createItemView(aElementNode, aLabel, aValue) { let labelNode = document.createElement("label"); let valueNode = document.createElement("label"); let spacer = document.createElement("spacer"); labelNode.setAttribute("value", aLabel); valueNode.setAttribute("value", aValue); spacer.setAttribute("flex", "1"); aElementNode.appendChild(labelNode); aElementNode.appendChild(spacer); aElementNode.appendChild(valueNode); aElementNode.labelNode = labelNode; aElementNode.valueNode = valueNode; }, /** * Creates and appends a label signaling that this container is empty. */ _appendEmptyNotice: function DVSL__appendEmptyNotice() { if (this._emptyTextNode) { return; } let label = document.createElement("label"); label.className = "empty list-item"; label.setAttribute("value", this._emptyTextValue); this._parent.appendChild(label); this._emptyTextNode = label; }, /** * Removes the label signaling that this container is empty. */ _removeEmptyNotice: function DVSL__removeEmptyNotice() { if (!this._emptyTextNode) { return; } this._parent.removeChild(this._emptyTextNode); this._emptyTextNode = null; }, _parent: null, _list: null, _selectedIndex: -1, _selectedItem: null, _emptyTextNode: null, _emptyTextValue: "" }; /** * A simple way of displaying a "Connect to..." prompt. */ function RemoteDebuggerPrompt() { this.remote = {}; } RemoteDebuggerPrompt.prototype = { /** * Shows the prompt and waits for a remote host and port to connect to. * * @param boolean aIsReconnectingFlag * True to show the reconnect message instead of the connect request. */ show: function RDP_show(aIsReconnectingFlag) { let check = { value: Prefs.remoteAutoConnect }; let input = { value: Prefs.remoteHost + ":" + Prefs.remotePort }; let parts; while (true) { let result = Services.prompt.prompt(null, L10N.getStr("remoteDebuggerPromptTitle"), L10N.getStr(aIsReconnectingFlag ? "remoteDebuggerReconnectMessage" : "remoteDebuggerPromptMessage"), input, L10N.getStr("remoteDebuggerPromptCheck"), check); if (!result) { return false; } if ((parts = input.value.split(":")).length == 2) { let [host, port] = parts; if (host.length && port.length) { this.remote = { host: host, port: port, auto: check.value }; return true; } } } } };