From b66b05dcf673b01a3c68d4283088e93ae9a61400 Mon Sep 17 00:00:00 2001
From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com>
Date: Sun, 28 Dec 2025 17:58:18 +0100
Subject: [PATCH] feat: Allow pinned tabs to be collapsible, p=#11753, c=tabs,
folders, workspaces
* fix: Fixed restoring the previous pinned state clearing up the custom icon, b=no-bug, c=tabs
* feat: Allow pinned tabs to be collapsible, b=no-bug, c=tabs, folders, workspaces
* fix: Fixed new folder context menu item not working, b=no-bug, c=common, folders
---
.../sessionstore/SessionStore-sys-mjs.patch | 4 +-
.../tabbrowser/content/tab-js.patch | 18 ++-
.../tabbrowser/content/tabgroup-js.patch | 38 +++---
.../tabbrowser/content/tabs-js.patch | 22 ++--
src/browser/themes/shared/zen-icons/icons.css | 3 +-
.../shared/zen-icons/lin/arrow-right.svg | 2 +-
src/zen/common/modules/ZenUIManager.mjs | 2 -
src/zen/folders/ZenFolder.mjs | 18 +--
src/zen/folders/ZenFolders.mjs | 39 ++++---
src/zen/folders/zen-folders.css | 15 +--
.../sessionstore/ZenSessionManager.sys.mjs | 3 +-
src/zen/sessionstore/ZenWindowSync.sys.mjs | 4 +-
src/zen/tabs/ZenPinnedTabManager.mjs | 5 +-
src/zen/workspaces/ZenGradientGenerator.mjs | 6 +-
src/zen/workspaces/ZenWorkspace.mjs | 110 ++++++++++++++++--
src/zen/workspaces/ZenWorkspaceCreation.mjs | 6 +-
src/zen/workspaces/ZenWorkspaceIcons.mjs | 6 +-
src/zen/workspaces/ZenWorkspaces.mjs | 39 ++++++-
src/zen/workspaces/ZenWorkspacesStorage.mjs | 6 +-
src/zen/workspaces/zen-workspaces.css | 52 ++++++++-
20 files changed, 299 insertions(+), 99 deletions(-)
diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
index fe61001fd..7eaf8210e 100644
--- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
+++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
-index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..4439fe5fb3c7002b173415b615892ef356b22959 100644
+index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..697dde4378c43ae6db46a6b7eb2997982201ec27 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -127,6 +127,8 @@ const TAB_EVENTS = [
@@ -177,7 +177,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..4439fe5fb3c7002b173415b615892ef3
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
+ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null;
-+ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces();
++ winData.spaces = aWindow.gZenWorkspaces?.getWorkspacesForSessionStore();
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch
index dd0edd05f..c7be0f3eb 100644
--- a/src/browser/components/tabbrowser/content/tab-js.patch
+++ b/src/browser/components/tabbrowser/content/tab-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
-index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
+index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..160e277d64eaac8408aed90eaf62606479424001 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -87,7 +87,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
}
get lastAccessed() {
-@@ -382,7 +395,12 @@
+@@ -382,7 +395,18 @@
}
get group() {
@@ -97,11 +97,17 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
+ }
+ if (gBrowser.isTabGroup(this.parentElement?.parentElement)) {
+ return this.parentElement.parentElement;
++ }
++ if (this.pinned) {
++ let collapsiblePins = gZenWorkspaces.workspaceElement(this.getAttribute('zen-workspace-id'))?.collapsiblePins;
++ if (collapsiblePins?.collapsed) {
++ return collapsiblePins;
++ }
+ }
}
get splitview() {
-@@ -473,6 +491,8 @@
+@@ -473,6 +497,8 @@
this.style.MozUserFocus = "ignore";
} else if (
event.target.classList.contains("tab-close-button") ||
@@ -110,7 +116,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
-@@ -527,6 +547,10 @@
+@@ -527,6 +553,10 @@
this.style.MozUserFocus = "";
}
@@ -121,7 +127,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
on_click(event) {
if (event.button != 0) {
return;
-@@ -587,6 +611,14 @@
+@@ -587,6 +617,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -136,7 +142,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
}
on_dblclick(event) {
-@@ -610,6 +642,8 @@
+@@ -610,6 +648,8 @@
animate: true,
triggeringEvent: event,
});
diff --git a/src/browser/components/tabbrowser/content/tabgroup-js.patch b/src/browser/components/tabbrowser/content/tabgroup-js.patch
index 3842442c7..1fb4ef153 100644
--- a/src/browser/components/tabbrowser/content/tabgroup-js.patch
+++ b/src/browser/components/tabbrowser/content/tabgroup-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabgroup.js b/browser/components/tabbrowser/content/tabgroup.js
-index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057ebc71c2f5f 100644
+index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..e92927612abf12631c384a8f54b6a607fb699424 100644
--- a/browser/components/tabbrowser/content/tabgroup.js
+++ b/browser/components/tabbrowser/content/tabgroup.js
@@ -14,11 +14,11 @@
@@ -68,10 +68,10 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
- return false;
- });
+ this.appendChild = function (child) {
-+ this.querySelector(".tab-group-container").appendChild(child);
++ this.groupContainer.appendChild(child);
+ for (let tab of this.tabs) {
+ if (tab.hasAttribute("zen-empty-tab") && tab.group === this) {
-+ this.querySelector(".zen-tab-group-start").after(tab);
++ this.groupStartElement.after(tab);
+ }
+ }
+ };
@@ -92,7 +92,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
});
}
- this.#tabChangeObserver.observe(this, { childList: true });
-+ const container = this.querySelector(".tab-group-container");
++ const container = this.groupContainer;
+ if (container) {
+ this.#tabChangeObserver.observe(container, { childList: true });
+ }
@@ -117,7 +117,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
});
}
-@@ -466,13 +492,57 @@
+@@ -466,13 +492,65 @@
* @returns {MozTabbrowserTab[]}
*/
get tabs() {
@@ -126,20 +126,29 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
- if (childrenArray[i].tagName == "tab-split-view-wrapper") {
- childrenArray.splice(i, 1, ...childrenArray[i].tabs);
+ // add other group tabs if they are under this group
-+ let childs = Array.from(this.querySelector(".tab-group-container")?.children ?? []);
++ let childs = Array.from(this.groupContainer?.children ?? []);
+ const tabsCollect = [];
+ for (let item of childs) {
+ tabsCollect.push(item);
+ if (gBrowser.isTabGroup(item)) {
+ tabsCollect.push(...item.tabs);
-+ }
-+ }
+ }
+ }
+- return childrenArray.filter(node => node.matches("tab"));
+ return tabsCollect.filter(node => node.matches("tab"));
+ }
+
++ get groupContainer() {
++ return this.querySelector(".tab-group-container");
++ }
++
++ get groupStartElement() {
++ return this.querySelector(".zen-tab-group-start");
++ }
++
+ get childGroupsAndTabs() {
+ const result = [];
-+ const container = this.querySelector(".tab-group-container");
++ const container = this.groupContainer;
+
+ for (const item of Array.from(container.children)) {
+ if (gBrowser.isTab(item)) {
@@ -169,9 +178,8 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
+ currentGroup = currentGroup?.group;
+ if (currentGroup.collapsed) {
+ return false;
- }
- }
-- return childrenArray.filter(node => node.matches("tab"));
++ }
++ }
+ return true;
+ }
+
@@ -180,7 +188,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
}
/**
-@@ -553,7 +623,6 @@
+@@ -553,7 +631,6 @@
addTabs(tabs, metricsContext) {
for (let tab of tabs) {
if (tab.pinned) {
@@ -188,7 +196,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
}
let tabToMove =
this.ownerGlobal === tab.ownerGlobal
-@@ -616,7 +685,7 @@
+@@ -616,7 +693,7 @@
*/
on_click(event) {
let isToggleElement =
@@ -197,7 +205,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
event.target === this.#overflowCountLabel;
if (isToggleElement && event.button === 0) {
event.preventDefault();
-@@ -687,5 +756,6 @@
+@@ -687,5 +764,6 @@
}
}
diff --git a/src/browser/components/tabbrowser/content/tabs-js.patch b/src/browser/components/tabbrowser/content/tabs-js.patch
index ef5bb94e3..9c2dcf099 100644
--- a/src/browser/components/tabbrowser/content/tabs-js.patch
+++ b/src/browser/components/tabbrowser/content/tabs-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js
-index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac8793422474052f476573 100644
+index 6b6c04599fe80983d13d2069ca62b99d8ad70271..04144081560f1678dc9673736ef2bd9d9ca3f478 100644
--- a/browser/components/tabbrowser/content/tabs.js
+++ b/browser/components/tabbrowser/content/tabs.js
@@ -436,7 +436,7 @@
@@ -54,7 +54,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
return this.hasAttribute("overflow");
}
-@@ -837,29 +839,54 @@
+@@ -837,29 +839,56 @@
if (pinnedChildren?.at(-1)?.id == "pinned-tabs-container-periphery") {
pinnedChildren.pop();
}
@@ -81,6 +81,8 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
+ tabs.splice(i, 1);
+ // add the tabs in the group to the list
+ tabs.splice(i, 0, ...tab.tabs);
++ } else if (tab.classList.contains("zen-tab-group-start")) {
++ tabs.splice(i, 1);
+ }
+ }
+ };
@@ -119,7 +121,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
}
/**
-@@ -926,17 +953,10 @@
+@@ -926,17 +955,10 @@
let elementIndex = 0;
@@ -139,7 +141,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
if (isTab(child) && child.visible) {
child.elementIndex = elementIndex++;
focusableItems.push(child);
-@@ -944,11 +964,13 @@
+@@ -944,11 +966,13 @@
child.labelElement.elementIndex = elementIndex++;
focusableItems.push(child.labelElement);
@@ -154,7 +156,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
} else if (child.tagName == "tab-split-view-wrapper") {
let visibleTabsInSplitView = child.tabs.filter(tab => tab.visible);
visibleTabsInSplitView.forEach(tab => {
-@@ -992,6 +1014,7 @@
+@@ -992,6 +1016,7 @@
_invalidateCachedTabs() {
this.#allTabs = null;
this._invalidateCachedVisibleTabs();
@@ -162,7 +164,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
}
_invalidateCachedVisibleTabs() {
-@@ -1095,7 +1118,7 @@
+@@ -1095,7 +1120,7 @@
if (node == null) {
// We have a container for non-tab elements at the end of the scrollbox.
@@ -171,7 +173,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
}
node.before(tab);
-@@ -1193,7 +1216,7 @@
+@@ -1193,7 +1218,7 @@
// There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and
// for when the tab strip is overflowed (which is shared by vertical and horizontal tabs);
// Attach the long click popup to all of them.
@@ -180,7 +182,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
const newTab2 = this.newTabButton;
const newTabVertical = document.getElementById(
"vertical-tabs-newtab-button"
-@@ -1294,8 +1317,10 @@
+@@ -1294,8 +1319,10 @@
*/
_handleTabSelect(aInstant) {
let selectedTab = this.selectedItem;
@@ -191,7 +193,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
selectedTab._notselectedsinceload = false;
}
-@@ -1304,7 +1329,7 @@
+@@ -1304,7 +1331,7 @@
* @param {boolean} [shouldScrollInstantly=false]
*/
#ensureTabIsVisible(tab, shouldScrollInstantly = false) {
@@ -200,7 +202,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
if (arrowScrollbox?.overflowing) {
arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly);
}
-@@ -1437,7 +1462,7 @@
+@@ -1437,7 +1464,7 @@
}
_notifyBackgroundTab(aTab) {
diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css
index 80db0d71d..37443d6bc 100644
--- a/src/browser/themes/shared/zen-icons/icons.css
+++ b/src/browser/themes/shared/zen-icons/icons.css
@@ -7,6 +7,7 @@
.subviewbutton,
#zen-welcome-start-button,
.zen-toast button,
+.zen-current-workspace-indicator-chevron,
.pinned-tabs-container-separator toolbarbutton {
-moz-context-properties: fill, fill-opacity !important;
fill: currentColor !important;
@@ -116,7 +117,7 @@
}
}
-#zen-rice-share-options .options-header,
+.zen-current-workspace-indicator-chevron,
#PanelUI-zen-gradient-generator-color-page-right {
list-style-image: url('arrow-right.svg');
}
diff --git a/src/browser/themes/shared/zen-icons/lin/arrow-right.svg b/src/browser/themes/shared/zen-icons/lin/arrow-right.svg
index 6f6854344..88847cd96 100644
--- a/src/browser/themes/shared/zen-icons/lin/arrow-right.svg
+++ b/src/browser/themes/shared/zen-icons/lin/arrow-right.svg
@@ -2,4 +2,4 @@
# 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/.
-
+
diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs
index ae63ace1e..70c9d10ba 100644
--- a/src/zen/common/modules/ZenUIManager.mjs
+++ b/src/zen/common/modules/ZenUIManager.mjs
@@ -125,8 +125,6 @@ window.gZenUIManager = {
}
menu.setAttribute('hidden', 'true');
}
- // The first separator in the tab context menu is now useless.
- document.getElementById('tabContextMenu').querySelector('menuseparator').remove();
},
_initCreateNewPopup() {
diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs
index 6c1d2dbc7..3c3413e6a 100644
--- a/src/zen/folders/ZenFolder.mjs
+++ b/src/zen/folders/ZenFolder.mjs
@@ -2,7 +2,7 @@
// 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/.
-class ZenFolder extends MozTabbrowserTabGroup {
+export class nsZenFolder extends MozTabbrowserTabGroup {
#initialized = false;
static markup = `
@@ -68,7 +68,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
}
this.#initialized = true;
this._activeTabs = [];
- this.icon.appendChild(ZenFolder.rawIcon.cloneNode(true));
+ this.icon.appendChild(nsZenFolder.rawIcon.cloneNode(true));
this.labelElement.parentElement.setAttribute('context', 'zenFolderActions');
@@ -81,7 +81,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
};
if (this.collapsed) {
- this.querySelector('.tab-group-container').setAttribute('hidden', true);
+ this.groupContainer.setAttribute('hidden', true);
}
}
@@ -141,7 +141,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
gZenFolders.createFolder([], {
renameFolder: !gZenUIManager.testingEnabled,
label: 'Subfolder',
- insertAfter: this.querySelector('.tab-group-container').lastElementChild,
+ insertAfter: this.groupContainer.lastElementChild,
});
}
@@ -181,8 +181,12 @@ class ZenFolder extends MozTabbrowserTabGroup {
}
get allItems() {
- return [...this.querySelector('.tab-group-container').children].filter(
- (child) => !child.classList.contains('zen-tab-group-start')
+ return [...this.groupContainer.children].filter(
+ (child) =>
+ !(
+ child.classList.contains('zen-tab-group-start') ||
+ child.classList.contains('pinned-tabs-container-separator')
+ )
);
}
@@ -274,4 +278,4 @@ class ZenFolder extends MozTabbrowserTabGroup {
}
}
-customElements.define('zen-folder', ZenFolder);
+customElements.define('zen-folder', nsZenFolder);
diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs
index 4cf42f469..e1fcd7c97 100644
--- a/src/zen/folders/ZenFolders.mjs
+++ b/src/zen/folders/ZenFolders.mjs
@@ -189,6 +189,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
window.addEventListener('TabSelect', this);
window.addEventListener('TabOpen', this);
const onNewFolder = this.#onNewFolder.bind(this);
+ document.getElementById('zen-context-menu-new-folder').addEventListener('command', onNewFolder);
document
.getElementById('zen-context-menu-new-folder-toolbar')
.addEventListener('command', onNewFolder);
@@ -803,6 +804,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (!isTab && !groupElem?.hasAttribute('selected') && !forCollapse) {
groupElem = null; // Don't indent if the group is not selected
}
+ if (groupElem?.tagName.toLowerCase() === 'zen-workspace-collapsible-pins') {
+ groupElem = null; // Don't indent if it's inside the collapsible pinned tabs
+ }
let level = groupElem?.level + 1 || 0;
if (gBrowser.isTabGroupLabel(groupElem)) {
// If it is a group label, we should not increase its level by one.
@@ -1036,8 +1040,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
}
default: {
// Should insert after zen-empty-tab
- const start =
- parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling;
+ const start = parentWorkingData.node.groupStartElement.nextElementSibling;
start.after(node);
}
}
@@ -1128,8 +1131,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
- let firstGroupElem =
- dropElementGroup?.querySelector('.zen-tab-group-start')?.nextElementSibling;
+ let firstGroupElem = dropElementGroup?.groupStartElement.nextElementSibling;
if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
const isInMiddleZone =
@@ -1212,6 +1214,11 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
return heightShift;
} else {
heightShift += window.windowUtils.getBoundsWithoutFlushing(tabsContainer).height;
+ if (tabsContainer.separatorElement) {
+ heightShift -= window.windowUtils.getBoundsWithoutFlushing(
+ tabsContainer.separatorElement
+ ).height;
+ }
}
return heightShift;
}
@@ -1225,8 +1232,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const activeFoldersIds = new Set();
const itemsToHide = [];
- const tabsContainer = group.querySelector('.tab-group-container');
- const groupStart = group.querySelector('.zen-tab-group-start');
+ const tabsContainer = group.groupContainer;
+ const groupStart = group.groupStartElement;
const groupItems = this.#collectGroupItems(group, {
selectedTabs,
@@ -1304,11 +1311,11 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const animations = [];
const itemsToHide = [];
- const tabsContainer = group.querySelector('.tab-group-container');
+ const tabsContainer = group.groupContainer;
tabsContainer.removeAttribute('hidden');
tabsContainer.style.overflow = 'hidden';
- const groupStart = group.querySelector('.zen-tab-group-start');
+ const groupStart = group.groupStartElement;
const itemsToShow = this.#normalizeGroupItems(group.childGroupsAndTabs);
const activeFolders = group.childActiveGroups;
@@ -1422,7 +1429,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
folder.removeAttribute('has-active');
folder.activeTabs = [];
const groupItems = this.#normalizeGroupItems(folder.allItems);
- const tabsContainer = folder.querySelector('.tab-group-container');
+ const tabsContainer = folder.groupContainer;
// Set correct margin-top after animation
const afterAnimate = () => {
@@ -1436,7 +1443,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
};
- const groupStart = folder.querySelector('.zen-tab-group-start');
+ const groupStart = folder.groupStartElement;
const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
// Collect animations for this specific folder becoming inactive
@@ -1474,7 +1481,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
animations.push(async () => {
folder.removeAttribute('has-active');
const groupItems = this.#normalizeGroupItems(folder.allItems);
- const tabsContainer = folder.querySelector('.tab-group-container');
+ const tabsContainer = folder.groupContainer;
// Set correct margin-top after animation
const afterAnimate = () => {
@@ -1488,7 +1495,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
};
- const groupStart = folder.querySelector('.zen-tab-group-start');
+ const groupStart = folder.groupStartElement;
const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
// Collect animations for this specific folder becoming inactive
@@ -1573,8 +1580,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
currentGroup.activeTabs = activeTabs;
}
- const tabsContainer = currentGroup.querySelector('.tab-group-container');
- const groupStart = currentGroup.querySelector('.zen-tab-group-start');
+ const tabsContainer = currentGroup.groupContainer;
+ const groupStart = currentGroup.groupStartElement;
tabsContainer.style.overflow = 'clip';
if (tabsContainer.hasAttribute('hidden')) tabsContainer.removeAttribute('hidden');
@@ -1673,8 +1680,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
animateGroupMove(group, expand = false) {
if (!group?.isZenFolder) return;
- const groupStart = group.querySelector('.zen-tab-group-start');
- const tabsContainer = group.querySelector('.tab-group-container');
+ const groupStart = group.groupStartElement;
+ const tabsContainer = group.groupContainer;
const heightContainer = expand ? 0 : this.#calculateHeightShift(tabsContainer, []);
tabsContainer.style.overflow = 'clip';
diff --git a/src/zen/folders/zen-folders.css b/src/zen/folders/zen-folders.css
index f1be64211..1cd1cb43d 100644
--- a/src/zen/folders/zen-folders.css
+++ b/src/zen/folders/zen-folders.css
@@ -202,16 +202,6 @@ zen-folder {
}
}
- &[collapsed] {
- & > .tabbrowser-tab:not([hidden]) {
- display: flex;
- }
-
- &:not([has-active]) > .tab-group-container {
- overflow-y: clip;
- }
- }
-
:root[zen-sidebar-expanded] &[has-active] > .tab-group-label-container {
& .tab-reset-button {
display: flex;
@@ -224,6 +214,11 @@ zen-folder {
}
}
+zen-workspace[collapsedpinnedtabs] .zen-workspace-pinned-tabs-section,
+zen-folder[collapsed]:not([has-active]) > .tab-group-container {
+ overflow-y: clip;
+}
+
/* Tabs popup */
#zen-folder-tabs-popup {
--arrowpanel-padding: 0;
diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs
index 254d1e379..9b82ae703 100644
--- a/src/zen/sessionstore/ZenSessionManager.sys.mjs
+++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs
@@ -224,6 +224,7 @@ export class nsZenSessionManager {
// If there's no initial state, nothing to restore. This would
// happen if the file is empty or corrupted.
if (!initialState) {
+ this.log('No initial state to restore!');
return;
}
// If there are no windows, we create an empty one. By default,
@@ -249,7 +250,7 @@ export class nsZenSessionManager {
// guarantee that all tabs, groups, folders and split view data
// are properly synced across all windows.
this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`);
- for (const winData of initialState.windows || []) {
+ for (const winData of initialState.windows) {
this.#restoreWindowData(winData);
}
}
diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs
index 2d8878751..c8f2dfd17 100644
--- a/src/zen/sessionstore/ZenWindowSync.sys.mjs
+++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs
@@ -176,8 +176,8 @@ class nsZenWindowSync {
// This should only happen really when updating from an older version
// that didn't have this feature.
this.#runOnAllWindows(null, (aWindow) => {
- const { gBrowser } = aWindow;
- for (let tab of gBrowser.tabs) {
+ const { gZenWorkspaces } = aWindow;
+ for (let tab of gZenWorkspaces.allStoredTabs) {
if (!tab.id) {
tab.id = this.#newTabSyncId;
lazy.TabStateFlusher.flush(tab.linkedBrowser);
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index fda9a0ebd..1ab19ee08 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -322,11 +322,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
const state = this.#getTabState(tab);
const initialState = tab._zenPinnedInitialState;
+ if (!initialState?.entry) {
+ return;
+ }
// Remove everything except the entry we want to keep
state.entries = [initialState.entry];
- state.image = initialState.image;
+ state.image = tab.zenStaticIcon || initialState.image;
state.index = 0;
SessionStore.setTabState(tab, state);
diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs
index a1495d4ed..ad0ee2497 100644
--- a/src/zen/workspaces/ZenGradientGenerator.mjs
+++ b/src/zen/workspaces/ZenGradientGenerator.mjs
@@ -1,6 +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/.
+/* 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/. */
import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs
index 7cb1af16d..a888b6732 100644
--- a/src/zen/workspaces/ZenWorkspace.mjs
+++ b/src/zen/workspaces/ZenWorkspace.mjs
@@ -1,17 +1,59 @@
-// 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/.
+/* 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/. */
+
+import { nsZenFolder } from 'chrome://browser/content/zen-components/ZenFolder.mjs';
+
+// A helper class to manage collapsible pinned tabs in a workspace.
+class nsZenCollapsiblePins extends nsZenFolder {
+ #spaceElement;
+
+ connectedCallback() {
+ this.setAttribute('hidden', 'true');
+ this.#spaceElement = this.parentElement;
+ super.connectedCallback();
+ }
+
+ get groupContainer() {
+ return this.#spaceElement.pinnedTabsContainer;
+ }
+
+ get groupStartElement() {
+ // Fetch this instead of the tab-group-start since it is not guaranteed this
+ // element will be the first child of the pinned tabs container.
+ return this.#spaceElement.pinnedTabsContainer.querySelector('.space-fake-collapsible-start');
+ }
+
+ get collapsed() {
+ return super.collapsed;
+ }
+
+ set collapsed(value) {
+ if (value) {
+ this.#spaceElement.setAttribute('collapsedpinnedtabs', 'true');
+ } else {
+ this.#spaceElement.removeAttribute('collapsedpinnedtabs');
+ }
+ super.collapsed = value;
+ }
+}
+
+export class nsZenWorkspace extends MozXULElement {
+ #initialPinnedElementChildrenCount;
-class nsZenWorkspace extends MozXULElement {
static get markup() {
return `
-
+
+
+
+
+
{
+ if (this.hasPinnedTabs) {
+ // Prevent renaming when there are pinned tabs
+ event.stopPropagation();
+ }
+ });
this.pinnedTabsContainer.scrollbox = this.scrollbox;
+ this.#initialPinnedElementChildrenCount = this.pinnedTabsContainer.children.length;
this.indicator
.querySelector('.zen-workspaces-actions')
@@ -92,10 +144,20 @@ class nsZenWorkspace extends MozXULElement {
this.indicator
.querySelector('.zen-current-workspace-indicator-icon')
.addEventListener('dblclick', (event) => {
+ if (this.hasPinnedTabs) {
+ return;
+ }
event.stopPropagation();
gZenWorkspaces.changeWorkspaceIcon();
});
+ this.indicator.addEventListener('click', (event) => {
+ if (this.hasPinnedTabs) {
+ event.stopPropagation();
+ this.collapsiblePins.collapsed = !this.collapsiblePins.collapsed;
+ }
+ });
+
if (!gZenWorkspaces.currentWindowIsSyncing) {
let actionsButton = this.indicator.querySelector('.zen-workspaces-actions');
const moveTabToFragment = window.MozXULElement.parseXULToFragment(
@@ -169,11 +231,26 @@ class nsZenWorkspace extends MozXULElement {
this.tabsContainer.setAttribute('zen-workspace-id', this.id);
this.pinnedTabsContainer.setAttribute('zen-workspace-id', this.id);
+ this.collapsiblePins = document.createXULElement('zen-workspace-collapsible-pins');
+ this.prepend(this.collapsiblePins);
+
this.#updateOverflow();
this.onGradientCacheChanged = this.#onGradientCacheChanged.bind(this);
window.addEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
+ const tabPinCallback = () => {
+ this.checkPinsExistence();
+ };
+
+ this.addEventListener('TabPinned', tabPinCallback);
+ this.addEventListener('TabUnpinned', tabPinCallback);
+ this.addEventListener('TabClose', (event) => {
+ if (event.target.pinned) {
+ tabPinCallback();
+ }
+ });
+
this.dispatchEvent(
new CustomEvent('ZenWorkspaceAttached', {
bubbles: true,
@@ -185,6 +262,7 @@ class nsZenWorkspace extends MozXULElement {
disconnectedCallback() {
window.removeEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
+ super.disconnectedCallback();
}
get active() {
@@ -200,6 +278,14 @@ class nsZenWorkspace extends MozXULElement {
this.#updateOverflow();
}
+ get hasPinnedTabs() {
+ return this.hasAttribute('haspinnedtabs');
+ }
+
+ get hasCollapsedPinnedTabs() {
+ return this.hasAttribute('collapsedpinnedtabs');
+ }
+
#updateOverflow() {
if (!this.scrollbox) return;
if (this.overflows) {
@@ -273,6 +359,15 @@ class nsZenWorkspace extends MozXULElement {
this.style.setProperty('--zen-primary-color', primaryColor);
}
+ checkPinsExistence() {
+ if (this.pinnedTabsContainer.children.length > this.#initialPinnedElementChildrenCount) {
+ this.setAttribute('haspinnedtabs', 'true');
+ } else {
+ this.removeAttribute('haspinnedtabs');
+ this.collapsiblePins.collapsed = false;
+ }
+ }
+
clearThemeStyles() {
this.style.colorScheme = '';
this.style.removeProperty('--toolbox-textcolor');
@@ -311,3 +406,4 @@ class nsZenWorkspace extends MozXULElement {
}
customElements.define('zen-workspace', nsZenWorkspace);
+customElements.define('zen-workspace-collapsible-pins', nsZenCollapsiblePins);
diff --git a/src/zen/workspaces/ZenWorkspaceCreation.mjs b/src/zen/workspaces/ZenWorkspaceCreation.mjs
index d037bade7..23b74e073 100644
--- a/src/zen/workspaces/ZenWorkspaceCreation.mjs
+++ b/src/zen/workspaces/ZenWorkspaceCreation.mjs
@@ -1,6 +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/.
+/* 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/. */
class nsZenWorkspaceCreation extends MozXULElement {
#wasInCollapsedMode = false;
diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs
index 9672c4a27..a9b8b99a3 100644
--- a/src/zen/workspaces/ZenWorkspaceIcons.mjs
+++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs
@@ -1,6 +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/.
+/* 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/. */
class nsZenWorkspaceIcons extends MozXULElement {
constructor() {
diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs
index 872b6af93..d70e70dc7 100644
--- a/src/zen/workspaces/ZenWorkspaces.mjs
+++ b/src/zen/workspaces/ZenWorkspaces.mjs
@@ -1,6 +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/.
+/* 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/. */
import { nsZenThemePicker } from 'chrome://browser/content/zen-components/ZenGradientGenerator.mjs';
@@ -465,6 +465,7 @@ class nsZenWorkspaces {
workspaceWrapper.pinnedTabsContainer,
tabs
);
+ workspaceWrapper.checkPinsExistence();
resolve();
},
{ once: true }
@@ -827,6 +828,20 @@ class nsZenWorkspaces {
return [...this._workspaceCache];
}
+ getWorkspacesForSessionStore() {
+ const spaces = this.getWorkspaces();
+ let spacesForSS = [];
+ for (const space of spaces) {
+ let newSpace = { ...space };
+ const element = this.workspaceElement(space.uuid);
+ if (element) {
+ newSpace.hasCollapsedPinnedTabs = element.hasCollapsedPinnedTabs;
+ }
+ spacesForSS.push(newSpace);
+ }
+ return spacesForSS;
+ }
+
async workspaceBookmarks() {
if (this.privateWindowOrDisabled) {
this._workspaceBookmarksCache = {
@@ -854,11 +869,22 @@ class nsZenWorkspaces {
if (this.#hasInitialized) {
return;
}
- this._workspaceCache = aWinData.spaces?.length
- ? aWinData.spaces
+ const spacesFromStore = aWinData.spaces || [];
+ this._workspaceCache = spacesFromStore.length
+ ? [...spacesFromStore]
: [await this.createAndSaveWorkspace('Space', undefined, true)];
+ for (const workspace of this._workspaceCache) {
+ // We don't want to depend on this by mistake
+ delete workspace.hasCollapsedPinnedTabs;
+ }
this.activeWorkspace = aWinData.activeZenSpace || this._workspaceCache[0].uuid;
await this.initializeWorkspaces();
+ for (const workspace of spacesFromStore) {
+ const element = this.workspaceElement(workspace.uuid);
+ if (element) {
+ element.collapsiblePins.collapsed = workspace.hasCollapsedPinnedTabs || false;
+ }
+ }
this.#hasInitialized = true;
}
@@ -1788,11 +1814,14 @@ class nsZenWorkspaces {
}
const indicatorName = workspaceIndicator.querySelector('.zen-current-workspace-indicator-name');
const indicatorIcon = workspaceIndicator.querySelector('.zen-current-workspace-indicator-icon');
+ const iconStack = workspaceIndicator.querySelector('.zen-current-workspace-indicator-stack');
if (this.workspaceHasIcon(currentWorkspace)) {
indicatorIcon.removeAttribute('no-icon');
+ iconStack.removeAttribute('no-icon');
} else {
indicatorIcon.setAttribute('no-icon', 'true');
+ iconStack.setAttribute('no-icon', 'true');
}
const icon = this.getWorkspaceIcon(currentWorkspace);
indicatorIcon.innerHTML = '';
diff --git a/src/zen/workspaces/ZenWorkspacesStorage.mjs b/src/zen/workspaces/ZenWorkspacesStorage.mjs
index 80c7a82e5..f7df8a31e 100644
--- a/src/zen/workspaces/ZenWorkspacesStorage.mjs
+++ b/src/zen/workspaces/ZenWorkspacesStorage.mjs
@@ -1,6 +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/.
+/* 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/. */
// Integration of workspace-specific bookmarks into Places
window.ZenWorkspaceBookmarksStorage = {
diff --git a/src/zen/workspaces/zen-workspaces.css b/src/zen/workspaces/zen-workspaces.css
index 584c366b1..7c258c712 100644
--- a/src/zen/workspaces/zen-workspaces.css
+++ b/src/zen/workspaces/zen-workspaces.css
@@ -155,13 +155,14 @@
/* Mark workspaces indicator */
.zen-current-workspace-indicator {
+ --indicator-gap: 10px;
margin-top: 1px;
padding: calc(2px + var(--tab-inline-padding) + var(--zen-toolbox-padding));
font-weight: 500;
position: relative;
max-height: var(--zen-workspace-indicator-height);
min-height: var(--zen-workspace-indicator-height);
- gap: 10px;
+ gap: var(--indicator-gap);
align-items: center;
flex-direction: row !important;
max-width: 100%;
@@ -368,3 +369,52 @@ zen-workspace {
}
%include create-workspace-form.css
+
+/* Pinned tabs collapse styles */
+
+.zen-current-workspace-indicator-chevron {
+ display: none;
+}
+
+:root[zen-sidebar-expanded] {
+ .zen-current-workspace-indicator-stack {
+ transition: margin-inline-end 0.1s;
+
+ &[no-icon='true'] {
+ margin-inline-end: calc(-1 * (var(--indicator-gap) + 16px));
+ }
+ }
+
+ .zen-current-workspace-indicator-chevron {
+ width: 16px;
+ height: 16px;
+ transition: transform 0.2s, opacity 0.2s;
+ transform: rotate(90deg);
+ padding: 2px;
+
+ .zen-current-workspace-indicator-stack[no-icon='true'] & {
+ display: flex;
+ opacity: 0;
+ }
+ }
+
+ & zen-workspace[haspinnedtabs] .zen-current-workspace-indicator:hover,
+ & zen-workspace[collapsedpinnedtabs] .zen-current-workspace-indicator {
+ .zen-current-workspace-indicator-chevron {
+ display: flex;
+ opacity: 1;
+ }
+
+ .zen-current-workspace-indicator-stack {
+ margin-inline-end: 0;
+ }
+
+ .zen-current-workspace-indicator-icon {
+ display: none;
+ }
+ }
+
+ zen-workspace[collapsedpinnedtabs] .zen-current-workspace-indicator-chevron {
+ transform: rotate(0deg);
+ }
+}