tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;