This patch will apply the grid layout to the toolbar. If devtools's width is narrow, we expected that devtool display chevron and the controls elements only(i.e. chevron and meatball and close button). In order to display these button, a wrapper of toolbar will use grid layout. Basically, this patch define grid columns as follow: ------------------------------------------------ | Picker | tooltabs | commands | controls | | auto | 26px ~ 1fr | auto | max-content| ------------------------------------------------ We can disable the picker and command buttons, in this case, a toolbar will stretch the tooltabs width by using grid-column-start/end. MozReview-Commit-ID: ByY2qt2xhAg
457 lines
15 KiB
JavaScript
457 lines
15 KiB
JavaScript
/* 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 {openWebLink} = require("devtools/client/shared/link");
|
|
|
|
const Menu = require("devtools/client/framework/menu");
|
|
const MenuItem = require("devtools/client/framework/menu-item");
|
|
const ToolboxTabs = createFactory(require("devtools/client/framework/components/ToolboxTabs"));
|
|
|
|
/**
|
|
* 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. when it is "custom").
|
|
currentHostType: PropTypes.string,
|
|
// 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 select a tool based on its id.
|
|
selectTool: PropTypes.func,
|
|
// 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,
|
|
// 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,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The render function is kept fairly short for maintainability. See the individual
|
|
* render functions for how each of the sections is rendered.
|
|
*/
|
|
render() {
|
|
let classnames = ["devtools-tabbar"];
|
|
let startButtons = renderToolboxButtonsStart(this.props);
|
|
let endButtons = renderToolboxButtonsEnd(this.props);
|
|
|
|
if (!startButtons) {
|
|
classnames.push("devtools-tabbar-has-start");
|
|
}
|
|
if (!endButtons) {
|
|
classnames.push("devtools-tabbar-has-end");
|
|
}
|
|
|
|
return this.props.canRender
|
|
? (
|
|
div(
|
|
{
|
|
className: classnames.join(" ")
|
|
},
|
|
startButtons,
|
|
ToolboxTabs(this.props),
|
|
endButtons,
|
|
renderToolboxControls(this.props)
|
|
)
|
|
)
|
|
: div({ className: classnames.join(" ") });
|
|
}
|
|
}
|
|
|
|
module.exports = ToolboxToolbar;
|
|
|
|
/**
|
|
* A little helper function to call renderToolboxButtons for buttons at the start
|
|
* of the toolbox.
|
|
*/
|
|
function renderToolboxButtonsStart(props) {
|
|
return renderToolboxButtons(props, true);
|
|
}
|
|
|
|
/**
|
|
* A little helper function to call renderToolboxButtons for buttons at the end
|
|
* of the toolbox.
|
|
*/
|
|
function renderToolboxButtonsEnd(props) {
|
|
return renderToolboxButtons(props, 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.
|
|
*/
|
|
function renderToolboxButtons({focusedButton, toolboxButtons, focusButton}, isStart) {
|
|
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;
|
|
return button({
|
|
id,
|
|
title: description,
|
|
disabled,
|
|
className: (
|
|
"command-button devtools-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.
|
|
let children = renderedButtons;
|
|
if (renderedButtons.length) {
|
|
if (isStart) {
|
|
children.push(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 && visibleButtons.length > 1) {
|
|
children.splice(
|
|
children.length - 1,
|
|
0,
|
|
renderSeparator()
|
|
);
|
|
}
|
|
}
|
|
|
|
return div({id: `toolbox-buttons-${isStart ? "start" : "end"}`}, ...children);
|
|
}
|
|
|
|
/**
|
|
* Render a separator.
|
|
*/
|
|
function renderSeparator() {
|
|
return div({className: "devtools-separator"});
|
|
}
|
|
|
|
/**
|
|
* Render the toolbox control buttons. The following props are expected:
|
|
*
|
|
* @param {string} focusedButton
|
|
* The id of the focused button.
|
|
* @param {Object[]} hostTypes
|
|
* Array of host type objects.
|
|
* @param {string} hostTypes[].position
|
|
* Position name.
|
|
* @param {Function} hostTypes[].switchHost
|
|
* Function to switch the host.
|
|
* @param {string} currentHostType
|
|
* The current docking configuration.
|
|
* @param {boolean} areDockOptionsEnabled
|
|
* They are not enabled in certain situations like when they are in the
|
|
* WebIDE.
|
|
* @param {boolean} canCloseToolbox
|
|
* Do we need to add UI for closing the toolbox? We don't when the
|
|
* toolbox is undocked, for example.
|
|
* @param {boolean} isSplitConsoleActive
|
|
* Is the split console currently visible?
|
|
* toolbox is undocked, for example.
|
|
* @param {boolean|undefined} disableAutohide
|
|
* Are we disabling the behavior where pop-ups are automatically
|
|
* closed when clicking outside them?
|
|
* (Only defined for the browser toolbox.)
|
|
* @param {Function} selectTool
|
|
* Function to select a tool based on its id.
|
|
* @param {Function} toggleOptions
|
|
* Function to turn the options panel on / off.
|
|
* @param {Function} toggleSplitConsole
|
|
* Function to turn the split console on / off.
|
|
* @param {Function} toggleNoAutohide
|
|
* Function to turn the disable pop-up autohide behavior on / off.
|
|
* @param {Function} closeToolbox
|
|
* Completely close the toolbox.
|
|
* @param {Function} focusButton
|
|
* Keep a record of the currently focused button.
|
|
* @param {Object} L10N
|
|
* Localization interface.
|
|
*/
|
|
function renderToolboxControls(props) {
|
|
const {
|
|
focusedButton,
|
|
closeToolbox,
|
|
hostTypes,
|
|
focusButton,
|
|
L10N,
|
|
areDockOptionsEnabled,
|
|
canCloseToolbox,
|
|
} = props;
|
|
|
|
const meatballMenuButtonId = "toolbox-meatball-menu-button";
|
|
|
|
const meatballMenuButton = button({
|
|
id: meatballMenuButtonId,
|
|
onFocus: () => focusButton(meatballMenuButtonId),
|
|
className: "devtools-button",
|
|
title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
|
|
onClick: evt => {
|
|
showMeatballMenu(evt.target, {
|
|
...props,
|
|
hostTypes: areDockOptionsEnabled ? hostTypes : [],
|
|
});
|
|
},
|
|
tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
|
|
});
|
|
|
|
const closeButtonId = "toolbox-close";
|
|
|
|
const closeButton = canCloseToolbox
|
|
? button({
|
|
id: closeButtonId,
|
|
onFocus: () => focusButton(closeButtonId),
|
|
className: "devtools-button",
|
|
title: L10N.getStr("toolbox.closebutton.tooltip"),
|
|
onClick: () => {
|
|
closeToolbox();
|
|
},
|
|
tabIndex: focusedButton === "toolbox-close" ? "0" : "-1",
|
|
})
|
|
: null;
|
|
|
|
return div({id: "toolbox-controls"},
|
|
meatballMenuButton,
|
|
closeButton
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Display the "..." menu (affectionately known as the meatball menu).
|
|
*
|
|
* @param {Object} menuButton
|
|
* The <button> element from which the menu should pop out. The geometry
|
|
* of this element is used to position the menu.
|
|
* @param {Object} props
|
|
* Properties as described below.
|
|
* @param {string} props.currentToolId
|
|
* The id of the currently selected tool.
|
|
* @param {Object[]} props.hostTypes
|
|
* Array of host type objects.
|
|
* This array will be empty if we shouldn't shouldn't show any dock
|
|
* options.
|
|
* @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} isSplitConsoleActive
|
|
* Is the split console currently visible?
|
|
* @param {boolean|undefined} disableAutohide
|
|
* Are we disabling the behavior where pop-ups are automatically
|
|
* closed when clicking outside them.
|
|
* (Only defined for the browser toolbox.)
|
|
* @param {Function} selectTool
|
|
* Function to select a tool based on its id.
|
|
* @param {Function} toggleOptions
|
|
* Function to turn the options panel on / off.
|
|
* @param {Function} toggleSplitConsole
|
|
* Function to turn the split console on / off.
|
|
* @param {Function} toggleNoAutohide
|
|
* Function to turn the disable pop-up autohide behavior on / off.
|
|
* @param {Object} props.L10N
|
|
* Localization interface.
|
|
* @param {Object} props.toolbox
|
|
* The devtools toolbox. Used by the Menu component to determine which
|
|
* document to use.
|
|
*/
|
|
function showMeatballMenu(
|
|
menuButton,
|
|
{
|
|
currentToolId,
|
|
hostTypes,
|
|
currentHostType,
|
|
isSplitConsoleActive,
|
|
disableAutohide,
|
|
toggleOptions,
|
|
toggleSplitConsole,
|
|
toggleNoAutohide,
|
|
L10N,
|
|
toolbox,
|
|
}
|
|
) {
|
|
const menu = new Menu({ id: "toolbox-meatball-menu" });
|
|
|
|
// Dock options
|
|
for (const hostType of hostTypes) {
|
|
const l10nkey =
|
|
hostType.position === "window"
|
|
? "separateWindow"
|
|
: hostType.position;
|
|
menu.append(
|
|
new MenuItem({
|
|
id: `toolbox-meatball-menu-dock-${hostType.position}`,
|
|
label: L10N.getStr(`toolbox.meatballMenu.dock.${l10nkey}.label`),
|
|
click: () => hostType.switchHost(),
|
|
type: "checkbox",
|
|
checked: hostType.position === currentHostType,
|
|
})
|
|
);
|
|
}
|
|
|
|
if (menu.items.length) {
|
|
menu.append(new MenuItem({ type: "separator" }));
|
|
}
|
|
|
|
// Split console
|
|
if (currentToolId !== "webconsole") {
|
|
menu.append(new MenuItem({
|
|
id: "toolbox-meatball-menu-splitconsole",
|
|
label: L10N.getStr(
|
|
`toolbox.meatballMenu.${
|
|
isSplitConsoleActive ? "hideconsole" : "splitconsole"
|
|
}.label`
|
|
),
|
|
accelerator: "Esc",
|
|
click: toggleSplitConsole,
|
|
}));
|
|
}
|
|
|
|
// Disable pop-up autohide
|
|
//
|
|
// If |disableAutohide| is undefined, it means this feature is not available
|
|
// in this context.
|
|
if (typeof disableAutohide !== "undefined") {
|
|
menu.append(new MenuItem({
|
|
id: "toolbox-meatball-menu-noautohide",
|
|
label: L10N.getStr("toolbox.meatballMenu.noautohide.label"),
|
|
type: "checkbox",
|
|
checked: disableAutohide,
|
|
click: toggleNoAutohide,
|
|
}));
|
|
}
|
|
|
|
// Settings
|
|
menu.append(new MenuItem({
|
|
id: "toolbox-meatball-menu-settings",
|
|
label: L10N.getStr("toolbox.meatballMenu.settings.label"),
|
|
accelerator: L10N.getStr("toolbox.help.key"),
|
|
click: () => toggleOptions(),
|
|
}));
|
|
|
|
if (menu.items.length) {
|
|
menu.append(new MenuItem({ type: "separator" }));
|
|
}
|
|
|
|
// Getting started
|
|
menu.append(new MenuItem({
|
|
id: "toolbox-meatball-menu-documentation",
|
|
label: L10N.getStr("toolbox.meatballMenu.documentation.label"),
|
|
click: () => {
|
|
openWebLink(
|
|
"https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu");
|
|
},
|
|
}));
|
|
|
|
// Give feedback
|
|
menu.append(new MenuItem({
|
|
id: "toolbox-meatball-menu-community",
|
|
label: L10N.getStr("toolbox.meatballMenu.community.label"),
|
|
click: () => {
|
|
openWebLink(
|
|
"https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu");
|
|
},
|
|
}));
|
|
|
|
const rect = menuButton.getBoundingClientRect();
|
|
const screenX = menuButton.ownerDocument.defaultView.mozInnerScreenX;
|
|
const screenY = menuButton.ownerDocument.defaultView.mozInnerScreenY;
|
|
|
|
// Display the popup below the button.
|
|
menu.popupWithZoom(rect.left + screenX, rect.bottom + screenY, toolbox);
|
|
}
|