tor-browser

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

ToolboxToolbar.js (18785B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const {
      7  Component,
      8  createFactory,
      9 } = require("resource://devtools/client/shared/vendor/react.mjs");
     10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     12 const { div, button } = dom;
     13 const MenuButton = createFactory(
     14  require("resource://devtools/client/shared/components/menu/MenuButton.js")
     15 );
     16 const ToolboxTabs = createFactory(
     17  require("resource://devtools/client/framework/components/ToolboxTabs.js")
     18 );
     19 loader.lazyGetter(this, "MeatballMenu", function () {
     20  return createFactory(
     21    require("resource://devtools/client/framework/components/MeatballMenu.js")
     22  );
     23 });
     24 loader.lazyGetter(this, "MenuItem", function () {
     25  return createFactory(
     26    require("resource://devtools/client/shared/components/menu/MenuItem.js")
     27  );
     28 });
     29 loader.lazyGetter(this, "MenuList", function () {
     30  return createFactory(
     31    require("resource://devtools/client/shared/components/menu/MenuList.js")
     32  );
     33 });
     34 loader.lazyGetter(this, "LocalizationProvider", function () {
     35  return createFactory(
     36    require("resource://devtools/client/shared/vendor/fluent-react.js")
     37      .LocalizationProvider
     38  );
     39 });
     40 loader.lazyGetter(this, "DebugTargetInfo", () =>
     41  createFactory(
     42    require("resource://devtools/client/framework/components/DebugTargetInfo.js")
     43  )
     44 );
     45 loader.lazyGetter(this, "ChromeDebugToolbar", () =>
     46  createFactory(
     47    require("resource://devtools/client/framework/components/ChromeDebugToolbar.js")
     48  )
     49 );
     50 
     51 loader.lazyRequireGetter(
     52  this,
     53  "getUnicodeUrl",
     54  "resource://devtools/client/shared/unicode-url.js",
     55  true
     56 );
     57 
     58 /**
     59 * This is the overall component for the toolbox toolbar. It is designed to not know how
     60 * the state is being managed, and attempts to be as pure as possible. The
     61 * ToolboxController component controls the changing state, and passes in everything as
     62 * props.
     63 */
     64 class ToolboxToolbar extends Component {
     65  static get propTypes() {
     66    return {
     67      // The currently focused item (for arrow keyboard navigation)
     68      // This ID determines the tabindex being 0 or -1.
     69      focusedButton: PropTypes.string,
     70      // List of command button definitions.
     71      toolboxButtons: PropTypes.array,
     72      // The id of the currently selected tool, e.g. "inspector"
     73      currentToolId: PropTypes.string,
     74      // An optionally highlighted tools, e.g. "inspector" (used by ToolboxTabs
     75      // component).
     76      highlightedTools: PropTypes.instanceOf(Set),
     77      // List of tool panel definitions (used by ToolboxTabs component).
     78      panelDefinitions: PropTypes.array,
     79      // List of possible docking options.
     80      hostTypes: PropTypes.arrayOf(
     81        PropTypes.shape({
     82          position: PropTypes.string.isRequired,
     83          switchHost: PropTypes.func.isRequired,
     84        })
     85      ),
     86      // Current docking type. Typically one of the position values in
     87      // |hostTypes| but this is not always the case (e.g. for "browsertoolbox").
     88      currentHostType: PropTypes.string,
     89      // Are docking options enabled? They are not enabled in certain situations
     90      // like when the toolbox is opened in a tab.
     91      areDockOptionsEnabled: PropTypes.bool,
     92      // Do we need to add UI for closing the toolbox? We don't when the
     93      // toolbox is undocked, for example.
     94      canCloseToolbox: PropTypes.bool,
     95      // Is the split console currently visible?
     96      isSplitConsoleActive: PropTypes.bool,
     97      // Are we disabling the behavior where pop-ups are automatically closed
     98      // when clicking outside them?
     99      //
    100      // This is a tri-state value that may be true/false or undefined where
    101      // undefined means that the option is not relevant in this context
    102      // (i.e. we're not in a browser toolbox).
    103      disableAutohide: PropTypes.bool,
    104      // Are we displaying the window always on top?
    105      //
    106      // This is a tri-state value that may be true/false or undefined where
    107      // undefined means that the option is not relevant in this context
    108      // (i.e. we're not in a local web extension toolbox).
    109      alwaysOnTop: PropTypes.bool,
    110      // Is the toolbox currently focused?
    111      //
    112      // This will only be defined when alwaysOnTop is true.
    113      focusedState: PropTypes.bool,
    114      // Function to turn the options panel on / off.
    115      toggleOptions: PropTypes.func.isRequired,
    116      // Function to turn the split console on / off.
    117      toggleSplitConsole: PropTypes.func,
    118      // Function to turn the disable pop-up autohide behavior on / off.
    119      toggleNoAutohide: PropTypes.func,
    120      // Function to turn the always on top behavior on / off.
    121      toggleAlwaysOnTop: PropTypes.func,
    122      // Function to completely close the toolbox.
    123      closeToolbox: PropTypes.func,
    124      // Keep a record of what button is focused.
    125      focusButton: PropTypes.func,
    126      // Hold off displaying the toolbar until enough information is ready for
    127      // it to render nicely.
    128      canRender: PropTypes.bool,
    129      // Localization interface.
    130      L10N: PropTypes.object.isRequired,
    131      // The devtools toolbox
    132      toolbox: PropTypes.object,
    133      // Call back function to detect tabs order updated.
    134      onTabsOrderUpdated: PropTypes.func.isRequired,
    135      // Count of visible toolbox buttons which is used by ToolboxTabs component
    136      // to recognize that the visibility of toolbox buttons were changed.
    137      // Because in the component we cannot compare the visibility since the
    138      // button definition instance in toolboxButtons will be unchanged.
    139      visibleToolboxButtonCount: PropTypes.number,
    140      // Data to show debug target info, if needed
    141      debugTargetData: PropTypes.shape({
    142        runtimeInfo: PropTypes.object.isRequired,
    143        descriptorType: PropTypes.string.isRequired,
    144      }),
    145      // The loaded Fluent localization bundles.
    146      fluentBundles: PropTypes.array.isRequired,
    147    };
    148  }
    149 
    150  constructor(props) {
    151    super(props);
    152 
    153    this.hideMenu = this.hideMenu.bind(this);
    154    this.createFrameList = this.createFrameList.bind(this);
    155    this.highlightFrame = this.highlightFrame.bind(this);
    156  }
    157 
    158  componentDidMount() {
    159    this.props.toolbox.on("panel-changed", this.hideMenu);
    160  }
    161 
    162  componentWillUnmount() {
    163    this.props.toolbox.off("panel-changed", this.hideMenu);
    164  }
    165 
    166  hideMenu() {
    167    if (this.refs.meatballMenuButton) {
    168      this.refs.meatballMenuButton.hideMenu();
    169    }
    170 
    171    if (this.refs.frameMenuButton) {
    172      this.refs.frameMenuButton.hideMenu();
    173    }
    174  }
    175 
    176  /**
    177   * A little helper function to call renderToolboxButtons for buttons at the start
    178   * of the toolbox.
    179   */
    180  renderToolboxButtonsStart() {
    181    return this.renderToolboxButtons(true);
    182  }
    183 
    184  /**
    185   * A little helper function to call renderToolboxButtons for buttons at the end
    186   * of the toolbox.
    187   */
    188  renderToolboxButtonsEnd() {
    189    return this.renderToolboxButtons(false);
    190  }
    191 
    192  /**
    193   * Render all of the tabs, this takes in a list of toolbox button states. These are plain
    194   * objects that have all of the relevant information needed to render the button.
    195   * See Toolbox.prototype._createButtonState in devtools/client/framework/toolbox.js for
    196   * documentation on this object.
    197   *
    198   * @param {string} focusedButton - The id of the focused button.
    199   * @param {Array} toolboxButtons - Array of objects that define the command buttons.
    200   * @param {Function} focusButton - Keep a record of the currently focused button.
    201   * @param {boolean} isStart - Render either the starting buttons, or ending buttons.
    202   */
    203  renderToolboxButtons(isStart) {
    204    const { focusedButton, toolboxButtons, focusButton } = this.props;
    205    const visibleButtons = toolboxButtons.filter(command => {
    206      const { isVisible, isInStartContainer } = command;
    207      return isVisible && (isStart ? isInStartContainer : !isInStartContainer);
    208    });
    209 
    210    if (visibleButtons.length === 0) {
    211      return null;
    212    }
    213 
    214    // The RDM button, if present, should always go last
    215    const rdmIndex = visibleButtons.findIndex(
    216      button => button.id === "command-button-responsive"
    217    );
    218    if (rdmIndex !== -1 && rdmIndex !== visibleButtons.length - 1) {
    219      const rdm = visibleButtons.splice(rdmIndex, 1)[0];
    220      visibleButtons.push(rdm);
    221    }
    222 
    223    const renderedButtons = visibleButtons.map(command => {
    224      const {
    225        id,
    226        description,
    227        disabled,
    228        onClick,
    229        isChecked,
    230        isToggle,
    231        className: buttonClass,
    232        onKeyDown,
    233      } = command;
    234 
    235      // If button is frame button, create menu button in order to
    236      // use the doorhanger menu.
    237      if (id === "command-button-frames") {
    238        return this.renderFrameButton(command);
    239      }
    240 
    241      if (id === "command-button-errorcount") {
    242        return this.renderErrorIcon(command);
    243      }
    244 
    245      return button({
    246        id,
    247        title: description,
    248        disabled,
    249        "aria-pressed": !isToggle ? null : isChecked,
    250        className: `devtools-tabbar-button command-button ${
    251          buttonClass || ""
    252        } ${isChecked ? "checked" : ""}`,
    253        onClick: event => {
    254          onClick(event);
    255          focusButton(id);
    256        },
    257        onFocus: () => focusButton(id),
    258        tabIndex: id === focusedButton ? "0" : "-1",
    259        onKeyDown: event => {
    260          onKeyDown(event);
    261        },
    262        onContextMenu: event => {
    263          const menu = command.getContextMenu();
    264          if (!menu) {
    265            return;
    266          }
    267 
    268          event.preventDefault();
    269          event.stopPropagation();
    270 
    271          menu.popup(event.screenX, event.screenY, window.parent.document);
    272        },
    273      });
    274    });
    275 
    276    // Add the appropriate separator, if needed.
    277    const children = renderedButtons;
    278    if (renderedButtons.length) {
    279      if (isStart) {
    280        children.push(this.renderSeparator());
    281        // For the end group we add a separator *before* the RDM button if it
    282        // exists, but only if it is not the only button.
    283      } else if (rdmIndex !== -1 && renderedButtons.length > 1) {
    284        children.splice(children.length - 1, 0, this.renderSeparator());
    285      }
    286    }
    287 
    288    return div(
    289      { id: `toolbox-buttons-${isStart ? "start" : "end"}` },
    290      ...children
    291    );
    292  }
    293 
    294  renderFrameButton(command) {
    295    const { id, isChecked, disabled, description } = command;
    296 
    297    const { toolbox } = this.props;
    298 
    299    return MenuButton(
    300      {
    301        id,
    302        disabled,
    303        menuId: id + "-panel",
    304        toolboxDoc: toolbox.doc,
    305        className: `devtools-tabbar-button command-button ${
    306          isChecked ? "checked" : ""
    307        }`,
    308        ref: "frameMenuButton",
    309        title: description,
    310        onCloseButton: async () => {
    311          // Only try to unhighlight if the inspectorFront has been created already
    312          const inspectorFront = toolbox.target.getCachedFront("inspector");
    313          if (inspectorFront) {
    314            const highlighter = toolbox.getHighlighter();
    315            await highlighter.unhighlight();
    316          }
    317        },
    318      },
    319      this.createFrameList
    320    );
    321  }
    322 
    323  renderErrorIcon(command) {
    324    let { errorCount, id } = command;
    325 
    326    if (!errorCount) {
    327      return null;
    328    }
    329 
    330    if (errorCount > 99) {
    331      errorCount = "99+";
    332    }
    333 
    334    const errorIconTooltip = this.props.toolbox.isSplitConsoleEnabled()
    335      ? this.props.L10N.getStr("toolbox.errorCountButton.tooltip")
    336      : this.props.L10N.getStr("toolbox.errorCountButtonConsoleTab.tooltip");
    337 
    338    return button(
    339      {
    340        id,
    341        className: "devtools-tabbar-button command-button toolbox-error",
    342        onClick: () => {
    343          if (this.props.currentToolId !== "webconsole") {
    344            this.props.toolbox.openSplitConsole();
    345          }
    346        },
    347        title:
    348          this.props.currentToolId !== "webconsole" ? errorIconTooltip : null,
    349      },
    350      errorCount
    351    );
    352  }
    353 
    354  highlightFrame(id) {
    355    const { toolbox } = this.props;
    356    if (!id) {
    357      return;
    358    }
    359 
    360    toolbox.onHighlightFrame(id);
    361  }
    362 
    363  createFrameList() {
    364    const { toolbox } = this.props;
    365    if (toolbox.frameMap.size < 1) {
    366      return null;
    367    }
    368 
    369    const items = [];
    370    toolbox.frameMap.forEach(frame => {
    371      const label = toolbox.commands.descriptorFront.isWebExtensionDescriptor
    372        ? toolbox.getExtensionPathName(frame.url)
    373        : getUnicodeUrl(frame.url);
    374 
    375      const item = MenuItem({
    376        id: frame.id.toString(),
    377        key: "toolbox-frame-key-" + frame.id,
    378        label,
    379        checked: frame.id === toolbox.selectedFrameId,
    380        onClick: () => toolbox.onIframePickerFrameSelected(frame.id),
    381      });
    382 
    383      // Always put the top level frame at the top
    384      if (frame.isTopLevel) {
    385        items.unshift(item);
    386      } else {
    387        items.push(item);
    388      }
    389    });
    390 
    391    return MenuList(
    392      {
    393        id: "toolbox-frame-menu",
    394        onHighlightedChildChange: this.highlightFrame,
    395      },
    396      items
    397    );
    398  }
    399 
    400  /**
    401   * Render a separator.
    402   */
    403  renderSeparator() {
    404    return div({ className: "devtools-separator" });
    405  }
    406 
    407  /**
    408   * Render the toolbox control buttons. The following props are expected:
    409   *
    410   * @param {string} props.focusedButton
    411   *        The id of the focused button.
    412   * @param {string} props.currentToolId
    413   *        The id of the currently selected tool, e.g. "inspector".
    414   * @param {object[]} props.hostTypes
    415   *        Array of host type objects.
    416   * @param {string} props.hostTypes[].position
    417   *        Position name.
    418   * @param {Function} props.hostTypes[].switchHost
    419   *        Function to switch the host.
    420   * @param {string} props.currentHostType
    421   *        The current docking configuration.
    422   * @param {boolean} props.areDockOptionsEnabled
    423   *        They are not enabled in certain situations like when the toolbox is
    424   *        in a tab.
    425   * @param {boolean} props.canCloseToolbox
    426   *        Do we need to add UI for closing the toolbox? We don't when the
    427   *        toolbox is undocked, for example.
    428   * @param {boolean} props.isSplitConsoleActive
    429   *         Is the split console currently visible?
    430   *        toolbox is undocked, for example.
    431   * @param {boolean|undefined} props.disableAutohide
    432   *        Are we disabling the behavior where pop-ups are automatically
    433   *        closed when clicking outside them?
    434   *        (Only defined for the browser toolbox.)
    435   * @param {Function} props.selectTool
    436   *        Function to select a tool based on its id.
    437   * @param {Function} props.toggleOptions
    438   *        Function to turn the options panel on / off.
    439   * @param {Function} props.toggleSplitConsole
    440   *        Function to turn the split console on / off.
    441   * @param {Function} props.toggleNoAutohide
    442   *        Function to turn the disable pop-up autohide behavior on / off.
    443   * @param {Function} props.toggleAlwaysOnTop
    444   *        Function to turn the always on top behavior on / off.
    445   * @param {Function} props.closeToolbox
    446   *        Completely close the toolbox.
    447   * @param {Function} props.focusButton
    448   *        Keep a record of the currently focused button.
    449   * @param {object} props.L10N
    450   *        Localization interface.
    451   * @param {object} props.toolbox
    452   *        The devtools toolbox. Used by the MenuButton component to display
    453   *        the menu popup.
    454   * @param {object} refs
    455   *        The components refs object. Used to keep a reference to the MenuButton
    456   *        for the meatball menu so that we can tell it to resize its contents
    457   *        when they change.
    458   */
    459  renderToolboxControls() {
    460    const {
    461      focusedButton,
    462      canCloseToolbox,
    463      closeToolbox,
    464      focusButton,
    465      L10N,
    466      toolbox,
    467    } = this.props;
    468 
    469    const meatballMenuButtonId = "toolbox-meatball-menu-button";
    470 
    471    const meatballMenuButton = MenuButton(
    472      {
    473        id: meatballMenuButtonId,
    474        menuId: meatballMenuButtonId + "-panel",
    475        toolboxDoc: toolbox.doc,
    476        onFocus: () => focusButton(meatballMenuButtonId),
    477        className: "devtools-tabbar-button",
    478        title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
    479        tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
    480        ref: "meatballMenuButton",
    481      },
    482      MeatballMenu({
    483        ...this.props,
    484        hostTypes: this.props.areDockOptionsEnabled ? this.props.hostTypes : [],
    485        onResize: () => {
    486          this.refs.meatballMenuButton.resizeContent();
    487        },
    488      })
    489    );
    490 
    491    const closeButtonId = "toolbox-close";
    492 
    493    const closeButton = canCloseToolbox
    494      ? button({
    495          id: closeButtonId,
    496          onFocus: () => focusButton(closeButtonId),
    497          className: "devtools-tabbar-button",
    498          title: L10N.getStr("toolbox.closebutton.tooltip"),
    499          onClick: () => closeToolbox(),
    500          tabIndex: focusedButton === "toolbox-close" ? "0" : "-1",
    501        })
    502      : null;
    503 
    504    return div({ id: "toolbox-controls" }, meatballMenuButton, closeButton);
    505  }
    506 
    507  /**
    508   * The render function is kept fairly short for maintainability. See the individual
    509   * render functions for how each of the sections is rendered.
    510   */
    511  render() {
    512    const { L10N, debugTargetData, toolbox, fluentBundles } = this.props;
    513    const classnames = ["devtools-tabbar"];
    514    const startButtons = this.renderToolboxButtonsStart();
    515    const endButtons = this.renderToolboxButtonsEnd();
    516 
    517    if (!startButtons) {
    518      classnames.push("devtools-tabbar-has-start");
    519    }
    520    if (!endButtons) {
    521      classnames.push("devtools-tabbar-has-end");
    522    }
    523 
    524    const toolbar = this.props.canRender
    525      ? div(
    526          {
    527            className: classnames.join(" "),
    528          },
    529          startButtons,
    530          ToolboxTabs(this.props),
    531          endButtons,
    532          this.renderToolboxControls()
    533        )
    534      : div({ className: classnames.join(" ") });
    535 
    536    const debugTargetInfo = debugTargetData
    537      ? DebugTargetInfo({
    538          alwaysOnTop: this.props.alwaysOnTop,
    539          focusedState: this.props.focusedState,
    540          toggleAlwaysOnTop: this.props.toggleAlwaysOnTop,
    541          debugTargetData,
    542          L10N,
    543          toolbox,
    544        })
    545      : null;
    546 
    547    // Display the toolbar in the MBT and about:debugging MBT if we have server support for it.
    548    const chromeDebugToolbar = toolbox.commands.targetCommand.descriptorFront
    549      .isBrowserProcessDescriptor
    550      ? ChromeDebugToolbar()
    551      : null;
    552 
    553    return LocalizationProvider(
    554      { bundles: fluentBundles },
    555      div({}, chromeDebugToolbar, debugTargetInfo, toolbar)
    556    );
    557  }
    558 }
    559 
    560 module.exports = ToolboxToolbar;