Bug 1690340 - Part 5: Remove the menu separators from the developer tools menu. r...
[gecko.git] / devtools / startup / DevToolsShim.jsm
blob4d14002b3e60f79e7c641aaf2007c91cdac52f40
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 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9 const { XPCOMUtils } = ChromeUtils.import(
10   "resource://gre/modules/XPCOMUtils.jsm"
12 XPCOMUtils.defineLazyGetter(this, "DevtoolsStartup", () => {
13   return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
14     Ci.nsICommandLineHandler
15   ).wrappedJSObject;
16 });
18 // We don't want to spend time initializing the full loader here so we create
19 // our own lazy require.
20 XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
21   const { require } = ChromeUtils.import(
22     "resource://devtools/shared/Loader.jsm"
23   );
24   // eslint-disable-next-line no-shadow
25   const Telemetry = require("devtools/client/shared/telemetry");
27   return Telemetry;
28 });
30 const DEVTOOLS_ENABLED_PREF = "devtools.enabled";
31 const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
33 const EXPORTED_SYMBOLS = ["DevToolsShim"];
35 function removeItem(array, callback) {
36   const index = array.findIndex(callback);
37   if (index >= 0) {
38     array.splice(index, 1);
39   }
42 /**
43  * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
44  * that work whether Devtools are enabled or not.
45  *
46  * It can be used to start listening to devtools events before DevTools are ready. As soon
47  * as DevTools are enabled, the DevToolsShim will forward all the requests received until
48  * then to the real DevTools instance.
49  */
50 const DevToolsShim = {
51   _gDevTools: null,
52   listeners: [],
54   get telemetry() {
55     if (!this._telemetry) {
56       this._telemetry = new Telemetry();
57       this._telemetry.setEventRecordingEnabled(true);
58     }
59     return this._telemetry;
60   },
62   /**
63    * Returns true if DevTools are enabled for the current profile. If devtools are not
64    * enabled, initializing DevTools will open the onboarding page. Some entry points
65    * should no-op in this case.
66    */
67   isEnabled: function() {
68     const enabled = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF);
69     return enabled && !this.isDisabledByPolicy();
70   },
72   /**
73    * Returns true if the devtools are completely disabled and can not be enabled. All
74    * entry points should return without throwing, initDevTools should never be called.
75    */
76   isDisabledByPolicy: function() {
77     return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
78   },
80   /**
81    * Check if DevTools have already been initialized.
82    *
83    * @return {Boolean} true if DevTools are initialized.
84    */
85   isInitialized: function() {
86     return !!this._gDevTools;
87   },
89   /**
90    * Returns the array of the existing toolboxes. This method is part of the compatibility
91    * layer for webextensions.
92    *
93    * @return {Array<Toolbox>}
94    *   An array of toolboxes.
95    */
96   getToolboxes: function() {
97     if (this.isInitialized()) {
98       return this._gDevTools.getToolboxes();
99     }
101     return [];
102   },
104   /**
105    * Register an instance of gDevTools. Should be called by DevTools during startup.
106    *
107    * @param {DevTools} a devtools instance (from client/framework/devtools)
108    */
109   register: function(gDevTools) {
110     this._gDevTools = gDevTools;
111     this._onDevToolsRegistered();
112     this._gDevTools.emit("devtools-registered");
113   },
115   /**
116    * Unregister the current instance of gDevTools. Should be called by DevTools during
117    * shutdown.
118    */
119   unregister: function() {
120     if (this.isInitialized()) {
121       this._gDevTools.emit("devtools-unregistered");
122       this._gDevTools = null;
123     }
124   },
126   /**
127    * The following methods can be called before DevTools are initialized:
128    * - on
129    * - off
130    *
131    * If DevTools are not initialized when calling the method, DevToolsShim will call the
132    * appropriate method as soon as a gDevTools instance is registered.
133    */
135   /**
136    * This method is used by browser/components/extensions/ext-devtools.js for the events:
137    * - toolbox-created
138    * - toolbox-destroyed
139    */
140   on: function(event, listener) {
141     if (this.isInitialized()) {
142       this._gDevTools.on(event, listener);
143     } else {
144       this.listeners.push([event, listener]);
145     }
146   },
148   /**
149    * This method is currently only used by devtools code, but is kept here for consistency
150    * with on().
151    */
152   off: function(event, listener) {
153     if (this.isInitialized()) {
154       this._gDevTools.off(event, listener);
155     } else {
156       removeItem(this.listeners, ([e, l]) => e === event && l === listener);
157     }
158   },
160   /**
161    * Called from SessionStore.jsm in mozilla-central when saving the current state.
162    *
163    * @param {Object} state
164    *                 A SessionStore state object that gets modified by reference
165    */
166   saveDevToolsSession: function(state) {
167     if (!this.isInitialized()) {
168       return;
169     }
171     this._gDevTools.saveDevToolsSession(state);
172   },
174   /**
175    * Called from SessionStore.jsm in mozilla-central when restoring a previous session.
176    * Will always be called, even if the session does not contain DevTools related items.
177    */
178   restoreDevToolsSession: function(session) {
179     if (!this.isEnabled()) {
180       return;
181     }
183     const { browserConsole, browserToolbox } = session;
184     const hasDevToolsData = browserConsole || browserToolbox;
185     if (!hasDevToolsData) {
186       // Do not initialize DevTools unless there is DevTools specific data in the session.
187       return;
188     }
190     this.initDevTools("SessionRestore");
191     this._gDevTools.restoreDevToolsSession(session);
192   },
194   /**
195    * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
196    * context menu item.
197    *
198    * @param {XULTab} tab
199    *        The browser tab on which inspect accessibility was used.
200    * @param {ElementIdentifier} domReference
201    *        Identifier generated by ContentDOMReference. It is a unique pair of
202    *        BrowsingContext ID and a numeric ID.
203    * @return {Promise} a promise that resolves when the accessible node is selected in the
204    *         accessibility inspector or that resolves immediately if DevTools are not
205    *         enabled.
206    */
207   inspectA11Y: function(tab, domReference) {
208     if (!this.isEnabled()) {
209       if (!this.isDisabledByPolicy()) {
210         DevtoolsStartup.openInstallPage("ContextMenu");
211       }
212       return Promise.resolve();
213     }
215     // Record the timing at which this event started in order to compute later in
216     // gDevTools.showToolbox, the complete time it takes to open the toolbox.
217     // i.e. especially take `DevtoolsStartup.initDevTools` into account.
218     const startTime = Cu.now();
220     this.initDevTools("ContextMenu");
222     return this._gDevTools.inspectA11Y(tab, domReference, startTime);
223   },
225   /**
226    * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
227    * context menu item.
228    *
229    * @param {XULTab} tab
230    *        The browser tab on which inspect node was used.
231    * @param {ElementIdentifier} domReference
232    *        Identifier generated by ContentDOMReference. It is a unique pair of
233    *        BrowsingContext ID and a numeric ID.
234    * @return {Promise} a promise that resolves when the node is selected in the inspector
235    *         markup view or that resolves immediately if DevTools are not enabled.
236    */
237   inspectNode: function(tab, domReference) {
238     if (!this.isEnabled()) {
239       if (!this.isDisabledByPolicy()) {
240         DevtoolsStartup.openInstallPage("ContextMenu");
241       }
242       return Promise.resolve();
243     }
245     // Record the timing at which this event started in order to compute later in
246     // gDevTools.showToolbox, the complete time it takes to open the toolbox.
247     // i.e. especially take `DevtoolsStartup.initDevTools` into account.
248     const startTime = Cu.now();
250     this.initDevTools("ContextMenu");
252     return this._gDevTools.inspectNode(tab, domReference, startTime);
253   },
255   _onDevToolsRegistered: function() {
256     // Register all pending event listeners on the real gDevTools object.
257     for (const [event, listener] of this.listeners) {
258       this._gDevTools.on(event, listener);
259     }
261     this.listeners = [];
262   },
264   /**
265    * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are
266    * not enabled.. If the entry point is supposed to trigger the onboarding, call it
267    * explicitly via DevtoolsStartup.openInstallPage().
268    *
269    * @param {String} reason
270    *        optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
271    *        in toolkit/components/telemetry/Histograms.json
272    */
273   initDevTools: function(reason) {
274     if (!this.isEnabled()) {
275       throw new Error("DevTools are not enabled and can not be initialized.");
276     }
278     if (reason) {
279       const window = Services.wm.getMostRecentWindow("navigator:browser");
281       this.telemetry.addEventProperty(
282         window,
283         "open",
284         "tools",
285         null,
286         "shortcut",
287         ""
288       );
289       this.telemetry.addEventProperty(
290         window,
291         "open",
292         "tools",
293         null,
294         "entrypoint",
295         reason
296       );
297     }
299     if (!this.isInitialized()) {
300       DevtoolsStartup.initDevTools(reason);
301     }
302   },
306  * Compatibility layer for webextensions.
308  * Those methods are called only after a DevTools webextension was loaded in DevTools,
309  * therefore DevTools should always be available when they are called.
310  */
311 const webExtensionsMethods = [
312   "createDescriptorForTab",
313   "createWebExtensionInspectedWindowFront",
314   "getTargetForTab",
315   "getTheme",
316   "openBrowserConsole",
319 for (const method of webExtensionsMethods) {
320   DevToolsShim[method] = function() {
321     if (!this.isEnabled()) {
322       throw new Error(
323         "Could not call a DevToolsShim webextension method ('" +
324           method +
325           "'): DevTools are not initialized."
326       );
327     }
329     this.initDevTools();
330     return this._gDevTools[method].apply(this._gDevTools, arguments);
331   };