Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ];
9 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 Cu.import("resource://gre/modules/Services.jsm");
13 Cu.import("resource://gre/modules/devtools/event-emitter.js");
14 Cu.import("resource://gre/modules/devtools/Loader.jsm");
15 XPCOMUtils.defineLazyModuleGetter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
17 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
18 const MAX_ORDINAL = 99;
21 /**
22 * DevTools is a class that represents a set of developer tools, it holds a
23 * set of tools and keeps track of open toolboxes in the browser.
24 */
25 this.DevTools = function DevTools() {
26 this._tools = new Map(); // Map<toolId, tool>
27 this._toolboxes = new Map(); // Map<target, toolbox>
29 // destroy() is an observer's handler so we need to preserve context.
30 this.destroy = this.destroy.bind(this);
31 this._teardown = this._teardown.bind(this);
33 this._testing = false;
35 EventEmitter.decorate(this);
37 Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
38 Services.obs.addObserver(this.destroy, "quit-application", false);
39 }
41 DevTools.prototype = {
42 /**
43 * When the testing flag is set we take appropriate action to prevent race
44 * conditions in our testing environment. This means setting
45 * dom.send_after_paint_to_content to false to prevent infinite MozAfterPaint
46 * loops and not autohiding the highlighter.
47 */
48 get testing() {
49 return this._testing;
50 },
52 set testing(state) {
53 this._testing = state;
55 if (state) {
56 // dom.send_after_paint_to_content is set to true (non-default) in
57 // testing/profiles/prefs_general.js so lets set it to the same as it is
58 // in a default browser profile for the duration of the test.
59 Services.prefs.setBoolPref("dom.send_after_paint_to_content", false);
60 } else {
61 Services.prefs.setBoolPref("dom.send_after_paint_to_content", true);
62 }
63 },
65 /**
66 * Register a new developer tool.
67 *
68 * A definition is a light object that holds different information about a
69 * developer tool. This object is not supposed to have any operational code.
70 * See it as a "manifest".
71 * The only actual code lives in the build() function, which will be used to
72 * start an instance of this tool.
73 *
74 * Each toolDefinition has the following properties:
75 * - id: Unique identifier for this tool (string|required)
76 * - visibilityswitch: Property name to allow us to hide this tool from the
77 * DevTools Toolbox.
78 * A falsy value indicates that it cannot be hidden.
79 * - icon: URL pointing to a graphic which will be used as the src for an
80 * 16x16 img tag (string|required)
81 * - invertIconForLightTheme: The icon can automatically have an inversion
82 * filter applied (default is false). All builtin tools are true, but
83 * addons may omit this to prevent unwanted changes to the `icon`
84 * image. See browser/themes/shared/devtools/filters.svg#invert for
85 * the filter being applied to the images (boolean|optional)
86 * - url: URL pointing to a XUL/XHTML document containing the user interface
87 * (string|required)
88 * - label: Localized name for the tool to be displayed to the user
89 * (string|required)
90 * - build: Function that takes an iframe, which has been populated with the
91 * markup from |url|, and also the toolbox containing the panel.
92 * And returns an instance of ToolPanel (function|required)
93 */
94 registerTool: function DT_registerTool(toolDefinition) {
95 let toolId = toolDefinition.id;
97 if (!toolId || FORBIDDEN_IDS.has(toolId)) {
98 throw new Error("Invalid definition.id");
99 }
101 // Make sure that additional tools will always be able to be hidden.
102 // When being called from main.js, defaultTools has not yet been exported.
103 // But, we can assume that in this case, it is a default tool.
104 if (devtools.defaultTools && devtools.defaultTools.indexOf(toolDefinition) == -1) {
105 toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
106 }
108 this._tools.set(toolId, toolDefinition);
110 this.emit("tool-registered", toolId);
111 },
113 /**
114 * Removes all tools that match the given |toolId|
115 * Needed so that add-ons can remove themselves when they are deactivated
116 *
117 * @param {string|object} tool
118 * Definition or the id of the tool to unregister. Passing the
119 * tool id should be avoided as it is a temporary measure.
120 * @param {boolean} isQuitApplication
121 * true to indicate that the call is due to app quit, so we should not
122 * cause a cascade of costly events
123 */
124 unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
125 let toolId = null;
126 if (typeof tool == "string") {
127 toolId = tool;
128 tool = this._tools.get(tool);
129 }
130 else {
131 toolId = tool.id;
132 }
133 this._tools.delete(toolId);
135 if (!isQuitApplication) {
136 this.emit("tool-unregistered", tool);
137 }
138 },
140 /**
141 * Sorting function used for sorting tools based on their ordinals.
142 */
143 ordinalSort: function DT_ordinalSort(d1, d2) {
144 let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
145 let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
146 return o1 - o2;
147 },
149 getDefaultTools: function DT_getDefaultTools() {
150 return devtools.defaultTools.sort(this.ordinalSort);
151 },
153 getAdditionalTools: function DT_getAdditionalTools() {
154 let tools = [];
155 for (let [key, value] of this._tools) {
156 if (devtools.defaultTools.indexOf(value) == -1) {
157 tools.push(value);
158 }
159 }
160 return tools.sort(this.ordinalSort);
161 },
163 /**
164 * Get a tool definition if it exists and is enabled.
165 *
166 * @param {string} toolId
167 * The id of the tool to show
168 *
169 * @return {ToolDefinition|null} tool
170 * The ToolDefinition for the id or null.
171 */
172 getToolDefinition: function DT_getToolDefinition(toolId) {
173 let tool = this._tools.get(toolId);
174 if (!tool) {
175 return null;
176 } else if (!tool.visibilityswitch) {
177 return tool;
178 }
180 let enabled;
181 try {
182 enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
183 } catch (e) {
184 enabled = true;
185 }
187 return enabled ? tool : null;
188 },
190 /**
191 * Allow ToolBoxes to get at the list of tools that they should populate
192 * themselves with.
193 *
194 * @return {Map} tools
195 * A map of the the tool definitions registered in this instance
196 */
197 getToolDefinitionMap: function DT_getToolDefinitionMap() {
198 let tools = new Map();
200 for (let [id, definition] of this._tools) {
201 if (this.getToolDefinition(id)) {
202 tools.set(id, definition);
203 }
204 }
206 return tools;
207 },
209 /**
210 * Tools have an inherent ordering that can't be represented in a Map so
211 * getToolDefinitionArray provides an alternative representation of the
212 * definitions sorted by ordinal value.
213 *
214 * @return {Array} tools
215 * A sorted array of the tool definitions registered in this instance
216 */
217 getToolDefinitionArray: function DT_getToolDefinitionArray() {
218 let definitions = [];
220 for (let [id, definition] of this._tools) {
221 if (this.getToolDefinition(id)) {
222 definitions.push(definition);
223 }
224 }
226 return definitions.sort(this.ordinalSort);
227 },
229 /**
230 * Show a Toolbox for a target (either by creating a new one, or if a toolbox
231 * already exists for the target, by bring to the front the existing one)
232 * If |toolId| is specified then the displayed toolbox will have the
233 * specified tool selected.
234 * If |hostType| is specified then the toolbox will be displayed using the
235 * specified HostType.
236 *
237 * @param {Target} target
238 * The target the toolbox will debug
239 * @param {string} toolId
240 * The id of the tool to show
241 * @param {Toolbox.HostType} hostType
242 * The type of host (bottom, window, side)
243 * @param {object} hostOptions
244 * Options for host specifically
245 *
246 * @return {Toolbox} toolbox
247 * The toolbox that was opened
248 */
249 showToolbox: function(target, toolId, hostType, hostOptions) {
250 let deferred = promise.defer();
252 let toolbox = this._toolboxes.get(target);
253 if (toolbox) {
255 let hostPromise = (hostType != null && toolbox.hostType != hostType) ?
256 toolbox.switchHost(hostType) :
257 promise.resolve(null);
259 if (toolId != null && toolbox.currentToolId != toolId) {
260 hostPromise = hostPromise.then(function() {
261 return toolbox.selectTool(toolId);
262 });
263 }
265 return hostPromise.then(function() {
266 toolbox.raise();
267 return toolbox;
268 });
269 }
270 else {
271 // No toolbox for target, create one
272 toolbox = new devtools.Toolbox(target, toolId, hostType, hostOptions);
274 this._toolboxes.set(target, toolbox);
276 toolbox.once("destroyed", function() {
277 this._toolboxes.delete(target);
278 this.emit("toolbox-destroyed", target);
279 }.bind(this));
281 // If we were asked for a specific tool then we need to wait for the
282 // tool to be ready, otherwise we can just wait for toolbox open
283 if (toolId != null) {
284 toolbox.once(toolId + "-ready", function(event, panel) {
285 this.emit("toolbox-ready", toolbox);
286 deferred.resolve(toolbox);
287 }.bind(this));
288 toolbox.open();
289 }
290 else {
291 toolbox.open().then(function() {
292 deferred.resolve(toolbox);
293 this.emit("toolbox-ready", toolbox);
294 }.bind(this));
295 }
296 }
298 return deferred.promise;
299 },
301 /**
302 * Return the toolbox for a given target.
303 *
304 * @param {object} target
305 * Target value e.g. the target that owns this toolbox
306 *
307 * @return {Toolbox} toolbox
308 * The toobox that is debugging the given target
309 */
310 getToolbox: function DT_getToolbox(target) {
311 return this._toolboxes.get(target);
312 },
314 /**
315 * Close the toolbox for a given target
316 *
317 * @return promise
318 * This promise will resolve to false if no toolbox was found
319 * associated to the target. true, if the toolbox was successfuly
320 * closed.
321 */
322 closeToolbox: function DT_closeToolbox(target) {
323 let toolbox = this._toolboxes.get(target);
324 if (toolbox == null) {
325 return promise.resolve(false);
326 }
327 return toolbox.destroy().then(() => true);
328 },
330 /**
331 * Called to tear down a tools provider.
332 */
333 _teardown: function DT_teardown() {
334 for (let [target, toolbox] of this._toolboxes) {
335 toolbox.destroy();
336 }
337 },
339 /**
340 * All browser windows have been closed, tidy up remaining objects.
341 */
342 destroy: function() {
343 Services.obs.removeObserver(this.destroy, "quit-application");
344 Services.obs.removeObserver(this._teardown, "devtools-unloaded");
346 for (let [key, tool] of this.getToolDefinitionMap()) {
347 this.unregisterTool(key, true);
348 }
350 // Cleaning down the toolboxes: i.e.
351 // for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
352 // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
353 },
355 /**
356 * Iterator that yields each of the toolboxes.
357 */
358 '@@iterator': function*() {
359 for (let toolbox of this._toolboxes) {
360 yield toolbox;
361 }
362 }
363 };
365 /**
366 * gDevTools is a singleton that controls the Firefox Developer Tools.
367 *
368 * It is an instance of a DevTools class that holds a set of tools. It has the
369 * same lifetime as the browser.
370 */
371 let gDevTools = new DevTools();
372 this.gDevTools = gDevTools;
374 /**
375 * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
376 * Firefox instance.
377 */
378 let gDevToolsBrowser = {
379 /**
380 * A record of the windows whose menus we altered, so we can undo the changes
381 * as the window is closed
382 */
383 _trackedBrowserWindows: new Set(),
385 /**
386 * This function is for the benefit of Tools:DevToolbox in
387 * browser/base/content/browser-sets.inc and should not be used outside
388 * of there
389 */
390 toggleToolboxCommand: function(gBrowser) {
391 let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
392 let toolbox = gDevTools.getToolbox(target);
394 toolbox ? toolbox.destroy() : gDevTools.showToolbox(target);
395 },
397 toggleBrowserToolboxCommand: function(gBrowser) {
398 let target = devtools.TargetFactory.forWindow(gBrowser.ownerDocument.defaultView);
399 let toolbox = gDevTools.getToolbox(target);
401 toolbox ? toolbox.destroy()
402 : gDevTools.showToolbox(target, "inspector", Toolbox.HostType.WINDOW);
403 },
405 /**
406 * This function ensures the right commands are enabled in a window,
407 * depending on their relevant prefs. It gets run when a window is registered,
408 * or when any of the devtools prefs change.
409 */
410 updateCommandAvailability: function(win) {
411 let doc = win.document;
413 function toggleCmd(id, isEnabled) {
414 let cmd = doc.getElementById(id);
415 if (isEnabled) {
416 cmd.removeAttribute("disabled");
417 cmd.removeAttribute("hidden");
418 } else {
419 cmd.setAttribute("disabled", "true");
420 cmd.setAttribute("hidden", "true");
421 }
422 };
424 // Enable developer toolbar?
425 let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
426 toggleCmd("Tools:DevToolbar", devToolbarEnabled);
427 let focusEl = doc.getElementById("Tools:DevToolbarFocus");
428 if (devToolbarEnabled) {
429 focusEl.removeAttribute("disabled");
430 } else {
431 focusEl.setAttribute("disabled", "true");
432 }
433 if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
434 win.DeveloperToolbar.show(false);
435 }
437 // Enable App Manager?
438 let appMgrEnabled = Services.prefs.getBoolPref("devtools.appmanager.enabled");
439 toggleCmd("Tools:DevAppMgr", appMgrEnabled);
441 // Enable Browser Toolbox?
442 let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
443 let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
444 let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled &&
445 Services.prefs.getBoolPref("devtools.debugger.chrome-enabled");
446 toggleCmd("Tools:BrowserToolbox", remoteEnabled);
448 // Enable Error Console?
449 let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled");
450 toggleCmd("Tools:ErrorConsole", consoleEnabled);
452 // Enable DevTools connection screen, if the preference allows this.
453 toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled);
454 },
456 observe: function(subject, topic, prefName) {
457 if (prefName.endsWith("enabled")) {
458 for (let win of this._trackedBrowserWindows) {
459 this.updateCommandAvailability(win);
460 }
461 }
462 },
464 _prefObserverRegistered: false,
466 ensurePrefObserver: function() {
467 if (!this._prefObserverRegistered) {
468 this._prefObserverRegistered = true;
469 Services.prefs.addObserver("devtools.", this, false);
470 }
471 },
474 /**
475 * This function is for the benefit of Tools:{toolId} commands,
476 * triggered from the WebDeveloper menu and keyboard shortcuts.
477 *
478 * selectToolCommand's behavior:
479 * - if the toolbox is closed,
480 * we open the toolbox and select the tool
481 * - if the toolbox is open, and the targetted tool is not selected,
482 * we select it
483 * - if the toolbox is open, and the targetted tool is selected,
484 * and the host is NOT a window, we close the toolbox
485 * - if the toolbox is open, and the targetted tool is selected,
486 * and the host is a window, we raise the toolbox window
487 */
488 selectToolCommand: function(gBrowser, toolId) {
489 let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
490 let toolbox = gDevTools.getToolbox(target);
491 let toolDefinition = gDevTools.getToolDefinition(toolId);
493 if (toolbox &&
494 (toolbox.currentToolId == toolId ||
495 (toolId == "webconsole" && toolbox.splitConsole)))
496 {
497 toolbox.fireCustomKey(toolId);
499 if (toolDefinition.preventClosingOnKey || toolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
500 toolbox.raise();
501 } else {
502 toolbox.destroy();
503 }
504 } else {
505 gDevTools.showToolbox(target, toolId).then(() => {
506 let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
507 let toolbox = gDevTools.getToolbox(target);
509 toolbox.fireCustomKey(toolId);
510 });
511 }
512 },
514 /**
515 * Open a tab to allow connects to a remote browser
516 */
517 openConnectScreen: function(gBrowser) {
518 gBrowser.selectedTab = gBrowser.addTab("chrome://browser/content/devtools/connect.xhtml");
519 },
521 /**
522 * Open the App Manager
523 */
524 openAppManager: function(gBrowser) {
525 gBrowser.selectedTab = gBrowser.addTab("about:app-manager");
526 },
528 /**
529 * Add this DevTools's presence to a browser window's document
530 *
531 * @param {XULDocument} doc
532 * The document to which menuitems and handlers are to be added
533 */
534 registerBrowserWindow: function DT_registerBrowserWindow(win) {
535 this.updateCommandAvailability(win);
536 this.ensurePrefObserver();
537 gDevToolsBrowser._trackedBrowserWindows.add(win);
538 gDevToolsBrowser._addAllToolsToMenu(win.document);
540 if (this._isFirebugInstalled()) {
541 let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
542 broadcaster.removeAttribute("key");
543 }
545 let tabContainer = win.document.getElementById("tabbrowser-tabs")
546 tabContainer.addEventListener("TabSelect",
547 gDevToolsBrowser._updateMenuCheckbox, false);
548 },
550 /**
551 * Add a <key> to <keyset id="devtoolsKeyset">.
552 * Appending a <key> element is not always enough. The <keyset> needs
553 * to be detached and reattached to make sure the <key> is taken into
554 * account (see bug 832984).
555 *
556 * @param {XULDocument} doc
557 * The document to which keys are to be added
558 * @param {XULElement} or {DocumentFragment} keys
559 * Keys to add
560 */
561 attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) {
562 let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
564 if (!devtoolsKeyset) {
565 devtoolsKeyset = doc.createElement("keyset");
566 devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
567 }
568 devtoolsKeyset.appendChild(keys);
569 let mainKeyset = doc.getElementById("mainKeyset");
570 mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
571 },
574 /**
575 * Detect the presence of a Firebug.
576 *
577 * @return promise
578 */
579 _isFirebugInstalled: function DT_isFirebugInstalled() {
580 let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons");
581 return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1;
582 },
584 /**
585 * Add the menuitem for a tool to all open browser windows.
586 *
587 * @param {object} toolDefinition
588 * properties of the tool to add
589 */
590 _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
591 // No menu item or global shortcut is required for options panel.
592 if (!toolDefinition.inMenu) {
593 return;
594 }
596 // Skip if the tool is disabled.
597 try {
598 if (toolDefinition.visibilityswitch &&
599 !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
600 return;
601 }
602 } catch(e) {}
604 // We need to insert the new tool in the right place, which means knowing
605 // the tool that comes before the tool that we're trying to add
606 let allDefs = gDevTools.getToolDefinitionArray();
607 let prevDef;
608 for (let def of allDefs) {
609 if (!def.inMenu) {
610 continue;
611 }
612 if (def === toolDefinition) {
613 break;
614 }
615 prevDef = def;
616 }
618 for (let win of gDevToolsBrowser._trackedBrowserWindows) {
619 let doc = win.document;
620 let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
622 doc.getElementById("mainCommandSet").appendChild(elements.cmd);
624 if (elements.key) {
625 this.attachKeybindingsToBrowser(doc, elements.key);
626 }
628 doc.getElementById("mainBroadcasterSet").appendChild(elements.bc);
630 let amp = doc.getElementById("appmenu_webDeveloper_popup");
631 if (amp) {
632 let ref;
634 if (prevDef != null) {
635 let menuitem = doc.getElementById("appmenuitem_" + prevDef.id);
636 ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
637 } else {
638 ref = doc.getElementById("appmenu_devtools_separator");
639 }
641 if (ref) {
642 amp.insertBefore(elements.appmenuitem, ref);
643 }
644 }
646 let mp = doc.getElementById("menuWebDeveloperPopup");
647 if (mp) {
648 let ref;
650 if (prevDef != null) {
651 let menuitem = doc.getElementById("menuitem_" + prevDef.id);
652 ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
653 } else {
654 ref = doc.getElementById("menu_devtools_separator");
655 }
657 if (ref) {
658 mp.insertBefore(elements.menuitem, ref);
659 }
660 }
661 }
662 },
664 /**
665 * Add all tools to the developer tools menu of a window.
666 *
667 * @param {XULDocument} doc
668 * The document to which the tool items are to be added.
669 */
670 _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) {
671 let fragCommands = doc.createDocumentFragment();
672 let fragKeys = doc.createDocumentFragment();
673 let fragBroadcasters = doc.createDocumentFragment();
674 let fragAppMenuItems = doc.createDocumentFragment();
675 let fragMenuItems = doc.createDocumentFragment();
677 for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
678 if (!toolDefinition.inMenu) {
679 continue;
680 }
682 let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
684 if (!elements) {
685 return;
686 }
688 fragCommands.appendChild(elements.cmd);
689 if (elements.key) {
690 fragKeys.appendChild(elements.key);
691 }
692 fragBroadcasters.appendChild(elements.bc);
693 fragAppMenuItems.appendChild(elements.appmenuitem);
694 fragMenuItems.appendChild(elements.menuitem);
695 }
697 let mcs = doc.getElementById("mainCommandSet");
698 mcs.appendChild(fragCommands);
700 this.attachKeybindingsToBrowser(doc, fragKeys);
702 let mbs = doc.getElementById("mainBroadcasterSet");
703 mbs.appendChild(fragBroadcasters);
705 let amp = doc.getElementById("appmenu_webDeveloper_popup");
706 if (amp) {
707 let amps = doc.getElementById("appmenu_devtools_separator");
708 amp.insertBefore(fragAppMenuItems, amps);
709 }
711 let mp = doc.getElementById("menuWebDeveloperPopup");
712 let mps = doc.getElementById("menu_devtools_separator");
713 mp.insertBefore(fragMenuItems, mps);
714 },
716 /**
717 * Add a menu entry for a tool definition
718 *
719 * @param {string} toolDefinition
720 * Tool definition of the tool to add a menu entry.
721 * @param {XULDocument} doc
722 * The document to which the tool menu item is to be added.
723 */
724 _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) {
725 let id = toolDefinition.id;
727 // Prevent multiple entries for the same tool.
728 if (doc.getElementById("Tools:" + id)) {
729 return;
730 }
732 let cmd = doc.createElement("command");
733 cmd.id = "Tools:" + id;
734 cmd.setAttribute("oncommand",
735 'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");');
737 let key = null;
738 if (toolDefinition.key) {
739 key = doc.createElement("key");
740 key.id = "key_" + id;
742 if (toolDefinition.key.startsWith("VK_")) {
743 key.setAttribute("keycode", toolDefinition.key);
744 } else {
745 key.setAttribute("key", toolDefinition.key);
746 }
748 key.setAttribute("command", cmd.id);
749 key.setAttribute("modifiers", toolDefinition.modifiers);
750 }
752 let bc = doc.createElement("broadcaster");
753 bc.id = "devtoolsMenuBroadcaster_" + id;
754 bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label);
755 bc.setAttribute("command", cmd.id);
757 if (key) {
758 bc.setAttribute("key", "key_" + id);
759 }
761 let appmenuitem = doc.createElement("menuitem");
762 appmenuitem.id = "appmenuitem_" + id;
763 appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
765 let menuitem = doc.createElement("menuitem");
766 menuitem.id = "menuitem_" + id;
767 menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
769 if (toolDefinition.accesskey) {
770 menuitem.setAttribute("accesskey", toolDefinition.accesskey);
771 }
773 return {
774 cmd: cmd,
775 key: key,
776 bc: bc,
777 appmenuitem: appmenuitem,
778 menuitem: menuitem
779 };
780 },
782 /**
783 * Update the "Toggle Tools" checkbox in the developer tools menu. This is
784 * called when a toolbox is created or destroyed.
785 */
786 _updateMenuCheckbox: function DT_updateMenuCheckbox() {
787 for (let win of gDevToolsBrowser._trackedBrowserWindows) {
789 let hasToolbox = false;
790 if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) {
791 let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab);
792 if (gDevTools._toolboxes.has(target)) {
793 hasToolbox = true;
794 }
795 }
797 let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
798 if (hasToolbox) {
799 broadcaster.setAttribute("checked", "true");
800 } else {
801 broadcaster.removeAttribute("checked");
802 }
803 }
804 },
806 /**
807 * Connects to the SPS profiler when the developer tools are open.
808 */
809 _connectToProfiler: function DT_connectToProfiler() {
810 let ProfilerController = devtools.require("devtools/profiler/controller");
812 for (let win of gDevToolsBrowser._trackedBrowserWindows) {
813 if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) {
814 let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab);
815 if (gDevTools._toolboxes.has(target)) {
816 target.makeRemote().then(() => {
817 let profiler = new ProfilerController(target);
818 profiler.connect();
819 }).then(null, Cu.reportError);
821 return;
822 }
823 }
824 }
825 },
827 /**
828 * Remove the menuitem for a tool to all open browser windows.
829 *
830 * @param {string} toolId
831 * id of the tool to remove
832 */
833 _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
834 for (let win of gDevToolsBrowser._trackedBrowserWindows) {
835 gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
836 }
837 },
839 /**
840 * Remove a tool's menuitem from a window
841 *
842 * @param {string} toolId
843 * Id of the tool to add a menu entry for
844 * @param {XULDocument} doc
845 * The document to which the tool menu item is to be removed from
846 */
847 _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) {
848 let command = doc.getElementById("Tools:" + toolId);
849 if (command) {
850 command.parentNode.removeChild(command);
851 }
853 let key = doc.getElementById("key_" + toolId);
854 if (key) {
855 key.parentNode.removeChild(key);
856 }
858 let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId);
859 if (bc) {
860 bc.parentNode.removeChild(bc);
861 }
863 let appmenuitem = doc.getElementById("appmenuitem_" + toolId);
864 if (appmenuitem) {
865 appmenuitem.parentNode.removeChild(appmenuitem);
866 }
868 let menuitem = doc.getElementById("menuitem_" + toolId);
869 if (menuitem) {
870 menuitem.parentNode.removeChild(menuitem);
871 }
872 },
874 /**
875 * Called on browser unload to remove menu entries, toolboxes and event
876 * listeners from the closed browser window.
877 *
878 * @param {XULWindow} win
879 * The window containing the menu entry
880 */
881 forgetBrowserWindow: function DT_forgetBrowserWindow(win) {
882 gDevToolsBrowser._trackedBrowserWindows.delete(win);
884 // Destroy toolboxes for closed window
885 for (let [target, toolbox] of gDevTools._toolboxes) {
886 if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) {
887 toolbox.destroy();
888 }
889 }
891 let tabContainer = win.document.getElementById("tabbrowser-tabs")
892 tabContainer.removeEventListener("TabSelect",
893 gDevToolsBrowser._updateMenuCheckbox, false);
894 },
896 /**
897 * All browser windows have been closed, tidy up remaining objects.
898 */
899 destroy: function() {
900 gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler);
901 Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
902 Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
903 },
904 }
906 this.gDevToolsBrowser = gDevToolsBrowser;
908 gDevTools.on("tool-registered", function(ev, toolId) {
909 let toolDefinition = gDevTools._tools.get(toolId);
910 gDevToolsBrowser._addToolToWindows(toolDefinition);
911 });
913 gDevTools.on("tool-unregistered", function(ev, toolId) {
914 if (typeof toolId != "string") {
915 toolId = toolId.id;
916 }
917 gDevToolsBrowser._removeToolFromWindows(toolId);
918 });
920 gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
921 gDevTools.on("toolbox-ready", gDevToolsBrowser._connectToProfiler);
922 gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
924 Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
926 // Load the browser devtools main module as the loader's main module.
927 devtools.main("main");