ext-devtools-panels.js (9559B)
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set sts=2 sw=2 et tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 "use strict"; 8 9 var { XPCOMUtils } = ChromeUtils.importESModule( 10 "resource://gre/modules/XPCOMUtils.sys.mjs" 11 ); 12 13 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]); 14 15 ChromeUtils.defineESModuleGetters(this, { 16 ExtensionChildDevToolsUtils: 17 "resource://gre/modules/ExtensionChildDevToolsUtils.sys.mjs", 18 }); 19 20 var { promiseDocumentLoaded } = ExtensionUtils; 21 22 /** 23 * Represents an addon devtools panel in the child process. 24 * 25 * @param {DevtoolsExtensionContext} 26 * A devtools extension context running in a child process. 27 * @param {object} panelOptions 28 * @param {string} panelOptions.id 29 * The id of the addon devtools panel registered in the main process. 30 */ 31 class ChildDevToolsPanel extends ExtensionCommon.EventEmitter { 32 constructor(context, { id }) { 33 super(); 34 35 this.context = context; 36 this.context.callOnClose(this); 37 38 this.id = id; 39 this._panelContext = null; 40 41 this.conduit = context.openConduit(this, { 42 recv: ["PanelHidden", "PanelShown"], 43 }); 44 } 45 46 get panelContext() { 47 if (this._panelContext) { 48 return this._panelContext; 49 } 50 51 for (let view of this.context.extension.devtoolsViews) { 52 if ( 53 view.viewType === "devtools_panel" && 54 view.devtoolsToolboxInfo.toolboxPanelId === this.id 55 ) { 56 this._panelContext = view; 57 58 // Reset the cached _panelContext property when the view is closed. 59 view.callOnClose({ 60 close: () => { 61 this._panelContext = null; 62 }, 63 }); 64 return view; 65 } 66 } 67 68 return null; 69 } 70 71 recvPanelShown() { 72 // Ignore received call before the panel context exist. 73 if (!this.panelContext || !this.panelContext.contentWindow) { 74 return; 75 } 76 const { document } = this.panelContext.contentWindow; 77 78 // Ensure that the onShown event is fired when the panel document has 79 // been fully loaded. 80 promiseDocumentLoaded(document).then(() => { 81 this.emit("shown", this.panelContext.contentWindow); 82 }); 83 } 84 85 recvPanelHidden() { 86 this.emit("hidden"); 87 } 88 89 api() { 90 return { 91 onShown: new EventManager({ 92 context: this.context, 93 name: "devtoolsPanel.onShown", 94 register: fire => { 95 const listener = (eventName, panelContentWindow) => { 96 fire.asyncWithoutClone(panelContentWindow); 97 }; 98 this.on("shown", listener); 99 return () => { 100 this.off("shown", listener); 101 }; 102 }, 103 }).api(), 104 105 onHidden: new EventManager({ 106 context: this.context, 107 name: "devtoolsPanel.onHidden", 108 register: fire => { 109 const listener = () => { 110 fire.async(); 111 }; 112 this.on("hidden", listener); 113 return () => { 114 this.off("hidden", listener); 115 }; 116 }, 117 }).api(), 118 119 // TODO(rpl): onSearch event and createStatusBarButton method 120 }; 121 } 122 123 close() { 124 this._panelContext = null; 125 this.context = null; 126 } 127 } 128 129 /** 130 * Represents an addon devtools inspector sidebar in the child process. 131 * 132 * @param {DevtoolsExtensionContext} 133 * A devtools extension context running in a child process. 134 * @param {object} sidebarOptions 135 * @param {string} sidebarOptions.id 136 * The id of the addon devtools sidebar registered in the main process. 137 */ 138 class ChildDevToolsInspectorSidebar extends ExtensionCommon.EventEmitter { 139 constructor(context, { id }) { 140 super(); 141 142 this.context = context; 143 this.context.callOnClose(this); 144 145 this.id = id; 146 147 this.conduit = context.openConduit(this, { 148 recv: ["InspectorSidebarHidden", "InspectorSidebarShown"], 149 }); 150 } 151 152 close() { 153 this.context = null; 154 } 155 156 recvInspectorSidebarShown() { 157 // TODO: wait and emit sidebar contentWindow once sidebar.setPage is supported. 158 this.emit("shown"); 159 } 160 161 recvInspectorSidebarHidden() { 162 this.emit("hidden"); 163 } 164 165 api() { 166 const { context, id } = this; 167 168 let extensionURL = new URL("/", context.uri.spec); 169 170 // This is currently needed by sidebar.setPage because API objects are not automatically wrapped 171 // by the API Schema validations and so the ExtensionURL type used in the JSON schema 172 // doesn't have any effect on the parameter received by the setPage API method. 173 function resolveExtensionURL(url) { 174 let sidebarPageURL = new URL(url, context.uri.spec); 175 176 if ( 177 extensionURL.protocol !== sidebarPageURL.protocol || 178 extensionURL.host !== sidebarPageURL.host 179 ) { 180 throw new context.cloneScope.Error( 181 `Invalid sidebar URL: ${sidebarPageURL.href} is not a valid extension URL` 182 ); 183 } 184 185 return sidebarPageURL.href; 186 } 187 188 return { 189 onShown: new EventManager({ 190 context, 191 name: "devtoolsInspectorSidebar.onShown", 192 register: fire => { 193 const listener = (eventName, panelContentWindow) => { 194 fire.asyncWithoutClone(panelContentWindow); 195 }; 196 this.on("shown", listener); 197 return () => { 198 this.off("shown", listener); 199 }; 200 }, 201 }).api(), 202 203 onHidden: new EventManager({ 204 context, 205 name: "devtoolsInspectorSidebar.onHidden", 206 register: fire => { 207 const listener = () => { 208 fire.async(); 209 }; 210 this.on("hidden", listener); 211 return () => { 212 this.off("hidden", listener); 213 }; 214 }, 215 }).api(), 216 217 setPage(extensionPageURL) { 218 let resolvedSidebarURL = resolveExtensionURL(extensionPageURL); 219 220 return context.childManager.callParentAsyncFunction( 221 "devtools.panels.elements.Sidebar.setPage", 222 [id, resolvedSidebarURL] 223 ); 224 }, 225 226 setObject(jsonObject, rootTitle) { 227 return context.cloneScope.Promise.resolve().then(() => { 228 return context.childManager.callParentAsyncFunction( 229 "devtools.panels.elements.Sidebar.setObject", 230 [id, jsonObject, rootTitle] 231 ); 232 }); 233 }, 234 235 setExpression(evalExpression, rootTitle) { 236 return context.cloneScope.Promise.resolve().then(() => { 237 return context.childManager.callParentAsyncFunction( 238 "devtools.panels.elements.Sidebar.setExpression", 239 [id, evalExpression, rootTitle] 240 ); 241 }); 242 }, 243 }; 244 } 245 } 246 247 this.devtools_panels = class extends ExtensionAPI { 248 getAPI(context) { 249 const themeChangeObserver = 250 ExtensionChildDevToolsUtils.getThemeChangeObserver(); 251 252 return { 253 devtools: { 254 panels: { 255 elements: { 256 createSidebarPane(title) { 257 // NOTE: this is needed to be able to return to the caller (the extension) 258 // a promise object that it had the privileges to use (e.g. by marking this 259 // method async we will return a promise object which can only be used by 260 // chrome privileged code). 261 return context.cloneScope.Promise.resolve().then(async () => { 262 const sidebarId = 263 await context.childManager.callParentAsyncFunction( 264 "devtools.panels.elements.createSidebarPane", 265 [title] 266 ); 267 268 const sidebar = new ChildDevToolsInspectorSidebar(context, { 269 id: sidebarId, 270 }); 271 272 const sidebarAPI = Cu.cloneInto( 273 sidebar.api(), 274 context.cloneScope, 275 { cloneFunctions: true } 276 ); 277 278 return sidebarAPI; 279 }); 280 }, 281 }, 282 create(title, icon, url) { 283 // NOTE: this is needed to be able to return to the caller (the extension) 284 // a promise object that it had the privileges to use (e.g. by marking this 285 // method async we will return a promise object which can only be used by 286 // chrome privileged code). 287 return context.cloneScope.Promise.resolve().then(async () => { 288 const panelId = 289 await context.childManager.callParentAsyncFunction( 290 "devtools.panels.create", 291 [title, icon, url] 292 ); 293 294 const devtoolsPanel = new ChildDevToolsPanel(context, { 295 id: panelId, 296 }); 297 298 const devtoolsPanelAPI = Cu.cloneInto( 299 devtoolsPanel.api(), 300 context.cloneScope, 301 { cloneFunctions: true } 302 ); 303 return devtoolsPanelAPI; 304 }); 305 }, 306 get themeName() { 307 return themeChangeObserver.themeName; 308 }, 309 onThemeChanged: new EventManager({ 310 context, 311 name: "devtools.panels.onThemeChanged", 312 register: fire => { 313 const listener = (eventName, themeName) => { 314 fire.async(themeName); 315 }; 316 themeChangeObserver.on("themeChanged", listener); 317 return () => { 318 themeChangeObserver.off("themeChanged", listener); 319 }; 320 }, 321 }).api(), 322 }, 323 }, 324 }; 325 } 326 };