diff --git a/index.js b/index.js index 6f31882..5a45c07 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const { attachTo } = require("sdk/content/mod"); const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm"); +const { getFavicon } = require("sdk/places/favicon"); const self = require("sdk/self"); const { Style } = require("sdk/stylesheet/style"); const tabs = require("sdk/tabs"); @@ -37,7 +38,10 @@ let ContainerService = { "hideTabs", "showTabs", "sortTabs", + "getTabs", + "showTab", "openTab", + "moveTabsToWindow", "queryIdentities", "getIdentity" ]; @@ -51,25 +55,22 @@ let ContainerService = { }); // It can happen that this jsm is loaded after the opening a container tab. - for (let tab of tabs) { - let xulTab = viewFor(tab); - let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10); + for (const tab of tabs) { + const userContextId = this._getUserContextIdFromTab(tab); if (userContextId) { ++this._identitiesState[userContextId].openTabs; } } tabs.on("open", tab => { - let xulTab = viewFor(tab); - let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10); + const userContextId = this._getUserContextIdFromTab(tab); if (userContextId) { ++this._identitiesState[userContextId].openTabs; } }); tabs.on("close", tab => { - let xulTab = viewFor(tab); - let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10); + const userContextId = this._getUserContextIdFromTab(tab); if (userContextId && this._identitiesState[userContextId].openTabs) { --this._identitiesState[userContextId].openTabs; } @@ -144,14 +145,28 @@ let ContainerService = { }; }, + _getUserContextIdFromTab(tab) { + return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10); + }, + + _getTabList(userContextId) { + let list = []; + for (const tab of tabs) { + if (userContextId === this._getUserContextIdFromTab(tab)) { + let object = { title: tab.title, url: tab.url, id: tab.id }; + list.push(object); + } + } + + return list; + }, + // Tabs management hideTabs(args) { return new Promise(resolve => { - for (let tab of tabs) { - let xulTab = viewFor(tab); - let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10); - + for (const tab of tabs) { + const userContextId = this._getUserContextIdFromTab(tab); if ("userContextId" in args && args.userContextId !== userContextId) { continue; } @@ -177,55 +192,145 @@ let ContainerService = { }, sortTabs() { - function sortTabsInternal(window, pinnedTabs) { - // From model to XUL window. - const xulWindow = viewFor(window); - - const tabs = tabsUtils.getTabs(xulWindow); - let pos = 0; - - // Let's collect UCIs/tabs for this window. - let map = new Map; - for (const tab of tabs) { - if (pinnedTabs && !tabsUtils.isPinned(tab)) { - // We don't have, or we already handled all the pinned tabs. - break; - } - - if (!pinnedTabs && tabsUtils.isPinned(tab)) { - // pinned tabs must be consider as taken positions. - ++pos; - continue; - } - - let userContextId = parseInt(tab.getAttribute("usercontextid") || 0, 10); - if (!map.has(userContextId)) { - map.set(userContextId, []); - } - map.get(userContextId).push(tab); - } - - // Let's sort the map. - const sortMap = new Map([...map.entries()].sort((a, b) => a[0] > b[0])); - - // Let's move tabs. - sortMap.forEach(tabs => { - for (const tab of tabs) { - xulWindow.gBrowser.moveTabTo(tab, pos++); - } - }); - } - return new Promise(resolve => { for (let window of windows.browserWindows) { // First the pinned tabs, then the normal ones. - sortTabsInternal(window, true); - sortTabsInternal(window, false); + this._sortTabsInternal(window, true); + this._sortTabsInternal(window, false); } resolve(null); }); }, + _sortTabsInternal(window, pinnedTabs) { + // From model to XUL window. + const xulWindow = viewFor(window); + + const tabs = tabsUtils.getTabs(xulWindow); + let pos = 0; + + // Let's collect UCIs/tabs for this window. + let map = new Map; + for (const tab of tabs) { + if (pinnedTabs && !tabsUtils.isPinned(tab)) { + // We don't have, or we already handled all the pinned tabs. + break; + } + + if (!pinnedTabs && tabsUtils.isPinned(tab)) { + // pinned tabs must be consider as taken positions. + ++pos; + continue; + } + + const userContextId = this._getUserContextIdFromTab(tab); + if (!map.has(userContextId)) { + map.set(userContextId, []); + } + map.get(userContextId).push(tab); + } + + // Let's sort the map. + const sortMap = new Map([...map.entries()].sort((a, b) => a[0] > b[0])); + + // Let's move tabs. + sortMap.forEach(tabs => { + for (const tab of tabs) { + xulWindow.gBrowser.moveTabTo(tab, pos++); + } + }); + }, + + getTabs(args) { + return new Promise((resolve, reject) => { + if (!("userContextId" in args)) { + reject("getTabs must be called with userContextId argument."); + return; + } + + const list = this._getTabList(args.userContextId); + let promises = []; + + for (let object of list) { + promises.push(getFavicon(object.url).then(url => { + object.favicon = url; + }, () => { + object.favicon = ""; + })); + } + + Promise.all(promises).then(() => { + resolve(list); + }); + }); + }, + + showTab(args) { + return new Promise((resolve, reject) => { + if (!("tabId" in args)) { + reject("showTab must be called with tabId argument."); + return; + } + + for (const tab of tabs) { + if (tab.id === args.tabId) { + tab.window.activate(); + tab.activate(); + break; + } + } + + resolve(null); + }); + }, + + moveTabsToWindow(args) { + return new Promise((resolve, reject) => { + if (!("userContextId" in args)) { + reject("moveTabsToWindow must be called with userContextId argument."); + return; + } + + // Let"s create a list of the tabs. + const list = this._getTabList(args.userContextId); + + // Nothing to do + if (list.length === 0) { + resolve(null); + return; + } + + windows.browserWindows.open({ + url: "about:blank", + onOpen: window => { + const newBrowserWindow = viewFor(window); + + // Let's move the tab to the new window. + for (const tab of list) { + const newTab = newBrowserWindow.gBrowser.addTab("about:blank"); + newBrowserWindow.gBrowser.swapBrowsersAndCloseOther(newTab, tab); + // swapBrowsersAndCloseOther is an internal method of gBrowser + // an it's not supported by addon SDK. This means that we + // don't receive an 'open' event, but only the 'close' one. + // We have to force a +1 in our tab counter. + ++this._identitiesState[args.userContextId].openTabs; + } + + // Let's close all the normal tab in the new window. In theory it + // should be only the first tab, but maybe there are addons doing + // crazy stuff. + for (const tab of window.tabs) { + const userContextId = this._getUserContextIdFromTab(tab); + if (args.userContextId !== userContextId) { + newBrowserWindow.gBrowser.removeTab(viewFor(tab)); + } + } + resolve(null); + }, + }); + }); + }, + openTab(args) { return new Promise(resolve => { let browserWin = windowUtils.getMostRecentBrowserWindow(); diff --git a/webextension/css/popup.css b/webextension/css/popup.css index 6a81027..2ce5d91 100644 --- a/webextension/css/popup.css +++ b/webextension/css/popup.css @@ -45,7 +45,7 @@ table { margin: 4px; } -table.unstriped tbody tr { +.container-panel > table.unstriped tbody tr { border-bottom: 1px solid #f1f1f1; background-color: #fefefe; cursor: pointer; @@ -62,6 +62,11 @@ table.unstriped tbody tr { block-size: 32px; } +.userContext-icon:hover { + background-image: url('/img/container-add.svg'); + fill: 'gray'; +} + .edit-identities { background: #DCDBDC; } @@ -134,3 +139,7 @@ table.unstriped tbody tr { [data-identity-icon="circle"] { --identity-icon: url("/img/usercontext.svg#circle"); } + +.container-info-has-tabs { + cursor: pointer; +} diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 16fb90c..278eeb3 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -2,33 +2,64 @@ const CONTAINER_HIDE_SRC = "/img/container-hide.svg"; const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg"; -function showOrHideContainerTabs(userContextId, hasHiddenTabs) { - // Let"s show/hide the tabs - return browser.runtime.sendMessage({ - method: hasHiddenTabs ? "showTabs" : "hideTabs", - userContextId: userContextId - }) - // We need to retrieve the new identity configuration in order to choose the - // correct icon. - .then(() => { - return browser.runtime.sendMessage({ - method: "getIdentity", - userContextId: userContextId - }); - }) - // Let"s update the icon. - .then((identity) => { - let hideorshowIcon = document.querySelector(`#uci-${identity.userContextId}-hideorshow-icon`); - if (!identity.hasHiddenTabs && !identity.hasOpenTabs) { - hideorshowIcon.style.display = "none"; - } else { - hideorshowIcon.style.display = ""; +function showContainerTabsPanel(identity) { + // Populating the panel: name and icon + document.getElementById("container-info-name").innerText = identity.name; + + let icon = document.getElementById("container-info-icon"); + icon.setAttribute("data-identity-icon", identity.image); + icon.setAttribute("data-identity-color", identity.color); + + // Show or not the has-tabs section. + for (let trHasTabs of document.getElementsByClassName("container-info-has-tabs")) { + trHasTabs.hidden = !identity.hasHiddenTabs && !identity.hasOpenTabs; + trHasTabs.setAttribute("data-user-context-id", identity.userContextId); + } + + const hideShowIcon = document.getElementById("container-info-hideorshow-icon"); + hideShowIcon.src = identity.hasHiddenTabs ? CONTAINER_UNHIDE_SRC : CONTAINER_HIDE_SRC; + + const hideShowLabel = document.getElementById("container-info-hideorshow-label"); + hideShowLabel.innerText = identity.hasHiddenTabs ? "Show these container tabs" : "Hide these container tabs"; + + // Let"s remove all the previous tabs. + for (const trTab of document.getElementsByClassName("container-info-tab")) { + trTab.remove(); + } + + // Let"s retrieve the list of tabs. + browser.runtime.sendMessage({ + method: "getTabs", + userContextId: identity.userContextId, + }).then(tabs => { + // For each one, let's create a new line. + let fragment = document.createDocumentFragment(); + for (const tab of tabs) { + let tr = document.createElement("tr"); + fragment.appendChild(tr); + tr.classList.add("container-info-tab"); + tr.innerHTML = ` +
+ | ++ |
+ |
+ + |
Open all tabs in new window | +