Source code
Revision control
Copy as Markdown
Other Tools
/* 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,
"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,
isToggle,
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,
"aria-pressed": !isToggle ? null : isChecked,
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);
},
onContextMenu: event => {
const menu = command.getContextMenu();
if (!menu) {
return;
}
event.preventDefault();
event.stopPropagation();
menu.popup(event.screenX, event.screenY, window.parent.document);
},
});
});
// 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+";
}
const errorIconTooltip = this.props.toolbox.isSplitConsoleEnabled()
? this.props.L10N.getStr("toolbox.errorCountButton.tooltip")
: this.props.L10N.getStr("toolbox.errorCountButtonConsoleTab.tooltip");
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" ? errorIconTooltip : 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 => {
const label = toolbox.commands.descriptorFront.isWebExtensionDescriptor
? toolbox.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;