summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/components
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/framework/components
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/framework/components')
-rw-r--r--devtools/client/framework/components/DebugTargetErrorPage.css21
-rw-r--r--devtools/client/framework/components/DebugTargetErrorPage.js47
-rw-r--r--devtools/client/framework/components/DebugTargetInfo.js344
-rw-r--r--devtools/client/framework/components/MeatballMenu.js241
-rw-r--r--devtools/client/framework/components/ToolboxController.js202
-rw-r--r--devtools/client/framework/components/ToolboxTab.js110
-rw-r--r--devtools/client/framework/components/ToolboxTabs.js331
-rw-r--r--devtools/client/framework/components/ToolboxToolbar.js501
-rw-r--r--devtools/client/framework/components/moz.build16
9 files changed, 1813 insertions, 0 deletions
diff --git a/devtools/client/framework/components/DebugTargetErrorPage.css b/devtools/client/framework/components/DebugTargetErrorPage.css
new file mode 100644
index 0000000000..ffac30cece
--- /dev/null
+++ b/devtools/client/framework/components/DebugTargetErrorPage.css
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.error-page {
+ --base-unit: 4px; /* from photon */
+
+ padding: calc(var(--base-unit) * 4);
+ font-size: 15px; /* from photon */
+ min-height: 100vh;
+}
+
+.error-page__title {
+ margin: 0;
+ font-size: 36px; /* from photon */
+ font-weight: 200; /* from photon */
+}
+
+.error-page__details {
+ font-family: monospace;
+}
diff --git a/devtools/client/framework/components/DebugTargetErrorPage.js b/devtools/client/framework/components/DebugTargetErrorPage.js
new file mode 100644
index 0000000000..e3247456b2
--- /dev/null
+++ b/devtools/client/framework/components/DebugTargetErrorPage.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * This component is displayed when the about:devtools-toolbox fails to load
+ * properly due to wrong parameters or debug targets that don't exist.
+ */
+class DebugTargetErrorPage extends PureComponent {
+ static get propTypes() {
+ return {
+ errorMessage: PropTypes.string.isRequired,
+ L10N: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const { errorMessage, L10N } = this.props;
+
+ return dom.article(
+ {
+ className: "error-page qa-error-page",
+ },
+ dom.h1(
+ {
+ className: "error-page__title",
+ },
+ L10N.getStr("toolbox.debugTargetErrorPage.title")
+ ),
+ dom.p({}, L10N.getStr("toolbox.debugTargetErrorPage.description")),
+ dom.output(
+ {
+ className: "error-page__details",
+ },
+ errorMessage
+ )
+ );
+ }
+}
+
+module.exports = DebugTargetErrorPage;
diff --git a/devtools/client/framework/components/DebugTargetInfo.js b/devtools/client/framework/components/DebugTargetInfo.js
new file mode 100644
index 0000000000..781ae30cf6
--- /dev/null
+++ b/devtools/client/framework/components/DebugTargetInfo.js
@@ -0,0 +1,344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Services = require("Services");
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ CONNECTION_TYPES,
+ DEBUG_TARGET_TYPES,
+} = require("devtools/client/shared/remote-debugging/constants");
+
+/**
+ * This is header that should be displayed on top of the toolbox when using
+ * about:devtools-toolbox.
+ */
+class DebugTargetInfo extends PureComponent {
+ static get propTypes() {
+ return {
+ debugTargetData: PropTypes.shape({
+ connectionType: PropTypes.oneOf(Object.values(CONNECTION_TYPES))
+ .isRequired,
+ runtimeInfo: PropTypes.shape({
+ deviceName: PropTypes.string,
+ icon: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ version: PropTypes.string.isRequired,
+ }).isRequired,
+ targetType: PropTypes.oneOf(Object.values(DEBUG_TARGET_TYPES))
+ .isRequired,
+ }).isRequired,
+ L10N: PropTypes.object.isRequired,
+ toolbox: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = { urlValue: props.toolbox.target.url };
+
+ this.onChange = this.onChange.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ }
+
+ componentDidMount() {
+ this.updateTitle();
+ }
+
+ updateTitle() {
+ const { L10N, debugTargetData, toolbox } = this.props;
+ const title = toolbox.target.name;
+ const targetTypeStr = L10N.getStr(
+ this.getAssetsForDebugTargetType().l10nId
+ );
+
+ const { connectionType } = debugTargetData;
+ if (connectionType === CONNECTION_TYPES.THIS_FIREFOX) {
+ toolbox.doc.title = L10N.getFormatStr(
+ "toolbox.debugTargetInfo.tabTitleLocal",
+ targetTypeStr,
+ title
+ );
+ } else {
+ const connectionTypeStr = L10N.getStr(
+ this.getAssetsForConnectionType().l10nId
+ );
+ toolbox.doc.title = L10N.getFormatStr(
+ "toolbox.debugTargetInfo.tabTitleRemote",
+ connectionTypeStr,
+ targetTypeStr,
+ title
+ );
+ }
+ }
+
+ getRuntimeText() {
+ const { debugTargetData, L10N } = this.props;
+ const { name, version } = debugTargetData.runtimeInfo;
+ const { connectionType } = debugTargetData;
+
+ return connectionType === CONNECTION_TYPES.THIS_FIREFOX
+ ? L10N.getFormatStr(
+ "toolbox.debugTargetInfo.runtimeLabel.thisFirefox",
+ version
+ )
+ : L10N.getFormatStr(
+ "toolbox.debugTargetInfo.runtimeLabel",
+ name,
+ version
+ );
+ }
+
+ getAssetsForConnectionType() {
+ const { connectionType } = this.props.debugTargetData;
+
+ switch (connectionType) {
+ case CONNECTION_TYPES.USB:
+ return {
+ image: "chrome://devtools/skin/images/aboutdebugging-usb-icon.svg",
+ l10nId: "toolbox.debugTargetInfo.connection.usb",
+ };
+ case CONNECTION_TYPES.NETWORK:
+ return {
+ image: "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg",
+ l10nId: "toolbox.debugTargetInfo.connection.network",
+ };
+ default:
+ return {};
+ }
+ }
+
+ getAssetsForDebugTargetType() {
+ const { targetType } = this.props.debugTargetData;
+
+ // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1520723
+ // Show actual favicon (currently toolbox.target.activeTab.favicon
+ // is unpopulated)
+ const favicon = "chrome://devtools/skin/images/globe.svg";
+
+ switch (targetType) {
+ case DEBUG_TARGET_TYPES.EXTENSION:
+ return {
+ image: "chrome://devtools/skin/images/debugging-addons.svg",
+ l10nId: "toolbox.debugTargetInfo.targetType.extension",
+ };
+ case DEBUG_TARGET_TYPES.PROCESS:
+ return {
+ image: "chrome://devtools/skin/images/settings.svg",
+ l10nId: "toolbox.debugTargetInfo.targetType.process",
+ };
+ case DEBUG_TARGET_TYPES.TAB:
+ return {
+ image: favicon,
+ l10nId: "toolbox.debugTargetInfo.targetType.tab",
+ };
+ case DEBUG_TARGET_TYPES.WORKER:
+ return {
+ image: "chrome://devtools/skin/images/debugging-workers.svg",
+ l10nId: "toolbox.debugTargetInfo.targetType.worker",
+ };
+ default:
+ return {};
+ }
+ }
+
+ onChange({ target }) {
+ this.setState({ urlValue: target.value });
+ }
+
+ onFocus({ target }) {
+ target.select();
+ }
+
+ onSubmit(event) {
+ event.preventDefault();
+ let url = this.state.urlValue;
+
+ if (!url || !url.length) {
+ return;
+ }
+
+ try {
+ // Get the URL from the fixup service:
+ const flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ const uriInfo = Services.uriFixup.getFixupURIInfo(url, flags);
+ url = uriInfo.fixedURI.spec;
+ } catch (ex) {
+ // The getFixupURIInfo service will throw an error if a malformed URI is
+ // produced from the input.
+ console.error(ex);
+ }
+
+ this.props.toolbox.target.navigateTo({ url });
+ }
+
+ shallRenderConnection() {
+ const { connectionType } = this.props.debugTargetData;
+ const renderableTypes = [CONNECTION_TYPES.USB, CONNECTION_TYPES.NETWORK];
+
+ return renderableTypes.includes(connectionType);
+ }
+
+ renderConnection() {
+ const { connectionType } = this.props.debugTargetData;
+ const { image, l10nId } = this.getAssetsForConnectionType();
+
+ return dom.span(
+ {
+ className: "iconized-label qa-connection-info",
+ },
+ dom.img({ src: image, alt: `${connectionType} icon` }),
+ this.props.L10N.getStr(l10nId)
+ );
+ }
+
+ renderRuntime() {
+ if (!this.props.debugTargetData.runtimeInfo) {
+ // Skip the runtime render if no runtimeInfo is available.
+ // Runtime info is retrieved from the remote-client-manager, which might not be
+ // setup if about:devtools-toolbox was not opened from about:debugging.
+ return null;
+ }
+
+ const { icon, deviceName } = this.props.debugTargetData.runtimeInfo;
+
+ return dom.span(
+ {
+ className: "iconized-label qa-runtime-info",
+ },
+ dom.img({ src: icon, className: "channel-icon qa-runtime-icon" }),
+ dom.b({ className: "devtools-ellipsis-text" }, this.getRuntimeText()),
+ dom.span({ className: "devtools-ellipsis-text" }, deviceName)
+ );
+ }
+
+ renderTargetTitle() {
+ const title = this.props.toolbox.target.name;
+
+ const { image, l10nId } = this.getAssetsForDebugTargetType();
+
+ return dom.span(
+ {
+ className: "iconized-label debug-target-title",
+ },
+ dom.img({ src: image, alt: this.props.L10N.getStr(l10nId) }),
+ title
+ ? dom.b({ className: "devtools-ellipsis-text qa-target-title" }, title)
+ : null
+ );
+ }
+
+ renderTargetURI() {
+ const url = this.props.toolbox.target.url;
+ const { targetType } = this.props.debugTargetData;
+ const isURLEditable = targetType === DEBUG_TARGET_TYPES.TAB;
+
+ return dom.span(
+ {
+ key: url,
+ className: "debug-target-url",
+ },
+ isURLEditable
+ ? this.renderTargetInput(url)
+ : dom.span(
+ { className: "debug-target-url-readonly devtools-ellipsis-text" },
+ url
+ )
+ );
+ }
+
+ renderTargetInput(url) {
+ return dom.form(
+ {
+ className: "debug-target-url-form",
+ onSubmit: this.onSubmit,
+ },
+ dom.input({
+ className: "devtools-textinput debug-target-url-input",
+ onChange: this.onChange,
+ onFocus: this.onFocus,
+ defaultValue: url,
+ })
+ );
+ }
+
+ renderNavigationButton(detail) {
+ const { L10N } = this.props;
+
+ return dom.button(
+ {
+ className: `iconized-label navigation-button ${detail.className}`,
+ onClick: detail.onClick,
+ title: L10N.getStr(detail.l10nId),
+ },
+ dom.img({
+ src: detail.icon,
+ alt: L10N.getStr(detail.l10nId),
+ })
+ );
+ }
+
+ renderNavigation() {
+ const { debugTargetData } = this.props;
+ const { targetType } = debugTargetData;
+
+ if (targetType !== DEBUG_TARGET_TYPES.TAB) {
+ return null;
+ }
+
+ const items = [];
+
+ if (this.props.toolbox.target.traits.navigation) {
+ items.push(
+ this.renderNavigationButton({
+ className: "qa-back-button",
+ icon: "chrome://browser/skin/back.svg",
+ l10nId: "toolbox.debugTargetInfo.back",
+ onClick: () => this.props.toolbox.target.goBack(),
+ }),
+ this.renderNavigationButton({
+ className: "qa-forward-button",
+ icon: "chrome://browser/skin/forward.svg",
+ l10nId: "toolbox.debugTargetInfo.forward",
+ onClick: () => this.props.toolbox.target.goForward(),
+ })
+ );
+ }
+
+ items.push(
+ this.renderNavigationButton({
+ className: "qa-reload-button",
+ icon: "chrome://browser/skin/reload.svg",
+ l10nId: "toolbox.debugTargetInfo.reload",
+ onClick: () => this.props.toolbox.target.reload(),
+ })
+ );
+
+ return dom.div(
+ {
+ className: "debug-target-navigation",
+ },
+ ...items
+ );
+ }
+
+ render() {
+ return dom.header(
+ {
+ className: "debug-target-info qa-debug-target-info",
+ },
+ this.shallRenderConnection() ? this.renderConnection() : null,
+ this.renderRuntime(),
+ this.renderTargetTitle(),
+ this.renderNavigation(),
+ this.renderTargetURI()
+ );
+ }
+}
+
+module.exports = DebugTargetInfo;
diff --git a/devtools/client/framework/components/MeatballMenu.js b/devtools/client/framework/components/MeatballMenu.js
new file mode 100644
index 0000000000..52043740d0
--- /dev/null
+++ b/devtools/client/framework/components/MeatballMenu.js
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ PureComponent,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { hr } = dom;
+
+loader.lazyGetter(this, "MenuItem", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuItem")
+ );
+});
+loader.lazyGetter(this, "MenuList", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuList")
+ );
+});
+
+loader.lazyRequireGetter(
+ this,
+ "openDocLink",
+ "devtools/client/shared/link",
+ true
+);
+loader.lazyRequireGetter(this, "assert", "devtools/shared/DevToolsUtils", true);
+
+const openDevToolsDocsLink = () => {
+ openDocLink(
+ "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu"
+ );
+};
+
+const openCommunityLink = () => {
+ openDocLink(
+ "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu"
+ );
+};
+
+class MeatballMenu extends PureComponent {
+ static get propTypes() {
+ return {
+ // The id of the currently selected tool, e.g. "inspector"
+ currentToolId: PropTypes.string,
+
+ // List of possible docking options.
+ hostTypes: PropTypes.arrayOf(
+ PropTypes.shape({
+ position: PropTypes.string.isRequired,
+ switchHost: PropTypes.func.isRequired,
+ })
+ ),
+
+ // Current docking type. Typically one of the position values in
+ // |hostTypes| but this is not always the case (e.g. for "browsertoolbox").
+ currentHostType: PropTypes.string,
+
+ // Is the split console currently visible?
+ isSplitConsoleActive: PropTypes.bool,
+
+ // Are we disabling the behavior where pop-ups are automatically closed
+ // when clicking outside them?
+ //
+ // This is a tri-state value that may be true/false or undefined where
+ // undefined means that the option is not relevant in this context
+ // (i.e. we're not in a browser toolbox).
+ disableAutohide: PropTypes.bool,
+
+ // Function to turn the options panel on / off.
+ toggleOptions: PropTypes.func.isRequired,
+
+ // Function to turn the split console on / off.
+ toggleSplitConsole: PropTypes.func,
+
+ // Function to turn the disable pop-up autohide behavior on / off.
+ toggleNoAutohide: PropTypes.func,
+
+ // Localization interface.
+ L10N: PropTypes.object.isRequired,
+
+ // Callback function that will be invoked any time the component contents
+ // update in such a way that its bounding box might change.
+ onResize: PropTypes.func,
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (!this.props.onResize) {
+ return;
+ }
+
+ // We are only expecting the following kinds of dynamic changes when a popup
+ // is showing:
+ //
+ // - The "Disable pop-up autohide" menu item being added after the Browser
+ // Toolbox is connected.
+ // - The split console label changing between "Show Split Console" and "Hide
+ // Split Console".
+ // - The "Show/Hide Split Console" entry being added removed or removed.
+ //
+ // The latter two cases are only likely to be noticed when "Disable pop-up
+ // autohide" is active, but for completeness we handle them here.
+ const didChange =
+ typeof this.props.disableAutohide !== typeof prevProps.disableAutohide ||
+ this.props.currentToolId !== prevProps.currentToolId ||
+ this.props.isSplitConsoleActive !== prevProps.isSplitConsoleActive;
+
+ if (didChange) {
+ this.props.onResize();
+ }
+ }
+
+ render() {
+ const items = [];
+
+ // Dock options
+ for (const hostType of this.props.hostTypes) {
+ // This is more verbose than it needs to be but lets us easily search for
+ // l10n entities.
+ let l10nkey;
+ switch (hostType.position) {
+ case "window":
+ l10nkey = "toolbox.meatballMenu.dock.separateWindow.label";
+ break;
+
+ case "bottom":
+ l10nkey = "toolbox.meatballMenu.dock.bottom.label";
+ break;
+
+ case "left":
+ l10nkey = "toolbox.meatballMenu.dock.left.label";
+ break;
+
+ case "right":
+ l10nkey = "toolbox.meatballMenu.dock.right.label";
+ break;
+
+ default:
+ assert(false, `Unexpected hostType.position: ${hostType.position}`);
+ break;
+ }
+
+ items.push(
+ MenuItem({
+ id: `toolbox-meatball-menu-dock-${hostType.position}`,
+ key: `dock-${hostType.position}`,
+ label: this.props.L10N.getStr(l10nkey),
+ onClick: hostType.switchHost,
+ checked: hostType.position === this.props.currentHostType,
+ className: "iconic",
+ })
+ );
+ }
+
+ if (items.length) {
+ items.push(hr({ key: "dock-separator" }));
+ }
+
+ // Split console
+ if (this.props.currentToolId !== "webconsole") {
+ const l10nkey = this.props.isSplitConsoleActive
+ ? "toolbox.meatballMenu.hideconsole.label"
+ : "toolbox.meatballMenu.splitconsole.label";
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-splitconsole",
+ key: "splitconsole",
+ label: this.props.L10N.getStr(l10nkey),
+ accelerator: "Esc",
+ onClick: this.props.toggleSplitConsole,
+ className: "iconic",
+ })
+ );
+ }
+
+ // Disable pop-up autohide
+ //
+ // If |disableAutohide| is undefined, it means this feature is not available
+ // in this context.
+ if (typeof this.props.disableAutohide !== "undefined") {
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-noautohide",
+ key: "noautohide",
+ label: this.props.L10N.getStr(
+ "toolbox.meatballMenu.noautohide.label"
+ ),
+ type: "checkbox",
+ checked: this.props.disableAutohide,
+ onClick: this.props.toggleNoAutohide,
+ className: "iconic",
+ })
+ );
+ }
+
+ // Settings
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-settings",
+ key: "settings",
+ label: this.props.L10N.getStr("toolbox.meatballMenu.settings.label"),
+ accelerator: this.props.L10N.getStr("toolbox.help.key"),
+ onClick: this.props.toggleOptions,
+ className: "iconic",
+ })
+ );
+
+ items.push(hr({ key: "docs-separator" }));
+
+ // Getting started
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-documentation",
+ key: "documentation",
+ label: this.props.L10N.getStr(
+ "toolbox.meatballMenu.documentation.label"
+ ),
+ onClick: openDevToolsDocsLink,
+ })
+ );
+
+ // Give feedback
+ items.push(
+ MenuItem({
+ id: "toolbox-meatball-menu-community",
+ key: "community",
+ label: this.props.L10N.getStr("toolbox.meatballMenu.community.label"),
+ onClick: openCommunityLink,
+ })
+ );
+
+ return MenuList({ id: "toolbox-meatball-menu" }, items);
+ }
+}
+
+module.exports = MeatballMenu;
diff --git a/devtools/client/framework/components/ToolboxController.js b/devtools/client/framework/components/ToolboxController.js
new file mode 100644
index 0000000000..de0aa58cb4
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxController.js
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const ToolboxToolbar = createFactory(
+ require("devtools/client/framework/components/ToolboxToolbar")
+);
+const ELEMENT_PICKER_ID = "command-button-pick";
+
+/**
+ * This component serves as a state controller for the toolbox React component. It's a
+ * thin layer for translating events and state of the outside world into the React update
+ * cycle. This solution was used to keep the amount of code changes to a minimimum while
+ * adapting the existing codebase to start using React.
+ */
+class ToolboxController extends Component {
+ constructor(props, context) {
+ super(props, context);
+
+ // See the ToolboxToolbar propTypes for documentation on each of these items in
+ // state, and for the definitions of the props that are expected to be passed in.
+ this.state = {
+ focusedButton: ELEMENT_PICKER_ID,
+ toolboxButtons: [],
+ visibleToolboxButtonCount: 0,
+ currentToolId: null,
+ highlightedTools: new Set(),
+ panelDefinitions: [],
+ hostTypes: [],
+ currentHostType: undefined,
+ areDockOptionsEnabled: true,
+ canCloseToolbox: true,
+ isSplitConsoleActive: false,
+ disableAutohide: undefined,
+ canRender: false,
+ buttonIds: [],
+ checkedButtonsUpdated: () => {
+ this.forceUpdate();
+ },
+ };
+
+ this.setFocusedButton = this.setFocusedButton.bind(this);
+ this.setToolboxButtons = this.setToolboxButtons.bind(this);
+ this.setCurrentToolId = this.setCurrentToolId.bind(this);
+ this.highlightTool = this.highlightTool.bind(this);
+ this.unhighlightTool = this.unhighlightTool.bind(this);
+ this.setHostTypes = this.setHostTypes.bind(this);
+ this.setCurrentHostType = this.setCurrentHostType.bind(this);
+ this.setDockOptionsEnabled = this.setDockOptionsEnabled.bind(this);
+ this.setCanCloseToolbox = this.setCanCloseToolbox.bind(this);
+ this.setIsSplitConsoleActive = this.setIsSplitConsoleActive.bind(this);
+ this.setDisableAutohide = this.setDisableAutohide.bind(this);
+ this.setCanRender = this.setCanRender.bind(this);
+ this.setPanelDefinitions = this.setPanelDefinitions.bind(this);
+ this.updateButtonIds = this.updateButtonIds.bind(this);
+ this.updateFocusedButton = this.updateFocusedButton.bind(this);
+ this.setDebugTargetData = this.setDebugTargetData.bind(this);
+ }
+
+ shouldComponentUpdate() {
+ return this.state.canRender;
+ }
+
+ componentWillUnmount() {
+ this.state.toolboxButtons.forEach(button => {
+ button.off("updatechecked", this.state.checkedButtonsUpdated);
+ });
+ }
+
+ /**
+ * The button and tab ids must be known in order to be able to focus left and right
+ * using the arrow keys.
+ */
+ updateButtonIds() {
+ const { toolboxButtons, panelDefinitions, canCloseToolbox } = this.state;
+
+ // This is a little gnarly, but go through all of the state and extract the IDs.
+ this.setState({
+ buttonIds: [
+ ...toolboxButtons
+ .filter(btn => btn.isInStartContainer)
+ .map(({ id }) => id),
+ ...panelDefinitions.map(({ id }) => id),
+ ...toolboxButtons
+ .filter(btn => !btn.isInStartContainer)
+ .map(({ id }) => id),
+ canCloseToolbox ? "toolbox-close" : null,
+ ].filter(id => id),
+ });
+
+ this.updateFocusedButton();
+ }
+
+ updateFocusedButton() {
+ this.setFocusedButton(this.state.focusedButton);
+ }
+
+ setFocusedButton(focusedButton) {
+ const { buttonIds } = this.state;
+
+ focusedButton =
+ focusedButton && buttonIds.includes(focusedButton)
+ ? focusedButton
+ : buttonIds[0];
+ if (this.state.focusedButton !== focusedButton) {
+ this.setState({
+ focusedButton,
+ });
+ }
+ }
+
+ setCurrentToolId(currentToolId) {
+ this.setState({ currentToolId }, () => {
+ // Also set the currently focused button to this tool.
+ this.setFocusedButton(currentToolId);
+ });
+ }
+
+ setCanRender() {
+ this.setState({ canRender: true }, this.updateButtonIds);
+ }
+
+ highlightTool(highlightedTool) {
+ const { highlightedTools } = this.state;
+ highlightedTools.add(highlightedTool);
+ this.setState({ highlightedTools });
+ }
+
+ unhighlightTool(id) {
+ const { highlightedTools } = this.state;
+ if (highlightedTools.has(id)) {
+ highlightedTools.delete(id);
+ this.setState({ highlightedTools });
+ }
+ }
+
+ setDockOptionsEnabled(areDockOptionsEnabled) {
+ this.setState({ areDockOptionsEnabled });
+ }
+
+ setHostTypes(hostTypes) {
+ this.setState({ hostTypes });
+ }
+
+ setCurrentHostType(currentHostType) {
+ this.setState({ currentHostType });
+ }
+
+ setCanCloseToolbox(canCloseToolbox) {
+ this.setState({ canCloseToolbox }, this.updateButtonIds);
+ }
+
+ setIsSplitConsoleActive(isSplitConsoleActive) {
+ this.setState({ isSplitConsoleActive });
+ }
+
+ setDisableAutohide(disableAutohide) {
+ this.setState({ disableAutohide });
+ }
+
+ setPanelDefinitions(panelDefinitions) {
+ this.setState({ panelDefinitions }, this.updateButtonIds);
+ }
+
+ get panelDefinitions() {
+ return this.state.panelDefinitions;
+ }
+
+ setToolboxButtons(toolboxButtons) {
+ // Listen for updates of the checked attribute.
+ this.state.toolboxButtons.forEach(button => {
+ button.off("updatechecked", this.state.checkedButtonsUpdated);
+ });
+ toolboxButtons.forEach(button => {
+ button.on("updatechecked", this.state.checkedButtonsUpdated);
+ });
+
+ const visibleToolboxButtonCount = toolboxButtons.filter(
+ button => button.isVisible
+ ).length;
+
+ this.setState(
+ { toolboxButtons, visibleToolboxButtonCount },
+ this.updateButtonIds
+ );
+ }
+
+ setDebugTargetData(data) {
+ this.setState({ debugTargetData: data });
+ }
+
+ render() {
+ return ToolboxToolbar(Object.assign({}, this.props, this.state));
+ }
+}
+
+module.exports = ToolboxController;
diff --git a/devtools/client/framework/components/ToolboxTab.js b/devtools/client/framework/components/ToolboxTab.js
new file mode 100644
index 0000000000..60f36e3107
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxTab.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { img, button, span } = dom;
+
+class ToolboxTab extends Component {
+ // See toolbox-toolbar propTypes for details on the props used here.
+ static get propTypes() {
+ return {
+ currentToolId: PropTypes.string,
+ focusButton: PropTypes.func,
+ focusedButton: PropTypes.string,
+ highlightedTools: PropTypes.object.isRequired,
+ panelDefinition: PropTypes.object,
+ selectTool: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.renderIcon = this.renderIcon.bind(this);
+ }
+
+ renderIcon(definition) {
+ const { icon } = definition;
+ if (!icon) {
+ return [];
+ }
+ return [
+ img({
+ alt: "",
+ src: icon,
+ }),
+ ];
+ }
+
+ render() {
+ const {
+ panelDefinition,
+ currentToolId,
+ highlightedTools,
+ selectTool,
+ focusedButton,
+ focusButton,
+ } = this.props;
+ const {
+ id,
+ extensionId,
+ tooltip,
+ label,
+ iconOnly,
+ badge,
+ } = panelDefinition;
+ const isHighlighted = id === currentToolId;
+
+ const className = [
+ "devtools-tab",
+ currentToolId === id ? "selected" : "",
+ highlightedTools.has(id) ? "highlighted" : "",
+ iconOnly ? "devtools-tab-icon-only" : "",
+ ].join(" ");
+
+ return button(
+ {
+ className,
+ id: `toolbox-tab-${id}`,
+ "data-id": id,
+ "data-extension-id": extensionId,
+ title: tooltip,
+ type: "button",
+ "aria-pressed": currentToolId === id ? "true" : "false",
+ tabIndex: focusedButton === id ? "0" : "-1",
+ onFocus: () => focusButton(id),
+ onMouseDown: () => selectTool(id, "tab_switch"),
+ onKeyDown: evt => {
+ if (evt.key === "Enter" || evt.key === " ") {
+ selectTool(id, "tab_switch");
+ }
+ },
+ },
+ span({
+ className: "devtools-tab-line",
+ }),
+ ...this.renderIcon(panelDefinition),
+ iconOnly
+ ? null
+ : span(
+ {
+ className: "devtools-tab-label",
+ },
+ label,
+ badge && !isHighlighted
+ ? span(
+ {
+ className: "devtools-tab-badge",
+ },
+ badge
+ )
+ : null
+ )
+ );
+ }
+}
+
+module.exports = ToolboxTab;
diff --git a/devtools/client/framework/components/ToolboxTabs.js b/devtools/client/framework/components/ToolboxTabs.js
new file mode 100644
index 0000000000..435a7f0368
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxTabs.js
@@ -0,0 +1,331 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ Component,
+ createFactory,
+ createRef,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+ ToolboxTabsOrderManager,
+} = require("devtools/client/framework/toolbox-tabs-order-manager");
+
+const { div } = dom;
+
+const ToolboxTab = createFactory(
+ require("devtools/client/framework/components/ToolboxTab")
+);
+
+loader.lazyGetter(this, "MenuButton", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuButton")
+ );
+});
+loader.lazyGetter(this, "MenuItem", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuItem")
+ );
+});
+loader.lazyGetter(this, "MenuList", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuList")
+ );
+});
+
+// 26px is chevron devtools button width.(i.e. tools-chevronmenu)
+const CHEVRON_BUTTON_WIDTH = 26;
+
+class ToolboxTabs extends Component {
+ // See toolbox-toolbar propTypes for details on the props used here.
+ static get propTypes() {
+ return {
+ currentToolId: PropTypes.string,
+ focusButton: PropTypes.func,
+ focusedButton: PropTypes.string,
+ highlightedTools: PropTypes.object,
+ panelDefinitions: PropTypes.array,
+ selectTool: PropTypes.func,
+ toolbox: PropTypes.object,
+ visibleToolboxButtonCount: PropTypes.number.isRequired,
+ L10N: PropTypes.object,
+ onTabsOrderUpdated: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ // Array of overflowed tool id.
+ overflowedTabIds: [],
+ };
+
+ this.wrapperEl = createRef();
+
+ // Map with tool Id and its width size. This lifecycle is out of React's
+ // lifecycle. If a tool is registered, ToolboxTabs will add target tool id
+ // to this map. ToolboxTabs will never remove tool id from this cache.
+ this._cachedToolTabsWidthMap = new Map();
+
+ this._resizeTimerId = null;
+ this.resizeHandler = this.resizeHandler.bind(this);
+
+ const { toolbox, onTabsOrderUpdated, panelDefinitions } = props;
+ this._tabsOrderManager = new ToolboxTabsOrderManager(
+ toolbox,
+ onTabsOrderUpdated,
+ panelDefinitions
+ );
+ }
+
+ componentDidMount() {
+ window.addEventListener("resize", this.resizeHandler);
+ this.updateCachedToolTabsWidthMap();
+ this.updateOverflowedTabs();
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ if (this.shouldUpdateToolboxTabs(this.props, nextProps)) {
+ // Force recalculate and render in this cycle if panel definition has
+ // changed or selected tool has changed.
+ nextState.overflowedTabIds = [];
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.shouldUpdateToolboxTabs(prevProps, this.props)) {
+ this.updateCachedToolTabsWidthMap();
+ this.updateOverflowedTabs();
+ this._tabsOrderManager.setCurrentPanelDefinitions(
+ this.props.panelDefinitions
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.resizeHandler);
+ window.cancelIdleCallback(this._resizeTimerId);
+ this._tabsOrderManager.destroy();
+ }
+
+ /**
+ * Check if two array of ids are the same or not.
+ */
+ equalToolIdArray(prevPanels, nextPanels) {
+ if (prevPanels.length !== nextPanels.length) {
+ return false;
+ }
+
+ // Check panel definitions even if both of array size is same.
+ // For example, the case of changing the tab's order.
+ return prevPanels.join("-") === nextPanels.join("-");
+ }
+
+ /**
+ * Return true if we should update the overflowed tabs.
+ */
+ shouldUpdateToolboxTabs(prevProps, nextProps) {
+ if (
+ prevProps.currentToolId !== nextProps.currentToolId ||
+ prevProps.visibleToolboxButtonCount !==
+ nextProps.visibleToolboxButtonCount
+ ) {
+ return true;
+ }
+
+ const prevPanels = prevProps.panelDefinitions.map(def => def.id);
+ const nextPanels = nextProps.panelDefinitions.map(def => def.id);
+ return !this.equalToolIdArray(prevPanels, nextPanels);
+ }
+
+ /**
+ * Update the Map of tool id and tool tab width.
+ */
+ updateCachedToolTabsWidthMap() {
+ const utils = window.windowUtils;
+ // Force a reflow before calling getBoundingWithoutFlushing on each tab.
+ this.wrapperEl.current.clientWidth;
+
+ for (const tab of this.wrapperEl.current.querySelectorAll(
+ ".devtools-tab"
+ )) {
+ const tabId = tab.id.replace("toolbox-tab-", "");
+ if (!this._cachedToolTabsWidthMap.has(tabId)) {
+ const rect = utils.getBoundsWithoutFlushing(tab);
+ this._cachedToolTabsWidthMap.set(tabId, rect.width);
+ }
+ }
+ }
+
+ /**
+ * Update the overflowed tab array from currently displayed tool tab.
+ * If calculated result is the same as the current overflowed tab array, this
+ * function will not update state.
+ */
+ updateOverflowedTabs() {
+ const toolboxWidth = parseInt(
+ getComputedStyle(this.wrapperEl.current).width,
+ 10
+ );
+ const { currentToolId } = this.props;
+ const enabledTabs = this.props.panelDefinitions.map(def => def.id);
+ let sumWidth = 0;
+ const visibleTabs = [];
+
+ for (const id of enabledTabs) {
+ const width = this._cachedToolTabsWidthMap.get(id);
+ sumWidth += width;
+ if (sumWidth <= toolboxWidth) {
+ visibleTabs.push(id);
+ } else {
+ sumWidth = sumWidth - width + CHEVRON_BUTTON_WIDTH;
+
+ // If toolbox can't display the Chevron, remove the last tool tab.
+ if (sumWidth > toolboxWidth) {
+ const removeTabId = visibleTabs.pop();
+ sumWidth -= this._cachedToolTabsWidthMap.get(removeTabId);
+ }
+ break;
+ }
+ }
+
+ // If the selected tab is in overflowed tabs, insert it into a visible
+ // toolbox.
+ if (
+ !visibleTabs.includes(currentToolId) &&
+ enabledTabs.includes(currentToolId)
+ ) {
+ const selectedToolWidth = this._cachedToolTabsWidthMap.get(currentToolId);
+ while (
+ sumWidth + selectedToolWidth > toolboxWidth &&
+ visibleTabs.length > 0
+ ) {
+ const removingToolId = visibleTabs.pop();
+ const removingToolWidth = this._cachedToolTabsWidthMap.get(
+ removingToolId
+ );
+ sumWidth -= removingToolWidth;
+ }
+
+ // If toolbox width is narrow, toolbox display only chevron menu.
+ // i.e. All tool tabs will overflow.
+ if (sumWidth + selectedToolWidth <= toolboxWidth) {
+ visibleTabs.push(currentToolId);
+ }
+ }
+
+ const willOverflowTabs = enabledTabs.filter(
+ id => !visibleTabs.includes(id)
+ );
+ if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
+ this.setState({ overflowedTabIds: willOverflowTabs });
+ }
+ }
+
+ resizeHandler(evt) {
+ window.cancelIdleCallback(this._resizeTimerId);
+ this._resizeTimerId = window.requestIdleCallback(
+ () => {
+ this.updateOverflowedTabs();
+ },
+ { timeout: 100 }
+ );
+ }
+
+ renderToolsChevronMenuList() {
+ const { panelDefinitions, selectTool } = this.props;
+
+ const items = [];
+ for (const { id, label, icon } of panelDefinitions) {
+ if (this.state.overflowedTabIds.includes(id)) {
+ items.push(
+ MenuItem({
+ key: id,
+ id: "tools-chevron-menupopup-" + id,
+ label,
+ type: "checkbox",
+ onClick: () => {
+ selectTool(id, "tab_switch");
+ },
+ icon,
+ })
+ );
+ }
+ }
+
+ return MenuList({ id: "tools-chevron-menupopup" }, items);
+ }
+
+ /**
+ * Render a button to access overflowed tools, displayed only when the toolbar
+ * presents an overflow.
+ */
+ renderToolsChevronButton() {
+ const { toolbox } = this.props;
+
+ return MenuButton(
+ {
+ id: "tools-chevron-menu-button",
+ menuId: "tools-chevron-menu-button-panel",
+ className: "devtools-tabbar-button tools-chevron-menu",
+ toolboxDoc: toolbox.doc,
+ },
+ this.renderToolsChevronMenuList()
+ );
+ }
+
+ /**
+ * Render all of the tabs, based on the panel definitions and builds out
+ * a toolbox tab for each of them. Will render the chevron button if the
+ * container has an overflow.
+ */
+ render() {
+ const {
+ currentToolId,
+ focusButton,
+ focusedButton,
+ highlightedTools,
+ panelDefinitions,
+ selectTool,
+ } = this.props;
+
+ const tabs = panelDefinitions.map(panelDefinition => {
+ // Don't display overflowed tab.
+ if (!this.state.overflowedTabIds.includes(panelDefinition.id)) {
+ return ToolboxTab({
+ key: panelDefinition.id,
+ currentToolId,
+ focusButton,
+ focusedButton,
+ highlightedTools,
+ panelDefinition,
+ selectTool,
+ });
+ }
+ return null;
+ });
+
+ return div(
+ {
+ className: "toolbox-tabs-wrapper",
+ ref: this.wrapperEl,
+ },
+ div(
+ {
+ className: "toolbox-tabs",
+ onMouseDown: e => this._tabsOrderManager.onMouseDown(e),
+ },
+ tabs,
+ this.state.overflowedTabIds.length > 0
+ ? this.renderToolsChevronButton()
+ : null
+ )
+ );
+ }
+}
+
+module.exports = ToolboxTabs;
diff --git a/devtools/client/framework/components/ToolboxToolbar.js b/devtools/client/framework/components/ToolboxToolbar.js
new file mode 100644
index 0000000000..20eb5f5832
--- /dev/null
+++ b/devtools/client/framework/components/ToolboxToolbar.js
@@ -0,0 +1,501 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { div, button } = dom;
+
+const DebugTargetInfo = createFactory(
+ require("devtools/client/framework/components/DebugTargetInfo")
+);
+const MenuButton = createFactory(
+ require("devtools/client/shared/components/menu/MenuButton")
+);
+const ToolboxTabs = createFactory(
+ require("devtools/client/framework/components/ToolboxTabs")
+);
+
+loader.lazyGetter(this, "MeatballMenu", function() {
+ return createFactory(
+ require("devtools/client/framework/components/MeatballMenu")
+ );
+});
+loader.lazyGetter(this, "MenuItem", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuItem")
+ );
+});
+loader.lazyGetter(this, "MenuList", function() {
+ return createFactory(
+ require("devtools/client/shared/components/menu/MenuList")
+ );
+});
+
+loader.lazyRequireGetter(
+ this,
+ "getUnicodeUrl",
+ "devtools/client/shared/unicode-url",
+ true
+);
+
+/**
+ * This is the overall component for the toolbox toolbar. It is designed to not know how
+ * the state is being managed, and attempts to be as pure as possible. The
+ * ToolboxController component controls the changing state, and passes in everything as
+ * props.
+ */
+class ToolboxToolbar extends Component {
+ static get propTypes() {
+ return {
+ // The currently focused item (for arrow keyboard navigation)
+ // This ID determines the tabindex being 0 or -1.
+ focusedButton: PropTypes.string,
+ // List of command button definitions.
+ toolboxButtons: PropTypes.array,
+ // The id of the currently selected tool, e.g. "inspector"
+ currentToolId: PropTypes.string,
+ // An optionally highlighted tools, e.g. "inspector" (used by ToolboxTabs
+ // component).
+ highlightedTools: PropTypes.instanceOf(Set),
+ // List of tool panel definitions (used by ToolboxTabs component).
+ panelDefinitions: PropTypes.array,
+ // List of possible docking options.
+ hostTypes: PropTypes.arrayOf(
+ PropTypes.shape({
+ position: PropTypes.string.isRequired,
+ switchHost: PropTypes.func.isRequired,
+ })
+ ),
+ // Current docking type. Typically one of the position values in
+ // |hostTypes| but this is not always the case (e.g. for "browsertoolbox").
+ currentHostType: PropTypes.string,
+ // Are docking options enabled? They are not enabled in certain situations
+ // like when the toolbox is opened in a tab.
+ areDockOptionsEnabled: PropTypes.bool,
+ // Do we need to add UI for closing the toolbox? We don't when the
+ // toolbox is undocked, for example.
+ canCloseToolbox: PropTypes.bool,
+ // Is the split console currently visible?
+ isSplitConsoleActive: PropTypes.bool,
+ // Are we disabling the behavior where pop-ups are automatically closed
+ // when clicking outside them?
+ //
+ // This is a tri-state value that may be true/false or undefined where
+ // undefined means that the option is not relevant in this context
+ // (i.e. we're not in a browser toolbox).
+ disableAutohide: PropTypes.bool,
+ // Function to turn the options panel on / off.
+ toggleOptions: PropTypes.func.isRequired,
+ // Function to turn the split console on / off.
+ toggleSplitConsole: PropTypes.func,
+ // Function to turn the disable pop-up autohide behavior on / off.
+ toggleNoAutohide: PropTypes.func,
+ // Function to completely close the toolbox.
+ closeToolbox: PropTypes.func,
+ // Keep a record of what button is focused.
+ focusButton: PropTypes.func,
+ // Hold off displaying the toolbar until enough information is ready for
+ // it to render nicely.
+ canRender: PropTypes.bool,
+ // Localization interface.
+ L10N: PropTypes.object.isRequired,
+ // The devtools toolbox
+ toolbox: PropTypes.object,
+ // Call back function to detect tabs order updated.
+ onTabsOrderUpdated: PropTypes.func.isRequired,
+ // Count of visible toolbox buttons which is used by ToolboxTabs component
+ // to recognize that the visibility of toolbox buttons were changed.
+ // Because in the component we cannot compare the visibility since the
+ // button definition instance in toolboxButtons will be unchanged.
+ visibleToolboxButtonCount: PropTypes.number,
+ // Data to show debug target info, if needed
+ debugTargetData: PropTypes.shape({
+ runtimeInfo: PropTypes.object.isRequired,
+ targetType: PropTypes.string.isRequired,
+ }),
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.hideMenu = this.hideMenu.bind(this);
+ this.createFrameList = this.createFrameList.bind(this);
+ this.highlightFrame = this.highlightFrame.bind(this);
+ this.clickFrameButton = this.clickFrameButton.bind(this);
+ }
+
+ componentDidMount() {
+ this.props.toolbox.on("panel-changed", this.hideMenu);
+ }
+
+ componentWillUnmount() {
+ this.props.toolbox.off("panel-changed", this.hideMenu);
+ }
+
+ hideMenu() {
+ if (this.refs.meatballMenuButton) {
+ this.refs.meatballMenuButton.hideMenu();
+ }
+
+ if (this.refs.frameMenuButton) {
+ this.refs.frameMenuButton.hideMenu();
+ }
+ }
+
+ /**
+ * A little helper function to call renderToolboxButtons for buttons at the start
+ * of the toolbox.
+ */
+ renderToolboxButtonsStart() {
+ return this.renderToolboxButtons(true);
+ }
+
+ /**
+ * A little helper function to call renderToolboxButtons for buttons at the end
+ * of the toolbox.
+ */
+ renderToolboxButtonsEnd() {
+ return this.renderToolboxButtons(false);
+ }
+
+ /**
+ * Render all of the tabs, this takes in a list of toolbox button states. These are plain
+ * objects that have all of the relevant information needed to render the button.
+ * See Toolbox.prototype._createButtonState in devtools/client/framework/toolbox.js for
+ * documentation on this object.
+ *
+ * @param {String} focusedButton - The id of the focused button.
+ * @param {Array} toolboxButtons - Array of objects that define the command buttons.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ * @param {boolean} isStart - Render either the starting buttons, or ending buttons.
+ */
+ renderToolboxButtons(isStart) {
+ const { focusedButton, toolboxButtons, focusButton } = this.props;
+ const visibleButtons = toolboxButtons.filter(command => {
+ const { isVisible, isInStartContainer } = command;
+ return isVisible && (isStart ? isInStartContainer : !isInStartContainer);
+ });
+
+ if (visibleButtons.length === 0) {
+ return null;
+ }
+
+ // The RDM button, if present, should always go last
+ const rdmIndex = visibleButtons.findIndex(
+ button => button.id === "command-button-responsive"
+ );
+ if (rdmIndex !== -1 && rdmIndex !== visibleButtons.length - 1) {
+ const rdm = visibleButtons.splice(rdmIndex, 1)[0];
+ visibleButtons.push(rdm);
+ }
+
+ const renderedButtons = visibleButtons.map(command => {
+ const {
+ id,
+ description,
+ disabled,
+ onClick,
+ isChecked,
+ className: buttonClass,
+ onKeyDown,
+ } = command;
+
+ // If button is frame button, create menu button in order to
+ // use the doorhanger menu.
+ if (id === "command-button-frames") {
+ return this.renderFrameButton(command);
+ }
+
+ if (id === "command-button-errorcount") {
+ return this.renderErrorIcon(command);
+ }
+
+ return button({
+ id,
+ title: description,
+ disabled,
+ className: `devtools-tabbar-button command-button ${buttonClass ||
+ ""} ${isChecked ? "checked" : ""}`,
+ onClick: event => {
+ onClick(event);
+ focusButton(id);
+ },
+ onFocus: () => focusButton(id),
+ tabIndex: id === focusedButton ? "0" : "-1",
+ onKeyDown: event => {
+ onKeyDown(event);
+ },
+ });
+ });
+
+ // Add the appropriate separator, if needed.
+ const children = renderedButtons;
+ if (renderedButtons.length) {
+ if (isStart) {
+ children.push(this.renderSeparator());
+ // For the end group we add a separator *before* the RDM button if it
+ // exists, but only if it is not the only button.
+ } else if (rdmIndex !== -1 && renderedButtons.length > 1) {
+ children.splice(children.length - 1, 0, this.renderSeparator());
+ }
+ }
+
+ return div(
+ { id: `toolbox-buttons-${isStart ? "start" : "end"}` },
+ ...children
+ );
+ }
+
+ renderFrameButton(command) {
+ const { id, isChecked, disabled, description } = command;
+
+ const { toolbox } = this.props;
+
+ return MenuButton(
+ {
+ id,
+ disabled,
+ menuId: id + "-panel",
+ toolboxDoc: toolbox.doc,
+ className: `devtools-tabbar-button command-button ${
+ isChecked ? "checked" : ""
+ }`,
+ ref: "frameMenuButton",
+ title: description,
+ onCloseButton: async () => {
+ // Only try to unhighlight if the inspectorFront has been created already
+ const inspectorFront = toolbox.target.getCachedFront("inspector");
+ if (inspectorFront) {
+ const highlighter = toolbox.getHighlighter();
+ await highlighter.unhighlight();
+ }
+ },
+ },
+ this.createFrameList
+ );
+ }
+
+ renderErrorIcon(command) {
+ let { errorCount, id } = command;
+
+ if (!errorCount) {
+ return null;
+ }
+
+ if (errorCount > 99) {
+ errorCount = "99+";
+ }
+
+ return button(
+ {
+ id,
+ className: "devtools-tabbar-button command-button toolbox-error",
+ onClick: () => {
+ if (this.props.currentToolId !== "webconsole") {
+ this.props.toolbox.openSplitConsole();
+ }
+ },
+ title:
+ this.props.currentToolId !== "webconsole"
+ ? this.props.L10N.getStr("toolbox.errorCountButton.tooltip")
+ : null,
+ },
+ errorCount
+ );
+ }
+
+ clickFrameButton(event) {
+ const { toolbox } = this.props;
+ toolbox.onSelectFrame(event.target.id);
+ }
+
+ highlightFrame(id) {
+ if (!id) {
+ return;
+ }
+
+ const { toolbox } = this.props;
+ toolbox.onHighlightFrame(id);
+ }
+
+ createFrameList() {
+ const { toolbox } = this.props;
+ if (toolbox.frameMap.size < 1) {
+ return null;
+ }
+
+ const items = [];
+ toolbox.frameMap.forEach((frame, index) => {
+ const label = toolbox.target.isWebExtension
+ ? toolbox.target.getExtensionPathName(frame.url)
+ : getUnicodeUrl(frame.url);
+ items.push(
+ MenuItem({
+ id: frame.id.toString(),
+ key: "toolbox-frame-key-" + frame.id,
+ label,
+ checked: frame.id === toolbox.selectedFrameId,
+ onClick: this.clickFrameButton,
+ })
+ );
+ });
+
+ return MenuList(
+ {
+ id: "toolbox-frame-menu",
+ onHighlightedChildChange: this.highlightFrame,
+ },
+ items
+ );
+ }
+
+ /**
+ * Render a separator.
+ */
+ renderSeparator() {
+ return div({ className: "devtools-separator" });
+ }
+
+ /**
+ * Render the toolbox control buttons. The following props are expected:
+ *
+ * @param {string} props.focusedButton
+ * The id of the focused button.
+ * @param {string} props.currentToolId
+ * The id of the currently selected tool, e.g. "inspector".
+ * @param {Object[]} props.hostTypes
+ * Array of host type objects.
+ * @param {string} props.hostTypes[].position
+ * Position name.
+ * @param {Function} props.hostTypes[].switchHost
+ * Function to switch the host.
+ * @param {string} props.currentHostType
+ * The current docking configuration.
+ * @param {boolean} props.areDockOptionsEnabled
+ * They are not enabled in certain situations like when the toolbox is
+ * in a tab.
+ * @param {boolean} props.canCloseToolbox
+ * Do we need to add UI for closing the toolbox? We don't when the
+ * toolbox is undocked, for example.
+ * @param {boolean} props.isSplitConsoleActive
+ * Is the split console currently visible?
+ * toolbox is undocked, for example.
+ * @param {boolean|undefined} props.disableAutohide
+ * Are we disabling the behavior where pop-ups are automatically
+ * closed when clicking outside them?
+ * (Only defined for the browser toolbox.)
+ * @param {Function} props.selectTool
+ * Function to select a tool based on its id.
+ * @param {Function} props.toggleOptions
+ * Function to turn the options panel on / off.
+ * @param {Function} props.toggleSplitConsole
+ * Function to turn the split console on / off.
+ * @param {Function} props.toggleNoAutohide
+ * Function to turn the disable pop-up autohide behavior on / off.
+ * @param {Function} props.closeToolbox
+ * Completely close the toolbox.
+ * @param {Function} props.focusButton
+ * Keep a record of the currently focused button.
+ * @param {Object} props.L10N
+ * Localization interface.
+ * @param {Object} props.toolbox
+ * The devtools toolbox. Used by the MenuButton component to display
+ * the menu popup.
+ * @param {Object} refs
+ * The components refs object. Used to keep a reference to the MenuButton
+ * for the meatball menu so that we can tell it to resize its contents
+ * when they change.
+ */
+ renderToolboxControls() {
+ const {
+ focusedButton,
+ canCloseToolbox,
+ closeToolbox,
+ focusButton,
+ L10N,
+ toolbox,
+ } = this.props;
+
+ const meatballMenuButtonId = "toolbox-meatball-menu-button";
+
+ const meatballMenuButton = MenuButton(
+ {
+ id: meatballMenuButtonId,
+ menuId: meatballMenuButtonId + "-panel",
+ toolboxDoc: toolbox.doc,
+ onFocus: () => focusButton(meatballMenuButtonId),
+ className: "devtools-tabbar-button",
+ title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
+ tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
+ ref: "meatballMenuButton",
+ },
+ MeatballMenu({
+ ...this.props,
+ hostTypes: this.props.areDockOptionsEnabled ? this.props.hostTypes : [],
+ onResize: () => {
+ this.refs.meatballMenuButton.resizeContent();
+ },
+ })
+ );
+
+ const closeButtonId = "toolbox-close";
+
+ const closeButton = canCloseToolbox
+ ? button({
+ id: closeButtonId,
+ onFocus: () => focusButton(closeButtonId),
+ className: "devtools-tabbar-button",
+ title: L10N.getStr("toolbox.closebutton.tooltip"),
+ onClick: () => closeToolbox(),
+ tabIndex: focusedButton === "toolbox-close" ? "0" : "-1",
+ })
+ : null;
+
+ return div({ id: "toolbox-controls" }, meatballMenuButton, closeButton);
+ }
+
+ /**
+ * The render function is kept fairly short for maintainability. See the individual
+ * render functions for how each of the sections is rendered.
+ */
+ render() {
+ const { L10N, debugTargetData, toolbox } = this.props;
+ const classnames = ["devtools-tabbar"];
+ const startButtons = this.renderToolboxButtonsStart();
+ const endButtons = this.renderToolboxButtonsEnd();
+
+ if (!startButtons) {
+ classnames.push("devtools-tabbar-has-start");
+ }
+ if (!endButtons) {
+ classnames.push("devtools-tabbar-has-end");
+ }
+
+ const toolbar = this.props.canRender
+ ? div(
+ {
+ className: classnames.join(" "),
+ },
+ startButtons,
+ ToolboxTabs(this.props),
+ endButtons,
+ this.renderToolboxControls()
+ )
+ : div({ className: classnames.join(" ") });
+
+ const debugTargetInfo = debugTargetData
+ ? DebugTargetInfo({ debugTargetData, L10N, toolbox })
+ : null;
+
+ return div({}, debugTargetInfo, toolbar);
+ }
+}
+
+module.exports = ToolboxToolbar;
diff --git a/devtools/client/framework/components/moz.build b/devtools/client/framework/components/moz.build
new file mode 100644
index 0000000000..9499fa6294
--- /dev/null
+++ b/devtools/client/framework/components/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+DevToolsModules(
+ "DebugTargetErrorPage.js",
+ "DebugTargetInfo.js",
+ "MeatballMenu.js",
+ "ToolboxController.js",
+ "ToolboxTab.js",
+ "ToolboxTabs.js",
+ "ToolboxToolbar.js",
+)