gecko/browser/devtools/debugger/debugger-view.js

1516 lines
46 KiB
JavaScript

/* -*- 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 BREAKPOINT_CONDITIONAL_POPUP_POSITION = "after_start";
const BREAKPOINT_CONDITIONAL_POPUP_OFFSET = 50; // px
const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars
const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50;
const GLOBAL_SEARCH_ACTION_MAX_DELAY = 1500; // ms
const SEARCH_GLOBAL_FLAG = "!";
const SEARCH_TOKEN_FLAG = "#";
const SEARCH_LINE_FLAG = ":";
const SEARCH_VARIABLE_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._initializeWindow();
this._initializePanes();
this.Toolbar.initialize();
this.Options.initialize();
this.ChromeGlobals.initialize();
this.Sources.initialize();
this.Filtering.initialize();
this.StackFrames.initialize();
this.Breakpoints.initialize();
this.WatchExpressions.initialize();
this.GlobalSearch.initialize();
this.Variables = new VariablesView(document.getElementById("variables"));
this.Variables.searchPlaceholder = L10N.getStr("emptyVariablesFilterText");
this.Variables.emptyText = L10N.getStr("emptyVariablesText");
this.Variables.nonEnumVisible = Prefs.variablesNonEnumVisible;
this.Variables.searchEnabled = Prefs.variablesSearchboxVisible;
this.Variables.eval = DebuggerController.StackFrames.evaluate;
this.Variables.lazyEmpty = true;
this._initializeEditor(aCallback);
},
/**
* 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.WatchExpressions.destroy();
this.GlobalSearch.destroy();
this._destroyWindow();
this._destroyPanes();
this._destroyEditor();
aCallback();
},
/**
* Initializes the UI for the window.
*/
_initializeWindow: function DV__initializeWindow() {
dumpn("Initializing the DebuggerView window");
let isRemote = window._isRemoteDebugger;
let isChrome = window._isChromeDebugger;
if (isRemote || isChrome) {
window.moveTo(Prefs.windowX, Prefs.windowY);
window.resizeTo(Prefs.windowWidth, Prefs.windowHeight);
if (isRemote) {
document.title = L10N.getStr("remoteDebuggerWindowTitle");
} else {
document.title = L10N.getStr("chromeDebuggerWindowTitle");
}
}
},
/**
* Destroys the UI for the window.
*/
_destroyWindow: function DV__initializeWindow() {
dumpn("Destroying the DebuggerView window");
if (window._isRemoteDebugger || window._isChromeDebugger) {
Prefs.windowX = window.screenX;
Prefs.windowY = window.screenY;
Prefs.windowWidth = window.outerWidth;
Prefs.windowHeight = window.outerHeight;
}
},
/**
* 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._variablesAndExpressions = document.getElementById("variables+expressions");
this._stackframesAndBreakpoints.setAttribute("width", Prefs.stackframesWidth);
this._variablesAndExpressions.setAttribute("width", Prefs.variablesWidth);
this.togglePanes({
visible: Prefs.panesVisibleOnStartup,
animated: false
});
},
/**
* 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._variablesAndExpressions.getAttribute("width");
this._togglePanesButton = null;
this._stackframesAndBreakpoints = null;
this._variablesAndExpressions = 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();
window.dispatchEvent("Debugger:EditorLoaded", this.editor);
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();
window.dispatchEvent("Debugger:EditorUnloaded", this.editor);
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.
* @param string aTextContent [optional]
* The script text content.
*/
setEditorMode:
function DV_setEditorMode(aUrl, aContentType = "", aTextContent = "") {
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 if (aTextContent.match(/^\s*</)) {
// Use HTML mode for files in which the first non whitespace character is
// &lt;, regardless of extension.
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.TEXT);
}
}
},
/**
* 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:
* - caretLine: 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) {
this.setEditorSource(aSource, aOptions);
}.bind(this));
}
// If the source is already loaded, display it immediately.
else {
if (this._editorSource != aSource) {
// Avoid setting the editor mode for very large files.
if (aSource.text.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
this.setEditorMode(aSource.url, aSource.contentType, aSource.text);
} else {
this.editor.setMode(SourceEditor.MODES.TEXT);
}
this.editor.setText(aSource.text);
this.editor.resetUndo();
}
this._editorSource = aSource;
this.updateEditor();
DebuggerView.Sources.selectedValue = aSource.url;
DebuggerController.Breakpoints.updateEditorBreakpoints();
// Handle any additional options for showing the source.
if (aOptions.caretLine) {
editor.setCaretPosition(aOptions.caretLine - 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._togglePanesButton.hasAttribute("panesHidden"),
/**
* 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
* - callback: a function to invoke when the panes toggle finishes
*/
togglePanes: function DV__togglePanes(aFlags = {}) {
// Avoid useless toggles.
if (aFlags.visible == !this.panesHidden) {
aFlags.callback && aFlags.callback();
return;
}
if (aFlags.visible) {
this._stackframesAndBreakpoints.style.marginLeft = "0";
this._variablesAndExpressions.style.marginRight = "0";
this._togglePanesButton.removeAttribute("panesHidden");
this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("collapsePanes"));
} else {
let marginL = ~~(this._stackframesAndBreakpoints.getAttribute("width")) + 1;
let marginR = ~~(this._variablesAndExpressions.getAttribute("width")) + 1;
this._stackframesAndBreakpoints.style.marginLeft = -marginL + "px";
this._variablesAndExpressions.style.marginRight = -marginR + "px";
this._togglePanesButton.setAttribute("panesHidden", "true");
this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("expandPanes"));
}
if (aFlags.animated) {
this._stackframesAndBreakpoints.setAttribute("animated", "");
this._variablesAndExpressions.setAttribute("animated", "");
// 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.
let self = this;
window.addEventListener("transitionend", function onEvent() {
window.removeEventListener("transitionend", onEvent, false);
aFlags.callback && aFlags.callback();
self.updateEditor();
}, false);
} else {
this._stackframesAndBreakpoints.removeAttribute("animated");
this._variablesAndExpressions.removeAttribute("animated");
aFlags.callback && aFlags.callback();
}
},
/**
* Sets all the panes visible after a short period of time.
*
* @param function aCallback
* A function to invoke when the panes toggle finishes.
*/
showPanesSoon: function DV__showPanesSoon(aCallback) {
// Try to keep animations as smooth as possible, so wait a few cycles.
window.setTimeout(function() {
DebuggerView.togglePanes({
visible: true,
animated: true,
callback: aCallback
});
}, PANES_APPEARANCE_DELAY);
},
/**
* 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.Breakpoints.unhighlightBreakpoint();
this.Variables.empty();
SourceUtils.clearLabelsCache();
if (this.editor) {
this.editor.setText("");
this._editorSource = null;
}
},
Toolbar: null,
Options: null,
ChromeGlobals: null,
Sources: null,
Filtering: null,
StackFrames: null,
Breakpoints: null,
GlobalSearch: null,
Variables: null,
_editor: null,
_editorSource: null,
_togglePanesButton: null,
_stackframesAndBreakpoints: null,
_variablesAndExpressions: null,
_isInitialized: false,
_isDestroyed: 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 <menulist> 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 appended
* - unsorted: true if the items should not always 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);
}
// Immediately insert the item at the specified index.
else if (aOptions.forced && aOptions.forced.atIndex !== undefined) {
return this._insertItemAt(aOptions.forced.atIndex, item, aOptions);
}
// 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 = [];
},
/**
* Toggles all the items in this container hidden or visible.
*
* @param boolean aVisibleFlag
* Specifies the intended visibility.
*/
toggleContents: function DVMC_toggleContents(aVisibleFlag) {
for (let [, item] of this._itemsByElement) {
item.target.hidden = !aVisibleFlag;
}
},
/**
* 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(aLabel);
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 index.
*
* @param number aIndex
* The index used to identify the element.
* @return MenuItem
* The matched item, or null if nothing is found.
*/
getItemAtIndex: function DVMC_getItemAtIndex(aIndex) {
return this.getItemForElement(this._container.getItemAtIndex(aIndex));
},
/**
* 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 items in this container.
* @return number
*/
get totalItems() {
return this._itemsByElement.size;
},
/**
* 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 permaText(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;
// 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 permanently in this container's header.
* @param string aValue
*/
set permaText(aValue) {
if (this._permaTextNode) {
this._permaTextNode.setAttribute("value", aValue);
}
this._permaTextValue = aValue;
this._appendPermaNotice();
},
/**
* 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;
this._appendEmptyNotice();
},
/**
* 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 displayed permanently in this container's header.
*/
_appendPermaNotice: function DVSL__appendPermaNotice() {
if (this._permaTextNode || !this._permaTextValue) {
return;
}
let label = document.createElement("label");
label.className = "empty list-item";
label.setAttribute("value", this._permaTextValue);
this._parent.insertBefore(label, this._list);
this._permaTextNode = label;
},
/**
* Creates and appends a label signaling that this container is empty.
*/
_appendEmptyNotice: function DVSL__appendEmptyNotice() {
if (this._emptyTextNode || !this._emptyTextValue) {
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,
_permaTextNode: null,
_permaTextValue: "",
_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;
}
}
}
}
};