--- a/addon-sdk/source/test/test-dev-panel.js
+++ b/addon-sdk/source/test/test-dev-panel.js
@@ -22,21 +22,23 @@ const iconURI = "data:image/png;base64,i
const makeHTML = fn =>
"data:text/html;charset=utf-8,<script>(" + fn + ")();</script>";
const test = function(unit) {
return function*(assert) {
assert.isRendered = (panel, toolbox) => {
const doc = toolbox.doc;
- assert.ok(doc.querySelector("[value='" + panel.label + "']"),
- "panel.label is found in the developer toolbox DOM");
- assert.ok(doc.querySelector("[tooltiptext='" + panel.tooltip + "']"),
- "panel.tooltip is found in the developer toolbox DOM");
-
+ assert.ok(Array.from(doc.querySelectorAll(".devtools-tab"))
+ .find(el => el.textContent === panel.label),
+ "panel.label is found in the developer toolbox DOM " + panel.label);
+ if (panel.tooltip) {
+ assert.ok(doc.querySelector("[title='" + panel.tooltip + "']"),
+ `panel.tooltip is found in the developer toolbox DOM "${panel.tooltip}"`);
+ }
assert.ok(doc.querySelector("#toolbox-panel-" + panel.id),
"toolbar panel with a matching id is present");
};
yield* unit(assert);
};
};
--- a/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
@@ -12,38 +12,38 @@ const ADDON_PATH = "addon3.xpi";
var gAddon, gClient, gThreadClient, gDebugger, gSources;
var PREFS = [
["devtools.canvasdebugger.enabled", true],
["devtools.shadereditor.enabled", true],
["devtools.performance.enabled", true],
["devtools.netmonitor.enabled", true],
["devtools.scratchpad.enabled", true]
];
+
function test() {
Task.spawn(function* () {
// Store and enable all optional dev tools panels
yield pushPrefs(...PREFS);
let addon = yield addTemporaryAddon(ADDON_PATH);
let addonDebugger = yield initAddonDebugger(ADDON_ID);
// Check only valid tabs are shown
- let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children;
+ let tabs = addonDebugger.frame.contentDocument.querySelectorAll(".toolbox-tabs button")
+
let expectedTabs = ["webconsole", "jsdebugger", "scratchpad"];
is(tabs.length, expectedTabs.length, "displaying only " + expectedTabs.length + " tabs in addon debugger");
Array.forEach(tabs, (tab, i) => {
let toolName = expectedTabs[i];
- is(tab.getAttribute("toolid"), toolName, "displaying " + toolName);
+ is(tab.getAttribute("data-id"), toolName, "displaying " + toolName);
});
// Check no toolbox buttons are shown
- let buttons = addonDebugger.frame.contentDocument.getElementById("toolbox-buttons").children;
- Array.forEach(buttons, (btn, i) => {
- is(btn.hidden, true, "no toolbox buttons for the addon debugger -- " + btn.className);
- });
+ let buttons = addonDebugger.frame.contentDocument.querySelectorAll("#toolbox-buttons-end button");
+ is(buttons.length, 0, "no toolbox buttons for the addon debugger");
yield addonDebugger.destroy();
yield removeAddon(addon);
finish();
});
}
--- a/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js
@@ -30,28 +30,24 @@ function test() {
}
function testPause() {
is(gDebugger.gThreadClient.paused, false,
"Should be running after starting test.");
gDebugger.gThreadClient.addOneTimeListener("paused", () => {
gToolbox.selectTool("webconsole").then(() => {
- ok(gToolboxTab.hasAttribute("highlighted") &&
- gToolboxTab.getAttribute("highlighted") == "true",
+ ok(gToolboxTab.classList.contains("highlighted"),
"The highlighted class is present");
- ok(!gToolboxTab.hasAttribute("selected") ||
- gToolboxTab.getAttribute("selected") != "true",
+ ok(!gToolboxTab.classList.contains("selected"),
"The tab is not selected");
}).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
- ok(gToolboxTab.hasAttribute("highlighted") &&
- gToolboxTab.getAttribute("highlighted") == "true",
+ ok(gToolboxTab.classList.contains("highlighted"),
"The highlighted class is present");
- ok(gToolboxTab.hasAttribute("selected") &&
- gToolboxTab.getAttribute("selected") == "true",
+ ok(gToolboxTab.classList.contains("selected"),
"...and the tab is selected, so the glow will not be present.");
}).then(testResume);
});
EventUtils.sendMouseEvent({ type: "mousedown" },
gDebugger.document.getElementById("resume"),
gDebugger);
@@ -61,18 +57,17 @@ function testPause() {
});
}
function testResume() {
gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
gToolbox.selectTool("webconsole").then(() => {
ok(!gToolboxTab.classList.contains("highlighted"),
"The highlighted class is not present now after the resume");
- ok(!gToolboxTab.hasAttribute("selected") ||
- gToolboxTab.getAttribute("selected") != "true",
+ ok(!gToolboxTab.classList.contains("selected"),
"The tab is not selected");
}).then(() => closeDebuggerAndFinish(gPanel));
});
EventUtils.sendMouseEvent({ type: "mousedown" },
gDebugger.document.getElementById("resume"),
gDebugger);
}
--- a/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js
@@ -74,46 +74,40 @@ add_task(function *() {
yield onTabSelect;
if (toolbox.hostType != Toolbox.HostType.WINDOW) {
is(gBrowser.selectedTab, tab,
"Debugger's tab got selected.");
}
yield toolbox.selectTool("webconsole");
- ok(toolboxTab.hasAttribute("highlighted") &&
- toolboxTab.getAttribute("highlighted") == "true",
+ ok(toolboxTab.classList.contains("highlighted"),
"The highlighted class is present");
- ok(!toolboxTab.hasAttribute("selected") ||
- toolboxTab.getAttribute("selected") != "true",
+ ok(!toolboxTab.classList.contains("selected"),
"The tab is not selected");
yield toolbox.selectTool("jsdebugger");
- ok(toolboxTab.hasAttribute("highlighted") &&
- toolboxTab.getAttribute("highlighted") == "true",
+ ok(toolboxTab.classList.contains("highlighted"),
"The highlighted class is present");
- ok(toolboxTab.hasAttribute("selected") &&
- toolboxTab.getAttribute("selected") == "true",
+ ok(toolboxTab.classList.contains("selected"),
"...and the tab is selected, so the glow will not be present.");
}
function* testResume() {
let onPaused = waitForEvent(panelWin.gThreadClient, "resumed");
EventUtils.sendMouseEvent({ type: "mousedown" },
panelWin.document.getElementById("resume"),
panelWin);
yield onPaused;
yield toolbox.selectTool("webconsole");
- ok(!toolboxTab.hasAttribute("highlighted") ||
- toolboxTab.getAttribute("highlighted") != "true",
+ ok(!toolboxTab.classList.contains("highlighted"),
"The highlighted class is not present now after the resume");
- ok(!toolboxTab.hasAttribute("selected") ||
- toolboxTab.getAttribute("selected") != "true",
+ ok(!toolboxTab.classList.contains("selected"),
"The tab is not selected");
}
});
registerCleanupFunction(function () {
// Revert to the default toolbox host, so that the following tests proceed
// normally and not inside a non-default host.
Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
--- a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -43,17 +43,17 @@ add_task(function* () {
done();
}
});
});
ok(toolbox.win.parent.document.title.includes(WORKER_URL),
"worker URL in host title");
let toolTabs = toolbox.doc.querySelectorAll(".devtools-tab");
- let activeTools = [...toolTabs].map(tab=>tab.getAttribute("toolid"));
+ let activeTools = [...toolTabs].map(tab=>tab.getAttribute("data-id"));
is(activeTools.join(","), "webconsole,jsdebugger,scratchpad,options",
"Correct set of tools supported by worker");
terminateWorkerInTab(tab, WORKER_URL);
yield waitForWorkerClose(workerClient);
yield close(client);
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -465,16 +465,21 @@ exports.defaultThemes = [
Tools.lightTheme,
Tools.firebugTheme,
];
// White-list buttons that can be toggled to prevent adding prefs for
// addons that have manually inserted toolbarbuttons into DOM.
// (By default, supported target is only local tab)
exports.ToolboxButtons = [
+ { id: "command-button-pick",
+ isTargetSupported: target => {
+ return target.activeTab && target.activeTab.traits.frames;
+ }
+ },
{ id: "command-button-frames",
isTargetSupported: target => {
return target.activeTab && target.activeTab.traits.frames;
}
},
{ id: "command-button-splitconsole",
isTargetSupported: target => !target.isAddon },
{ id: "command-button-responsive" },
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/moz.build
@@ -0,0 +1,12 @@
+# -*- 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(
+ 'toolbox-controller.js',
+ 'toolbox-tab.js',
+ 'toolbox-toolbar.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/toolbox-controller.js
@@ -0,0 +1,151 @@
+/* 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 {createClass, createFactory} = require("devtools/client/shared/vendor/react");
+const ToolboxToolbar = createFactory(require("devtools/client/framework/components/toolbox-toolbar"));
+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.
+ */
+module.exports = createClass({
+ displayName: "ToolboxController",
+
+ getInitialState() {
+ // See the ToolboxToolbar propTypes for documentation on each of these items in state,
+ // and for the defintions of the props that are expected to be passed in.
+ return {
+ focusedButton: ELEMENT_PICKER_ID,
+ currentToolId: null,
+ canRender: false,
+ highlightedTool: "",
+ areDockButtonsEnabled: true,
+ panelDefinitions: [],
+ hostTypes: [],
+ canCloseToolbox: true,
+ toolboxButtons: [],
+ buttonIds: [],
+ checkedButtonsUpdated: () => {
+ this.forceUpdate();
+ }
+ };
+ },
+
+ 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 {panelDefinitions, toolboxButtons, optionsPanel, hostTypes,
+ 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),
+ optionsPanel ? optionsPanel.id : null,
+ ...hostTypes.map(({position}) => "toolbox-dock-" + position),
+ canCloseToolbox ? "toolbox-close" : null
+ ].filter(id => id)
+ });
+
+ this.updateFocusedButton();
+ },
+
+ updateFocusedButton() {
+ this.setFocusedButton(this.state.focusedButton);
+ },
+
+ setFocusedButton(focusedButton) {
+ const {buttonIds} = this.state;
+
+ this.setState({
+ focusedButton: focusedButton && buttonIds.includes(focusedButton)
+ ? focusedButton
+ : buttonIds[0]
+ });
+ },
+
+ setCurrentToolId(currentToolId) {
+ this.setState({currentToolId});
+ // Also set the currently focused button to this tool.
+ this.setFocusedButton(currentToolId);
+ },
+
+ setCanRender() {
+ this.setState({ canRender: true });
+ this.updateButtonIds();
+ },
+
+ setOptionsPanel(optionsPanel) {
+ this.setState({ optionsPanel });
+ this.updateButtonIds();
+ },
+
+ highlightTool(highlightedTool) {
+ this.setState({ highlightedTool });
+ },
+
+ unhighlightTool(id) {
+ if (this.state.highlightedTool === id) {
+ this.setState({ highlightedTool: "" });
+ }
+ },
+
+ setDockButtonsEnabled(areDockButtonsEnabled) {
+ this.setState({ areDockButtonsEnabled });
+ this.updateButtonIds();
+ },
+
+ setHostTypes(hostTypes) {
+ this.setState({ hostTypes });
+ this.updateButtonIds();
+ },
+
+ setCanCloseToolbox(canCloseToolbox) {
+ this.setState({ canCloseToolbox });
+ this.updateButtonIds();
+ },
+
+ setPanelDefinitions(panelDefinitions) {
+ this.setState({ panelDefinitions });
+ this.updateButtonIds();
+ },
+
+ 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);
+ });
+
+ this.setState({ toolboxButtons });
+ this.updateButtonIds();
+ },
+
+ setCanMinimize(canMinimize) {
+ /* Bug 1177463 - The minimize button is currently hidden until we agree on
+ the UI for it, and until bug 1173849 is fixed too. */
+
+ // this.setState({ canMinimize });
+ },
+
+ render() {
+ return ToolboxToolbar(Object.assign({}, this.props, this.state));
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/toolbox-tab.js
@@ -0,0 +1,62 @@
+/* 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 {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {img, button} = DOM;
+
+module.exports = createClass({
+ displayName: "ToolboxTab",
+
+ renderIcon(definition, isHighlighted) {
+ const {icon, highlightedicon} = definition;
+ if (!icon) {
+ return [];
+ }
+ return [
+ img({
+ className: "default-icon",
+ src: icon
+ }),
+ img({
+ className: "highlighted-icon",
+ src: highlightedicon || icon
+ })
+ ];
+ },
+
+ render() {
+ const {panelDefinition, currentToolId, highlightedTool, selectTool,
+ focusedButton, focusButton} = this.props;
+ const {id, tooltip, label, iconOnly} = panelDefinition;
+ const isHighlighted = id === currentToolId;
+
+ const className = [
+ "devtools-tab",
+ panelDefinition.invertIconForLightTheme || panelDefinition.invertIconForDarkTheme
+ ? "icon-invertable"
+ : "",
+ panelDefinition.invertIconForLightTheme ? "icon-invertable-light-theme" : "",
+ panelDefinition.invertIconForDarkTheme ? "icon-invertable-dark-theme" : "",
+ currentToolId === id ? "selected" : "",
+ highlightedTool === id ? "highlighted" : "",
+ iconOnly ? "devtools-tab-icon-only" : ""
+ ].join(" ");
+
+ return button(
+ {
+ className,
+ id: `toolbox-tab-${id}`,
+ "data-id": id,
+ title: tooltip,
+ type: "button",
+ tabIndex: focusedButton === id ? "0" : "-1",
+ onFocus: () => focusButton(id),
+ onClick: () => selectTool(id),
+ },
+ ...this.renderIcon(panelDefinition, isHighlighted),
+ iconOnly ? null : label
+ );
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/toolbox-toolbar.js
@@ -0,0 +1,250 @@
+/* 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 {DOM, createClass, createFactory, PropTypes} = require("devtools/client/shared/vendor/react");
+const {div, button} = DOM;
+const ToolboxTab = createFactory(require("devtools/client/framework/components/toolbox-tab"));
+
+/**
+ * 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.
+ */
+module.exports = createClass({
+ displayName: "ToolboxToolbar",
+
+ propTypes: {
+ // 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 tool, e.g. "inspector"
+ highlightedTool: PropTypes.string,
+ // List of tool panel definitions.
+ panelDefinitions: PropTypes.array,
+ // Function to select a tool based on its id.
+ selectTool: PropTypes.func,
+ // Keep a record of what button is focused.
+ focusButton: PropTypes.func,
+ // The options button definition.
+ optionsPanel: PropTypes.object,
+ // Hold off displaying the toolbar until enough information is ready for it to render
+ // nicely.
+ canRender: PropTypes.bool,
+ // Localization interface.
+ L10N: PropTypes.object,
+ },
+
+ /**
+ * The render function is kept fairly short for maintainability. See the individual
+ * render functions for how each of the sections is rendered.
+ */
+ render() {
+ const containerProps = {className: "devtools-tabbar"};
+ return this.props.canRender
+ ? (
+ div(
+ containerProps,
+ renderToolboxButtonsStart(this.props),
+ renderTabs(this.props),
+ renderToolboxButtonsEnd(this.props),
+ renderOptions(this.props),
+ renderSeparator(),
+ renderDockButtons(this.props)
+ )
+ )
+ : div(containerProps);
+ }
+});
+
+/**
+ * Render all of the tabs, this takes in the panel definitions and builds out
+ * the buttons for each of them.
+ *
+ * @param {Array} panelDefinitions - Array of objects that define panels.
+ * @param {String} currentToolId - The currently selected tool's id; e.g. "inspector".
+ * @param {String} highlightedTool - If a tool is highlighted, this is it's id.
+ * @param {Function} selectTool - Function to select a tool in the toolbox.
+ * @param {String} focusedButton - The id of the focused button.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ */
+function renderTabs({panelDefinitions, currentToolId, highlightedTool, selectTool,
+ focusedButton, focusButton}) {
+ // A wrapper is needed to get flex sizing correct in XUL.
+ return div({className: "toolbox-tabs-wrapper"},
+ div({className: "toolbox-tabs"},
+ ...panelDefinitions.map(panelDefinition => ToolboxTab({
+ panelDefinition,
+ currentToolId,
+ highlightedTool,
+ selectTool,
+ focusedButton,
+ focusButton,
+ }))
+ )
+ );
+}
+
+/**
+ * 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 {Array} toolboxButtons - Array of objects that define the command buttons.
+ * @param {String} focusedButton - The id of the focused button.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ * @param {boolean} isStart - Render either the starting buttons, or ending buttons.
+ */
+function renderToolboxButtons({toolboxButtons, focusedButton, focusButton}, isStart) {
+ const visibleButtons = toolboxButtons.filter(command => {
+ const {isVisible, isInStartContainer} = command;
+ return isVisible && (isStart ? isInStartContainer : !isInStartContainer);
+ });
+
+ if (visibleButtons.length === 0) {
+ return null;
+ }
+
+ return div({id: `toolbox-buttons-${isStart ? "start" : "end"}`},
+ ...visibleButtons.map(command => {
+ const {id, description, onClick, isChecked, className: buttonClass} = command;
+ return button({
+ id,
+ title: description,
+ className: (
+ "command-button command-button-invertable devtools-button "
+ + buttonClass + (isChecked ? " checked" : "")
+ ),
+ onClick: (event) => {
+ onClick(event);
+ focusButton(id);
+ },
+ onFocus: () => focusButton(id),
+ tabIndex: id === focusedButton ? "0" : "-1"
+ });
+ })
+ );
+}
+
+/**
+ * The options button is a ToolboxTab just like in the renderTabs() function. However
+ * it is separate from the normal tabs, so deal with it separately here.
+ *
+ * @param {Object} optionsPanel - A single panel definition for the options panel.
+ * @param {String} currentToolId - The currently selected tool's id; e.g. "inspector".
+ * @param {Function} selectTool - Function to select a tool in the toolbox.
+ * @param {String} focusedButton - The id of the focused button.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ */
+function renderOptions({optionsPanel, currentToolId, selectTool, focusedButton,
+ focusButton}) {
+ return div({id: "toolbox-option-container"}, ToolboxTab({
+ panelDefinition: optionsPanel,
+ currentToolId,
+ selectTool,
+ focusedButton,
+ focusButton,
+ }));
+}
+
+/**
+ * Render a separator.
+ */
+function renderSeparator() {
+ return div({
+ id: "toolbox-controls-separator",
+ className: "devtools-separator"
+ });
+}
+
+/**
+ * Render the dock buttons, and handle all the cases for what type of host the toolbox
+ * is attached to. The following props are expected.
+ *
+ * @property {String} focusedButton - The id of the focused button.
+ * @property {Function} closeToolbox - Completely close the toolbox.
+ * @property {Array} hostTypes - Array of host type objects, containing:
+ * @property {String} position - Position name
+ * @property {Function} switchHost - Function to switch the host.
+ * @property {Function} focusButton - Keep a record of the currently focused button.
+ * @property {Object} L10N - Localization interface.
+ * @property {Boolean} areDockButtonsEnabled - They are not enabled in certain situations
+ * like when they are in the WebIDE.
+ * @property {Boolean} canCloseToolbox - Are the tools in a context where they can be
+ * closed? This is not always the case, e.g. in the
+ * WebIDE.
+ */
+function renderDockButtons(props) {
+ const {
+ focusedButton,
+ closeToolbox,
+ hostTypes,
+ focusButton,
+ L10N,
+ areDockButtonsEnabled,
+ canCloseToolbox,
+ } = props;
+
+ let buttons = [];
+
+ if (areDockButtonsEnabled) {
+ hostTypes.forEach(hostType => {
+ const id = "toolbox-dock-" + hostType.position;
+ buttons.push(button({
+ id,
+ onFocus: () => focusButton(id),
+ className: "toolbox-dock-button devtools-button",
+ title: L10N.getStr(`toolboxDockButtons.${hostType.position}.tooltip`),
+ onClick: e => {
+ hostType.switchHost();
+ focusButton(id);
+ },
+ tabIndex: focusedButton === id ? "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();
+ focusButton(closeButtonId);
+ },
+ tabIndex: focusedButton === "toolbox-close" ? "0" : "-1",
+ })
+ : null;
+
+ return div({id: "toolbox-controls"},
+ div({id: "toolbox-dock-buttons"}, ...buttons),
+ closeButton
+ );
+}
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -4,16 +4,20 @@
# 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/.
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
'test/shared-redux-head.js',
]
+DIRS += [
+ 'components',
+]
+
DevToolsModules(
'about-devtools-toolbox.js',
'attach-thread.js',
'browser-menus.js',
'devtools-browser.js',
'devtools.js',
'gDevTools.jsm',
'location-store.js',
--- a/devtools/client/framework/test/browser_devtools_api.js
+++ b/devtools/client/framework/test/browser_devtools_api.js
@@ -147,21 +147,23 @@ function runTests2() {
continueTests(toolbox);
});
}
var continueTests = Task.async(function* (toolbox, panel) {
ok(toolbox.getCurrentPanel(), "panel value is correct");
is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct");
- ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2).hasAttribute("icon-invertable"),
- "The tool tab does not have the invertable attribute");
+ ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2)
+ .classList.contains("icon-invertable"),
+ "The tool tab does not have the invertable class");
- ok(toolbox.doc.getElementById("toolbox-tab-inspector").hasAttribute("icon-invertable"),
- "The builtin tool tabs do have the invertable attribute");
+ ok(toolbox.doc.getElementById("toolbox-tab-inspector")
+ .classList.contains("icon-invertable"),
+ "The builtin tool tabs do have the invertable class");
let toolDefinitions = gDevTools.getToolDefinitionMap();
ok(toolDefinitions.has(toolId2), "The tool is in gDevTools");
let toolDefinition = toolDefinitions.get(toolId2);
is(toolDefinition.id, toolId2, "toolDefinition id is correct");
info("Testing toolbox tool-unregistered event");
--- a/devtools/client/framework/test/browser_new_activation_workflow.js
+++ b/devtools/client/framework/test/browser_new_activation_workflow.js
@@ -36,17 +36,17 @@ function checkToolLoading() {
testToggle();
});
});
}
function selectAndCheckById(id) {
return toolbox.selectTool(id).then(function () {
let tab = toolbox.doc.getElementById("toolbox-tab-" + id);
- is(tab.hasAttribute("selected"), true, "The " + id + " tab is selected");
+ is(tab.classList.contains("selected"), true, "The " + id + " tab is selected");
});
}
function testToggle() {
toolbox.once("destroyed", () => {
// Cannot reuse a target after it's destroyed.
target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "styleeditor").then(function (aToolbox) {
--- a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
+++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
@@ -92,13 +92,14 @@ function toolUnregistered(event, toolId)
ok(!menuitem, "menu item removed from every browser window");
}
cleanup();
}
function cleanup()
{
- toolbox.destroy();
- toolbox = null;
- gBrowser.removeCurrentTab();
- finish();
+ toolbox.destroy().then(() => {;
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ })
}
--- a/devtools/client/framework/test/browser_toolbox_highlight.js
+++ b/devtools/client/framework/test/browser_toolbox_highlight.js
@@ -3,79 +3,83 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var {Toolbox} = require("devtools/client/framework/toolbox");
var toolbox = null;
function test() {
- const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along";
+ Task.spawn(function* () {
+ const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along";
- const TOOL_ID_1 = "jsdebugger";
- const TOOL_ID_2 = "webconsole";
+ const TOOL_ID_1 = "jsdebugger";
+ const TOOL_ID_2 = "webconsole";
+ yield addTab(URL);
+
+ const target = TargetFactory.forTab(gBrowser.selectedTab);
+ toolbox = yield gDevTools.showToolbox(target, TOOL_ID_1, Toolbox.HostType.BOTTOM)
- addTab(URL).then(() => {
- let target = TargetFactory.forTab(gBrowser.selectedTab);
- gDevTools.showToolbox(target, TOOL_ID_1, Toolbox.HostType.BOTTOM)
- .then(aToolbox => {
- toolbox = aToolbox;
- // select tool 2
- toolbox.selectTool(TOOL_ID_2)
- // and highlight the first one
- .then(highlightTab.bind(null, TOOL_ID_1))
- // to see if it has the proper class.
- .then(checkHighlighted.bind(null, TOOL_ID_1))
- // Now switch back to first tool
- .then(() => toolbox.selectTool(TOOL_ID_1))
- // to check again. But there is no easy way to test if
- // it is showing orange or not.
- .then(checkNoHighlightWhenSelected.bind(null, TOOL_ID_1))
- // Switch to tool 2 again
- .then(() => toolbox.selectTool(TOOL_ID_2))
- // and check again.
- .then(checkHighlighted.bind(null, TOOL_ID_1))
- // Now unhighlight the tool
- .then(unhighlightTab.bind(null, TOOL_ID_1))
- // to see the classes gone.
- .then(checkNoHighlight.bind(null, TOOL_ID_1))
- // Now close the toolbox and exit.
- .then(() => executeSoon(() => {
- toolbox.destroy()
- .then(() => {
- toolbox = null;
- gBrowser.removeCurrentTab();
- finish();
- });
- }));
- });
+ // select tool 2
+ yield toolbox.selectTool(TOOL_ID_2)
+ // and highlight the first one
+ yield highlightTab(TOOL_ID_1);
+ // to see if it has the proper class.
+ yield checkHighlighted(TOOL_ID_1);
+ // Now switch back to first tool
+ yield toolbox.selectTool(TOOL_ID_1);
+ // to check again. But there is no easy way to test if
+ // it is showing orange or not.
+ yield checkNoHighlightWhenSelected(TOOL_ID_1);
+ // Switch to tool 2 again
+ yield toolbox.selectTool(TOOL_ID_2);
+ // and check again.
+ yield checkHighlighted(TOOL_ID_1);
+ // Now unhighlight the tool
+ yield unhighlightTab(TOOL_ID_1);
+ // to see the classes gone.
+ yield checkNoHighlight(TOOL_ID_1);
+
+ // Now close the toolbox and exit.
+ executeSoon(() => {
+ toolbox.destroy().then(() => {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ });
+ })
+ .catch(error => {
+ ok(false, "There was an error running the test.");
});
}
function highlightTab(toolId) {
- info("Highlighting tool " + toolId + "'s tab.");
- toolbox.highlightTool(toolId);
+ info(`Highlighting tool ${toolId}'s tab.`);
+ return toolbox.highlightTool(toolId);
}
function unhighlightTab(toolId) {
- info("Unhighlighting tool " + toolId + "'s tab.");
- toolbox.unhighlightTool(toolId);
+ info(`Unhighlighting tool ${toolId}'s tab.`);
+ return toolbox.unhighlightTool(toolId);
}
function checkHighlighted(toolId) {
let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
- ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
- ok(!tab.hasAttribute("selected") || tab.getAttribute("selected") != "true",
- "The tab is not selected");
+ ok(tab.classList.contains("highlighted"),
+ `The highlighted class is present in ${toolId}.`);
+ ok(!tab.classList.contains("selected"),
+ `The tab is not selected in ${toolId}`);
}
function checkNoHighlightWhenSelected(toolId) {
let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
- ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
- ok(tab.hasAttribute("selected") && tab.getAttribute("selected") == "true",
- "and the tab is selected, so the orange glow will not be present.");
+ ok(tab.classList.contains("highlighted"),
+ `The highlighted class is present in ${toolId}`);
+ ok(tab.classList.contains("selected"),
+ `And the tab is selected, so the orange glow will not be present. in ${toolId}`);
}
function checkNoHighlight(toolId) {
let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
- ok(!tab.hasAttribute("highlighted"),
- "The highlighted attribute is not present");
+ ok(!tab.classList.contains("highlighted"),
+ `The highlighted class is not present in ${toolId}`);
}
--- a/devtools/client/framework/test/browser_toolbox_hosts.js
+++ b/devtools/client/framework/test/browser_toolbox_hosts.js
@@ -129,11 +129,11 @@ function* testPreviousHost() {
info("Forcing the previous host to match the current (side)");
Services.prefs.setCharPref("devtools.toolbox.previousHost", SIDE);
info("Switching from side to bottom (since previous=current=side");
yield toolbox.switchToPreviousHost();
checkHostType(toolbox, BOTTOM, SIDE);
}
function checkToolboxLoaded(iframe) {
- let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
+ let tabs = iframe.contentDocument.querySelector(".toolbox-tabs");
ok(tabs, "toolbox UI has been loaded into iframe");
}
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -288,32 +288,18 @@ function checkUnregistered(toolId, defer
}
deferred.resolve();
}
function checkRegistered(toolId, deferred, event, data) {
if (data == toolId) {
ok(true, "Correct tool added back");
// checking tab on the toolbox
- let radio = doc.getElementById("toolbox-tab-" + toolId);
- ok(radio, "Tab added back for " + toolId);
- if (radio.previousSibling) {
- ok(+radio.getAttribute("ordinal") >=
- +radio.previousSibling.getAttribute("ordinal"),
- "Inserted tab's ordinal is greater than equal to its previous tab." +
- "Expected " + radio.getAttribute("ordinal") + " >= " +
- radio.previousSibling.getAttribute("ordinal"));
- }
- if (radio.nextSibling) {
- ok(+radio.getAttribute("ordinal") <
- +radio.nextSibling.getAttribute("ordinal"),
- "Inserted tab's ordinal is less than its next tab. Expected " +
- radio.getAttribute("ordinal") + " < " +
- radio.nextSibling.getAttribute("ordinal"));
- }
+ let button = doc.getElementById("toolbox-tab-" + toolId);
+ ok(button, "Tab added back for " + toolId);
} else {
ok(false, "Something went wrong, " + toolId + " was not registered");
}
deferred.resolve();
}
function GetPref(name) {
let type = Services.prefs.getPrefType(name);
--- a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
@@ -55,82 +55,83 @@ function testSelectTool(devtoolsToolbox)
return deferred.promise;
}
function testPreferenceAndUIStateIsConsistent() {
let checkNodes = [...panelWin.document.querySelectorAll(
"#enabled-toolbox-buttons-box input[type=checkbox]")];
let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
- toolboxButtonNodes.push(doc.getElementById("command-button-frames"));
- let toggleableTools = toolbox.toolboxButtons;
// The noautohide button is only displayed in the browser toolbox
- toggleableTools = toggleableTools.filter(
+ let toolbarButtons = toolbox.toolbarButtons.filter(
tool => tool.id != "command-button-noautohide");
- for (let tool of toggleableTools) {
+ for (let tool of toolbarButtons) {
let isVisible = getBoolPref(tool.visibilityswitch);
- let button = toolboxButtonNodes.filter(
- toolboxButton => toolboxButton.id === tool.id)[0];
- is(!button.hasAttribute("hidden"), isVisible,
+ let button = toolboxButtonNodes.find(toolboxButton => toolboxButton.id === tool.id);
+ is(!!button, isVisible,
"Button visibility matches pref for " + tool.id);
let check = checkNodes.filter(node => node.id === tool.id)[0];
is(check.checked, isVisible,
"Checkbox should be selected based on current pref for " + tool.id);
}
}
function testToggleToolboxButtons() {
let checkNodes = [...panelWin.document.querySelectorAll(
"#enabled-toolbox-buttons-box input[type=checkbox]")];
- let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
- let toggleableTools = toolbox.toolboxButtons;
// The noautohide button is only displayed in the browser toolbox, and the element
// picker button is not toggleable.
- toggleableTools = toggleableTools.filter(
- tool => tool.id != "command-button-noautohide" && tool.id != "command-button-pick");
- toolboxButtonNodes = toolboxButtonNodes.filter(
- btn => btn.id != "command-button-noautohide" && btn.id != "command-button-pick");
+ let toolbarButtons = toolbox.toolbarButtons.filter(
+ tool => tool.id != "command-button-noautohide");
+
+ let visibleToolbarButtons = toolbox.toolbarButtons.filter(tool => tool.isVisible);
- is(checkNodes.length, toggleableTools.length,
+ let toolbarButtonNodes = [...doc.querySelectorAll(".command-button")].filter(
+ btn => btn.id != "command-button-noautohide");
+
+ is(checkNodes.length, toolbarButtons.length,
"All of the buttons are toggleable.");
- is(checkNodes.length, toolboxButtonNodes.length,
+ is(visibleToolbarButtons.length, toolbarButtonNodes.length,
"All of the DOM buttons are toggleable.");
- for (let tool of toggleableTools) {
+ for (let tool of toolbarButtons) {
let id = tool.id;
let matchedCheckboxes = checkNodes.filter(node => node.id === id);
- let matchedButtons = toolboxButtonNodes.filter(button => button.id === id);
+ let matchedButtons = toolbarButtonNodes.filter(button => button.id === id);
is(matchedCheckboxes.length, 1,
"There should be a single toggle checkbox for: " + id);
- is(matchedButtons.length, 1,
- "There should be a DOM button for: " + id);
- is(matchedButtons[0], tool.button,
- "DOM buttons should match for: " + id);
+ if (tool.isVisible) {
+ is(matchedButtons.length, 1,
+ "There should be a DOM button for the visible: " + id);
+ is(matchedButtons[0].getAttribute("title"), tool.description,
+ "The tooltip for button matches the tool definition.");
+ } else {
+ is(matchedButtons.length, 0,
+ "There should not be a DOM button for the invisible: " + id);
+ }
- is(matchedCheckboxes[0].nextSibling.textContent, tool.label,
+ is(matchedCheckboxes[0].nextSibling.textContent, tool.description,
"The label for checkbox matches the tool definition.");
- is(matchedButtons[0].getAttribute("title"), tool.label,
- "The tooltip for button matches the tool definition.");
}
// Store modified pref names so that they can be cleared on error.
- for (let tool of toggleableTools) {
+ for (let tool of toolbarButtons) {
let pref = tool.visibilityswitch;
modifiedPrefs.push(pref);
}
// Try checking each checkbox, making sure that it changes the preference
for (let node of checkNodes) {
- let tool = toggleableTools.filter(
- toggleableTool => toggleableTool.id === node.id)[0];
+ let tool = toolbarButtons.filter(
+ commandButton => commandButton.id === node.id)[0];
let isVisible = getBoolPref(tool.visibilityswitch);
testPreferenceAndUIStateIsConsistent();
node.click();
testPreferenceAndUIStateIsConsistent();
let isVisibleAfterClick = getBoolPref(tool.visibilityswitch);
--- a/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js
+++ b/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js
@@ -127,13 +127,14 @@ function toolboxToolUnregistered() {
let panel = doc.getElementById("toolbox-panel-" + TOOL_ID);
ok(!panel, "tool's panel was removed from toolbox UI");
cleanup();
}
function cleanup() {
- toolbox.destroy();
- toolbox = null;
- gBrowser.removeCurrentTab();
- finish();
+ toolbox.destroy().then(() => {;
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
}
--- a/devtools/client/framework/test/browser_toolbox_transport_events.js
+++ b/devtools/client/framework/test/browser_toolbox_transport_events.js
@@ -25,20 +25,22 @@ function testResults(toolbox) {
cleanUp(toolbox);
}
function cleanUp(toolbox) {
gDevTools.off("toolbox-created", onToolboxCreated);
off(DebuggerClient, "connect", onDebuggerClientConnect);
toolbox.destroy().then(function () {
- gBrowser.removeCurrentTab();
- executeSoon(function () {
- finish();
- });
+ setTimeout(() => {
+ gBrowser.removeCurrentTab();
+ executeSoon(function () {
+ finish();
+ });
+ }, 1000);
});
}
function testPackets(sent, received) {
ok(sent.length > 0, "There must be at least one sent packet");
ok(received.length > 0, "There must be at leaset one received packet");
if (!sent.length || received.length) {
--- a/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -39,21 +39,21 @@ add_task(function* () {
// Wait for tick to avoid unexpected 'popuphidden' event, which
// blocks the frame popup menu opened below. See also bug 1276873
yield waitForTick();
// Open frame menu and wait till it's available on the screen.
// Also check 'open' attribute on the command button.
let btn = toolbox.doc.getElementById("command-button-frames");
- ok(!btn.getAttribute("open"), "The open attribute must not be present");
+ ok(!btn.classList.contains("checked"), "The checked class must not be present");
let menu = toolbox.showFramesMenu({target: btn});
yield once(menu, "open");
- is(btn.getAttribute("open"), "true", "The open attribute must be set");
+ ok(btn.classList.contains("checked"), "The checked class must be set");
// Verify that the frame list menu is populated
let frames = menu.items;
is(frames.length, 2, "We have both frames in the list");
let topFrameBtn = frames.filter(b => b.label == URL)[0];
let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
ok(topFrameBtn, "Got top level document in the list");
--- a/devtools/client/framework/toolbox-highlighter-utils.js
+++ b/devtools/client/framework/toolbox-highlighter-utils.js
@@ -117,17 +117,17 @@ exports.getHighlighterUtils = function (
* if it is already started
*/
let startPicker = exported.startPicker = requireInspector(function* (doFocus = false) {
if (isPicking) {
return;
}
isPicking = true;
- toolbox.pickerButtonChecked = true;
+ toolbox.pickerButton.isChecked = true;
yield toolbox.selectTool("inspector");
toolbox.on("select", cancelPicker);
if (isRemoteHighlightable()) {
toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
toolbox.walker.on("picker-node-picked", onPickerNodePicked);
toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed);
toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
@@ -151,17 +151,17 @@ exports.getHighlighterUtils = function (
* if it is already stopped
*/
let stopPicker = exported.stopPicker = requireInspector(function* () {
if (!isPicking) {
return;
}
isPicking = false;
- toolbox.pickerButtonChecked = false;
+ toolbox.pickerButton.isChecked = false;
if (isRemoteHighlightable()) {
yield toolbox.highlighter.cancelPick();
toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
toolbox.walker.off("picker-node-picked", onPickerNodePicked);
toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed);
toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
} else {
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -130,58 +130,64 @@ OptionsPanel.prototype = {
let themeBox = this.panelDoc.getElementById("devtools-theme-box");
let themeInput = themeBox.querySelector(`[value=${theme.id}]`);
if (themeInput) {
themeInput.parentNode.remove();
}
},
- setupToolbarButtonsList: function () {
+ setupToolbarButtonsList: Task.async(function* () {
+ // Ensure the toolbox is open, and the buttons are all set up.
+ yield this.toolbox.isOpen;
+
let enabledToolbarButtonsBox = this.panelDoc.getElementById(
"enabled-toolbox-buttons-box");
- let toggleableButtons = this.toolbox.toolboxButtons;
- let setToolboxButtonsVisibility =
- this.toolbox.setToolboxButtonsVisibility.bind(this.toolbox);
+ let toolbarButtons = this.toolbox.toolbarButtons;
+
+ if (!toolbarButtons) {
+ console.warn("The command buttons weren't initiated yet.");
+ return;
+ }
let onCheckboxClick = (checkbox) => {
- let toolDefinition = toggleableButtons.filter(
+ let commandButton = toolbarButtons.filter(
toggleableButton => toggleableButton.id === checkbox.id)[0];
Services.prefs.setBoolPref(
- toolDefinition.visibilityswitch, checkbox.checked);
- setToolboxButtonsVisibility();
+ commandButton.visibilityswitch, checkbox.checked);
+ this.toolbox.updateToolboxButtonsVisibility();
};
- let createCommandCheckbox = tool => {
+ let createCommandCheckbox = button => {
let checkboxLabel = this.panelDoc.createElement("label");
let checkboxSpanLabel = this.panelDoc.createElement("span");
- checkboxSpanLabel.textContent = tool.label;
+ checkboxSpanLabel.textContent = button.description;
let checkboxInput = this.panelDoc.createElement("input");
checkboxInput.setAttribute("type", "checkbox");
- checkboxInput.setAttribute("id", tool.id);
- if (InfallibleGetBoolPref(tool.visibilityswitch)) {
+ checkboxInput.setAttribute("id", button.id);
+ if (button.isVisible) {
checkboxInput.setAttribute("checked", true);
}
checkboxInput.addEventListener("change",
onCheckboxClick.bind(this, checkboxInput));
checkboxLabel.appendChild(checkboxInput);
checkboxLabel.appendChild(checkboxSpanLabel);
return checkboxLabel;
};
- for (let tool of toggleableButtons) {
- if (!tool.isTargetSupported(this.toolbox.target)) {
+ for (let button of toolbarButtons) {
+ if (!button.isTargetSupported(this.toolbox.target)) {
continue;
}
- enabledToolbarButtonsBox.appendChild(createCommandCheckbox(tool));
+ enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button));
}
- },
+ }),
setupToolsList: function () {
let defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
let additionalToolsBox = this.panelDoc.getElementById(
"additional-tools-box");
let toolsNotSupportedLabel = this.panelDoc.getElementById(
"tools-not-supported-label");
let atleastOneToolNotSupported = false;
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -2,16 +2,17 @@
* 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 MAX_ORDINAL = 99;
const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
+const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide";
const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const { SourceMapService } = require("./source-map-service");
var {Ci, Cu} = require("chrome");
@@ -57,16 +58,18 @@ loader.lazyRequireGetter(this, "getPrefe
loader.lazyRequireGetter(this, "KeyShortcuts",
"devtools/client/shared/key-shortcuts");
loader.lazyRequireGetter(this, "ZoomKeys",
"devtools/client/shared/zoom-keys");
loader.lazyRequireGetter(this, "settleAll",
"devtools/shared/ThreadSafeDevToolsUtils", true);
loader.lazyRequireGetter(this, "ToolboxButtons",
"devtools/client/definitions", true);
+loader.lazyRequireGetter(this, "ViewHelpers",
+ "devtools/client/shared/widgets/view-helpers", true);
loader.lazyGetter(this, "registerHarOverlay", () => {
return require("devtools/client/netmonitor/har/toolbox-overlay").register;
});
/**
* A "Toolbox" is the component that holds all the tools for one specific
* target. Visually, it's a document that includes the tools tabs and all
@@ -100,17 +103,17 @@ function Toolbox(target, selectedTool, h
// Map of frames (id => frame-info) and currently selected frame id.
this.frameMap = new Map();
this.selectedFrameId = null;
this._toolRegistered = this._toolRegistered.bind(this);
this._toolUnregistered = this._toolUnregistered.bind(this);
this._refreshHostTitle = this._refreshHostTitle.bind(this);
- this._toggleAutohide = this._toggleAutohide.bind(this);
+ this._toggleNoAutohide = this._toggleNoAutohide.bind(this);
this.showFramesMenu = this.showFramesMenu.bind(this);
this._updateFrames = this._updateFrames.bind(this);
this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
this.destroy = this.destroy.bind(this);
this.highlighterUtils = getHighlighterUtils(this);
this._highlighterReady = this._highlighterReady.bind(this);
this._highlighterHidden = this._highlighterHidden.bind(this);
this._applyCacheSettings = this._applyCacheSettings.bind(this);
@@ -122,32 +125,36 @@ function Toolbox(target, selectedTool, h
this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
- this._onTabbarFocus = this._onTabbarFocus.bind(this);
- this._onTabbarArrowKeypress = this._onTabbarArrowKeypress.bind(this);
+ this._onToolbarFocus = this._onToolbarFocus.bind(this);
+ this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this);
this._onPickerClick = this._onPickerClick.bind(this);
this._onPickerKeypress = this._onPickerKeypress.bind(this);
this._onPickerStarted = this._onPickerStarted.bind(this);
this._onPickerStopped = this._onPickerStopped.bind(this);
+ this.selectTool = this.selectTool.bind(this);
this._target.on("close", this.destroy);
if (!selectedTool) {
selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
}
this._defaultToolId = selectedTool;
this._hostType = hostType;
+ this._isOpenDeferred = defer();
+ this.isOpen = this._isOpenDeferred.promise;
+
EventEmitter.decorate(this);
this._target.on("navigate", this._refreshHostTitle);
this._target.on("frame-update", this._updateFrames);
this.on("host-changed", this._refreshHostTitle);
this.on("select", this._refreshHostTitle);
@@ -175,17 +182,62 @@ Toolbox.HostType = {
Toolbox.prototype = {
_URL: "about:devtools-toolbox",
_prefs: {
LAST_TOOL: "devtools.toolbox.selectedTool",
SIDE_ENABLED: "devtools.toolbox.sideEnabled",
},
- currentToolId: null,
+ get currentToolId() {
+ return this._currentToolId;
+ },
+
+ set currentToolId(id) {
+ this._currentToolId = id;
+ this.component.setCurrentToolId(id);
+ },
+
+ get panelDefinitions() {
+ return this._panelDefinitions;
+ },
+
+ set panelDefinitions(definitions) {
+ this._panelDefinitions = definitions;
+ this._combineAndSortPanelDefinitions();
+ },
+
+ get visibleAdditionalTools() {
+ if (!this._visibleAdditionalTools) {
+ this._visibleAdditionalTools = [];
+ }
+
+ return this._visibleAdditionalTools;
+ },
+
+ set visibleAdditionalTools(tools) {
+ this._visibleAdditionalTools = tools;
+ this._combineAndSortPanelDefinitions();
+ },
+
+ /**
+ * Combines the built-in panel definitions and the additional tool definitions that
+ * can be set by add-ons.
+ */
+ _combineAndSortPanelDefinitions() {
+ const definitions = [...this._panelDefinitions, ...this.getVisibleAdditionalTools()];
+ definitions.sort(definition => {
+ return -1 * (definition.ordinal == undefined || definition.ordinal < 0
+ ? MAX_ORDINAL
+ : definition.ordinal
+ );
+ });
+ this.component.setPanelDefinitions(definitions);
+ },
+
lastUsedToolId: null,
/**
* Returns a *copy* of the _toolPanels collection.
*
* @return {Map} panels
* All the running panels in the toolbox
*/
@@ -361,57 +413,47 @@ Toolbox.prototype = {
// Attach the thread
this._threadClient = yield attachThread(this);
yield domReady.promise;
this.isReady = true;
let framesPromise = this._listFrames();
- this.closeButton = this.doc.getElementById("toolbox-close");
- this.closeButton.addEventListener("click", this.destroy, true);
-
Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings,
false);
Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
this._applyServiceWorkersTestingSettings, false);
- Services.prefs.addObserver("devtools.screenshot.clipboard.enabled",
- this._buildButtons, false);
-
- let framesMenu = this.doc.getElementById("command-button-frames");
- framesMenu.addEventListener("click", this.showFramesMenu, false);
-
- let noautohideMenu = this.doc.getElementById("command-button-noautohide");
- noautohideMenu.addEventListener("click", this._toggleAutohide, true);
this.textBoxContextMenuPopup =
this.doc.getElementById("toolbox-textbox-context-popup");
this.textBoxContextMenuPopup.addEventListener("popupshowing",
this._updateTextBoxMenuItems, true);
this.shortcuts = new KeyShortcuts({
window: this.doc.defaultView
});
+ // Get the DOM element to mount the ToolboxController to.
+ this._componentMount = this.doc.getElementById("toolbox-toolbar-mount");
+
+ this._mountReactComponent();
this._buildDockButtons();
this._buildOptions();
this._buildTabs();
this._applyCacheSettings();
this._applyServiceWorkersTestingSettings();
this._addKeysToWindow();
this._addReloadKeys();
this._addHostListeners();
this._registerOverlays();
if (!this._hostOptions || this._hostOptions.zoom === true) {
ZoomKeys.register(this.win);
}
- this.tabbar = this.doc.querySelector(".devtools-tabbar");
- this.tabbar.addEventListener("focus", this._onTabbarFocus, true);
- this.tabbar.addEventListener("click", this._onTabbarFocus, true);
- this.tabbar.addEventListener("keypress", this._onTabbarArrowKeypress);
+ this._componentMount.addEventListener("keypress", this._onToolbarArrowKeypress);
this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
let buttonsPromise = this._buildButtons();
this._pingTelemetry();
@@ -419,16 +461,20 @@ Toolbox.prototype = {
// The isTargetSupported check needs to happen after the target is
// remoted, otherwise we could have done it in the toolbox constructor
// (bug 1072764).
let toolDef = gDevTools.getToolDefinition(this._defaultToolId);
if (!toolDef || !toolDef.isTargetSupported(this._target)) {
this._defaultToolId = "webconsole";
}
+ // Start rendering the toolbox toolbar before selecting the tool, as the tools
+ // can take a few hundred milliseconds seconds to start up.
+ this.component.setCanRender();
+
yield this.selectTool(this._defaultToolId);
// Wait until the original tool is selected so that the split
// console input will receive focus.
let splitConsolePromise = promise.resolve();
if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
splitConsolePromise = this.openSplitConsole();
}
@@ -446,16 +492,17 @@ Toolbox.prototype = {
// If in testing environment, wait for performance connection to finish,
// so we don't have to explicitly wait for this in tests; ideally, all tests
// will handle this on their own, but each have their own tear down function.
if (flags.testing) {
yield performanceFrontConnection;
}
this.emit("ready");
+ this._isOpenDeferred.resolve();
}.bind(this)).then(null, console.error.bind(console));
},
/**
* loading React modules when needed (to avoid performance penalties
* during Firefox start up time).
*/
get React() {
@@ -465,16 +512,20 @@ Toolbox.prototype = {
get ReactDOM() {
return this.browserRequire("devtools/client/shared/vendor/react-dom");
},
get ReactRedux() {
return this.browserRequire("devtools/client/shared/vendor/react-redux");
},
+ get ToolboxController() {
+ return this.browserRequire("devtools/client/framework/components/toolbox-controller");
+ },
+
// Return HostType id for telemetry
_getTelemetryHostId: function () {
switch (this.hostType) {
case Toolbox.HostType.BOTTOM: return 0;
case Toolbox.HostType.SIDE: return 1;
case Toolbox.HostType.WINDOW: return 2;
case Toolbox.HostType.CUSTOM: return 3;
default: return 9;
@@ -487,16 +538,61 @@ Toolbox.prototype = {
this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
Services.appinfo.is64Bit ? 1 : 0);
this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
system.getScreenDimensions());
this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
},
+ /**
+ * Create a simple object to store the state of a toolbox button. The checked state of
+ * a button can be updated arbitrarily outside of the scope of the toolbar and its
+ * controllers. In order to simplify this interaction this object emits an
+ * "updatechecked" event any time the isChecked value is updated, allowing any consuming
+ * components to listen and respond to updates.
+ *
+ * @param {Object} options:
+ *
+ * @property {String} id - The id of the button or command.
+ * @property {String} className - An optional additional className for the button.
+ * @property {String} description - The value that will display as a tooltip and in
+ * the options panel for enabling/disabling.
+ * @property {Function} onClick - The function to run when the button is activated by
+ * click or keyboard shortcut.
+ * @property {Boolean} isInStartContainer - Buttons can either be placed at the start
+ * of the toolbar, or at the end.
+ */
+ _createButtonState: function (options) {
+ let isChecked = false;
+ const {id, className, description, onClick, isInStartContainer} = options;
+ const button = {
+ id,
+ className,
+ description,
+ onClick,
+ get isChecked() {
+ return isChecked;
+ },
+ set isChecked(value) {
+ isChecked = value;
+ this.emit("updatechecked");
+ },
+ // The preference for having this button visible.
+ visibilityswitch: `devtools.${id}.enabled`,
+ // The toolbar has a container at the start and end of the toolbar for
+ // holding buttons. By default the buttons are placed in the end container.
+ isInStartContainer: !!isInStartContainer
+ };
+
+ EventEmitter.decorate(button);
+
+ return button;
+ },
+
_buildOptions: function () {
let selectOptions = (name, event) => {
// Flip back to the last used panel if we are already
// on the options panel.
if (this.currentToolId === "options" &&
gDevTools.getToolDefinition(this.lastUsedToolId)) {
this.selectTool(this.lastUsedToolId);
} else {
@@ -749,95 +845,60 @@ Toolbox.prototype = {
return this._notificationBox;
},
/**
* Build the buttons for changing hosts. Called every time
* the host changes.
*/
_buildDockButtons: function () {
- let dockBox = this.doc.getElementById("toolbox-dock-buttons");
-
- while (dockBox.firstChild) {
- dockBox.removeChild(dockBox.firstChild);
- }
-
if (!this._target.isLocalTab) {
+ this.component.setDockButtonsEnabled(false);
return;
}
// Bottom-type host can be minimized, add a button for this.
if (this.hostType == Toolbox.HostType.BOTTOM) {
- let minimizeBtn = this.doc.createElementNS(HTML_NS, "button");
- minimizeBtn.id = "toolbox-dock-bottom-minimize";
- minimizeBtn.className = "devtools-button";
- /* Bug 1177463 - The minimize button is currently hidden until we agree on
- the UI for it, and until bug 1173849 is fixed too. */
- minimizeBtn.setAttribute("hidden", "true");
-
- minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
- dockBox.appendChild(minimizeBtn);
- // Show the button in its maximized state.
- this._onBottomHostMaximized();
+ this.component.setCanMinimize(true);
// Maximize again when a tool gets selected.
this.on("before-select", this._onToolSelectWhileMinimized);
// Maximize and stop listening before the host type changes.
this.once("host-will-change", this._onBottomHostWillChange);
}
- if (this.hostType == Toolbox.HostType.WINDOW) {
- this.closeButton.setAttribute("hidden", "true");
- } else {
- this.closeButton.removeAttribute("hidden");
- }
+ this.component.setDockButtonsEnabled(true);
+ this.component.setCanCloseToolbox(this.hostType !== Toolbox.HostType.WINDOW);
let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
+ let hostTypes = [];
for (let type in Toolbox.HostType) {
let position = Toolbox.HostType[type];
if (position == this.hostType ||
position == Toolbox.HostType.CUSTOM ||
(!sideEnabled && position == Toolbox.HostType.SIDE)) {
continue;
}
- let button = this.doc.createElementNS(HTML_NS, "button");
- button.id = "toolbox-dock-" + position;
- button.className = "toolbox-dock-button devtools-button";
- button.setAttribute("title", L10N.getStr("toolboxDockButtons." +
- position + ".tooltip"));
- button.addEventListener("click", this.switchHost.bind(this, position));
+ hostTypes.push({
+ position,
+ switchHost: this.switchHost.bind(this, position)
+ });
+ }
- dockBox.appendChild(button);
- }
- },
-
- _getMinimizeButtonShortcutTooltip: function () {
- let str = L10N.getStr("toolbox.minimize.key");
- let key = KeyShortcuts.parseElectronKey(this.win, str);
- return "(" + KeyShortcuts.stringify(key) + ")";
+ this.component.setHostTypes(hostTypes);
},
_onBottomHostMinimized: function () {
- let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
- btn.className = "minimized";
-
- btn.setAttribute("title",
- L10N.getStr("toolboxDockButtons.bottom.maximize") + " " +
- this._getMinimizeButtonShortcutTooltip());
+ this.component.setMinimizeState("minimized");
},
_onBottomHostMaximized: function () {
- let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
- btn.className = "maximized";
-
- btn.setAttribute("title",
- L10N.getStr("toolboxDockButtons.bottom.minimize") + " " +
- this._getMinimizeButtonShortcutTooltip());
+ this.component.setMinimizeState("maximized");
},
_onToolSelectWhileMinimized: function () {
this.postMessage({
name: "maximize-host"
});
},
@@ -862,157 +923,176 @@ Toolbox.prototype = {
_toggleMinimizeMode: function () {
if (this.hostType !== Toolbox.HostType.BOTTOM) {
return;
}
// Calculate the height to which the host should be minimized so the
// tabbar is still visible.
- let toolbarHeight = this.tabbar.getBoxQuads({box: "content"})[0].bounds
- .height;
+ let toolbarHeight = this._componentMount.getBoxQuads({box: "content"})[0].bounds
+ .height;
this.postMessage({
name: "toggle-minimize-mode",
toolbarHeight
});
},
/**
- * Add tabs to the toolbox UI for registered tools
+ * Initiate ToolboxTabs React component and all it's properties. Do the initial render.
*/
_buildTabs: function () {
- // Build tabs for global registered tools.
- for (let definition of gDevTools.getToolDefinitionArray()) {
- this._buildTabForTool(definition);
- }
+ // Get the initial list of tab definitions. This list can be amended at a later time
+ // by tools registering themselves.
+ const definitions = gDevTools.getToolDefinitionArray();
+ definitions.forEach(definition => this._buildPanelForTool(definition));
+
+ // Get the definitions that will only affect the main tab area.
+ this.panelDefinitions = definitions.filter(definition =>
+ definition.isTargetSupported(this._target) && definition.id !== "options");
+
+ this.optionsDefinition = definitions.find(({id}) => id === "options");
+ // The options tool is treated slightly differently, and is in a different area.
+ this.component.setOptionsPanel(definitions.find(({id}) => id === "options"));
},
- /**
- * Get all dev tools tab bar focusable elements. These are visible elements
- * such as buttons or elements with tabindex.
- */
- get tabbarFocusableElms() {
- return [...this.tabbar.querySelectorAll(
- "[tabindex]:not([hidden]), button:not([hidden])")];
+ _mountReactComponent: function () {
+ // Ensure the toolbar doesn't try to render until the tool is ready.
+ const element = this.React.createElement(this.ToolboxController, {
+ L10N,
+ currentToolId: this.currentToolId,
+ selectTool: this.selectTool,
+ closeToolbox: this.destroy,
+ focusButton: this._onToolbarFocus,
+ toggleMinimizeMode: this._toggleMinimizeMode,
+ });
+
+ this.component = this.ReactDOM.render(element, this._componentMount);
},
/**
- * Reset tabindex attributes across all focusable elements inside the tabbar.
+ * Reset tabindex attributes across all focusable elements inside the toolbar.
* Only have one element with tabindex=0 at a time to make sure that tabbing
- * results in navigating away from the tabbar container.
+ * results in navigating away from the toolbar container.
* @param {FocusEvent} event
*/
- _onTabbarFocus: function (event) {
- this.tabbarFocusableElms.forEach(elm =>
- elm.setAttribute("tabindex", event.target === elm ? "0" : "-1"));
+ _onToolbarFocus: function (id) {
+ this.component.setFocusedButton(id);
},
/**
- * On left/right arrow press, attempt to move the focus inside the tabbar to
- * the previous/next focusable element.
+ * On left/right arrow press, attempt to move the focus inside the toolbar to
+ * the previous/next focusable element. This is not in the React component
+ * as it is difficult to coordinate between different component elements.
+ * The components are responsible for setting the correct tabindex value
+ * for if they are the focused element.
* @param {KeyboardEvent} event
*/
- _onTabbarArrowKeypress: function (event) {
+ _onToolbarArrowKeypress: function (event) {
let { key, target } = event;
- let focusableElms = this.tabbarFocusableElms;
- let curIndex = focusableElms.indexOf(target);
+ let buttons = [...this._componentMount.querySelectorAll("button")];
+ let curIndex = buttons.indexOf(target);
if (curIndex === -1) {
console.warn(target + " is not found among Developer Tools tab bar " +
- "focusable elements. It needs to either be a button or have " +
- "tabindex. If it is intended to be hidden, 'hidden' attribute must " +
- "be used.");
+ "focusable elements.");
return;
}
let newTarget;
if (key === "ArrowLeft") {
// Do nothing if already at the beginning.
if (curIndex === 0) {
return;
}
- newTarget = focusableElms[curIndex - 1];
+ newTarget = buttons[curIndex - 1];
} else if (key === "ArrowRight") {
// Do nothing if already at the end.
- if (curIndex === focusableElms.length - 1) {
+ if (curIndex === buttons.length - 1) {
return;
}
- newTarget = focusableElms[curIndex + 1];
+ newTarget = buttons[curIndex + 1];
} else {
return;
}
- focusableElms.forEach(elm =>
- elm.setAttribute("tabindex", newTarget === elm ? "0" : "-1"));
newTarget.focus();
event.preventDefault();
event.stopPropagation();
},
/**
* Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
*/
- _buildButtons: function () {
- if (this.target.getTrait("highlightable")) {
- this._buildPickerButton();
- }
-
- this.setToolboxButtonsVisibility();
+ _buildButtons: Task.async(function* () {
+ // Beyond the normal preference filtering
+ this.toolbarButtons = [
+ this._buildPickerButton(),
+ this._buildFrameButton(),
+ yield this._buildNoAutoHideButton()
+ ];
- // Old servers don't have a GCLI Actor, so just return
- if (!this.target.hasActor("gcli")) {
- return promise.resolve();
- }
- // Disable gcli in browser toolbox until there is usages of it
- if (this.target.chrome) {
- return promise.resolve();
+ // Build buttons from the GCLI commands only if the GCLI actor is supported and the
+ // target isn't chrome.
+ if (this.target.hasActor("gcli") && !this.target.chrome) {
+ const options = {
+ environment: CommandUtils.createEnvironment(this, "_target")
+ };
+
+ this._requisition = yield CommandUtils.createRequisition(this.target, options);
+ const spec = this.getToolbarSpec();
+ const commandButtons = yield CommandUtils.createCommandButtons(
+ spec, this.target, this.doc, this._requisition, this._createButtonState);
+ this.toolbarButtons = [...this.toolbarButtons, ...commandButtons];
}
- const options = {
- environment: CommandUtils.createEnvironment(this, "_target")
- };
+ // Mutate the objects here with their visibility.
+ this.toolbarButtons.forEach(command => {
+ const definition = ToolboxButtons.find(t => t.id === command.id);
+ command.isTargetSupported = definition.isTargetSupported
+ ? definition.isTargetSupported
+ : target => target.isLocalTab;
+ command.isVisible = this._commandIsVisible(command.id);
+ });
- return CommandUtils.createRequisition(this.target, options).then(requisition => {
- this._requisition = requisition;
+ this.component.setToolboxButtons(this.toolbarButtons);
+ }),
- let spec = this.getToolbarSpec();
- return CommandUtils.createButtons(spec, this.target, this.doc, requisition)
- .then(buttons => {
- let container = this.doc.getElementById("toolbox-buttons");
- buttons.forEach(button => {
- let currentButton = this.doc.getElementById(button.id);
- if (currentButton) {
- container.replaceChild(button, currentButton);
- } else {
- container.appendChild(button);
- }
- });
- this.setToolboxButtonsVisibility();
- });
+ /**
+ * Button to select a frame for the inspector to target.
+ */
+ _buildFrameButton() {
+ this.frameButton = this._createButtonState({
+ id: "command-button-frames",
+ description: L10N.getStr("toolbox.frames.tooltip"),
+ onClick: this.showFramesMenu
});
+
+ return this.frameButton;
},
/**
- * Adding the element picker button is done here unlike the other buttons
- * since we want it to work for remote targets too
+ * Button that disables/enables auto-hiding XUL pop-ups. When enabled, XUL
+ * pop-ups will not automatically close when they lose focus.
*/
- _buildPickerButton: function () {
- this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
- this._pickerButton.id = "command-button-pick";
- this._pickerButton.className =
- "command-button command-button-invertable devtools-button";
- this._pickerButton.setAttribute("title", L10N.getStr("pickButton.tooltip"));
+ _buildNoAutoHideButton: Task.async(function* () {
+ this.autohideButton = this._createButtonState({
+ id: "command-button-noautohide",
+ description: L10N.getStr("toolbox.noautohide.tooltip"),
+ onClick: this._toggleNoAutohide
+ });
- let container = this.doc.querySelector("#toolbox-picker-container");
- container.appendChild(this._pickerButton);
+ this._isDisableAutohideEnabled().then(enabled => {
+ this.autohideButton.isChecked = enabled;
+ });
- this._pickerButton.addEventListener("click", this._onPickerClick, false);
- },
+ return this.autohideButton;
+ }),
/**
* Toggle the picker, but also decide whether or not the highlighter should
* focus the window. This is only desirable when the toolbox is mounted to the
* window. When devtools is free floating, then the target window should not
* pop in front of the viewer when the picker is clicked.
*/
_onPickerClick: function () {
@@ -1037,16 +1117,31 @@ Toolbox.prototype = {
this.doc.addEventListener("keypress", this._onPickerKeypress, true);
},
_onPickerStopped: function () {
this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
},
/**
+ * The element picker button enables the ability to select a DOM node by clicking
+ * it on the page.
+ */
+ _buildPickerButton() {
+ this.pickerButton = this._createButtonState({
+ id: "command-button-pick",
+ description: L10N.getStr("pickButton.tooltip"),
+ onClick: this._onPickerClick,
+ isInStartContainer: true
+ });
+
+ return this.pickerButton;
+ },
+
+ /**
* Apply the current cache setting from devtools.cache.disabled to this
* toolbox's tab.
*/
_applyCacheSettings: function () {
let pref = "devtools.cache.disabled";
let cacheDisabled = Services.prefs.getBoolPref(pref);
if (this.target.activeTab) {
@@ -1065,201 +1160,101 @@ Toolbox.prototype = {
if (this.target.activeTab) {
this.target.activeTab.reconfigure({
"serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
});
}
},
- /**
- * Get the toolbar spec for toolbox
- */
+ /**
+ * Get the toolbar spec for toolbox
+ */
getToolbarSpec: function () {
let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
// Special case for screenshot command button to check for clipboard preference
const clipboardEnabled = Services.prefs
.getBoolPref("devtools.screenshot.clipboard.enabled");
if (clipboardEnabled) {
for (let i = 0; i < spec.length; i++) {
if (spec[i] == "screenshot --fullpage --file") {
spec[i] += " --clipboard";
}
}
}
return spec;
},
+ /**
+ * Return all toolbox buttons (command buttons, plus any others that were
+ * added manually).
+
/**
- * Setter for the checked state of the picker button in the toolbar
- * @param {Boolean} isChecked
+ * Update the visibility of the buttons.
*/
- set pickerButtonChecked(isChecked) {
- if (isChecked) {
- this._pickerButton.setAttribute("checked", "true");
- } else {
- this._pickerButton.removeAttribute("checked");
- }
+ updateToolboxButtonsVisibility() {
+ this.toolbarButtons.forEach(command => {
+ command.isVisible = this._commandIsVisible(command.id);
+ });
+ this.component.setToolboxButtons(this.toolbarButtons);
},
/**
- * Return all toolbox buttons (command buttons, plus any others that were
- * added manually).
+ * Ensure the visibility of each toolbox button matches the preference value.
*/
- get toolboxButtons() {
- return ToolboxButtons.map(options => {
- let button = this.doc.getElementById(options.id);
- // Some buttons may not exist inside of Browser Toolbox
- if (!button) {
- return false;
- }
+ _commandIsVisible: function (id) {
+ const {
+ isTargetSupported,
+ visibilityswitch
+ } = this.toolbarButtons.find(btn => btn.id === id);
- return {
- id: options.id,
- button: button,
- label: button.getAttribute("title"),
- visibilityswitch: "devtools." + options.id + ".enabled",
- isTargetSupported: options.isTargetSupported
- ? options.isTargetSupported
- : target => target.isLocalTab,
- };
- }).filter(button=>button);
+ let visible = true;
+ try {
+ visible = Services.prefs.getBoolPref(visibilityswitch);
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ if (isTargetSupported) {
+ return visible && isTargetSupported(this.target);
+ }
+
+ return visible;
},
/**
- * Ensure the visibility of each toolbox button matches the
- * preference value. Simply hide buttons that are preffed off.
- */
- setToolboxButtonsVisibility: function () {
- this.toolboxButtons.forEach(buttonSpec => {
- let { visibilityswitch, button, isTargetSupported } = buttonSpec;
- let on = true;
- try {
- on = Services.prefs.getBoolPref(visibilityswitch);
- } catch (ex) {
- // Do nothing.
- }
-
- on = on && isTargetSupported(this.target);
-
- if (button) {
- if (on) {
- button.removeAttribute("hidden");
- } else {
- button.setAttribute("hidden", "true");
- }
- }
- });
-
- this._updateNoautohideButton();
- },
-
- /**
- * Build a tab for one tool definition and add to the toolbox
+ * Build a panel for a tool definition.
*
* @param {string} toolDefinition
* Tool definition of the tool to build a tab for.
*/
- _buildTabForTool: function (toolDefinition) {
+ _buildPanelForTool: function (toolDefinition) {
if (!toolDefinition.isTargetSupported(this._target)) {
return;
}
- let tabs = this.doc.getElementById("toolbox-tabs");
let deck = this.doc.getElementById("toolbox-deck");
-
let id = toolDefinition.id;
if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
toolDefinition.ordinal = MAX_ORDINAL;
}
- let radio = this.doc.createElement("radio");
- // The radio element is not being used in the conventional way, thus
- // the devtools-tab class replaces the radio XBL binding with its base
- // binding (the control-item binding).
- radio.className = "devtools-tab";
- radio.id = "toolbox-tab-" + id;
- radio.setAttribute("toolid", id);
- radio.setAttribute("tabindex", "0");
- radio.setAttribute("ordinal", toolDefinition.ordinal);
- radio.setAttribute("tooltiptext", toolDefinition.tooltip);
- if (toolDefinition.invertIconForLightTheme) {
- radio.setAttribute("icon-invertable", "light-theme");
- } else if (toolDefinition.invertIconForDarkTheme) {
- radio.setAttribute("icon-invertable", "dark-theme");
- }
-
- radio.addEventListener("command", this.selectTool.bind(this, id));
-
- // spacer lets us center the image and label, while allowing cropping
- let spacer = this.doc.createElement("spacer");
- spacer.setAttribute("flex", "1");
- radio.appendChild(spacer);
-
- if (toolDefinition.icon) {
- let image = this.doc.createElement("image");
- image.className = "default-icon";
- image.setAttribute("src",
- toolDefinition.icon || toolDefinition.highlightedicon);
- radio.appendChild(image);
- // Adding the highlighted icon image
- image = this.doc.createElement("image");
- image.className = "highlighted-icon";
- image.setAttribute("src",
- toolDefinition.highlightedicon || toolDefinition.icon);
- radio.appendChild(image);
- }
-
- if (toolDefinition.label && !toolDefinition.iconOnly) {
- let label = this.doc.createElement("label");
- label.setAttribute("value", toolDefinition.label);
- label.setAttribute("crop", "end");
- label.setAttribute("flex", "1");
- radio.appendChild(label);
- }
-
if (!toolDefinition.bgTheme) {
toolDefinition.bgTheme = "theme-toolbar";
}
- let vbox = this.doc.createElement("vbox");
- vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
+ let panel = this.doc.createElement("vbox");
+ panel.className = "toolbox-panel " + toolDefinition.bgTheme;
// There is already a container for the webconsole frame.
if (!this.doc.getElementById("toolbox-panel-" + id)) {
- vbox.id = "toolbox-panel-" + id;
+ panel.id = "toolbox-panel-" + id;
}
- if (id === "options") {
- // Options panel is special. It doesn't belong in the same container as
- // the other tabs.
- radio.setAttribute("role", "button");
- let optionTabContainer = this.doc.getElementById("toolbox-option-container");
- optionTabContainer.appendChild(radio);
- deck.appendChild(vbox);
- } else {
- radio.setAttribute("role", "tab");
-
- // If there is no tab yet, or the ordinal to be added is the largest one.
- if (tabs.childNodes.length == 0 ||
- tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
- tabs.appendChild(radio);
- deck.appendChild(vbox);
- } else {
- // else, iterate over all the tabs to get the correct location.
- Array.some(tabs.childNodes, (node, i) => {
- if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
- tabs.insertBefore(radio, node);
- deck.insertBefore(vbox, deck.childNodes[i]);
- return true;
- }
- return false;
- });
- }
- }
+ deck.appendChild(panel);
this._addKeysToWindow();
},
/**
* Lazily created map of the additional tools registered to this toolbox.
*
* @returns {Map<string, object>}
@@ -1277,17 +1272,31 @@ Toolbox.prototype = {
/**
* Retrieve the array of the additional tools registered to this toolbox.
*
* @return {Array<object>}
* the array of additional tool definitions registered on this toolbox.
*/
getAdditionalTools() {
- return Array.from(this.additionalToolDefinitions.values());
+ if (this._additionalToolDefinitions) {
+ return Array.from(this.additionalToolDefinitions.values());
+ }
+ return [];
+ },
+
+ /**
+ * Get the additional tools that have been registered and are visible.
+ *
+ * @return {Array<object>}
+ * the array of additional tool definitions registered on this toolbox.
+ */
+ getVisibleAdditionalTools() {
+ return this.visibleAdditionalTools
+ .map(toolId => this.additionalToolDefinitions.get(toolId));
},
/**
* Test the existence of a additional tools registered to this toolbox by tool id.
*
* @param {string} toolId
* the id of the tool to test for existence.
*
@@ -1308,35 +1317,39 @@ Toolbox.prototype = {
if (!definition.id) {
throw new Error("Tool definition id is missing");
}
if (this.isToolRegistered(definition.id)) {
throw new Error("Tool definition already registered: " +
definition.id);
}
+ this.additionalToolDefinitions.set(definition.id, definition);
+ this.visibleAdditionalTools = [...this.visibleAdditionalTools, definition.id];
- this.additionalToolDefinitions.set(definition.id, definition);
- this._buildTabForTool(definition);
+ this._combineAndSortPanelDefinitions();
+ this._buildPanelForTool(definition);
},
/**
* Unregister and unload an additional tool from this particular toolbox.
*
* @param {string} toolId
* the id of the additional tool to unregister and remove.
*/
removeAdditionalTool(toolId) {
if (!this.hasAdditionalTool(toolId)) {
throw new Error("Tool definition not registered to this toolbox: " +
toolId);
}
+ this.additionalToolDefinitions.delete(toolId);
+ this.visibleAdditionalTools = this.visibleAdditionalTools
+ .filter(id => id !== toolId);
this.unloadTool(toolId);
- this.additionalToolDefinitions.delete(toolId);
},
/**
* Ensure the tool with the given id is loaded.
*
* @param {string} id
* The id of the tool to load.
*/
@@ -1380,16 +1393,17 @@ Toolbox.prototype = {
gDevTools.emit(id + "-init", this, iframe);
this.emit(id + "-init", iframe);
// If no parent yet, append the frame into default location.
if (!iframe.parentNode) {
let vbox = this.doc.getElementById("toolbox-panel-" + id);
vbox.appendChild(iframe);
+ vbox.visibility = "visible";
}
let onLoad = () => {
// Prevent flicker while loading by waiting to make visible until now.
iframe.style.visibility = "visible";
// Try to set the dir attribute as early as possible.
this.setIframeDocumentDir(iframe);
@@ -1517,28 +1531,16 @@ Toolbox.prototype = {
* Switch to the tool with the given id
*
* @param {string} id
* The id of the tool to switch to
*/
selectTool: function (id) {
this.emit("before-select", id);
- let tabs = this.doc.querySelectorAll(".devtools-tab");
- this.selectSingleNode(tabs, "toolbox-tab-" + id);
-
- // If options is selected, the separator between it and the
- // command buttons should be hidden.
- let sep = this.doc.getElementById("toolbox-controls-separator");
- if (id === "options") {
- sep.setAttribute("invisible", "true");
- } else {
- sep.removeAttribute("invisible");
- }
-
if (this.currentToolId == id) {
let panel = this._toolPanels.get(id);
if (panel) {
// We have a panel instance, so the tool is already fully loaded.
// re-focus tool to get key events again
this.focusTool(id);
@@ -1549,33 +1551,28 @@ Toolbox.prototype = {
// so we are racing another call to selectTool with the same id.
return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
}
if (!this.isReady) {
throw new Error("Can't select tool, wait for toolbox 'ready' event");
}
- let tab = this.doc.getElementById("toolbox-tab-" + id);
-
- if (tab) {
+ // Check if the tool exists.
+ if (this.panelDefinitions.find((definition) => definition.id === id) ||
+ id === "options" ||
+ this.additionalToolDefinitions.get(id)) {
if (this.currentToolId) {
this._telemetry.toolClosed(this.currentToolId);
}
this._telemetry.toolOpened(id);
} else {
throw new Error("No tool found");
}
- let tabstrip = this.doc.getElementById("toolbox-tabs");
-
- // select the right tab, making 0th index the default tab if right tab not
- // found.
- tabstrip.selectedItem = tab || tabstrip.childNodes[0];
-
// and select the right iframe
let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
this.lastUsedToolId = this.currentToolId;
this.currentToolId = id;
this._refreshConsoleDisplay();
if (id != "options") {
@@ -1639,19 +1636,19 @@ Toolbox.prototype = {
*
* @returns {Promise} a promise that resolves once the tool has been
* loaded and focused.
*/
openSplitConsole: function () {
this._splitConsole = true;
Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
this._refreshConsoleDisplay();
- this.emit("split-console");
return this.loadTool("webconsole").then(() => {
+ this.emit("split-console");
this.focusConsoleInput();
});
},
/**
* Closes the split console.
*
* @returns {Promise} a promise that resolves once the tool has been
@@ -1692,57 +1689,65 @@ Toolbox.prototype = {
reloadTarget: function (force) {
this.target.activeTab.reload({ force: force });
},
/**
* Loads the tool next to the currently selected tool.
*/
selectNextTool: function () {
- let tools = this.doc.querySelectorAll(".devtools-tab");
- let selected = this.doc.querySelector(".devtools-tab[selected]");
- let nextIndex = [...tools].indexOf(selected) + 1;
- let next = tools[nextIndex] || tools[0];
- let tool = next.getAttribute("toolid");
- return this.selectTool(tool);
+ const index = this.panelDefinitions.findIndex(({id}) => id === this.currentToolId);
+ let definition = this.panelDefinitions[index + 1];
+ if (!definition) {
+ definition = index === -1
+ ? this.panelDefinitions[0]
+ : this.optionsDefinition;
+ }
+ return this.selectTool(definition.id);
},
/**
* Loads the tool just left to the currently selected tool.
*/
selectPreviousTool: function () {
- let tools = this.doc.querySelectorAll(".devtools-tab");
- let selected = this.doc.querySelector(".devtools-tab[selected]");
- let prevIndex = [...tools].indexOf(selected) - 1;
- let prev = tools[prevIndex] || tools[tools.length - 1];
- let tool = prev.getAttribute("toolid");
- return this.selectTool(tool);
+ const index = this.panelDefinitions.findIndex(({id}) => id === this.currentToolId);
+ let definition = this.panelDefinitions[index - 1];
+ if (!definition) {
+ definition = index === -1
+ ? this.panelDefinitions[this.panelDefinitions.length - 1]
+ : this.optionsDefinition;
+ }
+ return this.selectTool(definition.id);
},
/**
* Highlights the tool's tab if it is not the currently selected tool.
*
* @param {string} id
* The id of the tool to highlight
*/
- highlightTool: function (id) {
- let tab = this.doc.getElementById("toolbox-tab-" + id);
- tab && tab.setAttribute("highlighted", "true");
- },
+ highlightTool: Task.async(function* (id) {
+ if (!this.component) {
+ yield this.isOpen;
+ }
+ this.component.highlightTool(id);
+ }),
/**
* De-highlights the tool's tab.
*
* @param {string} id
* The id of the tool to unhighlight
*/
- unhighlightTool: function (id) {
- let tab = this.doc.getElementById("toolbox-tab-" + id);
- tab && tab.removeAttribute("highlighted");
- },
+ unhighlightTool: Task.async(function* (id) {
+ if (!this.component) {
+ yield this.isOpen;
+ }
+ this.component.unhighlightTool(id);
+ }),
/**
* Raise the toolbox host.
*/
raise: function () {
this.postMessage({
name: "raise-host"
});
@@ -1762,46 +1767,45 @@ Toolbox.prototype = {
this.postMessage({
name: "set-host-title",
title
});
},
// Returns an instance of the preference actor
get _preferenceFront() {
- return this.target.root.then(rootForm => {
- return getPreferenceFront(this.target.client, rootForm);
+ return this.isOpen.then(() => {
+ return this.target.root.then(rootForm => {
+ return getPreferenceFront(this.target.client, rootForm);
+ });
});
},
- _toggleAutohide: Task.async(function* () {
- let prefName = "ui.popup.disable_autohide";
+ _toggleNoAutohide: Task.async(function* () {
let front = yield this._preferenceFront;
- let current = yield front.getBoolPref(prefName);
- yield front.setBoolPref(prefName, !current);
+ let toggledValue = !(yield this._isDisableAutohideEnabled(front));
- this._updateNoautohideButton();
+ front.setBoolPref(DISABLE_AUTOHIDE_PREF, toggledValue);
+
+ this.autohideButton.isChecked = toggledValue;
}),
- _updateNoautohideButton: Task.async(function* () {
- let menu = this.doc.getElementById("command-button-noautohide");
- if (menu.getAttribute("hidden") === "true") {
- return;
- }
- if (!this.target.root) {
- return;
+ _isDisableAutohideEnabled: Task.async(function* (prefFront) {
+ // Ensure that the tools are open, and the button is visible.
+ yield this.isOpen;
+ if (!this.autohideButton.isVisible) {
+ return false;
}
- let prefName = "ui.popup.disable_autohide";
- let front = yield this._preferenceFront;
- let current = yield front.getBoolPref(prefName);
- if (current) {
- menu.setAttribute("checked", "true");
- } else {
- menu.removeAttribute("checked");
+
+ // If no prefFront was provided, then get one.
+ if (!prefFront) {
+ prefFront = yield this._preferenceFront;
}
+
+ return yield prefFront.getBoolPref(DISABLE_AUTOHIDE_PREF);
}),
_listFrames: function (event) {
if (!this._target.activeTab || !this._target.activeTab.traits.frames) {
// We are not targetting a regular TabActor
// it can be either an addon or browser toolbox actor
return promise.resolve();
}
@@ -1833,21 +1837,21 @@ Toolbox.prototype = {
checked,
click: () => {
this.onSelectFrame(frame.id);
}
}));
});
menu.once("open").then(() => {
- target.setAttribute("open", "true");
+ this.frameButton.isChecked = true;
});
menu.once("close").then(() => {
- target.removeAttribute("open");
+ this.frameButton.isChecked = false;
});
// Show a drop down menu with frames.
// XXX Missing menu API for specifying target (anchor)
// and relative position to it. See also:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
// https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
let rect = target.getBoundingClientRect();
@@ -1925,23 +1929,22 @@ Toolbox.prototype = {
let topFrames = frames.filter(frame => !frame.parentID);
this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
}
// Check out whether top frame is currently selected.
// Note that only child frame has parentID.
let frame = this.frameMap.get(this.selectedFrameId);
let topFrameSelected = frame ? !frame.parentID : false;
- let button = this.doc.getElementById("command-button-frames");
- button.removeAttribute("checked");
+ this._framesButtonChecked = false;
// If non-top level frame is selected the toolbar button is
// marked as 'checked' indicating that a child frame is active.
if (!topFrameSelected && this.selectedFrameId) {
- button.setAttribute("checked", "true");
+ this._framesButtonChecked = false;
}
},
/**
* Switch to the last used host for the toolbox UI.
*/
switchToPreviousHost: function () {
return this.switchHost("previous");
@@ -2035,35 +2038,42 @@ Toolbox.prototype = {
}
if (this._toolPanels.has(toolId)) {
let instance = this._toolPanels.get(toolId);
instance.destroy();
this._toolPanels.delete(toolId);
}
- let radio = this.doc.getElementById("toolbox-tab-" + toolId);
let panel = this.doc.getElementById("toolbox-panel-" + toolId);
- if (radio) {
- if (this.currentToolId == toolId) {
- let nextToolName = null;
- if (radio.nextSibling) {
- nextToolName = radio.nextSibling.getAttribute("toolid");
- }
- if (radio.previousSibling) {
- nextToolName = radio.previousSibling.getAttribute("toolid");
- }
- if (nextToolName) {
- this.selectTool(nextToolName);
- }
+ // Select another tool.
+ if (this.currentToolId == toolId) {
+ let index = this.panelDefinitions.findIndex(({id}) => id === toolId);
+ let nextTool = this.panelDefinitions[index + 1];
+ let previousTool = this.panelDefinitions[index - 1];
+ let toolNameToSelect;
+
+ if (nextTool) {
+ toolNameToSelect = nextTool.id;
}
- radio.parentNode.removeChild(radio);
+ if (previousTool) {
+ toolNameToSelect = previousTool.id;
+ }
+ if (toolNameToSelect) {
+ this.selectTool(toolNameToSelect);
+ }
}
+ // Remove this tool from the current panel definitions.
+ this.panelDefinitions = this.panelDefinitions.filter(({id}) => id !== toolId);
+ this.visibleAdditionalTools = this.visibleAdditionalTools
+ .filter(id => id !== toolId);
+ this._combineAndSortPanelDefinitions();
+
if (panel) {
panel.parentNode.removeChild(panel);
}
if (this.hostType == Toolbox.HostType.WINDOW) {
let doc = this.win.parent.document;
let key = doc.getElementById("key_" + toolId);
if (key) {
@@ -2075,27 +2085,38 @@ Toolbox.prototype = {
/**
* Handler for the tool-registered event.
* @param {string} event
* Name of the event ("tool-registered")
* @param {string} toolId
* Id of the tool that was registered
*/
_toolRegistered: function (event, toolId) {
- let tool = this.getToolDefinition(toolId);
- if (!tool) {
- // Ignore if the tool is not found, when a per-toolbox tool
- // has been toggle in the toolbox options view, every toolbox will receive
- // the toolbox-register and toolbox-unregister events.
- return;
+ // Tools can either be in the global devtools, or added to this specific toolbox
+ // as an additional tool.
+ let definition = gDevTools.getToolDefinition(toolId);
+ let isAdditionalTool = false;
+ if (!definition) {
+ definition = this.additionalToolDefinitions.get(toolId);
+ isAdditionalTool = true;
}
- this._buildTabForTool(tool);
- // Emit the event so tools can listen to it from the toolbox level
- // instead of gDevTools.
- this.emit("tool-registered", toolId);
+
+ if (definition.isTargetSupported(this._target)) {
+ if (isAdditionalTool) {
+ this.visibleAdditionalTools = [...this.visibleAdditionalTools, toolId];
+ this._combineAndSortPanelDefinitions();
+ } else {
+ this.panelDefinitions = this.panelDefinitions.concat(definition);
+ }
+ this._buildPanelForTool(definition);
+
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools.
+ this.emit("tool-registered", toolId);
+ }
},
/**
* Handler for the tool-unregistered event.
* @param {string} event
* Name of the event ("tool-unregistered")
* @param {string} toolId
* id of the tool that was unregistered
@@ -2141,16 +2162,20 @@ Toolbox.prototype = {
return this._destroyingInspector;
}
this._destroyingInspector = Task.spawn(function* () {
if (!this._inspector) {
return;
}
+ // Ensure that the inspector isn't still being initiated, otherwise race conditions
+ // in the initialization process can throw errors.
+ yield this._initInspector;
+
// Releasing the walker (if it has been created)
// This can fail, but in any case, we want to continue destroying the
// inspector/highlighter/selection
// FF42+: Inspector actor starts managing Walker actor and auto destroy it.
if (this._walker && !this.walker.traits.autoReleased) {
try {
yield this._walker.release();
} catch (e) {
@@ -2231,30 +2256,25 @@ Toolbox.prototype = {
}
if (this.webconsolePanel) {
this._saveSplitConsoleHeight();
this.webconsolePanel.removeEventListener("resize",
this._saveSplitConsoleHeight);
this.webconsolePanel = null;
}
- if (this.closeButton) {
- this.closeButton.removeEventListener("click", this.destroy, true);
- this.closeButton = null;
- }
if (this.textBoxContextMenuPopup) {
this.textBoxContextMenuPopup.removeEventListener("popupshowing",
this._updateTextBoxMenuItems, true);
this.textBoxContextMenuPopup = null;
}
- if (this.tabbar) {
- this.tabbar.removeEventListener("focus", this._onTabbarFocus, true);
- this.tabbar.removeEventListener("click", this._onTabbarFocus, true);
- this.tabbar.removeEventListener("keypress", this._onTabbarArrowKeypress);
- this.tabbar = null;
+ if (this._componentMount) {
+ this._componentMount.removeEventListener("keypress", this._onToolbarArrowKeypress);
+ this.ReactDOM.unmountComponentAtNode(this._componentMount);
+ this._componentMount = null;
}
let outstanding = [];
for (let [id, panel] of this._toolPanels) {
try {
gDevTools.emit(id + "-destroy", this, panel);
this.emit(id + "-destroy", panel);
@@ -2273,27 +2293,24 @@ Toolbox.prototype = {
if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
this.target.activeTab.reconfigure({
"cacheDisabled": false,
"serviceWorkersTestingEnabled": false
});
}
// Destroying the walker and inspector fronts
- outstanding.push(this.destroyInspector().then(() => {
- // Removing buttons
- if (this._pickerButton) {
- this._pickerButton.removeEventListener("click", this._togglePicker, false);
- this._pickerButton = null;
- }
- }));
+ outstanding.push(this.destroyInspector());
// Destroy the profiler connection
outstanding.push(this.destroyPerformance());
+ // Destroy the preference front
+ outstanding.push(this.destroyPreference());
+
// Detach the thread
detachThread(this._threadClient);
this._threadClient = null;
// We need to grab a reference to win before this._host is destroyed.
let win = this.win;
if (this._requisition) {
@@ -2447,16 +2464,24 @@ Toolbox.prototype = {
yield this._performanceFrontConnection.promise;
}
this.performance.off("*", this._onPerformanceFrontEvent);
yield this.performance.destroy();
this._performance = null;
}),
/**
+ * Destroy the preferences actor when the toolbox is unloaded.
+ */
+ destroyPreference: Task.async(function* () {
+ let front = yield this._preferenceFront;
+ front.destroy();
+ }),
+
+ /**
* Called when any event comes from the PerformanceFront. If the performance tool is
* already loaded when the first event comes in, immediately unbind this handler, as
* this is only used to queue up observed recordings before the performance tool can
* handle them, which will only occur when `console.profile()` recordings are started
* before the tool loads.
*/
_onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
if (this.getPanel("performance")) {
--- a/devtools/client/framework/toolbox.xul
+++ b/devtools/client/framework/toolbox.xul
@@ -42,38 +42,17 @@
<menuitem id="cMenu_delete"/>
<menuseparator/>
<menuitem id="cMenu_selectAll"/>
</menupopup>
</popupset>
<vbox id="toolbox-container" flex="1">
<div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-notificationbox"/>
- <toolbar class="devtools-tabbar">
- <hbox id="toolbox-picker-container" />
- <hbox id="toolbox-tabs" flex="1" role="tablist" />
- <hbox id="toolbox-buttons" pack="end">
- <html:button id="command-button-frames"
- class="command-button command-button-invertable devtools-button"
- title="&toolboxFramesTooltip;"
- hidden="true" />
- <html:button id="command-button-noautohide"
- class="command-button command-button-invertable devtools-button"
- title="&toolboxNoAutoHideTooltip;"
- hidden="true" />
- </hbox>
- <vbox id="toolbox-controls-separator" class="devtools-separator"/>
- <hbox id="toolbox-option-container"/>
- <hbox id="toolbox-controls">
- <hbox id="toolbox-dock-buttons"/>
- <html:button id="toolbox-close"
- class="devtools-button"
- title="&toolboxCloseButton.tooltip;"/>
- </hbox>
- </toolbar>
+ <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-toolbar-mount" />
<vbox flex="1" class="theme-body">
<!-- Set large flex to allow the toolbox-panel-webconsole to have a
height set to a small value without flexing to fill up extra
space. There must be a flex on both to ensure that the console
panel itself is sized properly -->
<box id="toolbox-deck" flex="1000" minheight="75" />
<splitter id="toolbox-console-splitter" class="devtools-horizontal-splitter" hidden="true" />
<box minheight="75" flex="1" id="toolbox-panel-webconsole" collapsed="true" />
--- a/devtools/client/inspector/test/browser_inspector_highlighter-preview.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-preview.js
@@ -47,10 +47,10 @@ function* clickElement(selector, testAct
function* checkElementSelected(selector, inspector) {
let el = yield getNodeFront(selector, inspector);
is(inspector.selection.nodeFront, el, `The element ${selector} is now selected`);
}
function checkPickerMode(toolbox, isOn) {
let pickerButton = toolbox.doc.querySelector("#command-button-pick");
- is(pickerButton.hasAttribute("checked"), isOn, "The picker mode is correct");
+ is(pickerButton.classList.contains("checked"), isOn, "The picker mode is correct");
}
--- a/devtools/client/inspector/test/browser_inspector_initialization.js
+++ b/devtools/client/inspector/test/browser_inspector_initialization.js
@@ -52,19 +52,17 @@ function* testToolboxInitialization(test
yield testActor.scrollIntoView("span");
yield selectNode("span", inspector);
yield testMarkupView("span", inspector);
yield testBreadcrumbs("span", inspector);
info("Destroying toolbox");
- let destroyed = toolbox.once("destroyed");
- toolbox.destroy();
- yield destroyed;
+ yield toolbox.destroy();
ok("true", "'destroyed' notification received.");
ok(!gDevTools.getToolbox(target), "Toolbox destroyed.");
}
function* testContextMenuInitialization(testActor) {
info("Opening inspector by clicking on 'Inspect Element' context menu item");
yield clickOnInspectMenuItem(testActor, "#salutation");
--- a/devtools/client/locales/en-US/toolbox.dtd
+++ b/devtools/client/locales/en-US/toolbox.dtd
@@ -5,29 +5,16 @@
<!-- LOCALIZATION NOTE : FILE This file contains the Toolbox strings -->
<!-- LOCALIZATION NOTE : FILE Do not translate key -->
<!ENTITY closeCmd.key "W">
<!ENTITY toggleToolbox.key "I">
<!ENTITY toggleToolboxF12.keycode "VK_F12">
<!ENTITY toggleToolboxF12.keytext "F12">
-<!ENTITY toolboxCloseButton.tooltip "Close Developer Tools">
-
-<!-- LOCALIZATION NOTE (toolboxFramesButton): This is the label for
- - the iframes menu list that appears only when the document has some.
- - It allows you to switch the context of the whole toolbox. -->
-<!ENTITY toolboxFramesTooltip "Select an iframe as the currently targeted document">
-
-<!-- LOCALIZATION NOTE (toolboxNoAutoHideButton): This is the label for
- - the button to force the popups/panels to stay visible on blur.
- - This is only visible in the browser toolbox as it is meant for
- - addon developers and Firefox contributors. -->
-<!ENTITY toolboxNoAutoHideTooltip "Disable popup auto hide">
-
<!-- LOCALIZATION NOTE (browserToolboxErrorMessage): This is the label
- shown next to error details when the Browser Toolbox is unable to open. -->
<!ENTITY browserToolboxErrorMessage "Error opening Browser Toolbox:">
<!-- LOCALIZATION NOTE (options.context.advancedSettings): This is the label for
- the heading of the advanced settings group in the options panel. -->
<!ENTITY options.context.advancedSettings "Advanced settings">
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -153,8 +153,23 @@ toolbox.forceReload2.key=CmdOrCtrl+F5
# LOCALIZATION NOTE (toolbox.minimize.key)
# Key shortcut used to minimize the toolbox
toolbox.minimize.key=CmdOrCtrl+Shift+U
# LOCALIZATION NOTE (toolbox.toggleHost.key)
# Key shortcut used to move the toolbox in bottom or side of the browser window
toolbox.toggleHost.key=CmdOrCtrl+Shift+D
+
+# LOCALIZATION NOTE (toolbox.frames.tooltip): This is the label for
+# the iframes menu list that appears only when the document has some.
+# It allows you to switch the context of the whole toolbox.
+toolbox.frames.tooltip=Select an iframe as the currently targeted document
+
+# LOCALIZATION NOTE (toolbox.noautohide.tooltip): This is the label for
+# the button to force the popups/panels to stay visible on blur.
+# This is only visible in the browser toolbox as it is meant for
+# addon developers and Firefox contributors.
+toolbox.noautohide.tooltip=Disable popup auto hide
+
+# LOCALIZATION NOTE (toolbox.closebutton.tooltip): This is the tooltip for
+# the close button the developer tools toolbox.
+toolbox.closebutton.tooltip=Close Developer Tools
--- a/devtools/client/performance/test/browser_perf-highlighted.js
+++ b/devtools/client/performance/test/browser_perf-highlighted.js
@@ -16,33 +16,33 @@ add_task(function* () {
let { target, toolbox, console } = yield initConsoleInNewTab({
url: SIMPLE_URL,
win: window
});
let tab = toolbox.doc.getElementById("toolbox-tab-performance");
yield console.profile("rust");
- yield waitUntil(() => tab.hasAttribute("highlighted"));
+ yield waitUntil(() => tab.classList.contains("highlighted"));
- ok(tab.hasAttribute("highlighted"), "Performance tab is highlighted during recording " +
- "from console.profile when unloaded.");
+ ok(tab.classList.contains("highlighted"), "Performance tab is highlighted during " +
+ "recording from console.profile when unloaded.");
yield console.profileEnd("rust");
- yield waitUntil(() => !tab.hasAttribute("highlighted"));
+ yield waitUntil(() => !tab.classList.contains("highlighted"));
- ok(!tab.hasAttribute("highlighted"),
+ ok(!tab.classList.contains("highlighted"),
"Performance tab is no longer highlighted when console.profile recording finishes.");
let { panel } = yield initPerformanceInTab({ tab: target.tab });
yield startRecording(panel);
- ok(tab.hasAttribute("highlighted"),
+ ok(tab.classList.contains("highlighted"),
"Performance tab is highlighted during recording while in performance tool.");
yield stopRecording(panel);
- ok(!tab.hasAttribute("highlighted"),
+ ok(!tab.classList.contains("highlighted"),
"Performance tab is no longer highlighted when recording finishes.");
yield teardownToolboxAndRemoveTab(panel);
});
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -32,16 +32,17 @@ pref("devtools.toolbox.previousHost", "s
pref("devtools.toolbox.selectedTool", "webconsole");
pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","scratchpad","resize toggle","screenshot --fullpage --file", "rulers", "measure"]');
pref("devtools.toolbox.sideEnabled", true);
pref("devtools.toolbox.zoomValue", "1");
pref("devtools.toolbox.splitconsoleEnabled", false);
pref("devtools.toolbox.splitconsoleHeight", 100);
// Toolbox Button preferences
+pref("devtools.command-button-pick.enabled", true);
pref("devtools.command-button-frames.enabled", true);
pref("devtools.command-button-splitconsole.enabled", true);
pref("devtools.command-button-paintflashing.enabled", false);
pref("devtools.command-button-scratchpad.enabled", false);
pref("devtools.command-button-responsive.enabled", true);
pref("devtools.command-button-screenshot.enabled", false);
pref("devtools.command-button-rulers.enabled", false);
pref("devtools.command-button-measure.enabled", false);
--- a/devtools/client/shared/developer-toolbar.js
+++ b/devtools/client/shared/developer-toolbar.js
@@ -5,17 +5,16 @@
"use strict";
const { Ci } = require("chrome");
const promise = require("promise");
const defer = require("devtools/shared/defer");
const Services = require("Services");
const { TargetFactory } = require("devtools/client/framework/target");
const Telemetry = require("devtools/client/shared/telemetry");
-const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
const {Task} = require("devtools/shared/task");
const NS_XHTML = "http://www.w3.org/1999/xhtml";
const { PluralForm } = require("devtools/shared/plural-form");
@@ -62,81 +61,66 @@ var CommandUtils = {
* @param pref The name of the preference to read
*/
getCommandbarSpec: function (pref) {
let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
return JSON.parse(value);
},
/**
- * A toolbarSpec is an array of strings each of which is a GCLI command.
+ * Create a list of props for React components that manage the state of the buttons.
+ *
+ * @param {Array} toolbarSpec - An array of strings each of which is a GCLI command.
+ * @param {Object} target
+ * @param {Object} document - Used to listen to unload event of the window.
+ * @param {Requisition} requisition
+ * @param {Function} createButtonState - A function that provides a common interface
+ * to create a button for the toolbox.
+ *
+ * @return {Array} List of ToolboxButton objects..
*
* Warning: this method uses the unload event of the window that owns the
* buttons that are of type checkbox. this means that we don't properly
* unregister event handlers until the window is destroyed.
*/
- createButtons: function (toolbarSpec, target, document, requisition) {
+ createCommandButtons: function (toolbarSpec, target, document, requisition,
+ createButtonState) {
return util.promiseEach(toolbarSpec, typed => {
// Ask GCLI to parse the typed string (doesn't execute it)
return requisition.update(typed).then(() => {
- let button = document.createElementNS(NS_XHTML, "button");
-
// Ignore invalid commands
let command = requisition.commandAssignment.value;
if (command == null) {
throw new Error("No command '" + typed + "'");
}
-
- if (command.buttonId != null) {
- button.id = command.buttonId;
- if (command.buttonClass != null) {
- button.className = command.buttonClass;
- }
- } else {
- button.setAttribute("text-as-image", "true");
- button.setAttribute("label", command.name);
+ if (!command.buttonId) {
+ throw new Error("Attempting to add a button to the toolbar, and the command " +
+ "did not have an id.");
}
-
- button.classList.add("devtools-button");
+ // Create the ToolboxButton.
+ let button = createButtonState({
+ id: command.buttonId,
+ className: command.buttonClass,
+ description: command.tooltipText || command.description,
+ onClick: requisition.updateExec.bind(requisition, typed)
+ });
- if (command.tooltipText != null) {
- button.setAttribute("title", command.tooltipText);
- } else if (command.description != null) {
- button.setAttribute("title", command.description);
- }
-
- button.addEventListener("click",
- requisition.updateExec.bind(requisition, typed));
-
- button.addEventListener("keypress", (event) => {
- if (ViewHelpers.isSpaceOrReturn(event)) {
- event.preventDefault();
- requisition.updateExec(typed);
- }
- }, false);
-
- // Allow the command button to be toggleable
- let onChange = null;
+ // Allow the command button to be toggleable.
if (command.state) {
- button.setAttribute("autocheck", false);
-
/**
* The onChange event should be called with an event object that
* contains a target property which specifies which target the event
* applies to. For legacy reasons the event object can also contain
* a tab property.
*/
- onChange = (eventName, ev) => {
+ const onChange = (eventName, ev) => {
if (ev.target == target || ev.tab == target.tab) {
let updateChecked = (checked) => {
- if (checked) {
- button.setAttribute("checked", true);
- } else if (button.hasAttribute("checked")) {
- button.removeAttribute("checked");
- }
+ // This will emit a ToolboxButton update event.
+ button.isChecked = checked;
};
// isChecked would normally be synchronous. An annoying quirk
// of the 'csscoverage toggle' command forces us to accept a
// promise here, but doing Promise.resolve(reply).then(...) here
// makes this async for everyone, which breaks some tests so we
// treat non-promise replies separately to keep then synchronous.
let reply = command.state.isChecked(target);
@@ -145,24 +129,23 @@ var CommandUtils = {
} else {
updateChecked(reply);
}
}
};
command.state.onChange(target, onChange);
onChange("", { target: target });
+
+ document.defaultView.addEventListener("unload", function (event) {
+ if (command.state.offChange) {
+ command.state.offChange(target, onChange);
+ }
+ }, { once: true });
}
- document.defaultView.addEventListener("unload", function (event) {
- if (onChange && command.state.offChange) {
- command.state.offChange(target, onChange);
- }
- button.remove();
- button = null;
- }, { once: true });
requisition.clear();
return button;
});
});
},
--- a/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
+++ b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
@@ -8,16 +8,17 @@ const TEST_URI = "data:text/html;charset
// Because we need to gather stats for the period of time that a tool has been
// opened we make use of setTimeout() to create tool active times.
const TOOL_DELAY = 200;
add_task(function* () {
yield addTab(TEST_URI);
let Telemetry = loadTelemetryAndRecordLogs();
+ yield pushPref("devtools.command-button-paintflashing.enabled", true);
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
info("inspector opened");
info("testing the paintflashing button");
yield testButton(toolbox, Telemetry);
--- a/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
+++ b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
@@ -9,16 +9,18 @@ const TEST_URI = "data:text/html;charset
// Because we need to gather stats for the period of time that a tool has been
// opened we make use of setTimeout() to create tool active times.
const TOOL_DELAY = 200;
add_task(function* () {
yield addTab(TEST_URI);
let Telemetry = loadTelemetryAndRecordLogs();
+ yield pushPref("devtools.command-button-scratchpad.enabled", true);
+
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
info("inspector opened");
let onAllWindowsOpened = trackScratchpadWindows();
info("testing the scratchpad button");
yield testButton(toolbox, Telemetry);
--- a/devtools/client/themes/firebug-theme.css
+++ b/devtools/client/themes/firebug-theme.css
@@ -89,40 +89,56 @@
-moz-box-flex: initial;
min-width: 0;
}
/* Also add negative bottom margin for side panel tabs*/
.theme-firebug .devtools-sidebar-tabs tab {
}
+.theme-firebug .devtools-tab span {
+ padding-inline-end: 0;
+}
+
+/* Tweak the margin and padding values differently for sidebar and the main tab bar */
+.theme-firebug .devtools-tab,
+.theme-firebug .devtools-tab.selected {
+ padding: 2px 4px 0 4px;
+ margin: 3px 1px -1px;
+}
+
+.theme-firebug .devtools-sidebar-tabs tab {
+ margin: 3px 0 -1px 0;
+ padding: 2px 0 0 0;
+}
+
/* In order to hide bottom-border of side panel tabs we need
to make the parent element overflow visible, so child element
can move one pixel down to hide the bottom border of the parent. */
.theme-firebug .devtools-sidebar-tabs tabs {
overflow: visible;
}
.theme-firebug .devtools-tab:hover,
.theme-firebug .devtools-sidebar-tabs tab:hover {
border: 1px solid #C8C8C8 !important;
border-bottom: 1px solid transparent;
}
-.theme-firebug .devtools-tab[selected],
+.theme-firebug .devtools-tab.selected,
.theme-firebug .devtools-sidebar-tabs tab[selected] {
background-color: rgb(247, 251, 254);
border: 1px solid rgb(170, 188, 207) !important;
border-bottom-width: 0 !important;
padding-bottom: 2px;
color: inherit;
}
-.theme-firebug .devtools-tab spacer,
-.theme-firebug .devtools-tab image {
+.theme-firebug .devtools-tabbar .devtools-separator,
+.theme-firebug .devtools-tab img {
display: none;
}
.theme-firebug .toolbox-tab label {
margin: 0;
}
.theme-firebug .devtools-sidebar-tabs tab label {
@@ -130,17 +146,17 @@
}
/* Use different padding for labels inside tabs on Win platform.
Make sure this overrides the default in global.css */
:root[platform="win"].theme-firebug .devtools-sidebar-tabs tab label {
margin: 0 4px !important;
}
-.theme-firebug #panelSideBox .devtools-tab[selected],
+.theme-firebug #panelSideBox .devtools-tab.selected,
.theme-firebug .devtools-sidebar-tabs tab[selected] {
background-color: white;
}
.theme-firebug #panelSideBox .devtools-tab:first-child,
.theme-firebug .devtools-sidebar-tabs tab:first-child {
margin-inline-start: 5px;
}
@@ -150,23 +166,31 @@
.theme-firebug #toolbox-tab-options {
margin-inline-end: 4px;
background-color: white;
}
.theme-firebug #toolbox-tab-options::before {
content: url(chrome://devtools/skin/images/firebug/tool-options.svg);
display: block;
- margin: 4px 7px 0;
+ margin: 4px 4px 0;
}
.theme-firebug #toolbox-tab-options:not([selected]):hover::before {
filter: brightness(80%);
}
+/* Element picker */
+.theme-firebug #toolbox-buttons-start {
+ border: none;
+}
+
+.theme-firebug #command-button-pick {
+ top: 6px;
+}
/* Toolbar */
.theme-firebug .theme-toolbar,
.theme-firebug toolbar,
.theme-firebug .devtools-toolbar {
border-bottom: 1px solid rgb(170, 188, 207) !important;
background-color: var(--theme-tab-toolbar-background) !important;
background-image: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.2));
@@ -228,8 +252,14 @@
.theme-firebug .devtools-toolbarbutton {
min-width: 24px;
}
.theme-firebug #element-picker {
min-height: 21px;
}
+
+/* Make sure the toolbar buttons shrink nicely. */
+
+#toolbox-buttons-end {
+ background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+}
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -182,17 +182,17 @@
.devtools-sidebar-tabs tabs > tab[selected]:hover:active {
color: var(--theme-selection-color);
background: var(--theme-selection-background);
}
/* Invert the colors of certain light theme images for displaying
* inside of the dark theme.
*/
-.devtools-tab[icon-invertable] > image,
+.devtools-tab.icon-invertable > img,
.devtools-toolbarbutton > image,
.devtools-button::before,
#breadcrumb-separator-normal,
.scrollbutton-up > .toolbarbutton-icon,
.scrollbutton-down > .toolbarbutton-icon,
#black-boxed-message-button .button-icon,
#canvas-debugging-empty-notice-button .button-icon,
#toggle-breakpoints[checked] > image,
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -44,48 +44,81 @@
.devtools-tabbar {
-moz-appearance: none;
min-height: 24px;
border: 0px solid;
border-bottom-width: 1px;
padding: 0;
background: var(--theme-tab-toolbar-background);
border-bottom-color: var(--theme-splitter-color);
+ display: flex;
+}
+
+.toolbox-tabs {
+ margin: 0;
+ flex: 1;
}
-#toolbox-tabs {
- margin: 0;
+.toolbox-tabs-wrapper {
+ position: relative;
+ display: flex;
+ flex: 1;
+}
+
+.toolbox-tabs {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
}
/* Set flex attribute to Toolbox buttons and Picker container so,
- they don't overlapp with the tab bar */
-#toolbox-buttons {
+ they don't overlap with the tab bar */
+#toolbox-buttons-end {
display: flex;
}
#toolbox-picker-container {
display: flex;
}
+#toolbox-buttons-start {
+ border: solid 0 var(--theme-splitter-color);
+ border-inline-end-width: 1px;
+}
+
/* Toolbox tabs */
-
.devtools-tab {
- -moz-appearance: none;
- -moz-binding: url("chrome://global/content/bindings/general.xml#control-item");
- -moz-box-align: center;
min-width: 32px;
min-height: 24px;
- max-width: 100px;
margin: 0;
padding: 0;
border-style: solid;
border-width: 0;
border-inline-start-width: 1px;
- -moz-box-align: center;
- -moz-box-flex: 1;
+ padding-inline-end: 10px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ background-color: transparent;
+}
+
+.devtools-tab-icon-only {
+ padding-inline-end: 2px;
+}
+
+.devtools-tab span {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-inline-end: 13px;
+ position: relative;
+ top: 1px;
}
/* Save space on the tab-strip in Firebug theme */
.theme-firebug .devtools-tab {
-moz-box-flex: initial;
}
.theme-dark .devtools-tab {
@@ -109,125 +142,126 @@
.theme-dark .devtools-tab:hover:active {
color: var(--theme-selection-color);
}
.devtools-tab:hover:active {
background-color: var(--toolbar-tab-hover-active);
}
-.theme-dark .devtools-tab:not([selected])[highlighted] {
+.theme-dark .devtools-tab:not(.selected).highlighted {
background-color: hsla(99, 100%, 14%, .3);
}
-.theme-light .devtools-tab:not([selected])[highlighted] {
+.theme-light .devtools-tab:not(.selected).highlighted {
background-color: rgba(44, 187, 15, .2);
}
/* Display execution pointer in the Debugger tab to indicate
that the debugger is paused. */
-.theme-firebug #toolbox-tab-jsdebugger.devtools-tab:not([selected])[highlighted] {
+.theme-firebug #toolbox-tab-jsdebugger.devtools-tab:not(.selected).highlighted {
background-color: rgba(89, 178, 234, .2);
background-image: url(chrome://devtools/skin/images/firebug/tool-debugger-paused.svg);
background-repeat: no-repeat;
padding-left: 13px !important;
background-position: 3px 6px;
}
-.devtools-tab > image {
+.devtools-tab > img {
border: none;
margin: 0;
- margin-inline-start: 4px;
+ margin-inline-start: 10px;
opacity: 0.6;
max-height: 16px;
width: 16px; /* Prevents collapse during theme switching */
+ vertical-align: text-top;
+ margin-inline-end: 6px;
}
/* Support invertable icon flags and make icon white when it's on a blue background */
-.theme-light .devtools-tab[icon-invertable="light-theme"]:not([selected]) > image,
-.devtools-tab[icon-invertable="dark-theme"][selected] > image {
+.theme-light .devtools-tab.icon-invertable-light-theme:not(.selected) > img,
+.devtools-tab.icon-invertable-dark-theme.selected > img {
filter: invert(1);
}
/* Don't apply any filter to non-invertable command button icons */
.command-button:not(.command-button-invertable),
-/* [icon-invertable="light-theme"] icons are white, so do not invert them for the dark theme */
-.theme-dark .devtools-tab[icon-invertable="light-theme"] > image,
+/* icon-invertable-light-theme icons are white, so do not invert them for the dark theme */
+.theme-dark .devtools-tab.icon-invertable-light-theme > img,
/* Since "highlighted" icons are green, we should omit the filter */
-.devtools-tab[icon-invertable][highlighted]:not([selected]) > image {
+.devtools-tab.icon-invertable.highlighted:not(.selected) > img {
filter: none;
}
.devtools-tab > label {
white-space: nowrap;
margin: 0 4px;
}
-.devtools-tab:hover > image {
+.devtools-tab:hover > img {
opacity: 0.8;
}
-.devtools-tab:active > image,
-.devtools-tab[selected] > image {
+.devtools-tab:active > img,
+.devtools-tab.selected > img {
opacity: 1;
}
-.devtools-tabbar .devtools-tab[selected],
-.devtools-tabbar .devtools-tab[selected]:hover:active {
+.devtools-tabbar .devtools-tab.selected,
+.devtools-tabbar .devtools-tab.selected:hover:active {
color: var(--theme-selection-color);
background-color: var(--theme-selection-background);
}
-#toolbox-tabs .devtools-tab[selected],
-#toolbox-tabs .devtools-tab[highlighted] {
+.toolbox-tabs .devtools-tab.selected,
+.toolbox-tabs .devtools-tab.highlighted {
border-width: 0;
padding-inline-start: 1px;
}
-#toolbox-tabs .devtools-tab[selected]:last-child,
-#toolbox-tabs .devtools-tab[highlighted]:last-child {
- padding-inline-end: 1px;
-}
-
-#toolbox-tabs .devtools-tab[selected] + .devtools-tab,
-#toolbox-tabs .devtools-tab[highlighted] + .devtools-tab {
+.toolbox-tabs .devtools-tab.selected + .devtools-tab,
+.toolbox-tabs .devtools-tab.highlighted + .devtools-tab {
border-inline-start-width: 0;
padding-inline-start: 1px;
}
-#toolbox-tabs .devtools-tab:first-child[selected] {
+.toolbox-tabs .devtools-tab:first-child {
border-inline-start-width: 0;
}
-#toolbox-tabs .devtools-tab:last-child {
+.toolbox-tabs .devtools-tab:last-child {
border-inline-end-width: 1px;
}
-.devtools-tab:not([highlighted]) > .highlighted-icon,
-.devtools-tab[selected] > .highlighted-icon,
-.devtools-tab:not([selected])[highlighted] > .default-icon {
- visibility: collapse;
+.devtools-tab:not(.highlighted) > .highlighted-icon,
+.devtools-tab.selected > .highlighted-icon,
+.devtools-tab:not(.selected).highlighted > .default-icon {
+ display: none;
}
/* The options tab is special - it doesn't have the same parent
as the other tabs (toolbox-option-container vs toolbox-tabs) */
-#toolbox-option-container .devtools-tab:not([selected]) {
+#toolbox-option-container .devtools-tab:not(.selected) {
background-color: transparent;
}
#toolbox-option-container .devtools-tab {
border-color: transparent;
border-width: 0;
padding-inline-start: 1px;
}
-#toolbox-tab-options > image {
- margin: 0 8px;
+#toolbox-option-container img {
+ margin-inline-end: 6px;
+ margin-inline-start: 6px;
}
/* Toolbox controls */
+#toolbox-controls, #toolbox-dock-buttons {
+ display: flex;
+}
#toolbox-controls > button,
#toolbox-dock-buttons > button {
-moz-appearance: none;
border: none;
margin: 0 4px;
min-width: 16px;
width: 16px;
}
@@ -258,17 +292,26 @@
#toolbox-dock-bottom-minimize::before {
background-image: url("chrome://devtools/skin/images/dock-bottom-minimize@2x.png");
}
#toolbox-dock-bottom-minimize.minimized::before {
background-image: url("chrome://devtools/skin/images/dock-bottom-maximize@2x.png");
}
-#toolbox-buttons:empty + .devtools-separator,
+/**
+ * Ensure that when the toolbar collapses in on itself when there is not enough room
+ * that it still looks reasonable.
+ */
+.devtools-tabbar > div {
+ background-color: var(--theme-tab-toolbar-background);
+ z-index: 0;
+}
+
+#toolbox-buttons-end:empty + .devtools-separator,
.devtools-separator[invisible] {
visibility: hidden;
}
#toolbox-controls-separator {
margin: 0;
}
@@ -288,32 +331,32 @@
background-color: var(--toolbar-tab-hover);
}
.theme-light .command-button:hover {
background-color: inherit;
}
.command-button:hover:active,
-.command-button[checked=true]:not(:hover) {
+.command-button.checked:not(:hover) {
background-color: var(--toolbar-tab-hover-active)
}
.theme-light .command-button:hover:active,
-.theme-light .command-button[checked=true]:not(:hover) {
+.theme-light .command-button.checked:not(:hover) {
background-color: inherit;
}
.command-button:hover::before {
opacity: 0.85;
}
.command-button:hover:active::before,
-.command-button[checked=true]::before,
-.command-button[open=true]::before {
+.command-button.checked::before,
+.command-button.open::before {
opacity: 1;
}
/* Command button images */
#command-button-paintflashing::before {
background-image: var(--command-paintflashing-image);
}
--- a/devtools/client/webconsole/test/browser_webconsole_split.js
+++ b/devtools/client/webconsole/test/browser_webconsole_split.js
@@ -80,152 +80,143 @@ function runTest() {
let cmdButton = toolbox.doc.querySelector("#command-button-splitconsole");
return {
deckHeight: deckHeight,
containerHeight: containerHeight,
webconsoleHeight: webconsoleHeight,
splitterVisibility: splitterVisibility,
openedConsolePanel: openedConsolePanel,
- buttonSelected: cmdButton.hasAttribute("checked")
+ buttonSelected: cmdButton.classList.contains("checked")
};
}
- function checkWebconsolePanelOpened() {
+ const checkWebconsolePanelOpened = Task.async(function* () {
info("About to check special cases when webconsole panel is open.");
- let deferred = promise.defer();
-
// Start with console split, so we can test for transition to main panel.
- toolbox.toggleSplitConsole();
+ yield toolbox.toggleSplitConsole();
let currentUIState = getCurrentUIState();
ok(currentUIState.splitterVisibility,
"Splitter is visible when console is split");
ok(currentUIState.deckHeight > 0,
"Deck has a height > 0 when console is split");
ok(currentUIState.webconsoleHeight > 0,
"Web console has a height > 0 when console is split");
ok(!currentUIState.openedConsolePanel,
"The console panel is not the current tool");
ok(currentUIState.buttonSelected, "The command button is selected");
- openPanel("webconsole").then(() => {
- currentUIState = getCurrentUIState();
-
- ok(!currentUIState.splitterVisibility,
- "Splitter is hidden when console is opened.");
- is(currentUIState.deckHeight, 0,
- "Deck has a height == 0 when console is opened.");
- is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
- "Web console is full height.");
- ok(currentUIState.openedConsolePanel,
- "The console panel is the current tool");
- ok(currentUIState.buttonSelected,
- "The command button is still selected.");
+ yield openPanel("webconsole");
+ currentUIState = getCurrentUIState();
- // Make sure splitting console does nothing while webconsole is opened
- toolbox.toggleSplitConsole();
-
- currentUIState = getCurrentUIState();
+ ok(!currentUIState.splitterVisibility,
+ "Splitter is hidden when console is opened.");
+ is(currentUIState.deckHeight, 0,
+ "Deck has a height == 0 when console is opened.");
+ is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
+ "Web console is full height.");
+ ok(currentUIState.openedConsolePanel,
+ "The console panel is the current tool");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
- ok(!currentUIState.splitterVisibility,
- "Splitter is hidden when console is opened.");
- is(currentUIState.deckHeight, 0,
- "Deck has a height == 0 when console is opened.");
- is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
- "Web console is full height.");
- ok(currentUIState.openedConsolePanel,
- "The console panel is the current tool");
- ok(currentUIState.buttonSelected,
- "The command button is still selected.");
+ // Make sure splitting console does nothing while webconsole is opened
+ yield toolbox.toggleSplitConsole();
+
+ currentUIState = getCurrentUIState();
- // Make sure that split state is saved after opening another panel
- openPanel("inspector").then(() => {
- currentUIState = getCurrentUIState();
- ok(currentUIState.splitterVisibility,
- "Splitter is visible when console is split");
- ok(currentUIState.deckHeight > 0,
- "Deck has a height > 0 when console is split");
- ok(currentUIState.webconsoleHeight > 0,
- "Web console has a height > 0 when console is split");
- ok(!currentUIState.openedConsolePanel,
- "The console panel is not the current tool");
- ok(currentUIState.buttonSelected,
- "The command button is still selected.");
-
- toolbox.toggleSplitConsole();
- deferred.resolve();
- });
- });
- return deferred.promise;
- }
+ ok(!currentUIState.splitterVisibility,
+ "Splitter is hidden when console is opened.");
+ is(currentUIState.deckHeight, 0,
+ "Deck has a height == 0 when console is opened.");
+ is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
+ "Web console is full height.");
+ ok(currentUIState.openedConsolePanel,
+ "The console panel is the current tool");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
- function openPanel(toolId) {
- let deferred = promise.defer();
- let target = TargetFactory.forTab(gBrowser.selectedTab);
- gDevTools.showToolbox(target, toolId).then(function (box) {
- toolbox = box;
- deferred.resolve();
- }).then(null, console.error);
- return deferred.promise;
- }
+ // Make sure that split state is saved after opening another panel
+ yield openPanel("inspector");
+ currentUIState = getCurrentUIState();
+ ok(currentUIState.splitterVisibility,
+ "Splitter is visible when console is split");
+ ok(currentUIState.deckHeight > 0,
+ "Deck has a height > 0 when console is split");
+ ok(currentUIState.webconsoleHeight > 0,
+ "Web console has a height > 0 when console is split");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
- function openAndCheckPanel(toolId) {
- let deferred = promise.defer();
- openPanel(toolId).then(() => {
- info("Checking toolbox for " + toolId);
- checkToolboxUI(toolbox.getCurrentPanel());
- deferred.resolve();
- });
- return deferred.promise;
- }
+ yield toolbox.toggleSplitConsole();
+ });
- function checkToolboxUI() {
+ const checkToolboxUI = Task.async(function* () {
let currentUIState = getCurrentUIState();
ok(!currentUIState.splitterVisibility, "Splitter is hidden by default");
is(currentUIState.deckHeight, currentUIState.containerHeight,
"Deck has a height > 0 by default");
is(currentUIState.webconsoleHeight, 0,
"Web console is collapsed by default");
ok(!currentUIState.openedConsolePanel,
"The console panel is not the current tool");
ok(!currentUIState.buttonSelected, "The command button is not selected.");
- toolbox.toggleSplitConsole();
+ yield toolbox.toggleSplitConsole();
currentUIState = getCurrentUIState();
ok(currentUIState.splitterVisibility,
"Splitter is visible when console is split");
ok(currentUIState.deckHeight > 0,
"Deck has a height > 0 when console is split");
ok(currentUIState.webconsoleHeight > 0,
"Web console has a height > 0 when console is split");
is(Math.round(currentUIState.deckHeight + currentUIState.webconsoleHeight),
currentUIState.containerHeight,
"Everything adds up to container height");
ok(!currentUIState.openedConsolePanel,
"The console panel is not the current tool");
ok(currentUIState.buttonSelected, "The command button is selected.");
- toolbox.toggleSplitConsole();
+ yield toolbox.toggleSplitConsole();
currentUIState = getCurrentUIState();
ok(!currentUIState.splitterVisibility, "Splitter is hidden after toggling");
is(currentUIState.deckHeight, currentUIState.containerHeight,
"Deck has a height > 0 after toggling");
is(currentUIState.webconsoleHeight, 0,
"Web console is collapsed after toggling");
ok(!currentUIState.openedConsolePanel,
"The console panel is not the current tool");
ok(!currentUIState.buttonSelected, "The command button is not selected.");
+ });
+
+ function openPanel(toolId) {
+ let deferred = promise.defer();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, toolId).then(function (box) {
+ toolbox = box;
+ deferred.resolve();
+ }).then(null, console.error);
+ return deferred.promise;
+ }
+
+ function openAndCheckPanel(toolId) {
+ return openPanel(toolId).then(() => {
+ info("Checking toolbox for " + toolId);
+ return checkToolboxUI(toolbox.getCurrentPanel());
+ });
}
function testBottomHost() {
checkHostType(Toolbox.HostType.BOTTOM);
checkToolboxUI();
toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost);
--- a/devtools/client/webconsole/test/browser_webconsole_split_persist.js
+++ b/devtools/client/webconsole/test/browser_webconsole_split_persist.js
@@ -94,17 +94,17 @@
}
function getHeightPrefValue() {
return Services.prefs.getIntPref("devtools.toolbox.splitconsoleHeight");
}
function isCommandButtonChecked() {
return toolbox.doc.querySelector("#command-button-splitconsole")
- .hasAttribute("checked");
+ .classList.contains("checked");
}
function toggleSplitConsoleWithEscape() {
let onceSplitConsole = toolbox.once("split-console");
let contentWindow = toolbox.win;
contentWindow.focus();
EventUtils.sendKey("ESCAPE", contentWindow);
return onceSplitConsole;