Container tab listing

This commit is contained in:
baku 2017-01-10 13:19:18 +00:00
parent 8598c76e25
commit bb0713d0d4
4 changed files with 289 additions and 116 deletions

209
index.js
View file

@ -2,6 +2,7 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const { attachTo } = require("sdk/content/mod"); const { attachTo } = require("sdk/content/mod");
const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm"); const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm");
const { getFavicon } = require("sdk/places/favicon");
const self = require("sdk/self"); const self = require("sdk/self");
const { Style } = require("sdk/stylesheet/style"); const { Style } = require("sdk/stylesheet/style");
const tabs = require("sdk/tabs"); const tabs = require("sdk/tabs");
@ -37,7 +38,10 @@ let ContainerService = {
"hideTabs", "hideTabs",
"showTabs", "showTabs",
"sortTabs", "sortTabs",
"getTabs",
"showTab",
"openTab", "openTab",
"moveTabsToWindow",
"queryIdentities", "queryIdentities",
"getIdentity" "getIdentity"
]; ];
@ -51,25 +55,22 @@ let ContainerService = {
}); });
// It can happen that this jsm is loaded after the opening a container tab. // It can happen that this jsm is loaded after the opening a container tab.
for (let tab of tabs) { for (const tab of tabs) {
let xulTab = viewFor(tab); const userContextId = this._getUserContextIdFromTab(tab);
let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10);
if (userContextId) { if (userContextId) {
++this._identitiesState[userContextId].openTabs; ++this._identitiesState[userContextId].openTabs;
} }
} }
tabs.on("open", tab => { tabs.on("open", tab => {
let xulTab = viewFor(tab); const userContextId = this._getUserContextIdFromTab(tab);
let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10);
if (userContextId) { if (userContextId) {
++this._identitiesState[userContextId].openTabs; ++this._identitiesState[userContextId].openTabs;
} }
}); });
tabs.on("close", tab => { tabs.on("close", tab => {
let xulTab = viewFor(tab); const userContextId = this._getUserContextIdFromTab(tab);
let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10);
if (userContextId && this._identitiesState[userContextId].openTabs) { if (userContextId && this._identitiesState[userContextId].openTabs) {
--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 // Tabs management
hideTabs(args) { hideTabs(args) {
return new Promise(resolve => { return new Promise(resolve => {
for (let tab of tabs) { for (const tab of tabs) {
let xulTab = viewFor(tab); const userContextId = this._getUserContextIdFromTab(tab);
let userContextId = parseInt(xulTab.getAttribute("usercontextid") || 0, 10);
if ("userContextId" in args && args.userContextId !== userContextId) { if ("userContextId" in args && args.userContextId !== userContextId) {
continue; continue;
} }
@ -177,55 +192,145 @@ let ContainerService = {
}, },
sortTabs() { 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 => { return new Promise(resolve => {
for (let window of windows.browserWindows) { for (let window of windows.browserWindows) {
// First the pinned tabs, then the normal ones. // First the pinned tabs, then the normal ones.
sortTabsInternal(window, true); this._sortTabsInternal(window, true);
sortTabsInternal(window, false); this._sortTabsInternal(window, false);
} }
resolve(null); 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) { openTab(args) {
return new Promise(resolve => { return new Promise(resolve => {
let browserWin = windowUtils.getMostRecentBrowserWindow(); let browserWin = windowUtils.getMostRecentBrowserWindow();

View file

@ -45,7 +45,7 @@ table {
margin: 4px; margin: 4px;
} }
table.unstriped tbody tr { .container-panel > table.unstriped tbody tr {
border-bottom: 1px solid #f1f1f1; border-bottom: 1px solid #f1f1f1;
background-color: #fefefe; background-color: #fefefe;
cursor: pointer; cursor: pointer;
@ -62,6 +62,11 @@ table.unstriped tbody tr {
block-size: 32px; block-size: 32px;
} }
.userContext-icon:hover {
background-image: url('/img/container-add.svg');
fill: 'gray';
}
.edit-identities { .edit-identities {
background: #DCDBDC; background: #DCDBDC;
} }
@ -134,3 +139,7 @@ table.unstriped tbody tr {
[data-identity-icon="circle"] { [data-identity-icon="circle"] {
--identity-icon: url("/img/usercontext.svg#circle"); --identity-icon: url("/img/usercontext.svg#circle");
} }
.container-info-has-tabs {
cursor: pointer;
}

View file

@ -2,33 +2,64 @@
const CONTAINER_HIDE_SRC = "/img/container-hide.svg"; const CONTAINER_HIDE_SRC = "/img/container-hide.svg";
const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg"; const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg";
function showOrHideContainerTabs(userContextId, hasHiddenTabs) { function showContainerTabsPanel(identity) {
// Let"s show/hide the tabs // Populating the panel: name and icon
return browser.runtime.sendMessage({ document.getElementById("container-info-name").innerText = identity.name;
method: hasHiddenTabs ? "showTabs" : "hideTabs",
userContextId: userContextId let icon = document.getElementById("container-info-icon");
}) icon.setAttribute("data-identity-icon", identity.image);
// We need to retrieve the new identity configuration in order to choose the icon.setAttribute("data-identity-color", identity.color);
// correct icon.
.then(() => { // Show or not the has-tabs section.
return browser.runtime.sendMessage({ for (let trHasTabs of document.getElementsByClassName("container-info-has-tabs")) {
method: "getIdentity", trHasTabs.hidden = !identity.hasHiddenTabs && !identity.hasOpenTabs;
userContextId: userContextId trHasTabs.setAttribute("data-user-context-id", identity.userContextId);
}); }
})
// Let"s update the icon. const hideShowIcon = document.getElementById("container-info-hideorshow-icon");
.then((identity) => { hideShowIcon.src = identity.hasHiddenTabs ? CONTAINER_UNHIDE_SRC : CONTAINER_HIDE_SRC;
let hideorshowIcon = document.querySelector(`#uci-${identity.userContextId}-hideorshow-icon`);
if (!identity.hasHiddenTabs && !identity.hasOpenTabs) { const hideShowLabel = document.getElementById("container-info-hideorshow-label");
hideorshowIcon.style.display = "none"; hideShowLabel.innerText = identity.hasHiddenTabs ? "Show these container tabs" : "Hide these container tabs";
} else {
hideorshowIcon.style.display = ""; // 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 = `
<td><img class="icon" src="${tab.favicon}" /></td>
<td>${tab.title}</td>`;
// On click, we activate this tab.
tr.addEventListener("click", () => {
browser.runtime.sendMessage({
method: "showTab",
tabId: tab.id,
}).then(() => {
window.close();
});
});
} }
hideorshowIcon.src = hasHiddenTabs ? CONTAINER_HIDE_SRC : CONTAINER_UNHIDE_SRC; document.getElementById("container-info-table").appendChild(fragment);
})
// The new identity is returned. // Finally we are ready to show the panel.
return identity; .then(() => {
// FIXME: the animation...
document.getElementById("container-panel").classList.add("hide");
document.getElementById("container-info-panel").classList.remove("hide");
}); });
} }
@ -63,59 +94,34 @@ browser.runtime.sendMessage({method: "queryIdentities"}).then(identities => {
let fragment = document.createDocumentFragment(); let fragment = document.createDocumentFragment();
identities.forEach(identity => { identities.forEach(identity => {
let hideOrShowIconSrc = CONTAINER_HIDE_SRC;
if (identity.hasHiddenTabs) {
hideOrShowIconSrc = CONTAINER_UNHIDE_SRC;
}
let tr = document.createElement("tr"); let tr = document.createElement("tr");
fragment.appendChild(tr); fragment.appendChild(tr);
tr.setAttribute("data-identity-cookie-store-id", identity.userContextId); tr.setAttribute("data-identity-cookie-store-id", identity.userContextId);
tr.innerHTML = ` tr.innerHTML = `
<td> <td>
<div class="userContext-icon" <div class="userContext-icon open-newtab"
data-identity-icon="${identity.image}" data-identity-icon="${identity.image}"
data-identity-color="${identity.color}"> data-identity-color="${identity.color}">
</div> </div>
</td> </td>
<td>${identity.name}</td> <td class="open-newtab">${identity.name}</td>
<td class="newtab"> <td class="info">&gt;</td>`;
<img
title="Open a new ${identity.name} container tab"
src="/img/container-add.svg"
class="icon newtab-icon" />
</td>
<td class="hideorshow" >
<img
title="Hide or show ${identity.name} container tabs"
data-identity-cookie-store-id="${identity.userContextId}"
id="uci-${identity.userContextId}-hideorshow-icon"
class="icon hideorshow-icon"
src="${hideOrShowIconSrc}"
/>
</td>
<td>&gt;</td>`;
// No tabs, no icon.
if (!identity.hasHiddenTabs && !identity.hasOpenTabs) {
let hideorshowIcon = fragment.querySelector(`#uci-${identity.userContextId}-hideorshow-icon`);
hideorshowIcon.style.display = "none";
}
tr.addEventListener("click", e => { tr.addEventListener("click", e => {
if (e.target.matches(".hideorshow-icon")) { if (e.target.matches(".open-newtab")) {
showOrHideContainerTabs(identity.userContextId, browser.runtime.sendMessage({
identity.hasHiddenTabs).then(i => { identity = i; }); method: "showTabs",
} else if (e.target.matches(".newtab-icon")) { userContextId: identity.userContextId
showOrHideContainerTabs(identity.userContextId, true).then(() => { }).then(() => {
browser.runtime.sendMessage({ return browser.runtime.sendMessage({
method: "openTab", method: "openTab",
userContextId: identity.userContextId userContextId: identity.userContextId,
}).then(() => {
window.close();
}); });
}).then(() => {
window.close();
}); });
} else if (e.target.matches(".info")) {
showContainerTabsPanel(identity);
} }
}); });
}); });
@ -139,3 +145,33 @@ document.querySelector("#sort-containers-link").addEventListener("click", () =>
window.close(); window.close();
}); });
}); });
document.querySelector("#close-container-info-panel").addEventListener("click", () => {
// TODO: animation
document.getElementById("container-info-panel").classList.add("hide");
document.getElementById("container-panel").classList.remove("hide");
});
document.querySelector("#container-info-hideorshow").addEventListener("click", e => {
let userContextId = e.target.parentElement.getAttribute("data-user-context-id");
browser.runtime.sendMessage({
method: "getIdentity",
userContextId,
}).then(identity => {
return browser.runtime.sendMessage({
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
userContextId: identity.userContextId
});
}).then(() => {
window.close();
});
});
document.querySelector("#container-info-movetabs").addEventListener("click", e => {
return browser.runtime.sendMessage({
method: "moveTabsToWindow",
userContextId: e.target.parentElement.getAttribute("data-user-context-id"),
}).then(() => {
window.close();
});
});

View file

@ -40,6 +40,29 @@
</div> </div>
</div> </div>
</div> </div>
<div class="hide panel container-info-panel" id="container-info-panel">
<div class="row popup-bumper">
<div class="small-1 columns" id="close-container-info-panel">&lt;</div>
<div class="small-11 columns">
<table id="container-info-table">
<tr>
<td>
<div class="userContext-icon" id="container-info-icon"
data-identity-icon="" data-identity-color=""></div></td>
<td id="container-info-name"></td>
</tr>
<tr class="container-info-has-tabs" id="container-info-hideorshow">
<td>
<img class="icon" id="container-info-hideorshow-icon" src="" />
</td>
<td id="container-info-hideorshow-label"></td>
</tr>
<tr class="container-info-has-tabs" id="container-info-movetabs">
<td colspan="2">Open all tabs in new window</td>
</tr>
</div>
</div>
</div>
<script src="js/popup.js"></script> <script src="js/popup.js"></script>
</body> </body>
</html> </html>