--- a/devtools/client/framework/sidebar.js
+++ b/devtools/client/framework/sidebar.js
@@ -61,20 +61,23 @@ function ToolSidebar(tabbox, panel, uid,
this._tabbox = tabbox;
this._uid = uid;
this._panelDoc = this._tabbox.ownerDocument;
this._toolPanel = panel;
this._options = options;
this._onTabBoxOverflow = this._onTabBoxOverflow.bind(this);
this._onTabBoxUnderflow = this._onTabBoxUnderflow.bind(this);
+ this._onTabClicked = this._onTabClicked.bind(this);
try {
this._width = Services.prefs.getIntPref("devtools.toolsidebar-width." + this._uid);
- } catch (e) {}
+ } catch (e) {
+ // devtools.toolsidebar-width preference could not be retrieved.
+ }
if (!options.disableTelemetry) {
this._telemetry = new Telemetry();
}
this._tabbox.tabpanels.addEventListener("select", this, true);
this._tabs = new Map();
@@ -95,16 +98,18 @@ function ToolSidebar(tabbox, panel, uid,
exports.ToolSidebar = ToolSidebar;
ToolSidebar.prototype = {
TAB_ID_PREFIX: "sidebar-tab-",
TABPANEL_ID_PREFIX: "sidebar-panel-",
+ MENUITEM_ID_PREFIX: "sidebar-alltabs-item-",
+
/**
* Add a "…" button at the end of the tabstripe that toggles a dropdown menu
* containing the list of all tabs if any become hidden due to lack of room.
*
* If the ToolSidebar was created with the "showAllTabsMenu" option set to
* true, this is already done automatically. If not, you may call this
* function at any time to add the menu.
*/
@@ -126,35 +131,96 @@ ToolSidebar.prototype = {
// Create the dropdown menu next to the tabs
this._allTabsBtn = this._panelDoc.createElementNS(XULNS, "toolbarbutton");
this._allTabsBtn.setAttribute("class", "devtools-sidebar-alltabs");
this._allTabsBtn.setAttribute("end", "0");
this._allTabsBtn.setAttribute("top", "0");
this._allTabsBtn.setAttribute("width", "15");
this._allTabsBtn.setAttribute("type", "menu");
this._allTabsBtn.setAttribute("tooltiptext", l10n("sidebar.showAllTabs.tooltip"));
- this._allTabsBtn.setAttribute("hidden", "true");
allTabsContainer.appendChild(this._allTabsBtn);
let menuPopup = this._panelDoc.createElementNS(XULNS, "menupopup");
this._allTabsBtn.appendChild(menuPopup);
// Listening to tabs overflow event to toggle the alltabs button
tabs.addEventListener("overflow", this._onTabBoxOverflow, false);
tabs.addEventListener("underflow", this._onTabBoxUnderflow, false);
+ tabs.addEventListener("click", this._onTabClicked);
// Add menuitems to the alltabs menu if there are already tabs in the
// sidebar
for (let [id, tab] of this._tabs) {
let selected = tab.hasAttribute("selected");
- let item = this._addItemToAllTabsMenu(id, tab, selected);
- item.hidden = tab.hidden;
+ let item = this._addItemToAllTabsMenu(id, tab, {selected});
+ item.hidden = tab.hidden && !tab.optional;
+ }
+
+ this.updateAllTabsButtonVisibility();
+ },
+
+ _onTabClicked: function (e) {
+ let id = e.target.getAttribute("id");
+ if (id.startsWith(this.TAB_ID_PREFIX)) {
+ id = id.split(this.TAB_ID_PREFIX).pop();
+ }
+
+ let tab = this.getTab(id);
+ if (!tab || !tab.optional) {
+ return;
+ }
+
+ let {left, top} = e.target.getBoundingClientRect();
+ let isOnCloseIcon = e.clientX > left && e.clientX < left + 12 &&
+ e.clientY > top && e.clientY < top + 12;
+
+ if (isOnCloseIcon) {
+ this.toggleTab(false, id);
+ if (this._tabbox.selectedTab === tab) {
+ this._selectDefault();
+ }
+ this.emit("tab-closed", id);
}
},
+ _selectDefault: function (e) {
+ for (let [id, tab] of this._tabs) {
+ if (!tab.hidden) {
+ this.select(id);
+ return;
+ }
+ }
+ },
+
+ _onTabBoxUnderflow: function () {
+ this._hasOverflow = false;
+ this.updateAllTabsButtonVisibility();
+ },
+
+ _onTabBoxOverflow: function () {
+ this._hasOverflow = true;
+ this.updateAllTabsButtonVisibility();
+ },
+
+ updateAllTabsButtonVisibility: function () {
+ if (!this._allTabsBtn) {
+ return;
+ }
+
+ if (this._hasOverflow || this.hasHiddenOptionalTab()) {
+ this._allTabsBtn.removeAttribute("hidden");
+ } else {
+ this._allTabsBtn.setAttribute("hidden", "true");
+ }
+ },
+
+ hasHiddenOptionalTab: function () {
+ return [...this._tabs].some(([, tab]) => tab.hidden && tab.optional);
+ },
+
removeAllTabsMenu: function () {
if (!this._allTabsBtn) {
return;
}
let tabs = this._tabbox.tabs;
tabs.removeEventListener("overflow", this._onTabBoxOverflow, false);
@@ -162,77 +228,84 @@ ToolSidebar.prototype = {
// Moving back the tabs as a first child of the tabbox
this._tabbox.insertBefore(tabs, this._tabbox.tabpanels);
this._tabbox.querySelector("stack").remove();
this._allTabsBtn = null;
},
- _onTabBoxOverflow: function () {
- this._allTabsBtn.removeAttribute("hidden");
- },
-
- _onTabBoxUnderflow: function () {
- this._allTabsBtn.setAttribute("hidden", "true");
- },
-
/**
* Add an item in the allTabs menu for a given tab.
*/
- _addItemToAllTabsMenu: function (id, tab, selected = false) {
+ _addItemToAllTabsMenu: function (id, tab, {selected = false, before = ""}) {
if (!this._allTabsBtn) {
- return;
+ return undefined;
}
let item = this._panelDoc.createElementNS(XULNS, "menuitem");
- item.setAttribute("id", "sidebar-alltabs-item-" + id);
+ item.setAttribute("id", this.MENUITEM_ID_PREFIX + id);
item.setAttribute("label", tab.getAttribute("label"));
item.setAttribute("type", "checkbox");
if (selected) {
item.setAttribute("checked", true);
}
// The auto-checking of menuitems in this menu doesn't work, so let's do
// it manually
item.setAttribute("autocheck", false);
- this._allTabsBtn.querySelector("menupopup").appendChild(item);
+ let popup = this._allTabsBtn.querySelector("menupopup");
+ if (before) {
+ popup.insertBefore(item, this.getMenuItem(before));
+ } else {
+ popup.appendChild(item);
+ }
item.addEventListener("click", () => {
this._tabbox.selectedTab = tab;
}, false);
tab.allTabsMenuItem = item;
return item;
},
/**
* Register a tab. A tab is a document.
* The document must have a title, which will be used as the name of the tab.
*
- * @param {string} tab uniq id
- * @param {string} url
+ * @param {String} tab uniq id
+ * @param {String} url
+ * @param {Object} (optional)
+ * - {Boolean} selected: should the tab be selected, defaults to false
+ * - {String} before: tab id before which the new tab should be inserted,
+ * defaults to empty string (no tab id).
*/
- addTab: function (id, url, selected = false) {
+ addTab: function (id, url, {selected = false, before = ""} = {}) {
let iframe = this._panelDoc.createElementNS(XULNS, "iframe");
iframe.className = "iframe-" + id;
iframe.setAttribute("flex", "1");
iframe.setAttribute("src", url);
iframe.tooltip = "aHTMLTooltip";
// Creating the tab and adding it to the tabbox
let tab = this._panelDoc.createElementNS(XULNS, "tab");
- this._tabbox.tabs.appendChild(tab);
- tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading
+ if (before) {
+ this._tabbox.tabs.insertBefore(tab, this.getTab(before));
+ } else {
+ this._tabbox.tabs.appendChild(tab);
+ }
+
+ // Avoid showing "undefined" while the tab is loading
+ tab.setAttribute("label", "");
tab.setAttribute("id", this.TAB_ID_PREFIX + id);
tab.setAttribute("crop", "end");
// Add the tab to the allTabs menu if exists
- let allTabsItem = this._addItemToAllTabsMenu(id, tab, selected);
+ let allTabsItem = this._addItemToAllTabsMenu(id, tab, {selected, before});
let onIFrameLoaded = (event) => {
let doc = event.target;
let win = doc.defaultView;
tab.setAttribute("label", doc.title);
if (allTabsItem) {
allTabsItem.setAttribute("label", doc.title);
@@ -245,17 +318,21 @@ ToolSidebar.prototype = {
this.emit(id + "-ready");
};
iframe.addEventListener("load", onIFrameLoaded, true);
let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel");
tabpanel.setAttribute("id", this.TABPANEL_ID_PREFIX + id);
tabpanel.appendChild(iframe);
- this._tabbox.tabpanels.appendChild(tabpanel);
+ if (before) {
+ this._tabbox.tabpanels.insertBefore(tabpanel, this.getTabPanel(before));
+ } else {
+ this._tabbox.tabpanels.appendChild(tabpanel);
+ }
this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip");
this._tooltip.id = "aHTMLTooltip";
tabpanel.appendChild(this._tooltip);
this._tooltip.page = true;
tab.linkedPanel = this.TABPANEL_ID_PREFIX + id;
@@ -267,17 +344,17 @@ ToolSidebar.prototype = {
}
this.emit("new-tab-registered", id);
},
untitledTabsIndex: 0,
/**
- * Search for existing tabs in the markup that aren't know yet and add them.
+ * Search for existing tabs in the markup that aren't known yet and add them.
*/
addExistingTabs: function () {
let knownTabs = [...this._tabs.values()];
for (let tab of this._tabbox.tabs.querySelectorAll("tab")) {
if (knownTabs.indexOf(tab) !== -1) {
continue;
}
@@ -286,16 +363,18 @@ ToolSidebar.prototype = {
let id = tab.getAttribute("id") || "untitled-tab-" + (this.untitledTabsIndex++);
// If the existing tab contains the tab ID prefix, extract the ID of the
// tab
if (id.startsWith(this.TAB_ID_PREFIX)) {
id = id.split(this.TAB_ID_PREFIX).pop();
}
+ tab.optional = tab.getAttribute("optional") === "true";
+
// Register the tab
this._tabs.set(id, tab);
this.emit("new-tab-registered", id);
}
},
/**
* Remove an existing tab.
@@ -320,16 +399,17 @@ ToolSidebar.prototype = {
// Also remove the tabpanel
let panel = this.getTabPanel(tabPanelId || tabId);
if (panel) {
panel.remove();
}
this._tabs.delete(tabId);
+ this.updateAllTabsButtonVisibility();
this.emit("tab-unregistered", tabId);
}),
/**
* Show or hide a specific tab.
* @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
* @param {String} id The ID of the tab to be hidden.
*/
@@ -338,17 +418,18 @@ ToolSidebar.prototype = {
let tab = this.getTab(id);
if (!tab) {
return;
}
tab.hidden = !isVisible;
// Toggle the item in the allTabs menu.
if (this._allTabsBtn) {
- this._allTabsBtn.querySelector("#sidebar-alltabs-item-" + id).hidden = !isVisible;
+ this.getMenuItem(id).hidden = tab.hidden && !tab.optional;
+ this.updateAllTabsButtonVisibility();
}
},
/**
* Select a specific tab.
*/
select: function (id) {
let tab = this.getTab(id);
@@ -386,28 +467,33 @@ ToolSidebar.prototype = {
/**
* Returns the requested tab panel based on the id.
* @param {String} id
* @return {DOMNode}
*/
getTabPanel: function (id) {
// Search with and without the ID prefix as there might have been existing
// tabpanels by the time the sidebar got created
- return this._tabbox.tabpanels.querySelector("#" + this.TABPANEL_ID_PREFIX + id + ", #" + id);
+ let selector = "#" + this.TABPANEL_ID_PREFIX + id + ", #" + id;
+ return this._tabbox.tabpanels.querySelector(selector);
},
/**
* Return the tab based on the provided id, if one was registered with this id.
* @param {String} id
* @return {DOMNode}
*/
getTab: function (id) {
return this._tabs.get(id);
},
+ getMenuItem: function (id) {
+ return this._allTabsBtn.querySelector("#" + this.MENUITEM_ID_PREFIX + id);
+ },
+
/**
* Event handler.
*/
handleEvent: function (event) {
if (event.type !== "select" || this._destroyed) {
return;
}
@@ -442,16 +528,22 @@ ToolSidebar.prototype = {
// items except the selected one.
let tab = this._tabbox.selectedTab;
if (tab.allTabsMenuItem) {
for (let otherItem of this._allTabsBtn.querySelectorAll("menuitem")) {
otherItem.removeAttribute("checked");
}
tab.allTabsMenuItem.setAttribute("checked", true);
}
+ // When selecting a hidden optional tab, update the tab visibility.
+ if (tab.optional && tab.hidden) {
+ tab.hidden = false;
+ this.updateAllTabsButtonVisibility();
+ }
+ tab.scrollIntoView();
},
/**
* Toggle sidebar's visibility state.
*/
toggle: function () {
if (this._tabbox.hasAttribute("hidden")) {
this.show();
@@ -486,17 +578,18 @@ ToolSidebar.prototype = {
this.emit("show");
},
/**
* Show the sidebar.
*/
hide: function () {
- Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+ let width = this._tabbox.width;
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, width);
this._tabbox.setAttribute("hidden", "true");
this._panelDoc.activeElement.blur();
this.emit("hide");
},
/**
* Return the window containing the tab content.
@@ -504,37 +597,39 @@ ToolSidebar.prototype = {
getWindowForTab: function (id) {
if (!this._tabs.has(id)) {
return null;
}
// Get the tabpanel and make sure it contains an iframe
let panel = this.getTabPanel(id);
if (!panel || !panel.firstChild || !panel.firstChild.contentWindow) {
- return;
+ return null;
}
return panel.firstChild.contentWindow;
},
/**
* Clean-up.
*/
destroy: Task.async(function* () {
if (this._destroyed) {
return;
}
this._destroyed = true;
- Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+ let width = this._tabbox.width;
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, width);
if (this._allTabsBtn) {
this.removeAllTabsMenu();
}
this._tabbox.tabpanels.removeEventListener("select", this, true);
+ this._tabbox.tabs.removeEventListener("click", this._onTabClicked);
// Note that we check for the existence of this._tabbox.tabpanels at each
// step as the container window may have been closed by the time one of the
// panel's destroy promise resolves.
while (this._tabbox.tabpanels && this._tabbox.tabpanels.hasChildNodes()) {
let panel = this._tabbox.tabpanels.firstChild;
let win = panel.firstChild.contentWindow;
if (win && ("destroy" in win)) {
@@ -557,21 +652,21 @@ ToolSidebar.prototype = {
this._tabbox = null;
this._panelDoc = null;
this._toolPanel = null;
})
};
XPCOMUtils.defineLazyGetter(this, "l10n", function () {
let bundle = Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
- let l10n = function (aName, ...aArgs) {
+ let l10n = function (name, ...args) {
try {
- if (aArgs.length == 0) {
- return bundle.GetStringFromName(aName);
- } else {
- return bundle.formatStringFromName(aName, aArgs, aArgs.length);
+ if (args.length == 0) {
+ return bundle.GetStringFromName(name);
}
+ return bundle.formatStringFromName(name, args, args.length);
} catch (ex) {
- console.log("Error reading '" + aName + "'");
+ console.log("Error reading '" + name + "'");
+ return name;
}
};
return l10n;
});
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -1,16 +1,17 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
browser_toolbox_options_disable_js.html
browser_toolbox_options_disable_js_iframe.html
browser_toolbox_options_disable_cache.sjs
browser_toolbox_sidebar_tool.xul
+ browser_toolbox_sidebar_tool_optional_tabs.xul
browser_toolbox_sidebar_tool_toggle_tabs.xul
browser_toolbox_window_title_changes_page.html
browser_toolbox_window_title_frame_select_page.html
code_math.js
code_ugly.js
head.js
shared-head.js
shared-redux-head.js
@@ -58,16 +59,17 @@ skip-if = true # Bug 1177463 - Temporari
# skip-if = os == "win"
[browser_toolbox_ready.js]
[browser_toolbox_select_event.js]
skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
[browser_toolbox_selected_tool_unavailable.js]
[browser_toolbox_sidebar.js]
[browser_toolbox_sidebar_events.js]
[browser_toolbox_sidebar_existing_tabs.js]
+[browser_toolbox_sidebar_optional_tabs.js]
[browser_toolbox_sidebar_overflow_menu.js]
[browser_toolbox_sidebar_toggle_tabs.js]
[browser_toolbox_split_console.js]
[browser_toolbox_tabsswitch_shortcuts.js]
[browser_toolbox_textbox_context_menu.js]
[browser_toolbox_theme_registration.js]
[browser_toolbox_toggle.js]
[browser_toolbox_tool_ready.js]
--- a/devtools/client/framework/test/browser_toolbox_sidebar.js
+++ b/devtools/client/framework/test/browser_toolbox_sidebar.js
@@ -78,17 +78,17 @@ function test() {
});
panel.sidebar.once("tab1-selected", function (event) {
info(event);
tab1Selected = true;
allTabsReady(panel);
});
- panel.sidebar.addTab("tab1", tab1URL, true);
+ panel.sidebar.addTab("tab1", tab1URL, {selected: true});
panel.sidebar.addTab("tab2", tab2URL);
panel.sidebar.addTab("tab3", tab3URL);
panel.sidebar.show();
}).then(null, console.error);
});
function allTabsReady(panel) {
--- a/devtools/client/framework/test/browser_toolbox_sidebar_events.js
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_events.js
@@ -62,17 +62,17 @@ function test() {
collectedEvents.push(event);
});
panel.sidebar.once("hide", function (event, id) {
collectedEvents.push(event);
});
panel.sidebar.once("tab1-selected", () => finishUp(panel));
- panel.sidebar.addTab("tab1", tab1URL, true);
+ panel.sidebar.addTab("tab1", tab1URL, {selected: true});
panel.sidebar.show();
}).then(null, console.error);
});
function finishUp(panel) {
panel.sidebar.hide();
panel.sidebar.destroy();
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_optional_tabs.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the sidebar widget supports optional tabs behaving as follows:
+// - if tab is optional and visible, it has a close icon
+// - if tab is optional and hidden, it is still listed in the all tabs menu
+// - if any of the sidebar tabs is optional and hidden, the all tabs menu button is shown
+
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+
+const testToolDefinition = {
+ id: "testTool",
+ url: CHROME_URL_ROOT + "browser_toolbox_sidebar_tool_optional_tabs.xul",
+ label: "Test Tool",
+ isTargetSupported: () => true,
+ build: (iframeWindow, toolbox) => {
+ return promise.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document,
+ });
+ }
+};
+
+add_task(function* () {
+ let tab = yield addTab("about:blank");
+
+ let target = TargetFactory.forTab(tab);
+
+ gDevTools.registerTool(testToolDefinition);
+ let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+
+ let toolPanel = toolbox.getPanel(testToolDefinition.id);
+ let tabbox = toolPanel.panelDoc.getElementById("sidebar");
+
+ info("Creating the sidebar widget");
+ let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1278984", {
+ showAllTabsMenu: true
+ });
+
+ info("Checking that tab2 has been registered as an optional tab.");
+ ok(sidebar.getTab("tab2"), "Existing tab2 was found");
+ ok(sidebar.getTab("tab2").optional, "tab2 is optional");
+ ok(sidebar.getTabPanel("tabpanel2"), "Existing tabpanel 2 was found");
+
+ let allTabsMenu = toolPanel.panelDoc.querySelector(".devtools-sidebar-alltabs");
+ ok(allTabsMenu, "The all-tabs menu is available");
+ is(allTabsMenu.getAttribute("hidden"), "true", "The menu is hidden for now");
+
+ info("Check that the all tabs menu is displayed after hiding an optional tab");
+ sidebar.toggleTab(false, "tab2");
+ ok(!allTabsMenu.hasAttribute("hidden"), "The menu is displayed now");
+ is(sidebar.getTab("tab2").getAttribute("hidden"), "true", "tab2 is now hidden");
+
+ info("Check that even after simulating an underflow, the button is still visible");
+ sidebar._onTabBoxUnderflow();
+ ok(!allTabsMenu.hasAttribute("hidden"), "The all-tabs menu is still visible");
+
+ info("Check that tab2 becomes visible after clicking on its menu item");
+ let tab2MenuItem = allTabsMenu.querySelector("#sidebar-alltabs-item-tab2");
+ ok(tab2MenuItem, "tab2 is available in allTabsMenu");
+ EventUtils.sendMouseEvent({type: "click"}, tab2MenuItem,
+ toolPanel.panelDoc.defaultView);
+
+ ok(!sidebar.getTab("tab2").hasAttribute("hidden"), "tab2 is now visible");
+ is(allTabsMenu.getAttribute("hidden"), "true", "The menu is now hidden");
+
+ info("Check that clicking on the tab2 close icon hides tab2");
+ // In the current XUL implementation there is no dedicated element to handle the close
+ // icon so we have to rely on a click using a hardcoded offset.
+ EventUtils.synthesizeMouse(sidebar.getTab("tab2"), 5, 5, {},
+ toolPanel.panelDoc.defaultView);
+ is(sidebar.getTab("tab2").getAttribute("hidden"), "true", "tab2 is now hidden");
+ ok(!allTabsMenu.hasAttribute("hidden"), "The menu is displayed now");
+
+ sidebar.destroy();
+ gDevTools.unregisterTool(testToolDefinition.id);
+ gBrowser.removeCurrentTab();
+});
--- a/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js
@@ -43,17 +43,17 @@ add_task(function* () {
let allTabsMenu = toolPanel.panelDoc.querySelector(".devtools-sidebar-alltabs");
ok(allTabsMenu, "The all-tabs menu is available");
is(allTabsMenu.getAttribute("hidden"), "true", "The menu is hidden for now");
info("Adding 10 tabs to the sidebar widget");
for (let nb = 0; nb < 10; nb++) {
let url = `data:text/html;charset=utf8,<title>tab ${nb}</title><p>Test tab ${nb}</p>`;
- sidebar.addTab("tab" + nb, url, nb === 0);
+ sidebar.addTab("tab" + nb, url, {selected: nb === 0});
}
info("Fake an overflow event so that the all-tabs menu is visible");
sidebar._onTabBoxOverflow();
ok(!allTabsMenu.hasAttribute("hidden"), "The all-tabs menu is now shown");
info("Select each tab, one by one");
for (let nb = 0; nb < 10; nb++) {
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_tool_optional_tabs.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox flex="1">
+ <description flex="1">test tool</description>
+ <splitter class="devtools-side-splitter"/>
+ <tabbox flex="1" id="sidebar" class="devtools-sidebar-tabs">
+ <tabs>
+ <tab id="tab1" label="tab 1"></tab>
+ <tab id="tab2" label="tab 2" optional="true"></tab>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="tabpanel1">tab 1</tabpanel>
+ <tabpanel id="tabpanel2">tab 2</tabpanel>
+ </tabpanels>
+ </tabbox>
+ </hbox>
+</window>
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -38,16 +38,18 @@ loader.lazyGetter(this, "strings", () =>
});
loader.lazyGetter(this, "toolboxStrings", () => {
return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
});
loader.lazyGetter(this, "clipboardHelper", () => {
return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
});
+const ANIMATION_INSPECTOR_URL = "chrome://devtools/content/animationinspector/animation-inspector.xhtml";
+
/**
* Represents an open instance of the Inspector for a tab.
* The inspector controls the breadcrumbs, the markup view, and the sidebar
* (computed view, rule view, font view and layout view).
*
* Events:
* - ready
* Fired when the inspector panel is opened for the first time and ready to
@@ -376,35 +378,45 @@ InspectorPanel.prototype = {
if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
defaultTab == "fontinspector") {
defaultTab = "ruleview";
}
this._setDefaultSidebar = (event, toolId) => {
Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
+ if (toolId === "fontinspector") {
+ Services.prefs.setBoolPref("devtools.fontinspector.enabled", true);
+ }
+ };
+
+ this._onSidebarTabClosed = (event, toolId) => {
+ Services.prefs.setBoolPref("devtools.fontinspector.enabled", false);
};
this.sidebar.on("select", this._setDefaultSidebar);
+ this.sidebar.on("tab-closed", this._onSidebarTabClosed);
this.ruleview = new RuleViewTool(this, this.panelWin);
this.computedview = new ComputedViewTool(this, this.panelWin);
- if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
- this.canGetUsedFontFaces) {
+ if (this.canGetUsedFontFaces) {
this.fontInspector = new FontInspector(this, this.panelWin);
- this.sidebar.toggleTab(true, "fontinspector");
+ if (Services.prefs.getBoolPref("devtools.fontinspector.enabled")) {
+ this.sidebar.toggleTab(true, "fontinspector");
+ }
}
this.layoutview = new LayoutView(this, this.panelWin);
if (this.target.form.animationsActor) {
- this.sidebar.addTab("animationinspector",
- "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
- defaultTab == "animationinspector");
+ this.sidebar.addTab("animationinspector", ANIMATION_INSPECTOR_URL, {
+ selected: defaultTab == "animationinspector",
+ before: "fontinspector"
+ });
}
this.sidebar.show(defaultTab);
this.setupSidebarToggle();
},
/**
@@ -659,16 +671,17 @@ InspectorPanel.prototype = {
let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => {
if (front) {
front.destroy();
}
});
this.sidebar.off("select", this._setDefaultSidebar);
+ this.sidebar.off("tab-closed", this._onSidebarTabClosed);
let sidebarDestroyer = this.sidebar.destroy();
this.sidebar = null;
this.addNodeButton.removeEventListener("click", this.addNode);
this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
this.breadcrumbs.destroy();
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -176,23 +176,24 @@
<tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs" hidden="true">
<tabs>
<tab id="sidebar-tab-ruleview"
label="&ruleViewTitle;"
crop="end"/>
<tab id="sidebar-tab-computedview"
label="&computedViewTitle;"
crop="end"/>
+ <tab id="sidebar-tab-layoutview"
+ label="&layoutViewTitle;"
+ crop="end"/>
<tab id="sidebar-tab-fontinspector"
label="&fontInspectorTitle;"
crop="end"
- hidden="true"/>
- <tab id="sidebar-tab-layoutview"
- label="&layoutViewTitle;"
- crop="end"/>
+ hidden="true"
+ optional="true"/>
</tabs>
<tabpanels flex="1">
<tabpanel id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel">
<html:div id="ruleview-toolbar-container" class="devtools-toolbar">
<html:div id="ruleview-toolbar">
<html:div class="devtools-searchbox">
<html:input id="ruleview-searchbox"
class="devtools-searchinput devtools-rule-searchbox"
@@ -234,51 +235,16 @@
<html:div id="propertyContainer">
</html:div>
<html:div id="noResults" hidden="">
&noPropertiesFound;
</html:div>
</tabpanel>
- <tabpanel id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
- <html:div class="devtools-toolbar">
- <html:div class="devtools-searchbox">
- <html:input id="font-preview-text-input"
- class="devtools-textinput"
- type="search"
- placeholder="&previewHint;"/>
- </html:div>
- </html:div>
-
- <html:div id="font-container">
- <html:ul id="all-fonts"></html:ul>
- <html:button id="font-showall">&showAllFonts;</html:button>
- </html:div>
-
- <html:div id="font-template">
- <html:section class="font">
- <html:div class="font-preview-container">
- <html:img class="font-preview"></html:img>
- </html:div>
- <html:div class="font-info">
- <html:h1 class="font-name"></html:h1>
- <html:span class="font-is-local">&system;</html:span>
- <html:span class="font-is-remote">&remote;</html:span>
- <html:p class="font-format-url">
- <html:input readonly="readonly" class="font-url"></html:input>
- <html:span class="font-format"></html:span>
- </html:p>
- <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
- <html:pre class="font-css-code"></html:pre>
- </html:div>
- </html:section>
- </html:div>
- </tabpanel>
-
<tabpanel id="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
<html:div id="layout-wrapper">
<html:div id="layout-container">
<html:p id="layout-header">
<html:span id="layout-element-size"></html:span>
<html:section id="layout-position-group">
<html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
<html:span id="layout-element-position"></html:span>
@@ -317,12 +283,47 @@
</html:div>
<html:div style="display: none">
<html:p id="layout-dummy"></html:p>
</html:div>
</html:div>
</html:div>
</tabpanel>
+
+ <tabpanel id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
+ <html:div class="devtools-toolbar">
+ <html:div class="devtools-searchbox">
+ <html:input id="font-preview-text-input"
+ class="devtools-textinput"
+ type="search"
+ placeholder="&previewHint;"/>
+ </html:div>
+ </html:div>
+
+ <html:div id="font-container">
+ <html:ul id="all-fonts"></html:ul>
+ <html:button id="font-showall">&showAllFonts;</html:button>
+ </html:div>
+
+ <html:div id="font-template">
+ <html:section class="font">
+ <html:div class="font-preview-container">
+ <html:img class="font-preview"></html:img>
+ </html:div>
+ <html:div class="font-info">
+ <html:h1 class="font-name"></html:h1>
+ <html:span class="font-is-local">&system;</html:span>
+ <html:span class="font-is-remote">&remote;</html:span>
+ <html:p class="font-format-url">
+ <html:input readonly="readonly" class="font-url"></html:input>
+ <html:span class="font-format"></html:span>
+ </html:p>
+ <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
+ <html:pre class="font-css-code"></html:pre>
+ </html:div>
+ </html:section>
+ </html:div>
+ </tabpanel>
</tabpanels>
</tabbox>
</box>
</window>
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -302,16 +302,17 @@ devtools.jar:
skin/images/tool-profiler-active.svg (themes/images/tool-profiler-active.svg)
skin/images/tool-network.svg (themes/images/tool-network.svg)
skin/images/tool-scratchpad.svg (themes/images/tool-scratchpad.svg)
skin/images/tool-webaudio.svg (themes/images/tool-webaudio.svg)
skin/images/tool-memory.svg (themes/images/tool-memory.svg)
skin/images/tool-memory-active.svg (themes/images/tool-memory-active.svg)
skin/images/tool-dom.svg (themes/images/tool-dom.svg)
skin/images/close.svg (themes/images/close.svg)
+ skin/images/close-dark.svg (themes/images/close-dark.svg)
skin/images/clear.svg (themes/images/clear.svg)
skin/images/vview-delete.png (themes/images/vview-delete.png)
skin/images/vview-delete@2x.png (themes/images/vview-delete@2x.png)
skin/images/vview-edit.png (themes/images/vview-edit.png)
skin/images/vview-edit@2x.png (themes/images/vview-edit@2x.png)
skin/images/vview-lock.png (themes/images/vview-lock.png)
skin/images/vview-lock@2x.png (themes/images/vview-lock@2x.png)
skin/images/vview-open-inspector.png (themes/images/vview-open-inspector.png)
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -2303,17 +2303,17 @@ ScratchpadSidebar.prototype = {
this._update(aObject).then(() => deferred.resolve());
};
if (this._sidebar.getCurrentTabID() == "variablesview") {
onTabReady();
}
else {
this._sidebar.once("variablesview-ready", onTabReady);
- this._sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
+ this._sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true});
}
return deferred.promise;
},
/**
* Show the sidebar.
*/
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/close-dark.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#393F4C">
+ <path d="M6.7 8l3.6-3.6c.2-.2.2-.5 0-.7-.2-.2-.5-.2-.7 0L6 7.3 2.4 3.7c-.2-.2-.5-.2-.7 0-.2.2-.2.5 0 .7L5.3 8l-3.6 3.6c-.2.2-.2.5 0 .7.2.2.5.2.7 0L6 8.7l3.6 3.6c.2.2.5.2.7 0 .2-.2.2-.5 0-.7L6.7 8z"/>
+</svg>
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -10,16 +10,17 @@
--searchbox-background-color: #ffee99;
--searchbox-border-color: #ffbf00;
--searcbox-no-match-background-color: #ffe5e5;
--searcbox-no-match-border-color: #e52e2e;
--magnifying-glass-image: url(images/magnifying-glass-light.png);
--magnifying-glass-image-2x: url(images/magnifying-glass-light@2x.png);
--tool-options-image: url(images/tool-options.svg);
--close-button-image: url(chrome://devtools/skin/images/close.svg);
+ --close-dark-button-image: url(chrome://devtools/skin/images/close-dark.svg);
--icon-filter: invert(1);
--dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
--dock-side-image: url(chrome://devtools/skin/images/dock-side.svg);
--dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
--toolbar-button-border-color: rgba(170, 170, 170, .5);
/* Toolbox buttons */
--command-paintflashing-image: url(images/command-paintflashing.svg);
@@ -41,16 +42,17 @@
--searchbox-background-color: #4d4222;
--searchbox-border-color: #d99f2b;
--searcbox-no-match-background-color: #402325;
--searcbox-no-match-border-color: #cc3d3d;
--magnifying-glass-image: url(images/magnifying-glass.png);
--magnifying-glass-image-2x: url(images/magnifying-glass@2x.png);
--tool-options-image: url(images/tool-options.svg);
--close-button-image: url(chrome://devtools/skin/images/close.svg);
+ --close-dark-button-image: url(chrome://devtools/skin/images/close.svg);
--icon-filter: none;
--dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
--dock-side-image: url(chrome://devtools/skin/images/dock-side.svg);
--dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
--toolbar-button-border-color: rgba(0, 0, 0, .4);
/* Toolbox buttons */
--command-paintflashing-image: url(images/command-paintflashing.svg);
@@ -66,16 +68,17 @@
--command-measure-image: url(images/command-measure.svg);
}
.theme-firebug {
--magnifying-glass-image: url(images/firebug/filter.svg);
--magnifying-glass-image-2x: url(images/firebug/filter.svg);
--tool-options-image: url(images/firebug/tool-options.svg);
--close-button-image: url(chrome://devtools/skin/images/firebug/close.svg);
+ --close-dark-button-image: url(chrome://devtools/skin/images/firebug/close.svg);
--icon-filter: invert(1);
--dock-bottom-image: url(chrome://devtools/skin/images/firebug/dock-bottom.svg);
--dock-side-image: url(chrome://devtools/skin/images/firebug/dock-side.svg);
--dock-undock-image: url(chrome://devtools/skin/images/firebug/dock-undock.svg);
--toolbar-button-border-color: rgba(170, 170, 170, .5);
/* Toolbox buttons */
--command-paintflashing-image: url(images/firebug/command-paintflashing.svg);
@@ -597,16 +600,17 @@
border-inline-start-width: 1px;
border-style: solid;
border-radius: 0;
position: static;
text-shadow: none;
}
.devtools-sidebar-tabs tabs > tab {
+ position: relative;
border-image: linear-gradient(transparent 15%, var(--theme-splitter-color) 15%, var(--theme-splitter-color) 85%, transparent 85%) 1 1;
}
.devtools-sidebar-tabs tabs > tab[selected],
.devtools-sidebar-tabs tabs > tab[selected] + tab {
border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
}
@@ -623,16 +627,36 @@
}
.devtools-sidebar-tabs tabs > tab[selected],
.devtools-sidebar-tabs tabs > tab[selected]:hover:active {
color: var(--theme-selection-color);
background: var(--theme-selection-background);
}
+/* Optional toolbar item: close button. */
+
+.devtools-sidebar-tabs tabs > tab[optional="true"],
+.devtools-sidebar-tabs tabs > tab[optional="true"]:hover:active {
+ background-image: var(--close-dark-button-image);
+ background-position: 2px 1px;
+ background-size: 10px;
+ background-repeat: no-repeat;
+}
+
+.devtools-sidebar-tabs tabs > tab[selected][optional="true"],
+.devtools-sidebar-tabs tabs > tab[selected][optional="true"]:hover:active {
+ background-image: var(--close-button-image);
+}
+
+.theme-firebug .devtools-sidebar-tabs tabs > tab[optional="true"] {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
/* Toolbox - moved from toolbox.css.
* Rules that apply to the global toolbox like command buttons,
* devtools tabs, docking buttons, etc. */
#toolbox-controls > button,
#toolbox-dock-buttons > button {
-moz-appearance: none;
-moz-user-focus: normal;
--- a/devtools/client/webconsole/jsterm.js
+++ b/devtools/client/webconsole/jsterm.js
@@ -652,17 +652,17 @@ JSTerm.prototype = {
if (this.sidebar.getCurrentTabID() == "variablesview") {
onTabReady();
} else {
this.sidebar.once("variablesview-selected", onTabReady);
this.sidebar.select("variablesview");
}
} else {
this.sidebar.once("variablesview-ready", onTabReady);
- this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
+ this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true});
}
return deferred.promise;
},
/**
* The keypress event handler for the Variables View sidebar. Currently this
* is used for removing the sidebar when Escape is pressed.