michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ]; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: Cu.import("resource://gre/modules/devtools/Loader.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise"); michael@0: michael@0: const FORBIDDEN_IDS = new Set(["toolbox", ""]); michael@0: const MAX_ORDINAL = 99; michael@0: michael@0: michael@0: /** michael@0: * DevTools is a class that represents a set of developer tools, it holds a michael@0: * set of tools and keeps track of open toolboxes in the browser. michael@0: */ michael@0: this.DevTools = function DevTools() { michael@0: this._tools = new Map(); // Map michael@0: this._toolboxes = new Map(); // Map michael@0: michael@0: // destroy() is an observer's handler so we need to preserve context. michael@0: this.destroy = this.destroy.bind(this); michael@0: this._teardown = this._teardown.bind(this); michael@0: michael@0: this._testing = false; michael@0: michael@0: EventEmitter.decorate(this); michael@0: michael@0: Services.obs.addObserver(this._teardown, "devtools-unloaded", false); michael@0: Services.obs.addObserver(this.destroy, "quit-application", false); michael@0: } michael@0: michael@0: DevTools.prototype = { michael@0: /** michael@0: * When the testing flag is set we take appropriate action to prevent race michael@0: * conditions in our testing environment. This means setting michael@0: * dom.send_after_paint_to_content to false to prevent infinite MozAfterPaint michael@0: * loops and not autohiding the highlighter. michael@0: */ michael@0: get testing() { michael@0: return this._testing; michael@0: }, michael@0: michael@0: set testing(state) { michael@0: this._testing = state; michael@0: michael@0: if (state) { michael@0: // dom.send_after_paint_to_content is set to true (non-default) in michael@0: // testing/profiles/prefs_general.js so lets set it to the same as it is michael@0: // in a default browser profile for the duration of the test. michael@0: Services.prefs.setBoolPref("dom.send_after_paint_to_content", false); michael@0: } else { michael@0: Services.prefs.setBoolPref("dom.send_after_paint_to_content", true); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Register a new developer tool. michael@0: * michael@0: * A definition is a light object that holds different information about a michael@0: * developer tool. This object is not supposed to have any operational code. michael@0: * See it as a "manifest". michael@0: * The only actual code lives in the build() function, which will be used to michael@0: * start an instance of this tool. michael@0: * michael@0: * Each toolDefinition has the following properties: michael@0: * - id: Unique identifier for this tool (string|required) michael@0: * - visibilityswitch: Property name to allow us to hide this tool from the michael@0: * DevTools Toolbox. michael@0: * A falsy value indicates that it cannot be hidden. michael@0: * - icon: URL pointing to a graphic which will be used as the src for an michael@0: * 16x16 img tag (string|required) michael@0: * - invertIconForLightTheme: The icon can automatically have an inversion michael@0: * filter applied (default is false). All builtin tools are true, but michael@0: * addons may omit this to prevent unwanted changes to the `icon` michael@0: * image. See browser/themes/shared/devtools/filters.svg#invert for michael@0: * the filter being applied to the images (boolean|optional) michael@0: * - url: URL pointing to a XUL/XHTML document containing the user interface michael@0: * (string|required) michael@0: * - label: Localized name for the tool to be displayed to the user michael@0: * (string|required) michael@0: * - build: Function that takes an iframe, which has been populated with the michael@0: * markup from |url|, and also the toolbox containing the panel. michael@0: * And returns an instance of ToolPanel (function|required) michael@0: */ michael@0: registerTool: function DT_registerTool(toolDefinition) { michael@0: let toolId = toolDefinition.id; michael@0: michael@0: if (!toolId || FORBIDDEN_IDS.has(toolId)) { michael@0: throw new Error("Invalid definition.id"); michael@0: } michael@0: michael@0: // Make sure that additional tools will always be able to be hidden. michael@0: // When being called from main.js, defaultTools has not yet been exported. michael@0: // But, we can assume that in this case, it is a default tool. michael@0: if (devtools.defaultTools && devtools.defaultTools.indexOf(toolDefinition) == -1) { michael@0: toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled"; michael@0: } michael@0: michael@0: this._tools.set(toolId, toolDefinition); michael@0: michael@0: this.emit("tool-registered", toolId); michael@0: }, michael@0: michael@0: /** michael@0: * Removes all tools that match the given |toolId| michael@0: * Needed so that add-ons can remove themselves when they are deactivated michael@0: * michael@0: * @param {string|object} tool michael@0: * Definition or the id of the tool to unregister. Passing the michael@0: * tool id should be avoided as it is a temporary measure. michael@0: * @param {boolean} isQuitApplication michael@0: * true to indicate that the call is due to app quit, so we should not michael@0: * cause a cascade of costly events michael@0: */ michael@0: unregisterTool: function DT_unregisterTool(tool, isQuitApplication) { michael@0: let toolId = null; michael@0: if (typeof tool == "string") { michael@0: toolId = tool; michael@0: tool = this._tools.get(tool); michael@0: } michael@0: else { michael@0: toolId = tool.id; michael@0: } michael@0: this._tools.delete(toolId); michael@0: michael@0: if (!isQuitApplication) { michael@0: this.emit("tool-unregistered", tool); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sorting function used for sorting tools based on their ordinals. michael@0: */ michael@0: ordinalSort: function DT_ordinalSort(d1, d2) { michael@0: let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL; michael@0: let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL; michael@0: return o1 - o2; michael@0: }, michael@0: michael@0: getDefaultTools: function DT_getDefaultTools() { michael@0: return devtools.defaultTools.sort(this.ordinalSort); michael@0: }, michael@0: michael@0: getAdditionalTools: function DT_getAdditionalTools() { michael@0: let tools = []; michael@0: for (let [key, value] of this._tools) { michael@0: if (devtools.defaultTools.indexOf(value) == -1) { michael@0: tools.push(value); michael@0: } michael@0: } michael@0: return tools.sort(this.ordinalSort); michael@0: }, michael@0: michael@0: /** michael@0: * Get a tool definition if it exists and is enabled. michael@0: * michael@0: * @param {string} toolId michael@0: * The id of the tool to show michael@0: * michael@0: * @return {ToolDefinition|null} tool michael@0: * The ToolDefinition for the id or null. michael@0: */ michael@0: getToolDefinition: function DT_getToolDefinition(toolId) { michael@0: let tool = this._tools.get(toolId); michael@0: if (!tool) { michael@0: return null; michael@0: } else if (!tool.visibilityswitch) { michael@0: return tool; michael@0: } michael@0: michael@0: let enabled; michael@0: try { michael@0: enabled = Services.prefs.getBoolPref(tool.visibilityswitch); michael@0: } catch (e) { michael@0: enabled = true; michael@0: } michael@0: michael@0: return enabled ? tool : null; michael@0: }, michael@0: michael@0: /** michael@0: * Allow ToolBoxes to get at the list of tools that they should populate michael@0: * themselves with. michael@0: * michael@0: * @return {Map} tools michael@0: * A map of the the tool definitions registered in this instance michael@0: */ michael@0: getToolDefinitionMap: function DT_getToolDefinitionMap() { michael@0: let tools = new Map(); michael@0: michael@0: for (let [id, definition] of this._tools) { michael@0: if (this.getToolDefinition(id)) { michael@0: tools.set(id, definition); michael@0: } michael@0: } michael@0: michael@0: return tools; michael@0: }, michael@0: michael@0: /** michael@0: * Tools have an inherent ordering that can't be represented in a Map so michael@0: * getToolDefinitionArray provides an alternative representation of the michael@0: * definitions sorted by ordinal value. michael@0: * michael@0: * @return {Array} tools michael@0: * A sorted array of the tool definitions registered in this instance michael@0: */ michael@0: getToolDefinitionArray: function DT_getToolDefinitionArray() { michael@0: let definitions = []; michael@0: michael@0: for (let [id, definition] of this._tools) { michael@0: if (this.getToolDefinition(id)) { michael@0: definitions.push(definition); michael@0: } michael@0: } michael@0: michael@0: return definitions.sort(this.ordinalSort); michael@0: }, michael@0: michael@0: /** michael@0: * Show a Toolbox for a target (either by creating a new one, or if a toolbox michael@0: * already exists for the target, by bring to the front the existing one) michael@0: * If |toolId| is specified then the displayed toolbox will have the michael@0: * specified tool selected. michael@0: * If |hostType| is specified then the toolbox will be displayed using the michael@0: * specified HostType. michael@0: * michael@0: * @param {Target} target michael@0: * The target the toolbox will debug michael@0: * @param {string} toolId michael@0: * The id of the tool to show michael@0: * @param {Toolbox.HostType} hostType michael@0: * The type of host (bottom, window, side) michael@0: * @param {object} hostOptions michael@0: * Options for host specifically michael@0: * michael@0: * @return {Toolbox} toolbox michael@0: * The toolbox that was opened michael@0: */ michael@0: showToolbox: function(target, toolId, hostType, hostOptions) { michael@0: let deferred = promise.defer(); michael@0: michael@0: let toolbox = this._toolboxes.get(target); michael@0: if (toolbox) { michael@0: michael@0: let hostPromise = (hostType != null && toolbox.hostType != hostType) ? michael@0: toolbox.switchHost(hostType) : michael@0: promise.resolve(null); michael@0: michael@0: if (toolId != null && toolbox.currentToolId != toolId) { michael@0: hostPromise = hostPromise.then(function() { michael@0: return toolbox.selectTool(toolId); michael@0: }); michael@0: } michael@0: michael@0: return hostPromise.then(function() { michael@0: toolbox.raise(); michael@0: return toolbox; michael@0: }); michael@0: } michael@0: else { michael@0: // No toolbox for target, create one michael@0: toolbox = new devtools.Toolbox(target, toolId, hostType, hostOptions); michael@0: michael@0: this._toolboxes.set(target, toolbox); michael@0: michael@0: toolbox.once("destroyed", function() { michael@0: this._toolboxes.delete(target); michael@0: this.emit("toolbox-destroyed", target); michael@0: }.bind(this)); michael@0: michael@0: // If we were asked for a specific tool then we need to wait for the michael@0: // tool to be ready, otherwise we can just wait for toolbox open michael@0: if (toolId != null) { michael@0: toolbox.once(toolId + "-ready", function(event, panel) { michael@0: this.emit("toolbox-ready", toolbox); michael@0: deferred.resolve(toolbox); michael@0: }.bind(this)); michael@0: toolbox.open(); michael@0: } michael@0: else { michael@0: toolbox.open().then(function() { michael@0: deferred.resolve(toolbox); michael@0: this.emit("toolbox-ready", toolbox); michael@0: }.bind(this)); michael@0: } michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Return the toolbox for a given target. michael@0: * michael@0: * @param {object} target michael@0: * Target value e.g. the target that owns this toolbox michael@0: * michael@0: * @return {Toolbox} toolbox michael@0: * The toobox that is debugging the given target michael@0: */ michael@0: getToolbox: function DT_getToolbox(target) { michael@0: return this._toolboxes.get(target); michael@0: }, michael@0: michael@0: /** michael@0: * Close the toolbox for a given target michael@0: * michael@0: * @return promise michael@0: * This promise will resolve to false if no toolbox was found michael@0: * associated to the target. true, if the toolbox was successfuly michael@0: * closed. michael@0: */ michael@0: closeToolbox: function DT_closeToolbox(target) { michael@0: let toolbox = this._toolboxes.get(target); michael@0: if (toolbox == null) { michael@0: return promise.resolve(false); michael@0: } michael@0: return toolbox.destroy().then(() => true); michael@0: }, michael@0: michael@0: /** michael@0: * Called to tear down a tools provider. michael@0: */ michael@0: _teardown: function DT_teardown() { michael@0: for (let [target, toolbox] of this._toolboxes) { michael@0: toolbox.destroy(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * All browser windows have been closed, tidy up remaining objects. michael@0: */ michael@0: destroy: function() { michael@0: Services.obs.removeObserver(this.destroy, "quit-application"); michael@0: Services.obs.removeObserver(this._teardown, "devtools-unloaded"); michael@0: michael@0: for (let [key, tool] of this.getToolDefinitionMap()) { michael@0: this.unregisterTool(key, true); michael@0: } michael@0: michael@0: // Cleaning down the toolboxes: i.e. michael@0: // for (let [target, toolbox] of this._toolboxes) toolbox.destroy(); michael@0: // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow michael@0: }, michael@0: michael@0: /** michael@0: * Iterator that yields each of the toolboxes. michael@0: */ michael@0: '@@iterator': function*() { michael@0: for (let toolbox of this._toolboxes) { michael@0: yield toolbox; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * gDevTools is a singleton that controls the Firefox Developer Tools. michael@0: * michael@0: * It is an instance of a DevTools class that holds a set of tools. It has the michael@0: * same lifetime as the browser. michael@0: */ michael@0: let gDevTools = new DevTools(); michael@0: this.gDevTools = gDevTools; michael@0: michael@0: /** michael@0: * gDevToolsBrowser exposes functions to connect the gDevTools instance with a michael@0: * Firefox instance. michael@0: */ michael@0: let gDevToolsBrowser = { michael@0: /** michael@0: * A record of the windows whose menus we altered, so we can undo the changes michael@0: * as the window is closed michael@0: */ michael@0: _trackedBrowserWindows: new Set(), michael@0: michael@0: /** michael@0: * This function is for the benefit of Tools:DevToolbox in michael@0: * browser/base/content/browser-sets.inc and should not be used outside michael@0: * of there michael@0: */ michael@0: toggleToolboxCommand: function(gBrowser) { michael@0: let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); michael@0: let toolbox = gDevTools.getToolbox(target); michael@0: michael@0: toolbox ? toolbox.destroy() : gDevTools.showToolbox(target); michael@0: }, michael@0: michael@0: toggleBrowserToolboxCommand: function(gBrowser) { michael@0: let target = devtools.TargetFactory.forWindow(gBrowser.ownerDocument.defaultView); michael@0: let toolbox = gDevTools.getToolbox(target); michael@0: michael@0: toolbox ? toolbox.destroy() michael@0: : gDevTools.showToolbox(target, "inspector", Toolbox.HostType.WINDOW); michael@0: }, michael@0: michael@0: /** michael@0: * This function ensures the right commands are enabled in a window, michael@0: * depending on their relevant prefs. It gets run when a window is registered, michael@0: * or when any of the devtools prefs change. michael@0: */ michael@0: updateCommandAvailability: function(win) { michael@0: let doc = win.document; michael@0: michael@0: function toggleCmd(id, isEnabled) { michael@0: let cmd = doc.getElementById(id); michael@0: if (isEnabled) { michael@0: cmd.removeAttribute("disabled"); michael@0: cmd.removeAttribute("hidden"); michael@0: } else { michael@0: cmd.setAttribute("disabled", "true"); michael@0: cmd.setAttribute("hidden", "true"); michael@0: } michael@0: }; michael@0: michael@0: // Enable developer toolbar? michael@0: let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled"); michael@0: toggleCmd("Tools:DevToolbar", devToolbarEnabled); michael@0: let focusEl = doc.getElementById("Tools:DevToolbarFocus"); michael@0: if (devToolbarEnabled) { michael@0: focusEl.removeAttribute("disabled"); michael@0: } else { michael@0: focusEl.setAttribute("disabled", "true"); michael@0: } michael@0: if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) { michael@0: win.DeveloperToolbar.show(false); michael@0: } michael@0: michael@0: // Enable App Manager? michael@0: let appMgrEnabled = Services.prefs.getBoolPref("devtools.appmanager.enabled"); michael@0: toggleCmd("Tools:DevAppMgr", appMgrEnabled); michael@0: michael@0: // Enable Browser Toolbox? michael@0: let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled"); michael@0: let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled"); michael@0: let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled && michael@0: Services.prefs.getBoolPref("devtools.debugger.chrome-enabled"); michael@0: toggleCmd("Tools:BrowserToolbox", remoteEnabled); michael@0: michael@0: // Enable Error Console? michael@0: let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled"); michael@0: toggleCmd("Tools:ErrorConsole", consoleEnabled); michael@0: michael@0: // Enable DevTools connection screen, if the preference allows this. michael@0: toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled); michael@0: }, michael@0: michael@0: observe: function(subject, topic, prefName) { michael@0: if (prefName.endsWith("enabled")) { michael@0: for (let win of this._trackedBrowserWindows) { michael@0: this.updateCommandAvailability(win); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _prefObserverRegistered: false, michael@0: michael@0: ensurePrefObserver: function() { michael@0: if (!this._prefObserverRegistered) { michael@0: this._prefObserverRegistered = true; michael@0: Services.prefs.addObserver("devtools.", this, false); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * This function is for the benefit of Tools:{toolId} commands, michael@0: * triggered from the WebDeveloper menu and keyboard shortcuts. michael@0: * michael@0: * selectToolCommand's behavior: michael@0: * - if the toolbox is closed, michael@0: * we open the toolbox and select the tool michael@0: * - if the toolbox is open, and the targetted tool is not selected, michael@0: * we select it michael@0: * - if the toolbox is open, and the targetted tool is selected, michael@0: * and the host is NOT a window, we close the toolbox michael@0: * - if the toolbox is open, and the targetted tool is selected, michael@0: * and the host is a window, we raise the toolbox window michael@0: */ michael@0: selectToolCommand: function(gBrowser, toolId) { michael@0: let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); michael@0: let toolbox = gDevTools.getToolbox(target); michael@0: let toolDefinition = gDevTools.getToolDefinition(toolId); michael@0: michael@0: if (toolbox && michael@0: (toolbox.currentToolId == toolId || michael@0: (toolId == "webconsole" && toolbox.splitConsole))) michael@0: { michael@0: toolbox.fireCustomKey(toolId); michael@0: michael@0: if (toolDefinition.preventClosingOnKey || toolbox.hostType == devtools.Toolbox.HostType.WINDOW) { michael@0: toolbox.raise(); michael@0: } else { michael@0: toolbox.destroy(); michael@0: } michael@0: } else { michael@0: gDevTools.showToolbox(target, toolId).then(() => { michael@0: let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); michael@0: let toolbox = gDevTools.getToolbox(target); michael@0: michael@0: toolbox.fireCustomKey(toolId); michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Open a tab to allow connects to a remote browser michael@0: */ michael@0: openConnectScreen: function(gBrowser) { michael@0: gBrowser.selectedTab = gBrowser.addTab("chrome://browser/content/devtools/connect.xhtml"); michael@0: }, michael@0: michael@0: /** michael@0: * Open the App Manager michael@0: */ michael@0: openAppManager: function(gBrowser) { michael@0: gBrowser.selectedTab = gBrowser.addTab("about:app-manager"); michael@0: }, michael@0: michael@0: /** michael@0: * Add this DevTools's presence to a browser window's document michael@0: * michael@0: * @param {XULDocument} doc michael@0: * The document to which menuitems and handlers are to be added michael@0: */ michael@0: registerBrowserWindow: function DT_registerBrowserWindow(win) { michael@0: this.updateCommandAvailability(win); michael@0: this.ensurePrefObserver(); michael@0: gDevToolsBrowser._trackedBrowserWindows.add(win); michael@0: gDevToolsBrowser._addAllToolsToMenu(win.document); michael@0: michael@0: if (this._isFirebugInstalled()) { michael@0: let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox"); michael@0: broadcaster.removeAttribute("key"); michael@0: } michael@0: michael@0: let tabContainer = win.document.getElementById("tabbrowser-tabs") michael@0: tabContainer.addEventListener("TabSelect", michael@0: gDevToolsBrowser._updateMenuCheckbox, false); michael@0: }, michael@0: michael@0: /** michael@0: * Add a to . michael@0: * Appending a element is not always enough. The needs michael@0: * to be detached and reattached to make sure the is taken into michael@0: * account (see bug 832984). michael@0: * michael@0: * @param {XULDocument} doc michael@0: * The document to which keys are to be added michael@0: * @param {XULElement} or {DocumentFragment} keys michael@0: * Keys to add michael@0: */ michael@0: attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) { michael@0: let devtoolsKeyset = doc.getElementById("devtoolsKeyset"); michael@0: michael@0: if (!devtoolsKeyset) { michael@0: devtoolsKeyset = doc.createElement("keyset"); michael@0: devtoolsKeyset.setAttribute("id", "devtoolsKeyset"); michael@0: } michael@0: devtoolsKeyset.appendChild(keys); michael@0: let mainKeyset = doc.getElementById("mainKeyset"); michael@0: mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset); michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Detect the presence of a Firebug. michael@0: * michael@0: * @return promise michael@0: */ michael@0: _isFirebugInstalled: function DT_isFirebugInstalled() { michael@0: let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons"); michael@0: return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1; michael@0: }, michael@0: michael@0: /** michael@0: * Add the menuitem for a tool to all open browser windows. michael@0: * michael@0: * @param {object} toolDefinition michael@0: * properties of the tool to add michael@0: */ michael@0: _addToolToWindows: function DT_addToolToWindows(toolDefinition) { michael@0: // No menu item or global shortcut is required for options panel. michael@0: if (!toolDefinition.inMenu) { michael@0: return; michael@0: } michael@0: michael@0: // Skip if the tool is disabled. michael@0: try { michael@0: if (toolDefinition.visibilityswitch && michael@0: !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) { michael@0: return; michael@0: } michael@0: } catch(e) {} michael@0: michael@0: // We need to insert the new tool in the right place, which means knowing michael@0: // the tool that comes before the tool that we're trying to add michael@0: let allDefs = gDevTools.getToolDefinitionArray(); michael@0: let prevDef; michael@0: for (let def of allDefs) { michael@0: if (!def.inMenu) { michael@0: continue; michael@0: } michael@0: if (def === toolDefinition) { michael@0: break; michael@0: } michael@0: prevDef = def; michael@0: } michael@0: michael@0: for (let win of gDevToolsBrowser._trackedBrowserWindows) { michael@0: let doc = win.document; michael@0: let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc); michael@0: michael@0: doc.getElementById("mainCommandSet").appendChild(elements.cmd); michael@0: michael@0: if (elements.key) { michael@0: this.attachKeybindingsToBrowser(doc, elements.key); michael@0: } michael@0: michael@0: doc.getElementById("mainBroadcasterSet").appendChild(elements.bc); michael@0: michael@0: let amp = doc.getElementById("appmenu_webDeveloper_popup"); michael@0: if (amp) { michael@0: let ref; michael@0: michael@0: if (prevDef != null) { michael@0: let menuitem = doc.getElementById("appmenuitem_" + prevDef.id); michael@0: ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null; michael@0: } else { michael@0: ref = doc.getElementById("appmenu_devtools_separator"); michael@0: } michael@0: michael@0: if (ref) { michael@0: amp.insertBefore(elements.appmenuitem, ref); michael@0: } michael@0: } michael@0: michael@0: let mp = doc.getElementById("menuWebDeveloperPopup"); michael@0: if (mp) { michael@0: let ref; michael@0: michael@0: if (prevDef != null) { michael@0: let menuitem = doc.getElementById("menuitem_" + prevDef.id); michael@0: ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null; michael@0: } else { michael@0: ref = doc.getElementById("menu_devtools_separator"); michael@0: } michael@0: michael@0: if (ref) { michael@0: mp.insertBefore(elements.menuitem, ref); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add all tools to the developer tools menu of a window. michael@0: * michael@0: * @param {XULDocument} doc michael@0: * The document to which the tool items are to be added. michael@0: */ michael@0: _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) { michael@0: let fragCommands = doc.createDocumentFragment(); michael@0: let fragKeys = doc.createDocumentFragment(); michael@0: let fragBroadcasters = doc.createDocumentFragment(); michael@0: let fragAppMenuItems = doc.createDocumentFragment(); michael@0: let fragMenuItems = doc.createDocumentFragment(); michael@0: michael@0: for (let toolDefinition of gDevTools.getToolDefinitionArray()) { michael@0: if (!toolDefinition.inMenu) { michael@0: continue; michael@0: } michael@0: michael@0: let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc); michael@0: michael@0: if (!elements) { michael@0: return; michael@0: } michael@0: michael@0: fragCommands.appendChild(elements.cmd); michael@0: if (elements.key) { michael@0: fragKeys.appendChild(elements.key); michael@0: } michael@0: fragBroadcasters.appendChild(elements.bc); michael@0: fragAppMenuItems.appendChild(elements.appmenuitem); michael@0: fragMenuItems.appendChild(elements.menuitem); michael@0: } michael@0: michael@0: let mcs = doc.getElementById("mainCommandSet"); michael@0: mcs.appendChild(fragCommands); michael@0: michael@0: this.attachKeybindingsToBrowser(doc, fragKeys); michael@0: michael@0: let mbs = doc.getElementById("mainBroadcasterSet"); michael@0: mbs.appendChild(fragBroadcasters); michael@0: michael@0: let amp = doc.getElementById("appmenu_webDeveloper_popup"); michael@0: if (amp) { michael@0: let amps = doc.getElementById("appmenu_devtools_separator"); michael@0: amp.insertBefore(fragAppMenuItems, amps); michael@0: } michael@0: michael@0: let mp = doc.getElementById("menuWebDeveloperPopup"); michael@0: let mps = doc.getElementById("menu_devtools_separator"); michael@0: mp.insertBefore(fragMenuItems, mps); michael@0: }, michael@0: michael@0: /** michael@0: * Add a menu entry for a tool definition michael@0: * michael@0: * @param {string} toolDefinition michael@0: * Tool definition of the tool to add a menu entry. michael@0: * @param {XULDocument} doc michael@0: * The document to which the tool menu item is to be added. michael@0: */ michael@0: _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) { michael@0: let id = toolDefinition.id; michael@0: michael@0: // Prevent multiple entries for the same tool. michael@0: if (doc.getElementById("Tools:" + id)) { michael@0: return; michael@0: } michael@0: michael@0: let cmd = doc.createElement("command"); michael@0: cmd.id = "Tools:" + id; michael@0: cmd.setAttribute("oncommand", michael@0: 'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");'); michael@0: michael@0: let key = null; michael@0: if (toolDefinition.key) { michael@0: key = doc.createElement("key"); michael@0: key.id = "key_" + id; michael@0: michael@0: if (toolDefinition.key.startsWith("VK_")) { michael@0: key.setAttribute("keycode", toolDefinition.key); michael@0: } else { michael@0: key.setAttribute("key", toolDefinition.key); michael@0: } michael@0: michael@0: key.setAttribute("command", cmd.id); michael@0: key.setAttribute("modifiers", toolDefinition.modifiers); michael@0: } michael@0: michael@0: let bc = doc.createElement("broadcaster"); michael@0: bc.id = "devtoolsMenuBroadcaster_" + id; michael@0: bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label); michael@0: bc.setAttribute("command", cmd.id); michael@0: michael@0: if (key) { michael@0: bc.setAttribute("key", "key_" + id); michael@0: } michael@0: michael@0: let appmenuitem = doc.createElement("menuitem"); michael@0: appmenuitem.id = "appmenuitem_" + id; michael@0: appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id); michael@0: michael@0: let menuitem = doc.createElement("menuitem"); michael@0: menuitem.id = "menuitem_" + id; michael@0: menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id); michael@0: michael@0: if (toolDefinition.accesskey) { michael@0: menuitem.setAttribute("accesskey", toolDefinition.accesskey); michael@0: } michael@0: michael@0: return { michael@0: cmd: cmd, michael@0: key: key, michael@0: bc: bc, michael@0: appmenuitem: appmenuitem, michael@0: menuitem: menuitem michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Update the "Toggle Tools" checkbox in the developer tools menu. This is michael@0: * called when a toolbox is created or destroyed. michael@0: */ michael@0: _updateMenuCheckbox: function DT_updateMenuCheckbox() { michael@0: for (let win of gDevToolsBrowser._trackedBrowserWindows) { michael@0: michael@0: let hasToolbox = false; michael@0: if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) { michael@0: let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab); michael@0: if (gDevTools._toolboxes.has(target)) { michael@0: hasToolbox = true; michael@0: } michael@0: } michael@0: michael@0: let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox"); michael@0: if (hasToolbox) { michael@0: broadcaster.setAttribute("checked", "true"); michael@0: } else { michael@0: broadcaster.removeAttribute("checked"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Connects to the SPS profiler when the developer tools are open. michael@0: */ michael@0: _connectToProfiler: function DT_connectToProfiler() { michael@0: let ProfilerController = devtools.require("devtools/profiler/controller"); michael@0: michael@0: for (let win of gDevToolsBrowser._trackedBrowserWindows) { michael@0: if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) { michael@0: let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab); michael@0: if (gDevTools._toolboxes.has(target)) { michael@0: target.makeRemote().then(() => { michael@0: let profiler = new ProfilerController(target); michael@0: profiler.connect(); michael@0: }).then(null, Cu.reportError); michael@0: michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Remove the menuitem for a tool to all open browser windows. michael@0: * michael@0: * @param {string} toolId michael@0: * id of the tool to remove michael@0: */ michael@0: _removeToolFromWindows: function DT_removeToolFromWindows(toolId) { michael@0: for (let win of gDevToolsBrowser._trackedBrowserWindows) { michael@0: gDevToolsBrowser._removeToolFromMenu(toolId, win.document); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Remove a tool's menuitem from a window michael@0: * michael@0: * @param {string} toolId michael@0: * Id of the tool to add a menu entry for michael@0: * @param {XULDocument} doc michael@0: * The document to which the tool menu item is to be removed from michael@0: */ michael@0: _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) { michael@0: let command = doc.getElementById("Tools:" + toolId); michael@0: if (command) { michael@0: command.parentNode.removeChild(command); michael@0: } michael@0: michael@0: let key = doc.getElementById("key_" + toolId); michael@0: if (key) { michael@0: key.parentNode.removeChild(key); michael@0: } michael@0: michael@0: let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId); michael@0: if (bc) { michael@0: bc.parentNode.removeChild(bc); michael@0: } michael@0: michael@0: let appmenuitem = doc.getElementById("appmenuitem_" + toolId); michael@0: if (appmenuitem) { michael@0: appmenuitem.parentNode.removeChild(appmenuitem); michael@0: } michael@0: michael@0: let menuitem = doc.getElementById("menuitem_" + toolId); michael@0: if (menuitem) { michael@0: menuitem.parentNode.removeChild(menuitem); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called on browser unload to remove menu entries, toolboxes and event michael@0: * listeners from the closed browser window. michael@0: * michael@0: * @param {XULWindow} win michael@0: * The window containing the menu entry michael@0: */ michael@0: forgetBrowserWindow: function DT_forgetBrowserWindow(win) { michael@0: gDevToolsBrowser._trackedBrowserWindows.delete(win); michael@0: michael@0: // Destroy toolboxes for closed window michael@0: for (let [target, toolbox] of gDevTools._toolboxes) { michael@0: if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) { michael@0: toolbox.destroy(); michael@0: } michael@0: } michael@0: michael@0: let tabContainer = win.document.getElementById("tabbrowser-tabs") michael@0: tabContainer.removeEventListener("TabSelect", michael@0: gDevToolsBrowser._updateMenuCheckbox, false); michael@0: }, michael@0: michael@0: /** michael@0: * All browser windows have been closed, tidy up remaining objects. michael@0: */ michael@0: destroy: function() { michael@0: gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler); michael@0: Services.prefs.removeObserver("devtools.", gDevToolsBrowser); michael@0: Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application"); michael@0: }, michael@0: } michael@0: michael@0: this.gDevToolsBrowser = gDevToolsBrowser; michael@0: michael@0: gDevTools.on("tool-registered", function(ev, toolId) { michael@0: let toolDefinition = gDevTools._tools.get(toolId); michael@0: gDevToolsBrowser._addToolToWindows(toolDefinition); michael@0: }); michael@0: michael@0: gDevTools.on("tool-unregistered", function(ev, toolId) { michael@0: if (typeof toolId != "string") { michael@0: toolId = toolId.id; michael@0: } michael@0: gDevToolsBrowser._removeToolFromWindows(toolId); michael@0: }); michael@0: michael@0: gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox); michael@0: gDevTools.on("toolbox-ready", gDevToolsBrowser._connectToProfiler); michael@0: gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox); michael@0: michael@0: Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false); michael@0: michael@0: // Load the browser devtools main module as the loader's main module. michael@0: devtools.main("main");