In a following patch, all DevTools moz.build files will use DevToolsModules to install JS modules at a path that corresponds directly to their source tree location. Here we rewrite all require and import calls to match the new location that these files are installed to.
549 lines
16 KiB
JavaScript
549 lines
16 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* 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/. */
|
|
|
|
const {Cu} = require("chrome");
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
|
|
var EventEmitter = require("devtools/shared/event-emitter");
|
|
var Telemetry = require("devtools/client/shared/telemetry");
|
|
|
|
const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
/**
|
|
* ToolSidebar provides methods to register tabs in the sidebar.
|
|
* It's assumed that the sidebar contains a xul:tabbox.
|
|
* Typically, you'll want the tabbox parameter to be a XUL tabbox like this:
|
|
*
|
|
* <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs">
|
|
* <tabs/>
|
|
* <tabpanels flex="1"/>
|
|
* </tabbox>
|
|
*
|
|
* The ToolSidebar API has a method to add new tabs, so the tabs and tabpanels
|
|
* nodes can be empty. But they can also already contain items before the
|
|
* ToolSidebar is created.
|
|
*
|
|
* Tabs added through the addTab method are only identified by an ID and a URL
|
|
* which is used as the href of an iframe node that is inserted in the newly
|
|
* created tabpanel.
|
|
* Tabs already present before the ToolSidebar is created may contain anything.
|
|
* However, these tabs must have ID attributes if it is required for the various
|
|
* methods that accept an ID as argument to work here.
|
|
*
|
|
* @param {Node} tabbox
|
|
* <tabbox> node;
|
|
* @param {ToolPanel} panel
|
|
* Related ToolPanel instance;
|
|
* @param {String} uid
|
|
* Unique ID
|
|
* @param {Object} options
|
|
* - hideTabstripe: Should the tabs be hidden. Defaults to false
|
|
* - showAllTabsMenu: Should a drop-down menu be displayed in case tabs
|
|
* become hidden. Defaults to false.
|
|
* - disableTelemetry: By default, switching tabs on and off in the sidebar
|
|
* will record tool usage in telemetry, pass this option to true to avoid it.
|
|
*
|
|
* Events raised:
|
|
* - new-tab-registered : After a tab has been added via addTab. The tab ID
|
|
* is passed with the event. This however, is raised before the tab iframe
|
|
* is fully loaded.
|
|
* - <tabid>-ready : After the tab iframe has been loaded
|
|
* - <tabid>-selected : After tab <tabid> was selected
|
|
* - select : Same as above, but for any tab, the ID is passed with the event
|
|
* - <tabid>-unselected : After tab <tabid> is unselected
|
|
*/
|
|
function ToolSidebar(tabbox, panel, uid, options={}) {
|
|
EventEmitter.decorate(this);
|
|
|
|
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);
|
|
|
|
try {
|
|
this._width = Services.prefs.getIntPref("devtools.toolsidebar-width." + this._uid);
|
|
} catch(e) {}
|
|
|
|
if (!options.disableTelemetry) {
|
|
this._telemetry = new Telemetry();
|
|
}
|
|
|
|
this._tabbox.tabpanels.addEventListener("select", this, true);
|
|
|
|
this._tabs = new Map();
|
|
|
|
// Check for existing tabs in the DOM and add them.
|
|
this.addExistingTabs();
|
|
|
|
if (this._options.hideTabstripe) {
|
|
this._tabbox.setAttribute("hidetabs", "true");
|
|
}
|
|
|
|
if (this._options.showAllTabsMenu) {
|
|
this.addAllTabsMenu();
|
|
}
|
|
|
|
this._toolPanel.emit("sidebar-created", this);
|
|
}
|
|
|
|
exports.ToolSidebar = ToolSidebar;
|
|
|
|
ToolSidebar.prototype = {
|
|
TAB_ID_PREFIX: "sidebar-tab-",
|
|
|
|
TABPANEL_ID_PREFIX: "sidebar-panel-",
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
addAllTabsMenu: function() {
|
|
if (this._allTabsBtn) {
|
|
return;
|
|
}
|
|
|
|
let tabs = this._tabbox.tabs;
|
|
|
|
// Create a container and insert it first in the tabbox
|
|
let allTabsContainer = this._panelDoc.createElementNS(XULNS, "stack");
|
|
this._tabbox.insertBefore(allTabsContainer, tabs);
|
|
|
|
// Move the tabs inside and make them flex
|
|
allTabsContainer.appendChild(tabs);
|
|
tabs.setAttribute("flex", "1");
|
|
|
|
// 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("right", "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);
|
|
|
|
// Add menuitems to the alltabs menu if there are already tabs in the
|
|
// sidebar
|
|
for (let [id, tab] of this._tabs) {
|
|
this._addItemToAllTabsMenu(id, tab, tab.hasAttribute("selected"));
|
|
}
|
|
},
|
|
|
|
removeAllTabsMenu: function() {
|
|
if (!this._allTabsBtn) {
|
|
return;
|
|
}
|
|
|
|
let tabs = this._tabbox.tabs;
|
|
|
|
tabs.removeEventListener("overflow", this._onTabBoxOverflow, false);
|
|
tabs.removeEventListener("underflow", this._onTabBoxUnderflow, false);
|
|
|
|
// 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) {
|
|
if (!this._allTabsBtn) {
|
|
return;
|
|
}
|
|
|
|
let item = this._panelDoc.createElementNS(XULNS, "menuitem");
|
|
item.setAttribute("id", "sidebar-alltabs-item-" + 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);
|
|
|
|
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
|
|
*/
|
|
addTab: function(id, url, selected=false) {
|
|
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
|
|
tab.setAttribute("id", this.TAB_ID_PREFIX + id);
|
|
|
|
// Add the tab to the allTabs menu if exists
|
|
let allTabsItem = this._addItemToAllTabsMenu(id, tab, selected);
|
|
|
|
let onIFrameLoaded = (event) => {
|
|
let doc = event.target;
|
|
let win = doc.defaultView;
|
|
tab.setAttribute("label", doc.title);
|
|
|
|
if (allTabsItem) {
|
|
allTabsItem.setAttribute("label", doc.title);
|
|
}
|
|
|
|
iframe.removeEventListener("load", onIFrameLoaded, true);
|
|
if ("setPanel" in win) {
|
|
win.setPanel(this._toolPanel, iframe);
|
|
}
|
|
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);
|
|
|
|
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;
|
|
|
|
// We store the index of this tab.
|
|
this._tabs.set(id, tab);
|
|
|
|
if (selected) {
|
|
// For some reason I don't understand, if we call this.select in this
|
|
// event loop (after inserting the tab), the tab will never get the
|
|
// the "selected" attribute set to true.
|
|
this._panelDoc.defaultView.setTimeout(() => {
|
|
this.select(id);
|
|
}, 10);
|
|
}
|
|
|
|
this.emit("new-tab-registered", id);
|
|
},
|
|
|
|
untitledTabsIndex: 0,
|
|
|
|
/**
|
|
* Search for existing tabs in the markup that aren't know 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;
|
|
}
|
|
|
|
// Find an ID for this unknown tab
|
|
let id = tab.getAttribute("id") || "untitled-tab-" + (this.untitledTabsIndex++);
|
|
|
|
// Register the tab
|
|
this._tabs.set(id, tab);
|
|
this.emit("new-tab-registered", id);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove an existing tab.
|
|
* @param {String} tabId The ID of the tab that was used to register it, or
|
|
* the tab id attribute value if the tab existed before the sidebar got created.
|
|
* @param {String} tabPanelId Optional. If provided, this ID will be used
|
|
* instead of the tabId to retrieve and remove the corresponding <tabpanel>
|
|
*/
|
|
removeTab: Task.async(function*(tabId, tabPanelId) {
|
|
// Remove the tab if it can be found
|
|
let tab = this.getTab(tabId);
|
|
if (!tab) {
|
|
return;
|
|
}
|
|
|
|
let win = this.getWindowForTab(tabId);
|
|
if (win && ("destroy" in win)) {
|
|
yield win.destroy();
|
|
}
|
|
|
|
tab.remove();
|
|
|
|
// Also remove the tabpanel
|
|
let panel = this.getTabPanel(tabPanelId || tabId);
|
|
if (panel) {
|
|
panel.remove();
|
|
}
|
|
|
|
this._tabs.delete(tabId);
|
|
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.
|
|
*/
|
|
toggleTab: function(isVisible, id) {
|
|
// Toggle the tab.
|
|
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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Select a specific tab.
|
|
*/
|
|
select: function(id) {
|
|
let tab = this.getTab(id);
|
|
if (tab) {
|
|
this._tabbox.selectedTab = tab;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Return the id of the selected tab.
|
|
*/
|
|
getCurrentTabID: function() {
|
|
let currentID = null;
|
|
for (let [id, tab] of this._tabs) {
|
|
if (this._tabbox.tabs.selectedItem == tab) {
|
|
currentID = id;
|
|
break;
|
|
}
|
|
}
|
|
return currentID;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
/**
|
|
* Event handler.
|
|
*/
|
|
handleEvent: function(event) {
|
|
if (event.type !== "select" || this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
if (this._currentTool == this.getCurrentTabID()) {
|
|
// Tool hasn't changed.
|
|
return;
|
|
}
|
|
|
|
let previousTool = this._currentTool;
|
|
this._currentTool = this.getCurrentTabID();
|
|
if (previousTool) {
|
|
if (this._telemetry) {
|
|
this._telemetry.toolClosed(previousTool);
|
|
}
|
|
this.emit(previousTool + "-unselected");
|
|
}
|
|
|
|
if (this._telemetry) {
|
|
this._telemetry.toolOpened(this._currentTool);
|
|
}
|
|
|
|
this.emit(this._currentTool + "-selected");
|
|
this.emit("select", this._currentTool);
|
|
|
|
// Handlers for "select"/"...-selected"/"...-unselected" events might have
|
|
// destroyed the sidebar in the meantime.
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
// Handle menuitem selection if the allTabsMenu is there by unchecking all
|
|
// 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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Toggle sidebar's visibility state.
|
|
*/
|
|
toggle: function() {
|
|
if (this._tabbox.hasAttribute("hidden")) {
|
|
this.show();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Show the sidebar.
|
|
*/
|
|
show: function() {
|
|
if (this._width) {
|
|
this._tabbox.width = this._width;
|
|
}
|
|
this._tabbox.removeAttribute("hidden");
|
|
|
|
this.emit("show");
|
|
},
|
|
|
|
/**
|
|
* Show the sidebar.
|
|
*/
|
|
hide: function() {
|
|
Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
|
|
this._tabbox.setAttribute("hidden", "true");
|
|
|
|
this.emit("hide");
|
|
},
|
|
|
|
/**
|
|
* Return the window containing the tab content.
|
|
*/
|
|
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 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);
|
|
|
|
if (this._allTabsBtn) {
|
|
this.removeAllTabsMenu();
|
|
}
|
|
|
|
this._tabbox.tabpanels.removeEventListener("select", this, true);
|
|
|
|
// 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)) {
|
|
yield win.destroy();
|
|
}
|
|
panel.remove();
|
|
}
|
|
|
|
while (this._tabbox.tabs && this._tabbox.tabs.hasChildNodes()) {
|
|
this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
|
|
}
|
|
|
|
if (this._currentTool && this._telemetry) {
|
|
this._telemetry.toolClosed(this._currentTool);
|
|
}
|
|
|
|
this._toolPanel.emit("sidebar-destroyed", this);
|
|
|
|
this._tabs = null;
|
|
this._tabbox = null;
|
|
this._panelDoc = null;
|
|
this._toolPanel = null;
|
|
})
|
|
}
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "l10n", function() {
|
|
let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
|
|
let l10n = function(aName, ...aArgs) {
|
|
try {
|
|
if (aArgs.length == 0) {
|
|
return bundle.GetStringFromName(aName);
|
|
} else {
|
|
return bundle.formatStringFromName(aName, aArgs, aArgs.length);
|
|
}
|
|
} catch (ex) {
|
|
Services.console.logStringMessage("Error reading '" + aName + "'");
|
|
}
|
|
};
|
|
return l10n;
|
|
});
|