diff options
Diffstat (limited to 'devtools/client/framework/components/ToolboxToolbar.js')
-rw-r--r-- | devtools/client/framework/components/ToolboxToolbar.js | 545 |
1 files changed, 545 insertions, 0 deletions
diff --git a/devtools/client/framework/components/ToolboxToolbar.js b/devtools/client/framework/components/ToolboxToolbar.js new file mode 100644 index 0000000000..4b3cccc867 --- /dev/null +++ b/devtools/client/framework/components/ToolboxToolbar.js @@ -0,0 +1,545 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { div, button } = dom; +const MenuButton = createFactory( + require("resource://devtools/client/shared/components/menu/MenuButton.js") +); +const ToolboxTabs = createFactory( + require("resource://devtools/client/framework/components/ToolboxTabs.js") +); +loader.lazyGetter(this, "MeatballMenu", function () { + return createFactory( + require("resource://devtools/client/framework/components/MeatballMenu.js") + ); +}); +loader.lazyGetter(this, "MenuItem", function () { + return createFactory( + require("resource://devtools/client/shared/components/menu/MenuItem.js") + ); +}); +loader.lazyGetter(this, "MenuList", function () { + return createFactory( + require("resource://devtools/client/shared/components/menu/MenuList.js") + ); +}); +loader.lazyGetter(this, "LocalizationProvider", function () { + return createFactory( + require("resource://devtools/client/shared/vendor/fluent-react.js") + .LocalizationProvider + ); +}); +loader.lazyGetter(this, "DebugTargetInfo", () => + createFactory( + require("resource://devtools/client/framework/components/DebugTargetInfo.js") + ) +); +loader.lazyGetter(this, "ChromeDebugToolbar", () => + createFactory( + require("resource://devtools/client/framework/components/ChromeDebugToolbar.js") + ) +); + +loader.lazyRequireGetter( + this, + "getUnicodeUrl", + "resource://devtools/client/shared/unicode-url.js", + 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, + // Are we displaying the window always on top? + // + // 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 local web extension toolbox). + alwaysOnTop: PropTypes.bool, + // Is the toolbox currently focused? + // + // This will only be defined when alwaysOnTop is true. + focusedState: 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 turn the always on top behavior on / off. + toggleAlwaysOnTop: 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, + descriptorType: PropTypes.string.isRequired, + }), + // The loaded Fluent localization bundles. + fluentBundles: PropTypes.array.isRequired, + }; + } + + constructor(props) { + super(props); + + this.hideMenu = this.hideMenu.bind(this); + this.createFrameList = this.createFrameList.bind(this); + this.highlightFrame = this.highlightFrame.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 + ); + } + + highlightFrame(id) { + const { toolbox } = this.props; + if (!id) { + return; + } + + 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); + + const item = MenuItem({ + id: frame.id.toString(), + key: "toolbox-frame-key-" + frame.id, + label, + checked: frame.id === toolbox.selectedFrameId, + onClick: () => toolbox.onIframePickerFrameSelected(frame.id), + }); + + // Always put the top level frame at the top + if (frame.isTopLevel) { + items.unshift(item); + } else { + items.push(item); + } + }); + + 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.toggleAlwaysOnTop + * Function to turn the always on top 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, fluentBundles } = 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({ + alwaysOnTop: this.props.alwaysOnTop, + focusedState: this.props.focusedState, + toggleAlwaysOnTop: this.props.toggleAlwaysOnTop, + debugTargetData, + L10N, + toolbox, + }) + : null; + + // Display the toolbar in the MBT and about:debugging MBT if we have server support for it. + const chromeDebugToolbar = toolbox.commands.targetCommand.descriptorFront + .isBrowserProcessDescriptor + ? ChromeDebugToolbar() + : null; + + return LocalizationProvider( + { bundles: fluentBundles }, + div({}, chromeDebugToolbar, debugTargetInfo, toolbar) + ); + } +} + +module.exports = ToolboxToolbar; |