panel.js (10085B)
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/. */ 4 "use strict"; 5 6 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 7 8 loader.lazyRequireGetter( 9 this, 10 "AccessibilityProxy", 11 "resource://devtools/client/accessibility/accessibility-proxy.js", 12 true 13 ); 14 loader.lazyRequireGetter( 15 this, 16 "Picker", 17 "resource://devtools/client/accessibility/picker.js", 18 true 19 ); 20 21 // The panel's window global is an EventEmitter firing the following events: 22 const EVENTS = { 23 // When the accessibility inspector has a new accessible front selected. 24 NEW_ACCESSIBLE_FRONT_SELECTED: "Accessibility:NewAccessibleFrontSelected", 25 // When the accessibility inspector has a new accessible front highlighted. 26 NEW_ACCESSIBLE_FRONT_HIGHLIGHTED: 27 "Accessibility:NewAccessibleFrontHighlighted", 28 // When the accessibility inspector has a new accessible front inspected. 29 NEW_ACCESSIBLE_FRONT_INSPECTED: "Accessibility:NewAccessibleFrontInspected", 30 // When the accessibility inspector is updated. 31 ACCESSIBILITY_INSPECTOR_UPDATED: 32 "Accessibility:AccessibilityInspectorUpdated", 33 // When accessibility panel UI is initialized (rendered). 34 INITIALIZED: "Accessibility:Initialized", 35 // When accessibile object properties are updated in the panel sidebar for a 36 // new accessible object. 37 PROPERTIES_UPDATED: "Accessibility:PropertiesUpdated", 38 }; 39 40 /** 41 * This object represents Accessibility panel. It's responsibility is to 42 * render Accessibility Tree of the current debugger target and the sidebar that 43 * displays current relevant accessible details. 44 */ 45 class AccessibilityPanel { 46 constructor(iframeWindow, toolbox, commands) { 47 this.panelWin = iframeWindow; 48 this._toolbox = toolbox; 49 this._commands = commands; 50 51 this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this); 52 this.onNewAccessibleFrontSelected = 53 this.onNewAccessibleFrontSelected.bind(this); 54 this.onAccessibilityInspectorUpdated = 55 this.onAccessibilityInspectorUpdated.bind(this); 56 this.updateA11YServiceDurationTimer = 57 this.updateA11YServiceDurationTimer.bind(this); 58 this.forceUpdatePickerButton = this.forceUpdatePickerButton.bind(this); 59 this.onLifecycleEvent = this.onLifecycleEvent.bind(this); 60 61 EventEmitter.decorate(this); 62 } 63 /** 64 * Open is effectively an asynchronous constructor. 65 */ 66 async open() { 67 if (this._opening) { 68 await this._opening; 69 return this._opening; 70 } 71 72 // This first promise includes initialization of proxy *and* the call to forceRefresh 73 let resolver; 74 this._opening = new Promise(resolve => { 75 resolver = resolve; 76 }); 77 78 // This second promise only include the initialization of proxy and few other things, 79 // but not the call to forceRefresh. 80 const { promise, resolve } = Promise.withResolvers(); 81 this.initializedPromise = promise; 82 83 this._telemetry = this._toolbox.telemetry; 84 this.panelWin.gTelemetry = this._telemetry; 85 86 this._toolbox.on("select", this.onPanelVisibilityChange); 87 88 this.panelWin.EVENTS = EVENTS; 89 EventEmitter.decorate(this.panelWin); 90 this.panelWin.on( 91 EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED, 92 this.onNewAccessibleFrontSelected 93 ); 94 this.panelWin.on( 95 EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED, 96 this.onAccessibilityInspectorUpdated 97 ); 98 99 this.picker = new Picker(this); 100 this.fluentBundles = await this.createFluentBundles(); 101 102 this.accessibilityProxy = new AccessibilityProxy(this._commands, this); 103 104 await this.accessibilityProxy.initialize(); 105 106 this.accessibilityProxy.startListeningForLifecycleEvents({ 107 init: this.onLifecycleEvent, 108 shutdown: this.onLifecycleEvent, 109 }); 110 111 // Start recording the duration where a11y service is enabled via the proxy. 112 this.updateA11YServiceDurationTimer(); 113 114 // Resolve the `this.initializedPromise` 115 resolve(); 116 117 // Force rendering the panel once everything is initialized 118 await this.forceRefresh(); 119 120 resolver(this); 121 return this._opening; 122 } 123 124 /** 125 * Retrieve message contexts for the current locales, and return them as an 126 * array of FluentBundles elements. 127 */ 128 async createFluentBundles() { 129 const locales = Services.locale.appLocalesAsBCP47; 130 const generator = L10nRegistry.getInstance().generateBundles(locales, [ 131 "devtools/client/accessibility.ftl", 132 ]); 133 134 // Return value of generateBundles is a generator and should be converted to 135 // a sync iterable before using it with React. 136 const contexts = []; 137 for await (const message of generator) { 138 contexts.push(message); 139 } 140 141 return contexts; 142 } 143 144 onLifecycleEvent() { 145 this.updateA11YServiceDurationTimer(); 146 this.forceUpdatePickerButton(); 147 } 148 149 onNewAccessibleFrontSelected(selected) { 150 this.emit("new-accessible-front-selected", selected); 151 } 152 153 onAccessibilityInspectorUpdated() { 154 this.emit("accessibility-inspector-updated"); 155 } 156 157 /** 158 * Make sure the panel is refreshed when the page is reloaded. The panel is 159 * refreshed immediatelly if it's currently selected or lazily when the user 160 * actually selects it. 161 */ 162 async forceRefresh() { 163 this.shouldRefresh = true; 164 165 // Wait for initialization to be done, in case this is called early on. 166 await this.initializedPromise; 167 const onUpdated = this.panelWin.once(EVENTS.INITIALIZED); 168 this.refresh(); 169 await onUpdated; 170 171 this.emit("reloaded"); 172 } 173 174 /** 175 * Make sure the panel is refreshed (if needed) when it's selected. 176 */ 177 onPanelVisibilityChange() { 178 this._opening.then(() => this.refresh()); 179 } 180 181 refresh() { 182 this.cancelPicker(); 183 184 if (!this.isVisible) { 185 // Do not refresh if the panel isn't visible. 186 return; 187 } 188 189 // Do not refresh if it isn't necessary. 190 if (!this.shouldRefresh) { 191 return; 192 } 193 // Alright reset the flag we are about to refresh the panel. 194 this.shouldRefresh = false; 195 const { 196 supports, 197 getAccessibilityTreeRoot, 198 startListeningForAccessibilityEvents, 199 stopListeningForAccessibilityEvents, 200 audit, 201 simulate, 202 toggleDisplayTabbingOrder, 203 enableAccessibility, 204 resetAccessiblity, 205 startListeningForLifecycleEvents, 206 stopListeningForLifecycleEvents, 207 startListeningForParentLifecycleEvents, 208 stopListeningForParentLifecycleEvents, 209 highlightAccessible, 210 unhighlightAccessible, 211 } = this.accessibilityProxy; 212 this.postContentMessage("initialize", { 213 fluentBundles: this.fluentBundles, 214 toolbox: this._toolbox, 215 supports, 216 getAccessibilityTreeRoot, 217 startListeningForAccessibilityEvents, 218 stopListeningForAccessibilityEvents, 219 audit, 220 simulate, 221 toggleDisplayTabbingOrder, 222 enableAccessibility, 223 resetAccessiblity, 224 startListeningForLifecycleEvents, 225 stopListeningForLifecycleEvents, 226 startListeningForParentLifecycleEvents, 227 stopListeningForParentLifecycleEvents, 228 highlightAccessible, 229 unhighlightAccessible, 230 }); 231 } 232 233 updateA11YServiceDurationTimer() { 234 if (this.accessibilityProxy.enabled) { 235 this._timerID = Glean.devtools.accessibilityServiceTimeActive.start(); 236 } else if (this._timerID) { 237 Glean.devtools.accessibilityServiceTimeActive.stopAndAccumulate( 238 this._timerID 239 ); 240 this._timerID = null; 241 } 242 } 243 244 selectAccessible(accessibleFront) { 245 this.postContentMessage("selectAccessible", accessibleFront); 246 } 247 248 selectAccessibleForNode(nodeFront, reason) { 249 if (reason) { 250 Glean.devtoolsAccessibility.selectAccessibleForNode[reason].add(1); 251 } 252 253 this.postContentMessage("selectNodeAccessible", nodeFront); 254 } 255 256 highlightAccessible(accessibleFront) { 257 this.postContentMessage("highlightAccessible", accessibleFront); 258 } 259 260 postContentMessage(type, ...args) { 261 const event = new this.panelWin.MessageEvent("devtools/chrome/message", { 262 bubbles: true, 263 cancelable: true, 264 data: { type, args }, 265 }); 266 267 this.panelWin.dispatchEvent(event); 268 } 269 270 updatePickerButton() { 271 this.picker && this.picker.updateButton(); 272 } 273 274 forceUpdatePickerButton() { 275 // Only update picker button when the panel is selected. 276 if (!this.isVisible) { 277 return; 278 } 279 280 this.updatePickerButton(); 281 // Calling setToolboxButtons to make sure toolbar is forced to re-render. 282 this._toolbox.component.setToolboxButtons(this._toolbox.toolbarButtons); 283 } 284 285 togglePicker() { 286 this.picker && this.picker.toggle(); 287 } 288 289 cancelPicker() { 290 this.picker && this.picker.cancel(); 291 } 292 293 stopPicker() { 294 this.picker && this.picker.stop(); 295 } 296 297 /** 298 * Return true if the Accessibility panel is currently selected. 299 */ 300 get isVisible() { 301 return this._toolbox.currentToolId === "accessibility"; 302 } 303 304 destroy() { 305 if (this._destroyed) { 306 return; 307 } 308 this._destroyed = true; 309 310 this.postContentMessage("destroy"); 311 312 if (this.accessibilityProxy) { 313 this.accessibilityProxy.stopListeningForLifecycleEvents({ 314 init: this.onLifecycleEvent, 315 shutdown: this.onLifecycleEvent, 316 }); 317 this.accessibilityProxy.destroy(); 318 this.accessibilityProxy = null; 319 this.initializedPromise = null; 320 } 321 322 this._toolbox.off("select", this.onPanelVisibilityChange); 323 324 this.panelWin.off( 325 EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED, 326 this.onNewAccessibleFrontSelected 327 ); 328 this.panelWin.off( 329 EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED, 330 this.onAccessibilityInspectorUpdated 331 ); 332 333 // Older versions of devtools server do not support picker functionality. 334 if (this.picker) { 335 this.picker.release(); 336 this.picker = null; 337 } 338 339 this._telemetry = null; 340 this.panelWin.gTelemetry = null; 341 342 this.emit("destroyed"); 343 } 344 } 345 346 // Exports from this module 347 exports.AccessibilityPanel = AccessibilityPanel;