diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/framework/components | |
parent | Initial commit. (diff) | |
download | firefox-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.css | 21 | ||||
-rw-r--r-- | devtools/client/framework/components/DebugTargetErrorPage.js | 47 | ||||
-rw-r--r-- | devtools/client/framework/components/DebugTargetInfo.js | 344 | ||||
-rw-r--r-- | devtools/client/framework/components/MeatballMenu.js | 241 | ||||
-rw-r--r-- | devtools/client/framework/components/ToolboxController.js | 202 | ||||
-rw-r--r-- | devtools/client/framework/components/ToolboxTab.js | 110 | ||||
-rw-r--r-- | devtools/client/framework/components/ToolboxTabs.js | 331 | ||||
-rw-r--r-- | devtools/client/framework/components/ToolboxToolbar.js | 501 | ||||
-rw-r--r-- | devtools/client/framework/components/moz.build | 16 |
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", +) |