From 81e854a89f57862623c65c1fe4c70dc6507de6bd Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Wed, 27 Aug 2025 23:25:07 +0200 Subject: [PATCH 01/54] feat: Full cross-window workspace syncing, b=no-bug, c=workspaces --- src/browser/base/content/zen-assets.inc.xhtml | 1 + .../base/content/zen-assets.jar.inc.mn | 1 + src/zen/workspaces/ZenWindowSyncing.mjs | 226 ++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 src/zen/workspaces/ZenWindowSyncing.mjs diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index 0756714ea6..d94a632dda 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -58,3 +58,4 @@ + diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index 4c341e4e13..78d3f4bbd9 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -44,6 +44,7 @@ content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs) content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.mjs) content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.mjs) + content/browser/zen-components/ZenWindowSyncing.mjs (../../zen/workspaces/ZenWindowSyncing.mjs) content/browser/zen-components/ZenWorkspaceCreation.mjs (../../zen/workspaces/ZenWorkspaceCreation.mjs) content/browser/zen-components/ZenWorkspacesStorage.mjs (../../zen/workspaces/ZenWorkspacesStorage.mjs) content/browser/zen-components/ZenWorkspacesSync.mjs (../../zen/workspaces/ZenWorkspacesSync.mjs) diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs new file mode 100644 index 0000000000..19fbf09f42 --- /dev/null +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -0,0 +1,226 @@ +// 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 nsZenWorkspaceWindowSync extends nsZenMultiWindowFeature { + #ignoreNextEvents = false; + #waitForPromise = null; + + constructor() { + super(); + if (!window.closed) { + this.init(); + } + } + + async init() { + await gZenWorkspaces.promiseInitialized; + this.#makeSureAllTabsHaveIds(); + this.#setUpEventListeners(); + } + + #makeSureAllTabsHaveIds() { + const allTabs = gZenWorkspaces.allStoredTabs; + for (const tab of allTabs) { + if (!tab.hasAttribute('zen-sync-id')) { + const tabId = gZenUIManager.generateUuidv4(); + tab.setAttribute('zen-sync-id', tabId); + } + } + } + + #setUpEventListeners() { + const kEvents = [ + 'TabClose', + 'TabOpen', + 'TabPinned', + 'TabUnpinned', + 'TabAddedToEssentials', + 'TabRemovedFromEssentials', + 'TabHide', + 'TabShow', + 'TabMove', + ]; + const eventListener = this.#handleEvent.bind(this); + for (const event of kEvents) { + window.addEventListener(event, eventListener); + } + + window.addEventListener('unload', () => { + for (const event of kEvents) { + window.removeEventListener(event, eventListener); + } + }); + } + + #handleEvent(event) { + this.#propagateToOtherWindows(event); + } + + async #propagateToOtherWindows(event) { + if (this.#ignoreNextEvents) { + return; + } + if (this.#waitForPromise) { + await this.#waitForPromise; + } + this.#waitForPromise = new Promise(async (resolve) => { + await this.foreachWindowAsActive(async (browser) => { + if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) { + await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event); + } + }); + resolve(); + }); + } + + async onExternalTabEvent(event) { + this.#ignoreNextEvents = true; + switch (event.type) { + case 'TabClose': + this.#onTabClose(event); + break; + case 'TabOpen': + await this.#onTabOpen(event); + break; + case 'TabPinned': + this.#onTabPinned(event); + break; + case 'TabUnpinned': + this.#onTabUnpinned(event); + break; + case 'TabAddedToEssentials': + this.#onTabAddedToEssentials(event); + break; + case 'TabRemovedFromEssentials': + this.#onTabRemovedFromEssentials(event); + break; + case 'TabHide': + this.#onTabHide(event); + break; + case 'TabShow': + this.#onTabShow(event); + break; + case 'TabMove': + this.#onTabMove(event); + break; + default: + console.warn(`Unhandled event type: ${event.type}`); + break; + } + this.#ignoreNextEvents = false; + } + + #getTabId(tab) { + return tab.getAttribute('zen-sync-id'); + } + + #getTabWithId(tabId) { + for (const tab of gZenWorkspaces.allStoredTabs) { + if (this.#getTabId(tab) === tabId) { + return tab; + } + } + return null; + } + + #onTabClose(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToClose = this.#getTabWithId(tabId); + if (tabToClose) { + gBrowser.removeTab(tabToClose); + } + } + + #onTabPinned(event) { + const targetTab = event.target; + if (targetTab.hasAttribute('zen-essential')) { + return this.#onTabAddedToEssentials(event); + } + const tabId = this.#getTabId(targetTab); + const elementIndex = targetTab.elementIndex; + const tabToPin = this.#getTabWithId(tabId); + if (tabToPin) { + gBrowser.pinTab(tabToPin); + gBrowser.moveTabTo(tabToPin, { elementIndex, forceUngrouped: !!targetTab.group }); + } + } + + #onTabUnpinned(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToUnpin = this.#getTabWithId(tabId); + if (tabToUnpin) { + gBrowser.unpinTab(tabToUnpin); + } + } + + #onTabAddedToEssentials(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToAdd = this.#getTabWithId(tabId); + if (tabToAdd) { + gZenPinnedTabManager.addToEssentials(tabToAdd); + } + } + + #onTabRemovedFromEssentials(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToRemove = this.#getTabWithId(tabId); + if (tabToRemove) { + gZenPinnedTabManager.removeFromEssentials(tabToRemove); + } + } + + #onTabHide(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToHide = this.#getTabWithId(tabId); + if (tabToHide) { + gBrowser.hideTab(tabToHide); + } + } + + #onTabShow(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToShow = this.#getTabWithId(tabId); + if (tabToShow) { + gBrowser.showTab(tabToShow); + } + } + + #onTabMove(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const elementIndex = targetTab.elementIndex; + const tabToMove = this.#getTabWithId(tabId); + if (tabToMove) { + gBrowser.moveTabTo(tabToMove, { elementIndex, forceUngrouped: !!targetTab.group }); + } + } + + async #onTabOpen(event) { + await new Promise((resolve) => { + const targetTab = event.target; + const isPinned = targetTab.pinned; + const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); + const elementIndex = targetTab.elementIndex; + + const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0, true); + if (isEssential) { + gZenPinnedTabManager.addToEssentials(duplicatedTab); + } else if (isPinned) { + gBrowser.pinTab(duplicatedTab); + } + + gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); + resolve(); + }); + } + } + + window.gZenWorkspaceWindowSync = new nsZenWorkspaceWindowSync(); +} From 7a4cdaa45cbb98b4420eec8232787a803bc1b943 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Mon, 1 Sep 2025 16:09:59 +0200 Subject: [PATCH 02/54] feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces --- src/zen/tabs/ZenPinnedTabManager.mjs | 2 ++ src/zen/workspaces/ZenWindowSyncing.mjs | 30 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 408481622d..3f3ca47369 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -92,6 +92,7 @@ } onTabIconChanged(tab, url = null) { + tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); const iconUrl = url ?? tab.iconImage.src; if (!iconUrl && tab.hasAttribute('zen-pin-id')) { try { @@ -1436,6 +1437,7 @@ } async onTabLabelChanged(tab) { + tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } })); if (!this._pinsCache) { return; } diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index 19fbf09f42..bcb63d8f42 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -40,6 +40,8 @@ 'TabHide', 'TabShow', 'TabMove', + 'ZenTabIconChanged', + 'ZenTabLabelChanged', ]; const eventListener = this.#handleEvent.bind(this); for (const event of kEvents) { @@ -104,6 +106,12 @@ case 'TabMove': this.#onTabMove(event); break; + case 'ZenTabIconChanged': + this.#onTabIconChanged(event); + break; + case 'ZenTabLabelChanged': + this.#onTabLabelChanged(event); + break; default: console.warn(`Unhandled event type: ${event.type}`); break; @@ -156,6 +164,26 @@ } } + #onTabIconChanged(event) { + this.#updateTabIconAndLabel(event); + } + + #onTabLabelChanged(event) { + this.#updateTabIconAndLabel(event); + } + + #updateTabIconAndLabel(event) { + const targetTab = event.target; + if (targetTab.hasAttribute("pending")) { + const tabId = this.#getTabId(targetTab); + const tabToChange = this.#getTabWithId(tabId); + if (tabToChange) { + gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); + gBrowser._setTabLabel(tabToChange, targetTab.label); + } + } + } + #onTabAddedToEssentials(event) { const targetTab = event.target; const tabId = this.#getTabId(targetTab); @@ -209,7 +237,7 @@ const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); const elementIndex = targetTab.elementIndex; - const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0, true); + const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0); if (isEssential) { gZenPinnedTabManager.addToEssentials(duplicatedTab); } else if (isPinned) { From 91f5d58fbc1c62f415ea6ee4e5e7f69afe2705fa Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Tue, 2 Sep 2025 16:21:04 +0200 Subject: [PATCH 03/54] feat: Dont session duplicate the tabs, b=no-bug, c=workspaces --- src/zen/workspaces/ZenWindowSyncing.mjs | 55 +++++++++++++------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index bcb63d8f42..da5de683bd 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -66,13 +66,14 @@ if (this.#waitForPromise) { await this.#waitForPromise; } - this.#waitForPromise = new Promise(async (resolve) => { - await this.foreachWindowAsActive(async (browser) => { + this.#waitForPromise = new Promise((resolve) => { + this.foreachWindowAsActive(async (browser) => { if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) { await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event); } + }).then(() => { + resolve(); }); - resolve(); }); } @@ -174,13 +175,11 @@ #updateTabIconAndLabel(event) { const targetTab = event.target; - if (targetTab.hasAttribute("pending")) { - const tabId = this.#getTabId(targetTab); - const tabToChange = this.#getTabWithId(tabId); - if (tabToChange) { - gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); - gBrowser._setTabLabel(tabToChange, targetTab.label); - } + const tabId = this.#getTabId(targetTab); + const tabToChange = this.#getTabWithId(tabId); + if (tabToChange && tabToChange.hasAttribute('pending')) { + gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); + gBrowser._setTabLabel(tabToChange, targetTab.label); } } @@ -223,30 +222,34 @@ #onTabMove(event) { const targetTab = event.target; const tabId = this.#getTabId(targetTab); - const elementIndex = targetTab.elementIndex; + const tabIndex = targetTab._pPos; const tabToMove = this.#getTabWithId(tabId); if (tabToMove) { - gBrowser.moveTabTo(tabToMove, { elementIndex, forceUngrouped: !!targetTab.group }); + gBrowser.moveTabTo(tabToMove, { tabIndex, forceUngrouped: !!targetTab.group }); } } async #onTabOpen(event) { - await new Promise((resolve) => { - const targetTab = event.target; - const isPinned = targetTab.pinned; - const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); - const elementIndex = targetTab.elementIndex; - - const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0); - if (isEssential) { - gZenPinnedTabManager.addToEssentials(duplicatedTab); - } else if (isPinned) { - gBrowser.pinTab(duplicatedTab); - } + const targetTab = event.target; + const isPinned = targetTab.pinned; + const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); + const elementIndex = targetTab.elementIndex; - gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); - resolve(); + const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, { + createLazyBrowser: true, }); + + duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); + duplicatedTab.setAttribute('zen-tab-id', targetTab.getAttribute('zen-tab-id')); + duplicatedTab.setAttribute('zen-workspace-id', targetTab.getAttribute('zen-workspace-id')); + + if (isEssential) { + gZenPinnedTabManager.addToEssentials(duplicatedTab); + } else if (isPinned) { + gBrowser.pinTab(duplicatedTab); + } + + gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); } } From a55b1c7495cf1e1daa3bea602ef5a7f71cc7daeb Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 27 Sep 2025 17:54:28 +0200 Subject: [PATCH 04/54] feat: Properly handle tab moves, b=no-bug, c=workspaces --- src/zen/workspaces/ZenWindowSyncing.mjs | 83 ++++++++++++++++++++----- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index da5de683bd..857f9fc9cf 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -22,7 +22,7 @@ #makeSureAllTabsHaveIds() { const allTabs = gZenWorkspaces.allStoredTabs; for (const tab of allTabs) { - if (!tab.hasAttribute('zen-sync-id')) { + if (!tab.hasAttribute('zen-sync-id') && !tab.hasAttribute('zen-empty-tab')) { const tabId = gZenUIManager.generateUuidv4(); tab.setAttribute('zen-sync-id', tabId); } @@ -33,15 +33,25 @@ const kEvents = [ 'TabClose', 'TabOpen', + 'TabMove', + 'TabPinned', 'TabUnpinned', + 'TabAddedToEssentials', 'TabRemovedFromEssentials', + 'TabHide', 'TabShow', - 'TabMove', + 'ZenTabIconChanged', 'ZenTabLabelChanged', + + 'TabGroupCreate', + 'TabGroupRemoved', + 'TabGrouped', + 'TabUngrouped', + 'TabGroupMoved', ]; const eventListener = this.#handleEvent.bind(this); for (const event of kEvents) { @@ -105,6 +115,7 @@ this.#onTabShow(event); break; case 'TabMove': + case 'TabGroupMoved': this.#onTabMove(event); break; case 'ZenTabIconChanged': @@ -113,6 +124,14 @@ case 'ZenTabLabelChanged': this.#onTabLabelChanged(event); break; + case 'TabGroupCreate': + this.#onTabGroupCreate(event); + break; + case 'TabGroupRemoved': + case 'TabGrouped': + case 'TabUngrouped': + // Tab grouping changes are automatically synced by Firefox + break; default: console.warn(`Unhandled event type: ${event.type}`); break; @@ -222,10 +241,36 @@ #onTabMove(event) { const targetTab = event.target; const tabId = this.#getTabId(targetTab); - const tabIndex = targetTab._pPos; const tabToMove = this.#getTabWithId(tabId); + const workspaceId = targetTab.getAttribute('zen-workspace-id'); + const isEssential = targetTab.hasAttribute('zen-essential'); if (tabToMove) { - gBrowser.moveTabTo(tabToMove, { tabIndex, forceUngrouped: !!targetTab.group }); + let tabSibling = targetTab.previousElementSibling; + let isFirst = false; + if (!tabSibling?.hasAttribute('zen-sync-id')) { + isFirst = true; + } + gBrowser.zenHandleTabMove(tabToMove, () => { + if (isFirst) { + let container; + if (isEssential) { + container = gZenWorkspaces.getEssentialsSection(tabToMove); + } else { + const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); + container = tabToMove.pinned + ? workspaceElement.pinnedTabsContainer + : workspaceElement.tabsContainer; + } + container.insertBefore(tabToMove, container.firstChild); + } else { + let relativeTab = gZenWorkspaces.allStoredTabs.find((tab) => { + return this.#getTabId(tab) === this.#getTabId(tabSibling); + }); + if (relativeTab) { + relativeTab.after(tabToMove); + } + } + }); } } @@ -233,23 +278,29 @@ const targetTab = event.target; const isPinned = targetTab.pinned; const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); - const elementIndex = targetTab.elementIndex; - + if (!this.#getTabId(targetTab) && !targetTab.hasAttribute('zen-empty-tab')) { + const tabId = gZenUIManager.generateUuidv4(); + targetTab.setAttribute('zen-sync-id', tabId); + } const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, { createLazyBrowser: true, + essential: isEssential, + pinned: isPinned, }); - - duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); - duplicatedTab.setAttribute('zen-tab-id', targetTab.getAttribute('zen-tab-id')); - duplicatedTab.setAttribute('zen-workspace-id', targetTab.getAttribute('zen-workspace-id')); - - if (isEssential) { - gZenPinnedTabManager.addToEssentials(duplicatedTab); - } else if (isPinned) { - gBrowser.pinTab(duplicatedTab); + if (!isEssential) { + gZenWorkspaces.moveTabToWorkspace( + duplicatedTab, + targetTab.getAttribute('zen-workspace-id') + ); } + duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); + duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id')); + } - gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); + #onTabGroupCreate(event) { + const targetGroup = event.target; + const isSplitView = targetGroup.classList.contains('zen-split-view'); + const isFolder = targetGroup.isZenFolder; } } From 86006c889149bf4d44ea166637b859242f56194f Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 28 Sep 2025 23:45:13 +0200 Subject: [PATCH 05/54] feat: Start on new session restore, b=no-bug, c=no-component --- prefs/browser.yaml | 3 +- src/zen/sessionstore/ZenSessionFile.sys.mjs | 27 ++++++++++ .../sessionstore/ZenSessionManager.sys.mjs | 50 +++++++++++++++++++ src/zen/sessionstore/ZenSessionWindow.sys.mjs | 35 +++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/zen/sessionstore/ZenSessionFile.sys.mjs create mode 100644 src/zen/sessionstore/ZenSessionManager.sys.mjs create mode 100644 src/zen/sessionstore/ZenSessionWindow.sys.mjs diff --git a/prefs/browser.yaml b/prefs/browser.yaml index 30aa603e9c..f76c33f5c8 100644 --- a/prefs/browser.yaml +++ b/prefs/browser.yaml @@ -3,7 +3,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. - name: browser.startup.page - value: 3 + value: 0 + locked: true - name: browser.sessionstore.restore_pinned_tabs_on_demand value: true diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs new file mode 100644 index 0000000000..c50960ae10 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionFile.sys.mjs @@ -0,0 +1,27 @@ +// 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 FILE_NAME = 'zen-sessions.jsonlz4'; + +export class nsZenSessionFile { + #path; + + #windows; + + constructor() { + this.#path = PathUtils.join(profileDir, FILE_NAME); + } + + async read() { + try { + return await IOUtils.readJSON(this.#path, { compress: true }); + } catch (e) { + return {}; + } + } + + async write(data) { + await IOUtils.writeJSON(this.#path, data, { compress: true }); + } +} diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs new file mode 100644 index 0000000000..68aa829e40 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -0,0 +1,50 @@ +// 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 { + cancelIdleCallback, + clearTimeout, + requestIdleCallback, + setTimeout, +} from 'resource://gre/modules/Timer.sys.mjs'; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs', + PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', + RunState: 'resource:///modules/sessionstore/RunState.sys.mjs', +}); + +class nsZenSessionManager { + #file; + + constructor() { + this.#file = null; + } + + get file() { + if (!this.#file) { + this.#file = lazy.ZenSessionFile; + } + return this.#file; + } + + /** + * Saves the current session state. Collects data and writes to disk. + * + * @param forceUpdateAllWindows (optional) + * Forces us to recollect data for all windows and will bypass and + * update the corresponding caches. + */ + saveState(forceUpdateAllWindows = false) { + if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Don't save (or even collect) anything in permanent private + // browsing mode + return Promise.resolve(); + } + } +} + +export const ZenSessionStore = new nsZenSessionManager(); diff --git a/src/zen/sessionstore/ZenSessionWindow.sys.mjs b/src/zen/sessionstore/ZenSessionWindow.sys.mjs new file mode 100644 index 0000000000..4600702349 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionWindow.sys.mjs @@ -0,0 +1,35 @@ +// 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/. + +export class ZenSessionWindow { + #id; + #selectedWorkspace; + #selectedTab; + + constructor(id) { + this.#id = id; + this.#selectedWorkspace = null; + this.#selectedTab = null; + } + + get id() { + return this.#id; + } + + get selectedWorkspace() { + return this.#selectedWorkspace; + } + + set selectedWorkspace(workspace) { + this.#selectedWorkspace = workspace; + } + + get selectedTab() { + return this.#selectedTab; + } + + set selectedTab(tab) { + this.#selectedTab = tab; + } +} From 240a031e38195360057ab14aa6900b3932675d4f Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:52:53 +0200 Subject: [PATCH 06/54] Discard changes to prefs/browser.yaml --- prefs/browser.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/prefs/browser.yaml b/prefs/browser.yaml index f76c33f5c8..30aa603e9c 100644 --- a/prefs/browser.yaml +++ b/prefs/browser.yaml @@ -3,8 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. - name: browser.startup.page - value: 0 - locked: true + value: 3 - name: browser.sessionstore.restore_pinned_tabs_on_demand value: true From 79ff574978de1c5f723eb6515fccaf0d1ffa7baf Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Wed, 29 Oct 2025 23:57:20 +0100 Subject: [PATCH 07/54] feat: Start doing out own session restore, b=no-bug, c=folders, tabs --- .../sessionstore/SessionFile-sys-mjs.patch | 21 ++ .../sessionstore/SessionStartup-sys-mjs.patch | 21 ++ .../sessionstore/SessionStore-sys-mjs.patch | 51 +++-- src/zen/ZenComponents.manifest | 15 ++ src/zen/folders/ZenFolder.mjs | 2 - src/zen/moz.build | 5 + .../sessionstore/SessionComponents.manifest | 6 + src/zen/sessionstore/ZenSessionFile.sys.mjs | 39 ++-- .../sessionstore/ZenSessionManager.sys.mjs | 137 ++++++++++++- src/zen/sessionstore/ZenSessionWindow.sys.mjs | 35 ---- src/zen/sessionstore/moz.build | 8 + src/zen/tabs/ZenPinnedTabManager.mjs | 192 +----------------- src/zen/zen.globals.js | 1 - 13 files changed, 267 insertions(+), 266 deletions(-) create mode 100644 src/browser/components/sessionstore/SessionFile-sys-mjs.patch create mode 100644 src/browser/components/sessionstore/SessionStartup-sys-mjs.patch create mode 100644 src/zen/ZenComponents.manifest create mode 100644 src/zen/sessionstore/SessionComponents.manifest delete mode 100644 src/zen/sessionstore/ZenSessionWindow.sys.mjs create mode 100644 src/zen/sessionstore/moz.build diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch new file mode 100644 index 0000000000..04aa6f0cac --- /dev/null +++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs +index 157c55ab24a418b56690d2e26320582909b919e4..14755f57dc450583e69eee94eb11f16980d5e5cb 100644 +--- a/browser/components/sessionstore/SessionFile.sys.mjs ++++ b/browser/components/sessionstore/SessionFile.sys.mjs +@@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + RunState: "resource:///modules/sessionstore/RunState.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID"; +@@ -364,7 +365,7 @@ var SessionFileInternal = { + this._readOrigin = result.origin; + + result.noFilesFound = noFilesFound; +- ++ await lazy.ZenSessionStore.readFile(); + return result; + }, + diff --git a/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch new file mode 100644 index 0000000000..b106193cf3 --- /dev/null +++ b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs +index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644 +--- a/browser/components/sessionstore/SessionStartup.sys.mjs ++++ b/browser/components/sessionstore/SessionStartup.sys.mjs +@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + StartupPerformance: + "resource:///modules/sessionstore/StartupPerformance.sys.mjs", + sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const STATE_RUNNING_STR = "running"; +@@ -179,6 +180,8 @@ export var SessionStartup = { + this._initialState = parsed; + } + ++ lazy.ZenSessionStore.onFileRead(this._initialState); ++ + if (this._initialState == null) { + // No valid session found. + this._sessionType = this.NO_SESSION; diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 79c5bcfcff..66bcc83019 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 eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d89fb95494 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa477fa68794 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -126,6 +126,8 @@ const TAB_EVENTS = [ @@ -11,7 +11,15 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -1904,6 +1906,8 @@ var SessionStoreInternal = { +@@ -195,6 +197,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { +@@ -1904,6 +1907,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -20,7 +28,18 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2139,7 +2143,6 @@ var SessionStoreInternal = { +@@ -2041,6 +2046,10 @@ var SessionStoreInternal = { + // A regular window is not a private window, taskbar tab window, or popup window + let isRegularWindow = + !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; ++ if (!aInitialState && isRegularWindow) { ++ aInitialState = ZenSessionStore.getNewWindowData(this._windows); ++ this.restoreWindows(aWindow, aInitialState, {}); ++ } + + // perform additional initialization when the first window is loading + if (lazy.RunState.isStopped) { +@@ -2139,7 +2148,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -28,7 +47,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2372,11 +2375,9 @@ var SessionStoreInternal = { +@@ -2372,11 +2380,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -40,7 +59,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3361,7 +3362,7 @@ var SessionStoreInternal = { +@@ -3361,7 +3367,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -49,7 +68,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 return; } -@@ -4073,6 +4074,11 @@ var SessionStoreInternal = { +@@ -4073,6 +4079,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -61,7 +80,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4509,6 +4515,7 @@ var SessionStoreInternal = { +@@ -4509,6 +4520,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -69,7 +88,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5358,7 +5365,7 @@ var SessionStoreInternal = { +@@ -5358,7 +5370,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -78,7 +97,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 removableTabs.push(tab); } } -@@ -5418,7 +5425,7 @@ var SessionStoreInternal = { +@@ -5418,7 +5430,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -87,7 +106,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 winData.workspaceID = workspaceID; } }, -@@ -5609,11 +5616,12 @@ var SessionStoreInternal = { +@@ -5609,11 +5621,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -101,7 +120,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5624,6 +5632,7 @@ var SessionStoreInternal = { +@@ -5624,6 +5637,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -109,7 +128,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5636,7 +5645,7 @@ var SessionStoreInternal = { +@@ -5636,7 +5650,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -118,7 +137,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5748,8 +5757,8 @@ var SessionStoreInternal = { +@@ -5748,8 +5762,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -129,7 +148,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5792,6 +5801,8 @@ var SessionStoreInternal = { +@@ -5792,6 +5806,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -138,7 +157,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6348,6 +6359,25 @@ var SessionStoreInternal = { +@@ -6348,6 +6364,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -164,7 +183,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7263,7 +7293,7 @@ var SessionStoreInternal = { +@@ -7263,7 +7298,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/zen/ZenComponents.manifest b/src/zen/ZenComponents.manifest new file mode 100644 index 0000000000..46b75485e8 --- /dev/null +++ b/src/zen/ZenComponents.manifest @@ -0,0 +1,15 @@ +# 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/. + +# nsBrowserGlue.js + +# This component must restrict its registration for the app-startup category +# to the specific list of apps that use it so it doesn't get loaded in xpcshell. +# Thus we restrict it to these apps: +# +# browser: {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + +category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} + +#include sessionstore/SessionComponents.manifest diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index f6ac00a865..391425779c 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -150,7 +150,6 @@ for (let tab of this.allItems.reverse()) { tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab; if (tab.hasAttribute('zen-empty-tab')) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); gBrowser.removeTab(tab); } else { gBrowser.ungroupTab(tab); @@ -160,7 +159,6 @@ async delete() { for (const tab of this.allItemsRecursive) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); if (tab.hasAttribute('zen-empty-tab')) { // Manually remove the empty tabs as removeTabs() inside removeTabGroup // does ignore them. diff --git a/src/zen/moz.build b/src/zen/moz.build index 56782122f5..eb681597f0 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -2,6 +2,10 @@ # 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/. +EXTRA_PP_COMPONENTS += [ + "ZenComponents.manifest", +] + DIRS += [ "common", "glance", @@ -9,4 +13,5 @@ DIRS += [ "tests", "urlbar", "toolkit", + "sessionstore", ] diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest new file mode 100644 index 0000000000..f8f08d1d79 --- /dev/null +++ b/src/zen/sessionstore/SessionComponents.manifest @@ -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/. + +# Browser global components initializing before UI startup +category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs index c50960ae10..526f7ab047 100644 --- a/src/zen/sessionstore/ZenSessionFile.sys.mjs +++ b/src/zen/sessionstore/ZenSessionFile.sys.mjs @@ -2,26 +2,39 @@ // 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 FILE_NAME = 'zen-sessions.jsonlz4'; +// Note that changing this hidden pref will make the previous session file +// unused, causing a new session file to be created on next write. +const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true); -export class nsZenSessionFile { - #path; - - #windows; +const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json'; - constructor() { - this.#path = PathUtils.join(profileDir, FILE_NAME); - } +export class nsZenSessionFile { + #path = PathUtils.join(PathUtils.profileDir, FILE_NAME); + #sidebar = []; async read() { try { - return await IOUtils.readJSON(this.#path, { compress: true }); - } catch (e) { - return {}; + const data = await IOUtils.readJSON(this.#path, { compress: SHOULD_COMPRESS_FILE }); + this.#sidebar = data.sidebar || []; + } catch { + // File doesn't exist yet, that's fine. } } - async write(data) { - await IOUtils.writeJSON(this.#path, data, { compress: true }); + get sidebar() { + return this.#sidebar; + } + + set sidebar(data) { + this.#sidebar = data; + } + + async #write(data) { + await IOUtils.writeJSON(this.#path, data, { compress: SHOULD_COMPRESS_FILE }); + } + + async store() { + const data = { sidebar: this.#sidebar }; + await this.#write(data); } } diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 68aa829e40..5120319bcd 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -12,23 +12,74 @@ import { const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs', + nsZenSessionFile: 'resource:///modules/zen/ZenSessionFile.sys.mjs', PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', - RunState: 'resource:///modules/sessionstore/RunState.sys.mjs', + BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', + TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', + SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', }); +const TAB_CUSTOM_VALUES = new WeakMap(); +const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes +const OBSERVING = ['sessionstore-state-write-complete', 'browser-window-before-show']; + class nsZenSessionManager { #file; constructor() { - this.#file = null; + this.#file = new lazy.nsZenSessionFile(); + } + + // Called from SessionComponents.manifest on app-startup + init() { + this.#initObservers(); + } + + async readFile() { + await this.#file.read(); } - get file() { - if (!this.#file) { - this.#file = lazy.ZenSessionFile; + onFileRead(initialState) { + for (const winData of initialState.windows || []) { + this.restoreWindowData(winData); } - return this.#file; + } + + #initObservers() { + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + get #sidebar() { + return this.#file.sidebar; + } + + set #sidebar(data) { + this.#file.sidebar = data; + } + + observe(aSubject, aTopic) { + switch (aTopic) { + case 'sessionstore-state-write-complete': { + this.#saveState(true); + break; + } + case 'browser-window-before-show': // catch new windows + this.#onBeforeBrowserWindowShown(aSubject); + break; + default: + break; + } + } + + /** Handles the browser-window-before-show observer notification. */ + #onBeforeBrowserWindowShown(aWindow) { + // TODO: Initialize new window + } + + get #topMostWindow() { + return lazy.BrowserWindowTracker.getTopWindow(); } /** @@ -38,12 +89,82 @@ class nsZenSessionManager { * Forces us to recollect data for all windows and will bypass and * update the corresponding caches. */ - saveState(forceUpdateAllWindows = false) { + async #saveState(forceUpdateAllWindows = false) { if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { // Don't save (or even collect) anything in permanent private // browsing mode return Promise.resolve(); } + // Collect an initial snapshot of window data before we do the flush. + const window = this.#topMostWindow; + // We don't have any normal windows or no windows at all + if (!window) { + return; + } + this.#collectWindowData(this.#topMostWindow, forceUpdateAllWindows); + this.#file.store(); + } + + /** + * Collects session data for a given window. + * + * @param window + * The window to collect data for. + * @param forceUpdate + * Forces us to recollect data and will bypass and update the + * corresponding caches. + * @param zIndex + * The z-index of the window. + */ + #collectWindowData(window, forceUpdate = false, zIndex = 0) { + let sidebarData = this.#sidebar; + if (!sidebarData || forceUpdate) { + sidebarData = {}; + } + + // If it hasn't changed, don't update. + if ( + !forceUpdate && + sidebarData.lastCollected && + Date.now() - sidebarData.lastCollected < LAZY_COLLECT_THRESHOLD + ) { + return; + } + sidebarData.lastCollected = Date.now(); + this.#collectTabsData(window, sidebarData); + this.#sidebar = sidebarData; + } + + /** + * Collects session data for all tabs in a given window. + * + * @param aWindow + * The window to collect tab data for. + * @param winData + * The window data object to populate. + */ + #collectTabsData(aWindow, sidebarData) { + const winData = lazy.SessionStore.getWindowState(aWindow).windows[0]; + if (!winData) return; + sidebarData.tabs = winData.tabs; + sidebarData.folders = winData.folders; + sidebarData.splitViewData = winData.splitViewData; + sidebarData.groups = winData.groups; + } + + restoreWindowData(aWindowData) { + const sidebar = this.#file.sidebar; + if (!sidebar) { + return; + } + aWindowData.tabs = sidebar.tabs || []; + aWindowData.splitViewData = sidebar.splitViewData; + aWindowData.folders = sidebar.folders; + aWindowData.groups = sidebar.groups; + } + + getNewWindowData(aWindows) { + return { windows: [Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {})] }; } } diff --git a/src/zen/sessionstore/ZenSessionWindow.sys.mjs b/src/zen/sessionstore/ZenSessionWindow.sys.mjs deleted file mode 100644 index 4600702349..0000000000 --- a/src/zen/sessionstore/ZenSessionWindow.sys.mjs +++ /dev/null @@ -1,35 +0,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/. - -export class ZenSessionWindow { - #id; - #selectedWorkspace; - #selectedTab; - - constructor(id) { - this.#id = id; - this.#selectedWorkspace = null; - this.#selectedTab = null; - } - - get id() { - return this.#id; - } - - get selectedWorkspace() { - return this.#selectedWorkspace; - } - - set selectedWorkspace(workspace) { - this.#selectedWorkspace = workspace; - } - - get selectedTab() { - return this.#selectedTab; - } - - set selectedTab(tab) { - this.#selectedTab = tab; - } -} diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build new file mode 100644 index 0000000000..af5a7dd3bb --- /dev/null +++ b/src/zen/sessionstore/moz.build @@ -0,0 +1,8 @@ +# 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/. + +EXTRA_JS_MODULES.zen += [ + "ZenSessionFile.sys.mjs", + "ZenSessionManager.sys.mjs", +] diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 58992f4ca8..19586d3f73 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -211,197 +211,7 @@ } async #initializePinnedTabs(init = false) { - const pins = this._pinsCache; - if (!pins?.length || !init) { - this.#finishedInitializingPins(); - return; - } - - const pinnedTabsByUUID = new Map(); - const pinsToCreate = new Set(pins.map((p) => p.uuid)); - - // First pass: identify existing tabs and remove those without pins - for (let tab of gZenWorkspaces.allStoredTabs) { - const pinId = tab.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - - if (pinsToCreate.has(pinId)) { - // This is a valid pinned tab that matches a pin - pinnedTabsByUUID.set(pinId, tab); - pinsToCreate.delete(pinId); - - if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) { - this._resetTabToStoredState(tab); - } - } else { - // This is a pinned tab that no longer has a corresponding pin - gBrowser.removeTab(tab); - } - } - - for (const group of gZenWorkspaces.allTabGroups) { - const pinId = group.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - if (pinsToCreate.has(pinId)) { - // This is a valid pinned group that matches a pin - pinsToCreate.delete(pinId); - } - } - - // Second pass: For every existing tab, update its label - // and set 'zen-has-static-label' attribute if it's been edited - for (let pin of pins) { - const tab = pinnedTabsByUUID.get(pin.uuid); - if (!tab) { - continue; - } - - tab.removeAttribute('zen-has-static-label'); // So we can set it again - if (pin.title && pin.editedTitle) { - gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true }); - tab.setAttribute('zen-has-static-label', 'true'); - } - } - - const groups = new Map(); - const pendingTabsInsideGroups = {}; - - // Third pass: create new tabs for pins that don't have tabs - for (let pin of pins) { - try { - if (!pinsToCreate.has(pin.uuid)) { - continue; // Skip pins that already have tabs - } - - if (pin.isGroup) { - const tabs = []; - // If there's already existing tabs, let's use them - for (const [uuid, existingTab] of pinnedTabsByUUID) { - const pinObject = this._pinsCache.find((p) => p.uuid === uuid); - if (pinObject && pinObject.parentUuid === pin.uuid) { - tabs.push(existingTab); - } - } - // We still need to iterate through pending tabs since the database - // query doesn't guarantee the order of insertion - for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) { - if (parentUuid === pin.uuid) { - tabs.push(...folderTabs); - } - } - const group = gZenFolders.createFolder(tabs, { - label: pin.title, - collapsed: pin.isFolderCollapsed, - initialPinId: pin.uuid, - workspaceId: pin.workspaceUuid, - insertAfter: - groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || - null, - }); - gZenFolders.setFolderUserIcon(group, pin.folderIcon); - groups.set(pin.uuid, group); - continue; - } - - let params = { - skipAnimation: true, - allowInheritPrincipal: false, - skipBackgroundNotify: true, - userContextId: pin.containerTabId || 0, - createLazyBrowser: true, - skipLoad: true, - noInitialLabel: false, - }; - - // Create and initialize the tab - let newTab = gBrowser.addTrustedTab(pin.url, params); - newTab.setAttribute('zenDefaultUserContextId', true); - - // Set initial label/title - if (pin.title) { - gBrowser.setInitialTabTitle(newTab, pin.title); - } - - // Set the icon if we have it cached - if (pin.iconUrl) { - gBrowser.setIcon(newTab, pin.iconUrl); - } - - newTab.setAttribute('zen-pin-id', pin.uuid); - - if (pin.workspaceUuid) { - newTab.setAttribute('zen-workspace-id', pin.workspaceUuid); - } - - if (pin.isEssential) { - newTab.setAttribute('zen-essential', 'true'); - } - - if (pin.editedTitle) { - newTab.setAttribute('zen-has-static-label', 'true'); - } - - // Initialize browser state if needed - if (!newTab.linkedBrowser._remoteAutoRemoved) { - let state = { - entries: [ - { - url: pin.url, - title: pin.title, - triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, - }, - ], - userContextId: pin.containerTabId || 0, - image: pin.iconUrl, - }; - - SessionStore.setTabState(newTab, state); - } - - this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`); - gBrowser.pinTab(newTab); - - if (pin.parentUuid) { - const parentGroup = groups.get(pin.parentUuid); - if (parentGroup) { - parentGroup.querySelector('.tab-group-container').appendChild(newTab); - } else { - if (pendingTabsInsideGroups[pin.parentUuid]) { - pendingTabsInsideGroups[pin.parentUuid].push(newTab); - } else { - pendingTabsInsideGroups[pin.parentUuid] = [newTab]; - } - } - } else { - if (!pin.isEssential) { - const container = gZenWorkspaces.workspaceElement( - pin.workspaceUuid - )?.pinnedTabsContainer; - if (container) { - container.insertBefore(newTab, container.lastChild); - } - } else { - gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab); - } - } - - gBrowser.tabContainer._invalidateCachedTabs(); - newTab.initialize(); - } catch (ex) { - console.error('Failed to initialize pinned tabs:', ex); - } - } - - setTimeout(() => { - this.#finishedInitializingPins(); - }, 0); - - gBrowser._updateTabBarForPinnedTabs(); - gZenUIManager.updateTabsToolbar(); + this.#finishedInitializingPins(); } _onPinnedTabEvent(action, event) { diff --git a/src/zen/zen.globals.js b/src/zen/zen.globals.js index a225ca767e..ca472fc1e0 100644 --- a/src/zen/zen.globals.js +++ b/src/zen/zen.globals.js @@ -27,7 +27,6 @@ export default [ 'ZenWorkspaceBookmarksStorage', 'gZenPinnedTabManager', - 'ZenPinnedTabsStorage', 'gZenEmojiPicker', 'gZenSessionStore', From 76acc8b0e41e43fab2a1509825be91573be540b7 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 31 Oct 2025 23:07:38 +0100 Subject: [PATCH 08/54] feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces --- src/browser/base/content/zen-assets.inc.xhtml | 1 - .../base/content/zen-assets.jar.inc.mn | 1 - .../sessionstore/SessionStore-sys-mjs.patch | 8 +- .../sessionstore/TabState-sys-mjs.patch | 4 +- .../tabbrowser/content/tab-js.patch | 8 - .../tabbrowser/content/tabbrowser-js.patch | 18 - src/zen/common/ZenSessionStore.mjs | 4 +- src/zen/common/ZenUIManager.mjs | 8 - src/zen/folders/ZenFolders.mjs | 19 +- .../sessionstore/ZenSessionManager.sys.mjs | 16 +- src/zen/tabs/ZenPinnedTabManager.mjs | 430 +----------- src/zen/tabs/ZenPinnedTabsStorage.mjs | 635 ------------------ src/zen/workspaces/ZenWindowSyncing.mjs | 8 +- src/zen/workspaces/ZenWorkspaces.mjs | 6 - 14 files changed, 28 insertions(+), 1138 deletions(-) delete mode 100644 src/zen/tabs/ZenPinnedTabsStorage.mjs diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index 0ae64eab21..a61105b517 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -48,7 +48,6 @@ - diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index 8825284bad..efee1ce484 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -51,7 +51,6 @@ content/browser/zen-components/ZenKeyboardShortcuts.mjs (../../zen/kbs/ZenKeyboardShortcuts.mjs) - content/browser/zen-components/ZenPinnedTabsStorage.mjs (../../zen/tabs/ZenPinnedTabsStorage.mjs) content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs) * content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css) content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 66bcc83019..ef89b7892d 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 eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa477fa68794 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd499c66d7 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -126,6 +126,8 @@ const TAB_EVENTS = [ @@ -33,7 +33,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa47 let isRegularWindow = !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; + if (!aInitialState && isRegularWindow) { -+ aInitialState = ZenSessionStore.getNewWindowData(this._windows); ++ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); + this.restoreWindows(aWindow, aInitialState, {}); + } @@ -171,8 +171,8 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa47 + if (tabData.zenHasStaticLabel) { + tab.setAttribute("zen-has-static-label", "true"); + } -+ if (tabData.zenPinnedId) { -+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId); ++ if (tabData.zenSyncId) { ++ tab.setAttribute("zen-sync-id", tabData.zenSyncId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch index 2100e23343..cfeea50354 100644 --- a/src/browser/components/sessionstore/TabState-sys-mjs.patch +++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs -index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644 +index 82721356d191055bec0d4b0ca49e481221988801..d1323fe17c995611ebdfe2869b0ccd2d45bcfa11 100644 --- a/browser/components/sessionstore/TabState.sys.mjs +++ b/browser/components/sessionstore/TabState.sys.mjs @@ -85,7 +85,22 @@ class _TabState { @@ -7,7 +7,7 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a } + tabData.zenWorkspace = tab.getAttribute("zen-workspace-id"); -+ tabData.zenPinnedId = tab.getAttribute("zen-pin-id"); ++ tabData.zenSyncId = tab.getAttribute("zen-sync-id"); + tabData.zenEssential = tab.getAttribute("zen-essential"); + tabData.pinned = tabData.pinned || tabData.zenEssential; + tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId"); diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 9970988541..f9f19d12d4 100644 --- a/src/browser/components/tabbrowser/content/tab-js.patch +++ b/src/browser/components/tabbrowser/content/tab-js.patch @@ -121,14 +121,6 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef on_click(event) { if (event.button != 0) { return; -@@ -570,6 +592,7 @@ - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this, true); - gBrowser.removeTab(this, { - animate: true, - triggeringEvent: event, @@ -582,6 +605,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 11561149a6..ccccb80c70 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -477,16 +477,6 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 TabBarVisibility.update(); } -@@ -4553,6 +4680,9 @@ - return; - } - -+ for (let tab of selectedTabs) { -+ gZenPinnedTabManager._removePinnedAttributes(tab, true); -+ } - this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); - } - @@ -4814,6 +4944,7 @@ telemetrySource, } = {} @@ -838,11 +828,3 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), -@@ -9763,6 +9944,7 @@ var TabContextMenu = { - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true); - gBrowser.removeTab(this.contextTab, { - animate: true, - ...gBrowser.TabMetrics.userTriggeredContext( diff --git a/src/zen/common/ZenSessionStore.mjs b/src/zen/common/ZenSessionStore.mjs index 2b86d886ee..3ad0e706bb 100644 --- a/src/zen/common/ZenSessionStore.mjs +++ b/src/zen/common/ZenSessionStore.mjs @@ -15,8 +15,8 @@ if (tabData.zenWorkspace) { tab.setAttribute('zen-workspace-id', tabData.zenWorkspace); } - if (tabData.zenPinnedId) { - tab.setAttribute('zen-pin-id', tabData.zenPinnedId); + if (tabData.zenSyncId) { + tab.setAttribute('zen-sync-id', tabData.zenSyncId); } if (tabData.zenHasStaticLabel) { tab.setAttribute('zen-has-static-label', 'true'); diff --git a/src/zen/common/ZenUIManager.mjs b/src/zen/common/ZenUIManager.mjs index a95f7e60de..d597c463df 100644 --- a/src/zen/common/ZenUIManager.mjs +++ b/src/zen/common/ZenUIManager.mjs @@ -1249,14 +1249,6 @@ var gZenVerticalTabsManager = { } else { gBrowser.setTabTitle(this._tabEdited); } - if (this._tabEdited.getAttribute('zen-pin-id')) { - // Update pin title in storage - await gZenPinnedTabManager.updatePinTitle( - this._tabEdited, - this._tabEdited.label, - !!newName - ); - } // Maybe add some confetti here?!? gZenUIManager.motion.animate( diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 3d181132bb..c2b328378d 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -508,9 +508,6 @@ tabs = [emptyTab, ...filteredTabs]; const folder = this._createFolderNode(options); - if (options.initialPinId) { - folder.setAttribute('zen-pin-id', options.initialPinId); - } if (options.insertAfter) { options.insertAfter.after(folder); @@ -940,7 +937,7 @@ if (!parentFolder && folder.hasAttribute('split-view-group')) continue; const emptyFolderTabs = folder.tabs .filter((tab) => tab.hasAttribute('zen-empty-tab')) - .map((tab) => tab.getAttribute('zen-pin-id')); + .map((tab) => tab.getAttribute('zen-sync-id')); let prevSiblingInfo = null; const prevSibling = folder.previousElementSibling; @@ -949,8 +946,8 @@ if (prevSibling) { if (gBrowser.isTabGroup(prevSibling)) { prevSiblingInfo = { type: 'group', id: prevSibling.id }; - } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) { - const zenPinId = prevSibling.getAttribute('zen-pin-id'); + } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-sync-id')) { + const zenPinId = prevSibling.getAttribute('zen-sync-id'); prevSiblingInfo = { type: 'tab', id: zenPinId }; } else { prevSiblingInfo = { type: 'start', id: null }; @@ -969,7 +966,7 @@ prevSiblingInfo: prevSiblingInfo, emptyTabIds: emptyFolderTabs, userIcon: userIcon?.getAttribute('href'), - pinId: folder.getAttribute('zen-pin-id'), + syncId: folder.getAttribute('zen-sync-id'), // note: We shouldn't be using the workspace-id anywhere, we are just // remembering it for the pinned tabs manager to use it later. workspaceId: folder.getAttribute('zen-workspace-id'), @@ -996,9 +993,9 @@ tabFolderWorkingData.set(folderData.id, workingData); const oldGroup = document.getElementById(folderData.id); - folderData.emptyTabIds.forEach((zenPinId) => { + folderData.emptyTabIds.forEach((zenSyncId) => { oldGroup - ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`) + ?.querySelector(`tab[zen-sync-id="${zenSyncId}"]`) ?.setAttribute('zen-empty-tab', true); }); if (oldGroup) { @@ -1011,7 +1008,7 @@ saveOnWindowClose: folderData.saveOnWindowClose, workspaceId: folderData.workspaceId, }); - folder.setAttribute('zen-pin-id', folderData.pinId); + folder.setAttribute('zen-sync-id', folderData.syncId); workingData.node = folder; oldGroup.before(folder); } else { @@ -1044,7 +1041,7 @@ switch (stateData?.prevSiblingInfo?.type) { case 'tab': { const tab = parentWorkingData.node.querySelector( - `[zen-pin-id="${stateData.prevSiblingInfo.id}"]` + `[zen-sync-id="${stateData.prevSiblingInfo.id}"]` ); tab.after(node); break; diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 5120319bcd..e8cf59114d 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -2,13 +2,6 @@ // 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 { - cancelIdleCallback, - clearTimeout, - requestIdleCallback, - setTimeout, -} from 'resource://gre/modules/Timer.sys.mjs'; - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -19,7 +12,6 @@ ChromeUtils.defineESModuleGetters(lazy, { SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', }); -const TAB_CUSTOM_VALUES = new WeakMap(); const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes const OBSERVING = ['sessionstore-state-write-complete', 'browser-window-before-show']; @@ -76,6 +68,7 @@ class nsZenSessionManager { /** Handles the browser-window-before-show observer notification. */ #onBeforeBrowserWindowShown(aWindow) { // TODO: Initialize new window + void aWindow; } get #topMostWindow() { @@ -113,10 +106,8 @@ class nsZenSessionManager { * @param forceUpdate * Forces us to recollect data and will bypass and update the * corresponding caches. - * @param zIndex - * The z-index of the window. */ - #collectWindowData(window, forceUpdate = false, zIndex = 0) { + #collectWindowData(window, forceUpdate = false) { let sidebarData = this.#sidebar; if (!sidebarData || forceUpdate) { sidebarData = {}; @@ -164,7 +155,8 @@ class nsZenSessionManager { } getNewWindowData(aWindows) { - return { windows: [Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {})] }; + let newWindow = { ...Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {}), ...this.#sidebar }; + return { windows: [newWindow] }; } } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index e555a3346c..245819a91f 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -5,23 +5,7 @@ const lazy = {}; class ZenPinnedTabsObserver { - static ALL_EVENTS = [ - 'TabPinned', - 'TabUnpinned', - 'TabMove', - 'TabGroupCreate', - 'TabGroupRemoved', - 'TabGroupMoved', - 'ZenFolderRenamed', - 'ZenFolderIconChanged', - 'TabGroupCollapse', - 'TabGroupExpand', - 'TabGrouped', - 'TabUngrouped', - 'ZenFolderChangedWorkspace', - 'TabAddedToEssentials', - 'TabRemovedFromEssentials', - ]; + static ALL_EVENTS = ['TabPinned', 'TabUnpinned']; #listeners = []; @@ -103,21 +87,8 @@ onTabIconChanged(tab, url = null) { tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); const iconUrl = url ?? tab.iconImage.src; - if (!iconUrl && tab.hasAttribute('zen-pin-id')) { - try { - setTimeout(async () => { - const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); - if (favicon) { - gBrowser.setIcon(tab, favicon); - } - }); - } catch { - // Handle error - } - } else { - if (tab.hasAttribute('zen-essential')) { - tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); - } + if (tab.hasAttribute('zen-essential')) { + tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); } } @@ -149,71 +120,6 @@ return lazy.zenTabsEssentialsMax; } - async refreshPinnedTabs({ init = false } = {}) { - if (!this.enabled) { - return; - } - await ZenPinnedTabsStorage.promiseInitialized; - await this.#initializePinsCache(); - setTimeout(async () => { - // Execute in a separate task to avoid blocking the main thread - await SessionStore.promiseAllWindowsRestored; - await gZenWorkspaces.promiseInitialized; - await this.#initializePinnedTabs(init); - if (init) { - this._hasFinishedLoading = true; - } - }, 10); - } - - async #initializePinsCache() { - try { - // Get pin data - const pins = await ZenPinnedTabsStorage.getPins(); - - // Enhance pins with favicons - this._pinsCache = await Promise.all( - pins.map(async (pin) => { - try { - if (pin.isGroup) { - return pin; // Skip groups for now - } - const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); - return { - ...pin, - iconUrl: image || null, - }; - } catch { - // If favicon fetch fails, continue without icon - return { - ...pin, - iconUrl: null, - }; - } - }) - ); - } catch (ex) { - console.error('Failed to initialize pins cache:', ex); - this._pinsCache = []; - } - - this.log(`Initialized pins cache with ${this._pinsCache.length} pins`); - return this._pinsCache; - } - - #finishedInitializingPins() { - if (this.hasInitializedPins) { - return; - } - this._resolvePinnedInitializedInternal(); - delete this._resolvePinnedInitializedInternal; - this.hasInitializedPins = true; - } - - async #initializePinnedTabs(init = false) { - this.#finishedInitializingPins(); - } - _onPinnedTabEvent(action, event) { if (!this.enabled) return; const tab = event.target; @@ -223,230 +129,22 @@ } switch (action) { case 'TabPinned': - case 'TabAddedToEssentials': tab._zenClickEventListener = this._zenClickEventListener; tab.addEventListener('click', tab._zenClickEventListener); - this._setPinnedAttributes(tab); break; - case 'TabRemovedFromEssentials': - if (tab.pinned) { - this.#onTabMove(tab); - break; - } // [Fall through] case 'TabUnpinned': - this._removePinnedAttributes(tab); if (tab._zenClickEventListener) { tab.removeEventListener('click', tab._zenClickEventListener); delete tab._zenClickEventListener; } break; - case 'TabMove': - this.#onTabMove(tab); - break; - case 'TabGroupCreate': - this.#onTabGroupCreate(event); - break; - case 'TabGroupRemoved': - this.#onTabGroupRemoved(event); - break; - case 'TabGroupMoved': - this.#onTabGroupMoved(event); - break; - case 'ZenFolderRenamed': - case 'ZenFolderIconChanged': - case 'TabGroupCollapse': - case 'TabGroupExpand': - case 'ZenFolderChangedWorkspace': - this.#updateGroupInfo(event.originalTarget); - break; - case 'TabGrouped': - this.#onTabGrouped(event); - break; - case 'TabUngrouped': - this.#onTabUngrouped(event); - break; default: console.warn('ZenPinnedTabManager: Unhandled tab event', action); break; } } - async #onTabGroupCreate(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - if (group.hasAttribute('zen-pin-id')) { - return; // Group already exists in storage - } - const workspaceId = group.getAttribute('zen-workspace-id'); - let id = await ZenPinnedTabsStorage.createGroup( - group.name, - group.iconURL, - group.collapsed, - workspaceId, - group.getAttribute('zen-pin-id'), - group._pPos - ); - group.setAttribute('zen-pin-id', id); - for (const tab of group.tabs) { - // Only add it if the tab is directly under the group - if ( - tab.pinned && - tab.hasAttribute('zen-pin-id') && - tab.group === group && - this.hasInitializedPins - ) { - const tabPinId = tab.getAttribute('zen-pin-id'); - await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos); - } - } - await this.refreshPinnedTabs(); - } - - async #onTabGrouped(event) { - const tab = event.detail; - const group = tab.group; - if (!group.isZenFolder) { - return; - } - const pinId = group.getAttribute('zen-pin-id'); - const tabPinId = tab.getAttribute('zen-pin-id'); - const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId); - if (!tabPin || !tabPin.group) { - return; - } - ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos); - } - - async #onTabUngrouped(event) { - const tab = event.detail; - const group = tab.group; - if (!group?.isZenFolder) { - return; - } - const tabPinId = tab.getAttribute('zen-pin-id'); - const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId); - if (!tabPin) { - return; - } - ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos); - } - - async #updateGroupInfo(group) { - if (!group?.isZenFolder) { - return; - } - const pinId = group.getAttribute('zen-pin-id'); - const groupPin = this._pinsCache?.find((p) => p.uuid === pinId); - if (groupPin) { - groupPin.title = group.name; - groupPin.folderIcon = group.iconURL; - groupPin.isFolderCollapsed = group.collapsed; - groupPin.position = group._pPos; - groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null; - groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null; - await this.savePin(groupPin); - for (const item of group.allItems) { - if (gBrowser.isTabGroup(item)) { - await this.#updateGroupInfo(item); - } else { - await this.#onTabMove(item); - } - } - } - } - - async #onTabGroupRemoved(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id')); - group.removeAttribute('zen-pin-id'); - } - - async #onTabGroupMoved(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - const newIndex = group._pPos; - const pinId = group.getAttribute('zen-pin-id'); - if (!pinId) { - return; - } - for (const tab of group.allItemsRecursive) { - if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) { - const pin = this._pinsCache.find((p) => p.uuid === pinId); - if (pin) { - pin.position = tab._pPos; - pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null; - pin.workspaceUuid = group.getAttribute('zen-workspace-id'); - await this.savePin(pin, false); - } - break; - } - } - const groupPin = this._pinsCache?.find((p) => p.uuid === pinId); - if (groupPin) { - groupPin.position = newIndex; - groupPin.parentUuid = group.group?.getAttribute('zen-pin-id'); - groupPin.workspaceUuid = group.getAttribute('zen-workspace-id'); - await this.savePin(groupPin); - } - } - - async #onTabMove(tab) { - if (!tab.pinned || !this._pinsCache) { - return; - } - - const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups]; - for (let i = 0; i < allTabs.length; i++) { - const otherTab = allTabs[i]; - if ( - otherTab.pinned && - otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id') - ) { - const actualPin = this._pinsCache.find( - (pin) => pin.uuid === otherTab.getAttribute('zen-pin-id') - ); - if (!actualPin) { - continue; - } - actualPin.position = otherTab._pPos; - actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id'); - actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null; - await this.savePin(actualPin, false); - } - } - - const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - - if (!actualPin) { - return; - } - actualPin.position = tab._pPos; - actualPin.isEssential = tab.hasAttribute('zen-essential'); - actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null; - actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null; - - // There was a bug where the title and hasStaticLabel attribute were not being set - // This is a workaround to fix that - if (tab.hasAttribute('zen-has-static-label')) { - actualPin.editedTitle = true; - actualPin.title = tab.label; - } - await this.savePin(actualPin); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabMoved', { - detail: { tab }, - }) - ); - } - async _onTabClick(e) { const tab = e.target?.closest('tab'); if (e.button === 1 && tab) { @@ -476,106 +174,10 @@ return; } - const browser = tab.linkedBrowser; - - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - - if (!pin) { - return; - } - - const userContextId = tab.getAttribute('usercontextid'); - - pin.title = tab.label || browser.contentTitle; - pin.url = browser.currentURI.spec; - pin.workspaceUuid = tab.getAttribute('zen-workspace-id'); - pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0; - - await this.savePin(pin); this.resetPinChangedUrl(tab); - await this.refreshPinnedTabs(); gZenUIManager.showToast('zen-pinned-tab-replaced'); } - async _setPinnedAttributes(tab) { - if ( - tab.hasAttribute('zen-pin-id') || - !this._hasFinishedLoading || - tab.hasAttribute('zen-empty-tab') - ) { - return; - } - - this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`); - const browser = tab.linkedBrowser; - - const uuid = gZenUIManager.generateUuidv4(); - const userContextId = tab.getAttribute('usercontextid'); - - let entry = null; - - if (tab.getAttribute('zen-pinned-entry')) { - entry = JSON.parse(tab.getAttribute('zen-pinned-entry')); - } - - await this.savePin({ - uuid, - title: entry?.title || tab.label || browser.contentTitle, - url: entry?.url || browser.currentURI.spec, - containerTabId: userContextId ? parseInt(userContextId, 10) : 0, - workspaceUuid: tab.getAttribute('zen-workspace-id'), - isEssential: tab.getAttribute('zen-essential') === 'true', - parentUuid: tab.group?.getAttribute('zen-pin-id') || null, - position: tab._pPos, - }); - - tab.setAttribute('zen-pin-id', uuid); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabCreated', { - detail: { tab }, - }) - ); - - // This is used while migrating old pins to new system - we don't want to refresh when migrating - if (tab.getAttribute('zen-pinned-entry')) { - tab.removeAttribute('zen-pinned-entry'); - return; - } - this.onLocationChange(browser); - await this.refreshPinnedTabs(); - } - - async _removePinnedAttributes(tab, isClosing = false) { - tab.removeAttribute('zen-has-static-label'); - if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) { - return; - } - - if (Services.startup.shuttingDown || window.skipNextCanClose) { - return; - } - - this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`); - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); - this.resetPinChangedUrl(tab); - - if (!isClosing) { - tab.removeAttribute('zen-pin-id'); - tab.removeAttribute('zen-essential'); // Just in case - - if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) { - const workspace = await gZenWorkspaces.getActiveWorkspace(); - tab.setAttribute('zen-workspace-id', workspace.uuid); - } - } - await this.refreshPinnedTabs(); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabRemoved', { - detail: { tab }, - }) - ); - } - _initClosePinnedTabShortcut() { let cmdClose = document.getElementById('cmd_close'); @@ -584,21 +186,6 @@ } } - async savePin(pin, notifyObservers = true) { - if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) { - return; - } - const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid); - if (existingPin) { - Object.assign(existingPin, pin); - } else { - // We shouldn't need it, but just in case there's - // a race condition while making new pinned tabs. - this._pinsCache.push(pin); - } - await ZenPinnedTabsStorage.savePin(pin, notifyObservers); - } - async onCloseTabShortcut( event, selectedTab = gBrowser.selectedTab, @@ -821,12 +408,6 @@ tab.removeAttribute('zen-workspace-id'); } if (tab.pinned && tab.hasAttribute('zen-pin-id')) { - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (pin) { - pin.isEssential = true; - pin.workspaceUuid = null; - this.savePin(pin); - } gBrowser.zenHandleTabMove(tab, () => { if (tab.ownerGlobal !== window) { tab = gBrowser.adoptTab(tab, { @@ -1217,11 +798,8 @@ return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true'; } - async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) { + async updatePinTitle(tab, newTitle, isEdited = true) { const uuid = tab.getAttribute('zen-pin-id'); - await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers); - - await this.refreshPinnedTabs(); const browsers = Services.wm.getEnumerator('navigator:browser'); diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs deleted file mode 100644 index 425dbf2d11..0000000000 --- a/src/zen/tabs/ZenPinnedTabsStorage.mjs +++ /dev/null @@ -1,635 +0,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/. -var ZenPinnedTabsStorage = { - async init() { - await this._ensureTable(); - }, - - async _ensureTable() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => { - // Create the pins table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - title TEXT NOT NULL, - url TEXT, - container_id INTEGER, - workspace_uuid TEXT, - position INTEGER NOT NULL DEFAULT 0, - is_essential BOOLEAN NOT NULL DEFAULT 0, - is_group BOOLEAN NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - const columns = await db.execute(`PRAGMA table_info(zen_pins)`); - const columnNames = columns.map((row) => row.getResultByName('name')); - - // Helper function to add column if it doesn't exist - const addColumnIfNotExists = async (columnName, definition) => { - if (!columnNames.includes(columnName)) { - await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`); - } - }; - - await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL'); - await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL'); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) - `); - - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins_changes ( - uuid TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL - ) - `); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid) - `); - - this._resolveInitialized(); - }); - }, - - /** - * Private helper method to notify observers with a list of changed UUIDs. - * @param {string} event - The observer event name. - * @param {Array} uuids - Array of changed workspace UUIDs. - */ - _notifyPinsChanged(event, uuids) { - if (uuids.length === 0) return; // No changes to notify - - // Convert the array of UUIDs to a JSON string - const data = JSON.stringify(uuids); - - Services.obs.notifyObservers(null, event, data); - }, - - async savePin(pin, notifyObservers = true) { - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - let newPosition; - if ('position' in pin && Number.isFinite(pin.position)) { - newPosition = pin.position; - } else { - // Get the maximum position within the same parent group (or null for root level) - const maxPositionResult = await db.execute( - ` - SELECT MAX("position") as max_position - FROM zen_pins - WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '') - `, - { folder_parent_uuid: pin.parentUuid || null } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - // Insert or replace the pin - await db.executeCached( - ` - INSERT OR REPLACE INTO zen_pins ( - uuid, title, url, container_id, workspace_uuid, position, - is_essential, is_group, folder_parent_uuid, edited_title, created_at, - updated_at, is_folder_collapsed, folder_icon - ) VALUES ( - :uuid, :title, :url, :container_id, :workspace_uuid, :position, - :is_essential, :is_group, :folder_parent_uuid, :edited_title, - COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), - :now, :is_folder_collapsed, :folder_icon - ) - `, - { - uuid: pin.uuid, - title: pin.title, - url: pin.isGroup ? '' : pin.url, - container_id: pin.containerTabId || null, - workspace_uuid: pin.workspaceUuid || null, - position: newPosition, - is_essential: pin.isEssential || false, - is_group: pin.isGroup || false, - folder_parent_uuid: pin.parentUuid || null, - edited_title: pin.editedTitle || false, - now, - folder_icon: pin.folderIcon || null, - is_folder_collapsed: pin.isFolderCollapsed || false, - } - ); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - - changedUUIDs.add(pin.uuid); - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async getPins() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.executeCached(` - SELECT * FROM zen_pins - ORDER BY position ASC - `); - return rows.map((row) => ({ - uuid: row.getResultByName('uuid'), - title: row.getResultByName('title'), - url: row.getResultByName('url'), - containerTabId: row.getResultByName('container_id'), - workspaceUuid: row.getResultByName('workspace_uuid'), - position: row.getResultByName('position'), - isEssential: Boolean(row.getResultByName('is_essential')), - isGroup: Boolean(row.getResultByName('is_group')), - parentUuid: row.getResultByName('folder_parent_uuid'), - editedTitle: Boolean(row.getResultByName('edited_title')), - folderIcon: row.getResultByName('folder_icon'), - isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), - })); - }, - - /** - * Create a new group - * @param {string} title - The title of the group - * @param {string} workspaceUuid - The workspace UUID (optional) - * @param {string} parentUuid - The parent group UUID (optional, null for root level) - * @param {number} position - The position of the group (optional, will auto-calculate if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - * @returns {Promise} The UUID of the created group - */ - async createGroup( - title, - icon = null, - isCollapsed = false, - workspaceUuid = null, - parentUuid = null, - position = null, - notifyObservers = true - ) { - if (!title || typeof title !== 'string') { - throw new Error('Group title is required and must be a string'); - } - - const groupUuid = gZenUIManager.generateUuidv4(); - - const groupPin = { - uuid: groupUuid, - title, - folderIcon: icon || null, - isFolderCollapsed: isCollapsed || false, - workspaceUuid, - parentUuid, - position, - isGroup: true, - isEssential: false, - editedTitle: true, // Group titles are always considered edited - }; - - await this.savePin(groupPin, notifyObservers); - return groupUuid; - }, - - /** - * Add an existing tab/pin to a group - * @param {string} tabUuid - The UUID of the tab to add to the group - * @param {string} groupUuid - The UUID of the target group - * @param {number} position - The position within the group (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) { - if (!tabUuid || !groupUuid) { - throw new Error('Both tabUuid and groupUuid are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => { - await db.executeTransaction(async () => { - // Verify the group exists and is actually a group - const groupCheck = await db.execute( - `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`, - { groupUuid } - ); - - if (groupCheck.length === 0) { - throw new Error(`Group with UUID ${groupUuid} does not exist`); - } - - if (!groupCheck[0].getResultByName('is_group')) { - throw new Error(`Pin with UUID ${groupUuid} is not a group`); - } - - const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, { - tabUuid, - }); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - const now = Date.now(); - let newPosition; - - if (position !== null && Number.isFinite(position)) { - newPosition = position; - } else { - // Get the maximum position within the group - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`, - { groupUuid } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = :groupUuid, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - groupUuid, - newPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - /** - * Remove a tab from its group (move to root level) - * @param {string} tabUuid - The UUID of the tab to remove from its group - * @param {number} newPosition - The new position at root level (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) { - if (!tabUuid) { - throw new Error('tabUuid is required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.removeTabFromGroup', - async (db) => { - await db.executeTransaction(async () => { - // Verify the tab exists and is in a group - const tabCheck = await db.execute( - `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`, - { tabUuid } - ); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - if (!tabCheck[0].getResultByName('folder_parent_uuid')) { - return; - } - - const now = Date.now(); - let finalPosition; - - if (newPosition !== null && Number.isFinite(newPosition)) { - finalPosition = newPosition; - } else { - // Get the maximum position at root level (where folder_parent_uuid is null) - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL` - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - finalPosition = maxPosition + 1000; - } - - // Update the tab to be at root level - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = NULL, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - newPosition: finalPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async removePin(uuid, notifyObservers = true) { - const changedUUIDs = [uuid]; - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { - await db.executeTransaction(async () => { - // Get all child UUIDs first for change tracking - const children = await db.execute( - `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`, - { - uuid, - } - ); - - // Add child UUIDs to changedUUIDs array - for (const child of children) { - changedUUIDs.push(child.getResultByName('uuid')); - } - - // Delete the pin/group itself - await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid }); - - // Record the changes - const now = Math.floor(Date.now() / 1000); - for (const changedUuid of changedUUIDs) { - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: changedUuid, - timestamp: now, - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-removed', changedUUIDs); - } - }, - - async wipeAllPins() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => { - await db.execute(`DELETE FROM zen_pins`); - await db.execute(`DELETE FROM zen_pins_changes`); - await this.updateLastChangeTimestamp(db); - }); - }, - - async markChanged(uuid) { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - }); - }, - - async getChangedIDs() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.execute(` - SELECT uuid, timestamp FROM zen_pins_changes - `); - const changes = {}; - for (const row of rows) { - changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); - } - return changes; - }, - - async clearChangedIDs() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => { - await db.execute(`DELETE FROM zen_pins_changes`); - }); - }, - - shouldReorderPins(before, current, after) { - const minGap = 1; // Minimum allowed gap between positions - return ( - (before !== null && current - before < minGap) || (after !== null && after - current < minGap) - ); - }, - - async reorderAllPins(db, changedUUIDs) { - const pins = await db.execute(` - SELECT uuid - FROM zen_pins - ORDER BY position ASC - `); - - for (let i = 0; i < pins.length; i++) { - const newPosition = (i + 1) * 1000; // Use large increments - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pins[i].getResultByName('uuid') } - ); - changedUUIDs.add(pins[i].getResultByName('uuid')); - } - }, - - async updateLastChangeTimestamp(db) { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO moz_meta (key, value) - VALUES ('zen_pins_last_change', :now) - `, - { now } - ); - }, - - async getLastChangeTimestamp() { - const db = await PlacesUtils.promiseDBConnection(); - const result = await db.executeCached(` - SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change' - `); - return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; - }, - - async updatePinPositions(pins) { - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.updatePinPositions', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - for (let i = 0; i < pins.length; i++) { - const pin = pins[i]; - const newPosition = (i + 1) * 1000; - - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pin.uuid } - ); - - changedUUIDs.add(pin.uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - }, - - async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) { - if (!uuid || typeof newTitle !== 'string') { - throw new Error('Invalid parameters: uuid and newTitle are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - // Update the pin's title and edited_title flag - const result = await db.execute( - ` - UPDATE zen_pins - SET title = :newTitle, - edited_title = :isEdited, - updated_at = :now - WHERE uuid = :uuid - `, - { - uuid, - newTitle, - isEdited, - now, - } - ); - - // Only proceed with change tracking if a row was actually updated - if (result.rowsAffected > 0) { - changedUUIDs.add(uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - } - }); - }); - - if (notifyObservers && changedUUIDs.size > 0) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async __dropTables() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => { - await db.execute(`DROP TABLE IF EXISTS zen_pins`); - await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`); - }); - }, -}; - -ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => { - ZenPinnedTabsStorage._resolveInitialized = resolve; - ZenPinnedTabsStorage.init(); -}); diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index 857f9fc9cf..4c06ae0e06 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -293,14 +293,14 @@ targetTab.getAttribute('zen-workspace-id') ); } - duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id')); } #onTabGroupCreate(event) { - const targetGroup = event.target; - const isSplitView = targetGroup.classList.contains('zen-split-view'); - const isFolder = targetGroup.isZenFolder; + void event; + //const targetGroup = event.target; + //const isSplitView = targetGroup.classList.contains('zen-split-view'); + //const isFolder = targetGroup.isZenFolder; } } diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index a54c1ebfe4..f19ba72775 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -932,7 +932,6 @@ var gZenWorkspaces = new (class extends nsZenMultiWindowFeature { await this.workspaceBookmarks(); await this.initializeTabsStripSections(); this._initializeEmptyTab(); - await gZenPinnedTabManager.refreshPinnedTabs({ init: true }); await this.changeWorkspace(activeWorkspace, { onInit: true }); this.#fixTabPositions(); this.onWindowResize(); @@ -1471,11 +1470,6 @@ var gZenWorkspaces = new (class extends nsZenMultiWindowFeature { !tab.hasAttribute('zen-empty-tab') && !tab.hasAttribute('zen-essential') ); - for (const tab of tabs) { - if (tab.pinned) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); - } - } gBrowser.removeTabs(tabs, { animate: false, skipSessionStore: true, From bf1b0dcd4848a9b3846b75276797e4a3b0fa9a7b Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 1 Nov 2025 13:40:14 +0100 Subject: [PATCH 09/54] feat: Dont restore windows that are already initialized, b=no-bug, c=no-component --- .../sessionstore/SessionStore-sys-mjs.patch | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index ef89b7892d..e92e6ec41f 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 eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd499c66d7 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03cda47b6f 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -126,6 +126,8 @@ const TAB_EVENTS = [ @@ -28,18 +28,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2041,6 +2046,10 @@ var SessionStoreInternal = { - // A regular window is not a private window, taskbar tab window, or popup window - let isRegularWindow = - !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; -+ if (!aInitialState && isRegularWindow) { -+ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); -+ this.restoreWindows(aWindow, aInitialState, {}); -+ } - - // perform additional initialization when the first window is loading - if (lazy.RunState.isStopped) { -@@ -2139,7 +2148,6 @@ var SessionStoreInternal = { +@@ -2139,7 +2144,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -47,6 +36,17 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. +@@ -2203,6 +2207,10 @@ var SessionStoreInternal = { + }); + this._shouldRestoreLastSession = false; + } ++ else if (!aInitialState && isRegularWindow) { ++ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); ++ this.restoreWindows(aWindow, aInitialState, {}); ++ } + + if (this._restoreLastWindow && aWindow.toolbar.visible) { + // always reset (if not a popup window) @@ -2372,11 +2380,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } From c4dd4708647913ddb5045c7969da818df37031b5 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Thu, 13 Nov 2025 13:56:31 +0100 Subject: [PATCH 10/54] chore: Update patches to ff 145, b=no-bug, c=no-component --- .../sessionstore/SessionStore-sys-mjs.patch | 40 +++--- .../tabbrowser/content/tab-js.patch | 18 +-- .../tabbrowser/content/tabbrowser-js.patch | 134 +++++++++--------- 3 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index e92e6ec41f..7c625f2af6 100644 --- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch @@ -1,17 +1,17 @@ diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs -index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03cda47b6f 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be523e4feb 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs -@@ -126,6 +126,8 @@ const TAB_EVENTS = [ - "TabUngrouped", +@@ -127,6 +127,8 @@ const TAB_EVENTS = [ "TabGroupCollapse", "TabGroupExpand", + "TabSplitViewActivate", + "TabAddedToEssentials", + "TabRemovedFromEssentials", ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -195,6 +197,7 @@ ChromeUtils.defineESModuleGetters(lazy, { +@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, { TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", @@ -19,7 +19,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 }); ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { -@@ -1904,6 +1907,8 @@ var SessionStoreInternal = { +@@ -1911,6 +1914,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -28,7 +28,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2139,7 +2144,6 @@ var SessionStoreInternal = { +@@ -2151,7 +2156,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -36,7 +36,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2203,6 +2207,10 @@ var SessionStoreInternal = { +@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { }); this._shouldRestoreLastSession = false; } @@ -47,7 +47,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 if (this._restoreLastWindow && aWindow.toolbar.visible) { // always reset (if not a popup window) -@@ -2372,11 +2380,9 @@ var SessionStoreInternal = { +@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -59,7 +59,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3361,7 +3367,7 @@ var SessionStoreInternal = { +@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -68,7 +68,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 return; } -@@ -4073,6 +4079,11 @@ var SessionStoreInternal = { +@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -80,7 +80,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4509,6 +4520,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -88,7 +88,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5358,7 +5370,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -97,7 +97,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 removableTabs.push(tab); } } -@@ -5418,7 +5430,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -106,7 +106,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 winData.workspaceID = workspaceID; } }, -@@ -5609,11 +5621,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -120,7 +120,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5624,6 +5637,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -128,7 +128,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5636,7 +5650,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -137,7 +137,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5748,8 +5762,8 @@ var SessionStoreInternal = { +@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -148,7 +148,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5792,6 +5806,8 @@ var SessionStoreInternal = { +@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -157,7 +157,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6348,6 +6364,25 @@ var SessionStoreInternal = { +@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -183,7 +183,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7263,7 +7298,7 @@ var SessionStoreInternal = { +@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index f9f19d12d4..75153b1734 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 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362efc001afdd 100644 +index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577b5fad08c 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -42,7 +42,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef ".tab-label-container": "pinned,selected=visuallyselected,labeldirection", ".tab-label": -@@ -184,7 +187,7 @@ +@@ -186,7 +189,7 @@ } set _visuallySelected(val) { @@ -51,7 +51,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef return; } -@@ -220,11 +223,21 @@ +@@ -222,11 +225,21 @@ } get visible() { @@ -78,7 +78,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef } get hidden() { -@@ -295,7 +308,7 @@ +@@ -297,7 +310,7 @@ return false; } @@ -87,7 +87,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef } get lastAccessed() { -@@ -372,8 +385,11 @@ +@@ -374,8 +387,11 @@ } get group() { @@ -101,7 +101,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef } return null; } -@@ -468,6 +484,8 @@ +@@ -470,6 +486,8 @@ this.style.MozUserFocus = "ignore"; } else if ( event.target.classList.contains("tab-close-button") || @@ -110,7 +110,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef event.target.classList.contains("tab-icon-overlay") || event.target.classList.contains("tab-audio-button") ) { -@@ -522,6 +540,10 @@ +@@ -524,6 +542,10 @@ this.style.MozUserFocus = ""; } @@ -121,7 +121,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef on_click(event) { if (event.button != 0) { return; -@@ -582,6 +605,14 @@ +@@ -584,6 +607,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -136,7 +136,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef } on_dblclick(event) { -@@ -605,6 +636,8 @@ +@@ -607,6 +638,8 @@ animate: true, triggeringEvent: event, }); diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index ccccb80c70..2b2a58d5d2 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,8 +1,8 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394b7dbc6a7 100644 +index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc349868d0d0 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js -@@ -432,15 +432,64 @@ +@@ -450,15 +450,64 @@ return this.tabContainer.visibleTabs; } @@ -69,7 +69,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 set selectedTab(val) { if ( gSharedTabWarning.willShowSharedTabWarning(val) || -@@ -588,6 +637,7 @@ +@@ -613,6 +662,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -77,7 +77,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -873,13 +923,17 @@ +@@ -898,13 +948,17 @@ } this.showTab(aTab); @@ -96,7 +96,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 aTab.setAttribute("pinned", "true"); this._updateTabBarForPinnedTabs(); -@@ -892,11 +946,15 @@ +@@ -917,11 +971,15 @@ } this.#handleTabMove(aTab, () => { @@ -113,7 +113,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 }); aTab.style.marginInlineStart = ""; -@@ -1073,6 +1131,8 @@ +@@ -1098,6 +1156,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -122,7 +122,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if ( aIconURL && !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) -@@ -1082,6 +1142,9 @@ +@@ -1107,6 +1167,9 @@ ); return; } @@ -132,7 +132,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1445,6 +1508,7 @@ +@@ -1470,6 +1533,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -140,7 +140,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1597,6 +1661,9 @@ +@@ -1622,6 +1686,9 @@ } let activeEl = document.activeElement; @@ -150,7 +150,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1920,7 +1987,8 @@ +@@ -1945,7 +2012,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -160,7 +160,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 return false; } -@@ -2028,7 +2096,7 @@ +@@ -2053,7 +2121,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -169,7 +169,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2303,6 +2371,7 @@ +@@ -2328,6 +2396,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -177,7 +177,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2376,8 +2445,7 @@ +@@ -2401,8 +2470,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -187,7 +187,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 b.setAttribute("transparent", "true"); } -@@ -2542,7 +2610,7 @@ +@@ -2567,7 +2635,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -196,7 +196,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2601,8 +2669,8 @@ +@@ -2626,8 +2694,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -207,7 +207,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2779,7 +2847,6 @@ +@@ -2814,7 +2882,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -215,7 +215,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2859,6 +2926,8 @@ +@@ -2923,6 +2990,8 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -224,7 +224,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2869,6 +2938,12 @@ +@@ -2933,6 +3002,12 @@ ); } @@ -237,7 +237,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (!UserInteraction.running("browser.tabs.opening", window)) { UserInteraction.start("browser.tabs.opening", "initting", window); } -@@ -2932,6 +3007,19 @@ +@@ -2996,6 +3071,19 @@ noInitialLabel, skipBackgroundNotify, }); @@ -257,7 +257,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -2940,6 +3028,7 @@ +@@ -3004,6 +3092,7 @@ ownerTab, openerTab, pinned, @@ -265,7 +265,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -2958,6 +3047,7 @@ +@@ -3022,6 +3111,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -273,7 +273,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 })); if (focusUrlBar) { -@@ -3078,6 +3168,12 @@ +@@ -3146,6 +3236,12 @@ } } @@ -286,7 +286,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3248,10 +3344,10 @@ +@@ -3330,10 +3426,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -298,7 +298,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } if (!color) { -@@ -3272,9 +3368,14 @@ +@@ -3354,9 +3450,14 @@ label, isAdoptingGroup ); @@ -315,7 +315,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 ); group.addTabs(tabs); -@@ -3395,7 +3496,7 @@ +@@ -3477,7 +3578,7 @@ } this.#handleTabMove(tab, () => @@ -324,7 +324,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 ); } -@@ -3597,6 +3698,7 @@ +@@ -3679,6 +3780,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -332,7 +332,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3666,6 +3768,7 @@ +@@ -3748,6 +3850,7 @@ openWindowInfo, name, skipLoad, @@ -340,7 +340,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 }); } -@@ -3853,7 +3956,7 @@ +@@ -3935,7 +4038,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -349,7 +349,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 let url = "about:blank"; if (tabData.entries?.length) { -@@ -3890,8 +3993,10 @@ +@@ -3972,8 +4075,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -361,7 +361,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (select) { tabToSelect = tab; } -@@ -3903,7 +4008,8 @@ +@@ -3985,7 +4090,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -371,7 +371,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -3917,7 +4023,10 @@ +@@ -3999,7 +4105,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -383,7 +383,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 ); tabsFragment.appendChild(tabGroup.node); } -@@ -3962,9 +4071,23 @@ +@@ -4044,9 +4153,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -407,7 +407,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4155,11 +4278,14 @@ +@@ -4237,11 +4360,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -423,7 +423,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if ( !bulkOrderedOpen && ((openerTab && -@@ -4171,7 +4297,7 @@ +@@ -4253,7 +4379,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -432,7 +432,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 tabGroup = previousTab.group; } if ( -@@ -4182,7 +4308,7 @@ +@@ -4264,7 +4390,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -441,7 +441,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4210,14 +4336,14 @@ +@@ -4292,14 +4418,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -460,7 +460,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4228,7 +4354,7 @@ +@@ -4310,7 +4436,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -469,7 +469,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4264,6 +4390,7 @@ +@@ -4346,6 +4472,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -477,7 +477,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 TabBarVisibility.update(); } -@@ -4814,6 +4944,7 @@ +@@ -4896,6 +5026,7 @@ telemetrySource, } = {} ) { @@ -485,7 +485,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4903,6 +5034,7 @@ +@@ -4985,6 +5116,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -493,7 +493,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } catch (e) { console.error(e); } -@@ -4941,6 +5073,12 @@ +@@ -5023,6 +5155,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -506,7 +506,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -4955,6 +5093,9 @@ +@@ -5037,6 +5175,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -516,7 +516,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5003,7 +5144,13 @@ +@@ -5085,7 +5226,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -531,7 +531,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 return; } -@@ -5137,7 +5284,7 @@ +@@ -5219,7 +5366,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -540,7 +540,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5161,6 +5308,7 @@ +@@ -5243,6 +5390,7 @@ newTab = true; } @@ -548,7 +548,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5201,13 +5349,7 @@ +@@ -5283,13 +5431,7 @@ aTab._mouseleave(); if (newTab) { @@ -563,7 +563,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else { TabBarVisibility.update(); } -@@ -5340,6 +5482,7 @@ +@@ -5422,6 +5564,7 @@ this.tabs[i]._tPos = i; } @@ -571,7 +571,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5552,6 +5695,7 @@ +@@ -5643,6 +5786,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -579,7 +579,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5564,13 +5708,13 @@ +@@ -5655,13 +5799,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -595,7 +595,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5586,7 +5730,7 @@ +@@ -5677,7 +5821,7 @@ } if (tab) { @@ -604,7 +604,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } // If no qualifying visible tab was found, see if there is a tab in -@@ -5607,7 +5751,7 @@ +@@ -5698,7 +5842,7 @@ }); } @@ -613,7 +613,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } _blurTab(aTab) { -@@ -6013,10 +6157,10 @@ +@@ -6104,10 +6248,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -626,7 +626,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6075,6 +6219,7 @@ +@@ -6166,6 +6310,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -634,7 +634,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (this.tabs.length == 1) { return null; } -@@ -6208,7 +6353,7 @@ +@@ -6299,7 +6444,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -643,7 +643,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } /** -@@ -6284,8 +6429,8 @@ +@@ -6375,8 +6520,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -654,7 +654,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6311,10 +6456,16 @@ +@@ -6402,10 +6547,16 @@ this.#handleTabMove( element, () => { @@ -673,7 +673,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6372,23 +6523,28 @@ +@@ -6463,23 +6614,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -708,7 +708,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6401,14 +6557,34 @@ +@@ -6492,14 +6648,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -744,7 +744,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6417,7 +6593,7 @@ +@@ -6508,7 +6684,7 @@ element, () => { if (moveBefore) { @@ -753,7 +753,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else if (targetElement) { targetElement.after(element); } else { -@@ -6489,10 +6665,10 @@ +@@ -6580,10 +6756,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -766,7 +766,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6522,6 +6698,7 @@ +@@ -6613,6 +6789,7 @@ let state = { tabIndex: tab._tPos, @@ -774,7 +774,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6548,7 +6725,7 @@ +@@ -6639,7 +6816,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -783,7 +783,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6585,6 +6762,10 @@ +@@ -6676,6 +6853,10 @@ moveActionCallback(); @@ -794,7 +794,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7486,7 +7667,7 @@ +@@ -7576,7 +7757,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -803,7 +803,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 window.focus(); aEvent.preventDefault(); break; -@@ -7501,7 +7682,6 @@ +@@ -7593,7 +7774,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -811,7 +811,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 }); break; case "TabGroupCreateByUser": -@@ -8442,6 +8622,7 @@ +@@ -8542,6 +8722,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -819,7 +819,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9443,7 +9624,7 @@ var TabContextMenu = { +@@ -9543,7 +9724,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; From e5517eb1642bd3c9170373a65754232c1360015b Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:57:32 +0100 Subject: [PATCH 11/54] Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch --- .../sessionstore/SessionStore-sys-mjs.patch | 59 +++++++------------ 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 7c625f2af6..79c5bcfcff 100644 --- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch @@ -1,25 +1,17 @@ diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs -index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be523e4feb 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d89fb95494 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs -@@ -127,6 +127,8 @@ const TAB_EVENTS = [ +@@ -126,6 +126,8 @@ const TAB_EVENTS = [ + "TabUngrouped", "TabGroupCollapse", "TabGroupExpand", - "TabSplitViewActivate", + "TabAddedToEssentials", + "TabRemovedFromEssentials", ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, { - TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", - TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", - setTimeout: "resource://gre/modules/Timer.sys.mjs", -+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", - }); - - ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { -@@ -1911,6 +1914,8 @@ var SessionStoreInternal = { +@@ -1904,6 +1906,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -28,7 +20,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2151,7 +2156,6 @@ var SessionStoreInternal = { +@@ -2139,7 +2143,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -36,18 +28,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { - }); - this._shouldRestoreLastSession = false; - } -+ else if (!aInitialState && isRegularWindow) { -+ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); -+ this.restoreWindows(aWindow, aInitialState, {}); -+ } - - if (this._restoreLastWindow && aWindow.toolbar.visible) { - // always reset (if not a popup window) -@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { +@@ -2372,11 +2375,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -59,7 +40,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { +@@ -3361,7 +3362,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -68,7 +49,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be return; } -@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { +@@ -4073,6 +4074,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -80,7 +61,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { +@@ -4509,6 +4515,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -88,7 +69,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { +@@ -5358,7 +5365,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -97,7 +78,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be removableTabs.push(tab); } } -@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { +@@ -5418,7 +5425,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -106,7 +87,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { +@@ -5609,11 +5616,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -120,7 +101,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { +@@ -5624,6 +5632,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -128,7 +109,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { +@@ -5636,7 +5645,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -137,7 +118,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { +@@ -5748,8 +5757,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -148,7 +129,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { +@@ -5792,6 +5801,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -157,7 +138,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { +@@ -6348,6 +6359,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -171,8 +152,8 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be + if (tabData.zenHasStaticLabel) { + tab.setAttribute("zen-has-static-label", "true"); + } -+ if (tabData.zenSyncId) { -+ tab.setAttribute("zen-sync-id", tabData.zenSyncId); ++ if (tabData.zenPinnedId) { ++ tab.setAttribute("zen-pin-id", tabData.zenPinnedId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); @@ -183,7 +164,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { +@@ -7263,7 +7293,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { From 3e39ef2538fef944a4fa21e8610e07c2ba74d9e6 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:57:42 +0100 Subject: [PATCH 12/54] Discard changes to src/browser/components/tabbrowser/content/tab-js.patch --- .../tabbrowser/content/tab-js.patch | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 75153b1734..9970988541 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 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577b5fad08c 100644 +index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362efc001afdd 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -42,7 +42,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 ".tab-label-container": "pinned,selected=visuallyselected,labeldirection", ".tab-label": -@@ -186,7 +189,7 @@ +@@ -184,7 +187,7 @@ } set _visuallySelected(val) { @@ -51,7 +51,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 return; } -@@ -222,11 +225,21 @@ +@@ -220,11 +223,21 @@ } get visible() { @@ -78,7 +78,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } get hidden() { -@@ -297,7 +310,7 @@ +@@ -295,7 +308,7 @@ return false; } @@ -87,7 +87,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } get lastAccessed() { -@@ -374,8 +387,11 @@ +@@ -372,8 +385,11 @@ } get group() { @@ -101,7 +101,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } return null; } -@@ -470,6 +486,8 @@ +@@ -468,6 +484,8 @@ this.style.MozUserFocus = "ignore"; } else if ( event.target.classList.contains("tab-close-button") || @@ -110,7 +110,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 event.target.classList.contains("tab-icon-overlay") || event.target.classList.contains("tab-audio-button") ) { -@@ -524,6 +542,10 @@ +@@ -522,6 +540,10 @@ this.style.MozUserFocus = ""; } @@ -121,7 +121,15 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 on_click(event) { if (event.button != 0) { return; -@@ -584,6 +607,14 @@ +@@ -570,6 +592,7 @@ + ) + ); + } else { ++ gZenPinnedTabManager._removePinnedAttributes(this, true); + gBrowser.removeTab(this, { + animate: true, + triggeringEvent: event, +@@ -582,6 +605,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -136,7 +144,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } on_dblclick(event) { -@@ -607,6 +638,8 @@ +@@ -605,6 +636,8 @@ animate: true, triggeringEvent: event, }); From 7f225ac3eea3f205f4c47fc2c73c0dd5e3a31919 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:57:55 +0100 Subject: [PATCH 13/54] Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch --- .../tabbrowser/content/tabbrowser-js.patch | 152 ++++++++++-------- 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 2b2a58d5d2..11561149a6 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,8 +1,8 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc349868d0d0 100644 +index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394b7dbc6a7 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js -@@ -450,15 +450,64 @@ +@@ -432,15 +432,64 @@ return this.tabContainer.visibleTabs; } @@ -69,7 +69,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 set selectedTab(val) { if ( gSharedTabWarning.willShowSharedTabWarning(val) || -@@ -613,6 +662,7 @@ +@@ -588,6 +637,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -77,7 +77,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -898,13 +948,17 @@ +@@ -873,13 +923,17 @@ } this.showTab(aTab); @@ -96,7 +96,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab.setAttribute("pinned", "true"); this._updateTabBarForPinnedTabs(); -@@ -917,11 +971,15 @@ +@@ -892,11 +946,15 @@ } this.#handleTabMove(aTab, () => { @@ -113,7 +113,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }); aTab.style.marginInlineStart = ""; -@@ -1098,6 +1156,8 @@ +@@ -1073,6 +1131,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -122,7 +122,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if ( aIconURL && !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) -@@ -1107,6 +1167,9 @@ +@@ -1082,6 +1142,9 @@ ); return; } @@ -132,7 +132,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1470,6 +1533,7 @@ +@@ -1445,6 +1508,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -140,7 +140,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1622,6 +1686,9 @@ +@@ -1597,6 +1661,9 @@ } let activeEl = document.activeElement; @@ -150,7 +150,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1945,7 +2012,8 @@ +@@ -1920,7 +1987,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -160,7 +160,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 return false; } -@@ -2053,7 +2121,7 @@ +@@ -2028,7 +2096,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -169,7 +169,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2328,6 +2396,7 @@ +@@ -2303,6 +2371,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -177,7 +177,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2401,8 +2470,7 @@ +@@ -2376,8 +2445,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -187,7 +187,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 b.setAttribute("transparent", "true"); } -@@ -2567,7 +2635,7 @@ +@@ -2542,7 +2610,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -196,7 +196,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2626,8 +2694,8 @@ +@@ -2601,8 +2669,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -207,7 +207,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2814,7 +2882,6 @@ +@@ -2779,7 +2847,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -215,7 +215,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2990,8 @@ +@@ -2859,6 +2926,8 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -224,7 +224,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,6 +3002,12 @@ +@@ -2869,6 +2938,12 @@ ); } @@ -237,7 +237,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (!UserInteraction.running("browser.tabs.opening", window)) { UserInteraction.start("browser.tabs.opening", "initting", window); } -@@ -2996,6 +3071,19 @@ +@@ -2932,6 +3007,19 @@ noInitialLabel, skipBackgroundNotify, }); @@ -257,7 +257,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3092,7 @@ +@@ -2940,6 +3028,7 @@ ownerTab, openerTab, pinned, @@ -265,7 +265,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3111,7 @@ +@@ -2958,6 +3047,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -273,7 +273,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 })); if (focusUrlBar) { -@@ -3146,6 +3236,12 @@ +@@ -3078,6 +3168,12 @@ } } @@ -286,7 +286,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3330,10 +3426,10 @@ +@@ -3248,10 +3344,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -298,7 +298,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } if (!color) { -@@ -3354,9 +3450,14 @@ +@@ -3272,9 +3368,14 @@ label, isAdoptingGroup ); @@ -315,7 +315,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); group.addTabs(tabs); -@@ -3477,7 +3578,7 @@ +@@ -3395,7 +3496,7 @@ } this.#handleTabMove(tab, () => @@ -324,7 +324,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); } -@@ -3679,6 +3780,7 @@ +@@ -3597,6 +3698,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -332,7 +332,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3748,6 +3850,7 @@ +@@ -3666,6 +3768,7 @@ openWindowInfo, name, skipLoad, @@ -340,7 +340,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }); } -@@ -3935,7 +4038,7 @@ +@@ -3853,7 +3956,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -349,7 +349,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 let url = "about:blank"; if (tabData.entries?.length) { -@@ -3972,8 +4075,10 @@ +@@ -3890,8 +3993,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -361,7 +361,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (select) { tabToSelect = tab; } -@@ -3985,7 +4090,8 @@ +@@ -3903,7 +4008,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -371,7 +371,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -3999,7 +4105,10 @@ +@@ -3917,7 +4023,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -383,7 +383,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); tabsFragment.appendChild(tabGroup.node); } -@@ -4044,9 +4153,23 @@ +@@ -3962,9 +4071,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -407,7 +407,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4237,11 +4360,14 @@ +@@ -4155,11 +4278,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -423,7 +423,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if ( !bulkOrderedOpen && ((openerTab && -@@ -4253,7 +4379,7 @@ +@@ -4171,7 +4297,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -432,7 +432,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 tabGroup = previousTab.group; } if ( -@@ -4264,7 +4390,7 @@ +@@ -4182,7 +4308,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -441,7 +441,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4292,14 +4418,14 @@ +@@ -4210,14 +4336,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -460,7 +460,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4310,7 +4436,7 @@ +@@ -4228,7 +4354,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -469,7 +469,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4346,6 +4472,7 @@ +@@ -4264,6 +4390,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -477,7 +477,17 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 TabBarVisibility.update(); } -@@ -4896,6 +5026,7 @@ +@@ -4553,6 +4680,9 @@ + return; + } + ++ for (let tab of selectedTabs) { ++ gZenPinnedTabManager._removePinnedAttributes(tab, true); ++ } + this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); + } + +@@ -4814,6 +4944,7 @@ telemetrySource, } = {} ) { @@ -485,7 +495,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5116,7 @@ +@@ -4903,6 +5034,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -493,7 +503,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } catch (e) { console.error(e); } -@@ -5023,6 +5155,12 @@ +@@ -4941,6 +5073,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -506,7 +516,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5175,9 @@ +@@ -4955,6 +5093,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -516,7 +526,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5226,13 @@ +@@ -5003,7 +5144,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -531,7 +541,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 return; } -@@ -5219,7 +5366,7 @@ +@@ -5137,7 +5284,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -540,7 +550,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5390,7 @@ +@@ -5161,6 +5308,7 @@ newTab = true; } @@ -548,7 +558,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5431,7 @@ +@@ -5201,13 +5349,7 @@ aTab._mouseleave(); if (newTab) { @@ -563,7 +573,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else { TabBarVisibility.update(); } -@@ -5422,6 +5564,7 @@ +@@ -5340,6 +5482,7 @@ this.tabs[i]._tPos = i; } @@ -571,7 +581,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5786,7 @@ +@@ -5552,6 +5695,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -579,7 +589,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5799,13 @@ +@@ -5564,13 +5708,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -595,7 +605,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5821,7 @@ +@@ -5586,7 +5730,7 @@ } if (tab) { @@ -604,7 +614,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5842,7 @@ +@@ -5607,7 +5751,7 @@ }); } @@ -613,7 +623,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } _blurTab(aTab) { -@@ -6104,10 +6248,10 @@ +@@ -6013,10 +6157,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -626,7 +636,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6166,6 +6310,7 @@ +@@ -6075,6 +6219,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -634,7 +644,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (this.tabs.length == 1) { return null; } -@@ -6299,7 +6444,7 @@ +@@ -6208,7 +6353,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -643,7 +653,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } /** -@@ -6375,8 +6520,8 @@ +@@ -6284,8 +6429,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -654,7 +664,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6402,10 +6547,16 @@ +@@ -6311,10 +6456,16 @@ this.#handleTabMove( element, () => { @@ -673,7 +683,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6463,23 +6614,28 @@ +@@ -6372,23 +6523,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -708,7 +718,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6492,14 +6648,34 @@ +@@ -6401,14 +6557,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -744,7 +754,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6508,7 +6684,7 @@ +@@ -6417,7 +6593,7 @@ element, () => { if (moveBefore) { @@ -753,7 +763,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else if (targetElement) { targetElement.after(element); } else { -@@ -6580,10 +6756,10 @@ +@@ -6489,10 +6665,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -766,7 +776,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6613,6 +6789,7 @@ +@@ -6522,6 +6698,7 @@ let state = { tabIndex: tab._tPos, @@ -774,7 +784,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6639,7 +6816,7 @@ +@@ -6548,7 +6725,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -783,7 +793,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6676,6 +6853,10 @@ +@@ -6585,6 +6762,10 @@ moveActionCallback(); @@ -794,7 +804,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7576,7 +7757,7 @@ +@@ -7486,7 +7667,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -803,7 +813,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 window.focus(); aEvent.preventDefault(); break; -@@ -7593,7 +7774,6 @@ +@@ -7501,7 +7682,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -811,7 +821,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }); break; case "TabGroupCreateByUser": -@@ -8542,6 +8722,7 @@ +@@ -8442,6 +8622,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -819,7 +829,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9543,7 +9724,7 @@ var TabContextMenu = { +@@ -9443,7 +9624,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; @@ -828,3 +838,11 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), +@@ -9763,6 +9944,7 @@ var TabContextMenu = { + ) + ); + } else { ++ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true); + gBrowser.removeTab(this.contextTab, { + animate: true, + ...gBrowser.TabMetrics.userTriggeredContext( From 68b37ac73642566c31e5053b2896c0bc781e0bd5 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:58:18 +0100 Subject: [PATCH 14/54] Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs --- src/zen/tabs/ZenPinnedTabsStorage.mjs | 635 ++++++++++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 src/zen/tabs/ZenPinnedTabsStorage.mjs diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs new file mode 100644 index 0000000000..425dbf2d11 --- /dev/null +++ b/src/zen/tabs/ZenPinnedTabsStorage.mjs @@ -0,0 +1,635 @@ +// 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/. +var ZenPinnedTabsStorage = { + async init() { + await this._ensureTable(); + }, + + async _ensureTable() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => { + // Create the pins table if it doesn't exist + await db.execute(` + CREATE TABLE IF NOT EXISTS zen_pins ( + id INTEGER PRIMARY KEY, + uuid TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + url TEXT, + container_id INTEGER, + workspace_uuid TEXT, + position INTEGER NOT NULL DEFAULT 0, + is_essential BOOLEAN NOT NULL DEFAULT 0, + is_group BOOLEAN NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `); + + const columns = await db.execute(`PRAGMA table_info(zen_pins)`); + const columnNames = columns.map((row) => row.getResultByName('name')); + + // Helper function to add column if it doesn't exist + const addColumnIfNotExists = async (columnName, definition) => { + if (!columnNames.includes(columnName)) { + await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`); + } + }; + + await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0'); + await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0'); + await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL'); + await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL'); + + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) + `); + + await db.execute(` + CREATE TABLE IF NOT EXISTS zen_pins_changes ( + uuid TEXT PRIMARY KEY, + timestamp INTEGER NOT NULL + ) + `); + + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid) + `); + + this._resolveInitialized(); + }); + }, + + /** + * Private helper method to notify observers with a list of changed UUIDs. + * @param {string} event - The observer event name. + * @param {Array} uuids - Array of changed workspace UUIDs. + */ + _notifyPinsChanged(event, uuids) { + if (uuids.length === 0) return; // No changes to notify + + // Convert the array of UUIDs to a JSON string + const data = JSON.stringify(uuids); + + Services.obs.notifyObservers(null, event, data); + }, + + async savePin(pin, notifyObservers = true) { + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + let newPosition; + if ('position' in pin && Number.isFinite(pin.position)) { + newPosition = pin.position; + } else { + // Get the maximum position within the same parent group (or null for root level) + const maxPositionResult = await db.execute( + ` + SELECT MAX("position") as max_position + FROM zen_pins + WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '') + `, + { folder_parent_uuid: pin.parentUuid || null } + ); + const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; + newPosition = maxPosition + 1000; + } + + // Insert or replace the pin + await db.executeCached( + ` + INSERT OR REPLACE INTO zen_pins ( + uuid, title, url, container_id, workspace_uuid, position, + is_essential, is_group, folder_parent_uuid, edited_title, created_at, + updated_at, is_folder_collapsed, folder_icon + ) VALUES ( + :uuid, :title, :url, :container_id, :workspace_uuid, :position, + :is_essential, :is_group, :folder_parent_uuid, :edited_title, + COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), + :now, :is_folder_collapsed, :folder_icon + ) + `, + { + uuid: pin.uuid, + title: pin.title, + url: pin.isGroup ? '' : pin.url, + container_id: pin.containerTabId || null, + workspace_uuid: pin.workspaceUuid || null, + position: newPosition, + is_essential: pin.isEssential || false, + is_group: pin.isGroup || false, + folder_parent_uuid: pin.parentUuid || null, + edited_title: pin.editedTitle || false, + now, + folder_icon: pin.folderIcon || null, + is_folder_collapsed: pin.isFolderCollapsed || false, + } + ); + + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: pin.uuid, + timestamp: Math.floor(now / 1000), + } + ); + + changedUUIDs.add(pin.uuid); + await this.updateLastChangeTimestamp(db); + }); + }); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + async getPins() { + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.executeCached(` + SELECT * FROM zen_pins + ORDER BY position ASC + `); + return rows.map((row) => ({ + uuid: row.getResultByName('uuid'), + title: row.getResultByName('title'), + url: row.getResultByName('url'), + containerTabId: row.getResultByName('container_id'), + workspaceUuid: row.getResultByName('workspace_uuid'), + position: row.getResultByName('position'), + isEssential: Boolean(row.getResultByName('is_essential')), + isGroup: Boolean(row.getResultByName('is_group')), + parentUuid: row.getResultByName('folder_parent_uuid'), + editedTitle: Boolean(row.getResultByName('edited_title')), + folderIcon: row.getResultByName('folder_icon'), + isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), + })); + }, + + /** + * Create a new group + * @param {string} title - The title of the group + * @param {string} workspaceUuid - The workspace UUID (optional) + * @param {string} parentUuid - The parent group UUID (optional, null for root level) + * @param {number} position - The position of the group (optional, will auto-calculate if not provided) + * @param {boolean} notifyObservers - Whether to notify observers (default: true) + * @returns {Promise} The UUID of the created group + */ + async createGroup( + title, + icon = null, + isCollapsed = false, + workspaceUuid = null, + parentUuid = null, + position = null, + notifyObservers = true + ) { + if (!title || typeof title !== 'string') { + throw new Error('Group title is required and must be a string'); + } + + const groupUuid = gZenUIManager.generateUuidv4(); + + const groupPin = { + uuid: groupUuid, + title, + folderIcon: icon || null, + isFolderCollapsed: isCollapsed || false, + workspaceUuid, + parentUuid, + position, + isGroup: true, + isEssential: false, + editedTitle: true, // Group titles are always considered edited + }; + + await this.savePin(groupPin, notifyObservers); + return groupUuid; + }, + + /** + * Add an existing tab/pin to a group + * @param {string} tabUuid - The UUID of the tab to add to the group + * @param {string} groupUuid - The UUID of the target group + * @param {number} position - The position within the group (optional, will append if not provided) + * @param {boolean} notifyObservers - Whether to notify observers (default: true) + */ + async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) { + if (!tabUuid || !groupUuid) { + throw new Error('Both tabUuid and groupUuid are required'); + } + + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => { + await db.executeTransaction(async () => { + // Verify the group exists and is actually a group + const groupCheck = await db.execute( + `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`, + { groupUuid } + ); + + if (groupCheck.length === 0) { + throw new Error(`Group with UUID ${groupUuid} does not exist`); + } + + if (!groupCheck[0].getResultByName('is_group')) { + throw new Error(`Pin with UUID ${groupUuid} is not a group`); + } + + const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, { + tabUuid, + }); + + if (tabCheck.length === 0) { + throw new Error(`Tab with UUID ${tabUuid} does not exist`); + } + + const now = Date.now(); + let newPosition; + + if (position !== null && Number.isFinite(position)) { + newPosition = position; + } else { + // Get the maximum position within the group + const maxPositionResult = await db.execute( + `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`, + { groupUuid } + ); + const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; + newPosition = maxPosition + 1000; + } + + await db.execute( + ` + UPDATE zen_pins + SET folder_parent_uuid = :groupUuid, + position = :newPosition, + updated_at = :now + WHERE uuid = :tabUuid + `, + { + tabUuid, + groupUuid, + newPosition, + now, + } + ); + + changedUUIDs.add(tabUuid); + + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: tabUuid, + timestamp: Math.floor(now / 1000), + } + ); + + await this.updateLastChangeTimestamp(db); + }); + }); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + /** + * Remove a tab from its group (move to root level) + * @param {string} tabUuid - The UUID of the tab to remove from its group + * @param {number} newPosition - The new position at root level (optional, will append if not provided) + * @param {boolean} notifyObservers - Whether to notify observers (default: true) + */ + async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) { + if (!tabUuid) { + throw new Error('tabUuid is required'); + } + + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper( + 'ZenPinnedTabsStorage.removeTabFromGroup', + async (db) => { + await db.executeTransaction(async () => { + // Verify the tab exists and is in a group + const tabCheck = await db.execute( + `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`, + { tabUuid } + ); + + if (tabCheck.length === 0) { + throw new Error(`Tab with UUID ${tabUuid} does not exist`); + } + + if (!tabCheck[0].getResultByName('folder_parent_uuid')) { + return; + } + + const now = Date.now(); + let finalPosition; + + if (newPosition !== null && Number.isFinite(newPosition)) { + finalPosition = newPosition; + } else { + // Get the maximum position at root level (where folder_parent_uuid is null) + const maxPositionResult = await db.execute( + `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL` + ); + const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; + finalPosition = maxPosition + 1000; + } + + // Update the tab to be at root level + await db.execute( + ` + UPDATE zen_pins + SET folder_parent_uuid = NULL, + position = :newPosition, + updated_at = :now + WHERE uuid = :tabUuid + `, + { + tabUuid, + newPosition: finalPosition, + now, + } + ); + + changedUUIDs.add(tabUuid); + + // Record the change + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: tabUuid, + timestamp: Math.floor(now / 1000), + } + ); + + await this.updateLastChangeTimestamp(db); + }); + } + ); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + async removePin(uuid, notifyObservers = true) { + const changedUUIDs = [uuid]; + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { + await db.executeTransaction(async () => { + // Get all child UUIDs first for change tracking + const children = await db.execute( + `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`, + { + uuid, + } + ); + + // Add child UUIDs to changedUUIDs array + for (const child of children) { + changedUUIDs.push(child.getResultByName('uuid')); + } + + // Delete the pin/group itself + await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid }); + + // Record the changes + const now = Math.floor(Date.now() / 1000); + for (const changedUuid of changedUUIDs) { + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: changedUuid, + timestamp: now, + } + ); + } + + await this.updateLastChangeTimestamp(db); + }); + }); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-removed', changedUUIDs); + } + }, + + async wipeAllPins() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => { + await db.execute(`DELETE FROM zen_pins`); + await db.execute(`DELETE FROM zen_pins_changes`); + await this.updateLastChangeTimestamp(db); + }); + }, + + async markChanged(uuid) { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => { + const now = Date.now(); + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid, + timestamp: Math.floor(now / 1000), + } + ); + }); + }, + + async getChangedIDs() { + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.execute(` + SELECT uuid, timestamp FROM zen_pins_changes + `); + const changes = {}; + for (const row of rows) { + changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); + } + return changes; + }, + + async clearChangedIDs() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => { + await db.execute(`DELETE FROM zen_pins_changes`); + }); + }, + + shouldReorderPins(before, current, after) { + const minGap = 1; // Minimum allowed gap between positions + return ( + (before !== null && current - before < minGap) || (after !== null && after - current < minGap) + ); + }, + + async reorderAllPins(db, changedUUIDs) { + const pins = await db.execute(` + SELECT uuid + FROM zen_pins + ORDER BY position ASC + `); + + for (let i = 0; i < pins.length; i++) { + const newPosition = (i + 1) * 1000; // Use large increments + await db.execute( + ` + UPDATE zen_pins + SET position = :newPosition + WHERE uuid = :uuid + `, + { newPosition, uuid: pins[i].getResultByName('uuid') } + ); + changedUUIDs.add(pins[i].getResultByName('uuid')); + } + }, + + async updateLastChangeTimestamp(db) { + const now = Date.now(); + await db.execute( + ` + INSERT OR REPLACE INTO moz_meta (key, value) + VALUES ('zen_pins_last_change', :now) + `, + { now } + ); + }, + + async getLastChangeTimestamp() { + const db = await PlacesUtils.promiseDBConnection(); + const result = await db.executeCached(` + SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change' + `); + return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; + }, + + async updatePinPositions(pins) { + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper( + 'ZenPinnedTabsStorage.updatePinPositions', + async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + for (let i = 0; i < pins.length; i++) { + const pin = pins[i]; + const newPosition = (i + 1) * 1000; + + await db.execute( + ` + UPDATE zen_pins + SET position = :newPosition + WHERE uuid = :uuid + `, + { newPosition, uuid: pin.uuid } + ); + + changedUUIDs.add(pin.uuid); + + // Record the change + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: pin.uuid, + timestamp: Math.floor(now / 1000), + } + ); + } + + await this.updateLastChangeTimestamp(db); + }); + } + ); + + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + }, + + async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) { + if (!uuid || typeof newTitle !== 'string') { + throw new Error('Invalid parameters: uuid and newTitle are required'); + } + + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + // Update the pin's title and edited_title flag + const result = await db.execute( + ` + UPDATE zen_pins + SET title = :newTitle, + edited_title = :isEdited, + updated_at = :now + WHERE uuid = :uuid + `, + { + uuid, + newTitle, + isEdited, + now, + } + ); + + // Only proceed with change tracking if a row was actually updated + if (result.rowsAffected > 0) { + changedUUIDs.add(uuid); + + // Record the change + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid, + timestamp: Math.floor(now / 1000), + } + ); + + await this.updateLastChangeTimestamp(db); + } + }); + }); + + if (notifyObservers && changedUUIDs.size > 0) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + async __dropTables() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => { + await db.execute(`DROP TABLE IF EXISTS zen_pins`); + await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`); + }); + }, +}; + +ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => { + ZenPinnedTabsStorage._resolveInitialized = resolve; + ZenPinnedTabsStorage.init(); +}); From 12c921fd878393fe03a0b2f3549f09f407650ce2 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Thu, 13 Nov 2025 14:40:52 +0100 Subject: [PATCH 15/54] feat: Run session saver before opening a new winodw, b=no-bug, c=tabs --- .../sessionstore/SessionFile-sys-mjs.patch | 4 +- .../sessionstore/SessionStore-sys-mjs.patch | 55 +++++++++---- .../tabbrowser/content/tab-js.patch | 14 +--- .../tabbrowser/content/tabbrowser-js.patch | 82 ++++++++----------- .../sessionstore/ZenSessionManager.sys.mjs | 10 ++- src/zen/tabs/ZenPinnedTabManager.mjs | 1 - 6 files changed, 81 insertions(+), 85 deletions(-) diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch index 04aa6f0cac..895c4313fa 100644 --- a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs -index 157c55ab24a418b56690d2e26320582909b919e4..14755f57dc450583e69eee94eb11f16980d5e5cb 100644 +index 31140cb8be3b529a0952ca8dc55165690b0e2120..605c9e0aa84da0a2d3171a0573e8cd95e27bd0c4 100644 --- a/browser/components/sessionstore/SessionFile.sys.mjs +++ b/browser/components/sessionstore/SessionFile.sys.mjs @@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, { @@ -10,7 +10,7 @@ index 157c55ab24a418b56690d2e26320582909b919e4..14755f57dc450583e69eee94eb11f169 }); const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID"; -@@ -364,7 +365,7 @@ var SessionFileInternal = { +@@ -380,7 +381,7 @@ var SessionFileInternal = { this._readOrigin = result.origin; result.noFilesFound = noFilesFound; diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index df250c095a..c778b4fd59 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 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10540eb659 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc3315aa98 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -11,7 +11,15 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -1911,6 +1913,8 @@ var SessionStoreInternal = { +@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { +@@ -1911,6 +1914,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -20,7 +28,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2151,7 +2155,6 @@ var SessionStoreInternal = { +@@ -2151,7 +2156,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -28,7 +36,18 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2384,11 +2387,9 @@ var SessionStoreInternal = { +@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { + }); + this._shouldRestoreLastSession = false; + } ++ else if (!aInitialState && isRegularWindow) { ++ aInitialState = lazy.ZenSessionStore.getNewWindowData(); ++ this.restoreWindows(aWindow, aInitialState, {}); ++ } + + if (this._restoreLastWindow && aWindow.toolbar.visible) { + // always reset (if not a popup window) +@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -40,7 +59,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3374,7 @@ var SessionStoreInternal = { +@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -49,7 +68,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 return; } -@@ -4089,6 +4090,11 @@ var SessionStoreInternal = { +@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -61,7 +80,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4531,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -69,7 +88,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5381,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -78,7 +97,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 removableTabs.push(tab); } } -@@ -5434,7 +5441,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -87,7 +106,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5632,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -101,7 +120,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5648,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -109,7 +128,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5661,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -118,7 +137,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5773,8 @@ var SessionStoreInternal = { +@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -129,7 +148,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5817,8 @@ var SessionStoreInternal = { +@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -138,7 +157,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6382,25 @@ var SessionStoreInternal = { +@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -152,8 +171,8 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 + if (tabData.zenHasStaticLabel) { + tab.setAttribute("zen-has-static-label", "true"); + } -+ if (tabData.zenPinnedId) { -+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId); ++ if (tabData.zenSyncId) { ++ tab.setAttribute("zen-sync-id", tabData.zenSyncId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); @@ -164,7 +183,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..f13ed1412bb7ae6623aa2605d7691d10 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7319,7 @@ var SessionStoreInternal = { +@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 3452bced0a..fc652603cc 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 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577b5fad08c 100644 +index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..ce54ed0c8a93d5521a436c55c9432c090b0420ac 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -121,15 +121,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 on_click(event) { if (event.button != 0) { return; -@@ -572,6 +594,7 @@ - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this, true); - gBrowser.removeTab(this, { - animate: true, - triggeringEvent: event, -@@ -584,6 +607,14 @@ +@@ -584,6 +606,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -144,7 +136,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } on_dblclick(event) { -@@ -607,6 +638,8 @@ +@@ -607,6 +637,8 @@ animate: true, triggeringEvent: event, }); diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index c581a6389c..0582cbad4f 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad0c761fb5 100644 +index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4cd44d980e 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -450,15 +450,64 @@ @@ -420,10 +420,10 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad + gZenWorkspaces._initialTab._shouldRemove = true; + } + } - } ++ } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; -+ } + } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { @@ -498,17 +498,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad TabBarVisibility.update(); } -@@ -4635,6 +4763,9 @@ - return; - } - -+ for (let tab of selectedTabs) { -+ gZenPinnedTabManager._removePinnedAttributes(tab, true); -+ } - this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); - } - -@@ -4896,6 +5027,7 @@ +@@ -4896,6 +5024,7 @@ telemetrySource, } = {} ) { @@ -516,7 +506,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5117,7 @@ +@@ -4985,6 +5114,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -524,7 +514,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } catch (e) { console.error(e); } -@@ -5023,6 +5156,12 @@ +@@ -5023,6 +5153,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -537,7 +527,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5176,9 @@ +@@ -5037,6 +5173,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -547,7 +537,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5227,13 @@ +@@ -5085,7 +5224,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -562,7 +552,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad return; } -@@ -5219,7 +5367,7 @@ +@@ -5219,7 +5364,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -571,7 +561,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5391,7 @@ +@@ -5243,6 +5388,7 @@ newTab = true; } @@ -579,7 +569,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5432,7 @@ +@@ -5283,13 +5429,7 @@ aTab._mouseleave(); if (newTab) { @@ -594,7 +584,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } else { TabBarVisibility.update(); } -@@ -5422,6 +5565,7 @@ +@@ -5422,6 +5562,7 @@ this.tabs[i]._tPos = i; } @@ -602,7 +592,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5787,7 @@ +@@ -5643,6 +5784,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -610,7 +600,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5800,13 @@ +@@ -5655,13 +5797,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -626,7 +616,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5822,7 @@ +@@ -5677,7 +5819,7 @@ } if (tab) { @@ -635,7 +625,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5843,7 @@ +@@ -5698,7 +5840,7 @@ }); } @@ -644,7 +634,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } _blurTab(aTab) { -@@ -6104,10 +6249,10 @@ +@@ -6104,10 +6246,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -657,7 +647,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6166,6 +6311,7 @@ +@@ -6166,6 +6308,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -665,7 +655,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad if (this.tabs.length == 1) { return null; } -@@ -6299,7 +6445,7 @@ +@@ -6299,7 +6442,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -674,7 +664,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } /** -@@ -6375,8 +6521,8 @@ +@@ -6375,8 +6518,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -685,7 +675,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6402,10 +6548,16 @@ +@@ -6402,10 +6545,16 @@ this.#handleTabMove( element, () => { @@ -704,7 +694,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6463,23 +6615,28 @@ +@@ -6463,23 +6612,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -739,7 +729,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6492,14 +6649,34 @@ +@@ -6492,14 +6646,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -775,7 +765,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6508,7 +6685,7 @@ +@@ -6508,7 +6682,7 @@ element, () => { if (moveBefore) { @@ -784,7 +774,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad } else if (targetElement) { targetElement.after(element); } else { -@@ -6580,10 +6757,10 @@ +@@ -6580,10 +6754,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -797,7 +787,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6613,6 +6790,7 @@ +@@ -6613,6 +6787,7 @@ let state = { tabIndex: tab._tPos, @@ -805,7 +795,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6639,7 +6817,7 @@ +@@ -6639,7 +6814,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -814,7 +804,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6676,6 +6854,10 @@ +@@ -6676,6 +6851,10 @@ moveActionCallback(); @@ -825,7 +815,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7576,7 +7758,7 @@ +@@ -7576,7 +7755,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -834,7 +824,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad window.focus(); aEvent.preventDefault(); break; -@@ -7593,7 +7775,6 @@ +@@ -7593,7 +7772,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -842,7 +832,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad }); break; case "TabGroupCreateByUser": -@@ -8542,6 +8723,7 @@ +@@ -8542,6 +8720,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -850,7 +840,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9543,7 +9725,7 @@ var TabContextMenu = { +@@ -9543,7 +9722,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; @@ -859,11 +849,3 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..2ab3908f421d6bc126eb7a0f886646ad // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), -@@ -9863,6 +10045,7 @@ var TabContextMenu = { - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true); - gBrowser.removeTab(this.contextTab, { - animate: true, - ...gBrowser.TabMetrics.userTriggeredContext( diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index e8cf59114d..89ecc9bbb7 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -10,6 +10,7 @@ ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', + SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs', }); const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes @@ -86,7 +87,7 @@ class nsZenSessionManager { if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { // Don't save (or even collect) anything in permanent private // browsing mode - return Promise.resolve(); + return; } // Collect an initial snapshot of window data before we do the flush. const window = this.#topMostWindow; @@ -154,8 +155,11 @@ class nsZenSessionManager { aWindowData.groups = sidebar.groups; } - getNewWindowData(aWindows) { - let newWindow = { ...Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {}), ...this.#sidebar }; + getNewWindowData() { + lazy.SessionSaver.run(); + const state = lazy.SessionStore.getCurrentState(forceUpdateAllWindows); + const windows = state.windows || {}; + let newWindow = { ...Cu.cloneInto(windows[Object.keys(windows)[0]], {}), ...this.#sidebar }; return { windows: [newWindow] }; } } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 245819a91f..eb6be7034b 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -232,7 +232,6 @@ switch (behavior) { case 'close': { for (const tab of pinnedTabs) { - this._removePinnedAttributes(tab, true); gBrowser.removeTab(tab, { animate: true }); } break; From eefc8cb20c51baad30e526a454893770b67ef149 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Thu, 13 Nov 2025 17:03:27 +0100 Subject: [PATCH 16/54] feat: Clone the previous state, b=no-bug, c=no-component --- src/zen/sessionstore/ZenSessionManager.sys.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 89ecc9bbb7..2a229dacab 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -157,9 +157,9 @@ class nsZenSessionManager { getNewWindowData() { lazy.SessionSaver.run(); - const state = lazy.SessionStore.getCurrentState(forceUpdateAllWindows); + const state = lazy.SessionStore.getCurrentState(true); const windows = state.windows || {}; - let newWindow = { ...Cu.cloneInto(windows[Object.keys(windows)[0]], {}), ...this.#sidebar }; + let newWindow = Cu.cloneInto(windows[0], {}); return { windows: [newWindow] }; } } From c86875b7b07703ad93176b55854fb0e853f2b6e2 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Mon, 17 Nov 2025 13:36:57 +0100 Subject: [PATCH 17/54] feat: Move window sync to its own JS module, b=no-bug, c=workspaces --- src/browser/base/content/zen-assets.inc.xhtml | 1 - .../base/content/zen-assets.jar.inc.mn | 1 - .../sessionstore/SessionComponents.manifest | 5 + .../sessionstore/ZenSessionManager.sys.mjs | 16 +- src/zen/sessionstore/ZenWindowSync.sys.mjs | 23 ++ src/zen/sessionstore/moz.build | 1 + src/zen/workspaces/ZenWindowSyncing.mjs | 308 ------------------ 7 files changed, 38 insertions(+), 317 deletions(-) create mode 100644 src/zen/sessionstore/ZenWindowSync.sys.mjs delete mode 100644 src/zen/workspaces/ZenWindowSyncing.mjs diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index a61105b517..4d9cf90b3d 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -57,4 +57,3 @@ - diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index efee1ce484..d42187d8a0 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -41,7 +41,6 @@ content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs) content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.mjs) content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.mjs) - content/browser/zen-components/ZenWindowSyncing.mjs (../../zen/workspaces/ZenWindowSyncing.mjs) content/browser/zen-components/ZenWorkspaceCreation.mjs (../../zen/workspaces/ZenWorkspaceCreation.mjs) content/browser/zen-components/ZenWorkspacesStorage.mjs (../../zen/workspaces/ZenWorkspacesStorage.mjs) content/browser/zen-components/ZenWorkspacesSync.mjs (../../zen/workspaces/ZenWorkspacesSync.mjs) diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest index f8f08d1d79..9da9dc1054 100644 --- a/src/zen/sessionstore/SessionComponents.manifest +++ b/src/zen/sessionstore/SessionComponents.manifest @@ -4,3 +4,8 @@ # Browser global components initializing before UI startup category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init +category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init + +# App shutdown consumers +category browser-quit-application-granted resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.uninit +category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 2a229dacab..06478075e9 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -25,7 +25,15 @@ class nsZenSessionManager { // Called from SessionComponents.manifest on app-startup init() { - this.#initObservers(); + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + uninit() { + for (let topic of OBSERVING) { + Services.obs.removeObserver(this, topic); + } } async readFile() { @@ -38,12 +46,6 @@ class nsZenSessionManager { } } - #initObservers() { - for (let topic of OBSERVING) { - Services.obs.addObserver(this, topic); - } - } - get #sidebar() { return this.#file.sidebar; } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs new file mode 100644 index 0000000000..a96e096872 --- /dev/null +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -0,0 +1,23 @@ +// 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 OBSERVING = ['browser-window-before-show']; + +class nsZenWindowSync { + constructor() {} + + init() { + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + uninit() { + for (let topic of OBSERVING) { + Services.obs.removeObserver(this, topic); + } + } +} + +export const ZenWindowSync = new nsZenWindowSync(); diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build index af5a7dd3bb..902ae2a301 100644 --- a/src/zen/sessionstore/moz.build +++ b/src/zen/sessionstore/moz.build @@ -5,4 +5,5 @@ EXTRA_JS_MODULES.zen += [ "ZenSessionFile.sys.mjs", "ZenSessionManager.sys.mjs", + "ZenWindowSync.sys.mjs", ] diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs deleted file mode 100644 index 4c06ae0e06..0000000000 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ /dev/null @@ -1,308 +0,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 nsZenWorkspaceWindowSync extends nsZenMultiWindowFeature { - #ignoreNextEvents = false; - #waitForPromise = null; - - constructor() { - super(); - if (!window.closed) { - this.init(); - } - } - - async init() { - await gZenWorkspaces.promiseInitialized; - this.#makeSureAllTabsHaveIds(); - this.#setUpEventListeners(); - } - - #makeSureAllTabsHaveIds() { - const allTabs = gZenWorkspaces.allStoredTabs; - for (const tab of allTabs) { - if (!tab.hasAttribute('zen-sync-id') && !tab.hasAttribute('zen-empty-tab')) { - const tabId = gZenUIManager.generateUuidv4(); - tab.setAttribute('zen-sync-id', tabId); - } - } - } - - #setUpEventListeners() { - const kEvents = [ - 'TabClose', - 'TabOpen', - 'TabMove', - - 'TabPinned', - 'TabUnpinned', - - 'TabAddedToEssentials', - 'TabRemovedFromEssentials', - - 'TabHide', - 'TabShow', - - 'ZenTabIconChanged', - 'ZenTabLabelChanged', - - 'TabGroupCreate', - 'TabGroupRemoved', - 'TabGrouped', - 'TabUngrouped', - 'TabGroupMoved', - ]; - const eventListener = this.#handleEvent.bind(this); - for (const event of kEvents) { - window.addEventListener(event, eventListener); - } - - window.addEventListener('unload', () => { - for (const event of kEvents) { - window.removeEventListener(event, eventListener); - } - }); - } - - #handleEvent(event) { - this.#propagateToOtherWindows(event); - } - - async #propagateToOtherWindows(event) { - if (this.#ignoreNextEvents) { - return; - } - if (this.#waitForPromise) { - await this.#waitForPromise; - } - this.#waitForPromise = new Promise((resolve) => { - this.foreachWindowAsActive(async (browser) => { - if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) { - await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event); - } - }).then(() => { - resolve(); - }); - }); - } - - async onExternalTabEvent(event) { - this.#ignoreNextEvents = true; - switch (event.type) { - case 'TabClose': - this.#onTabClose(event); - break; - case 'TabOpen': - await this.#onTabOpen(event); - break; - case 'TabPinned': - this.#onTabPinned(event); - break; - case 'TabUnpinned': - this.#onTabUnpinned(event); - break; - case 'TabAddedToEssentials': - this.#onTabAddedToEssentials(event); - break; - case 'TabRemovedFromEssentials': - this.#onTabRemovedFromEssentials(event); - break; - case 'TabHide': - this.#onTabHide(event); - break; - case 'TabShow': - this.#onTabShow(event); - break; - case 'TabMove': - case 'TabGroupMoved': - this.#onTabMove(event); - break; - case 'ZenTabIconChanged': - this.#onTabIconChanged(event); - break; - case 'ZenTabLabelChanged': - this.#onTabLabelChanged(event); - break; - case 'TabGroupCreate': - this.#onTabGroupCreate(event); - break; - case 'TabGroupRemoved': - case 'TabGrouped': - case 'TabUngrouped': - // Tab grouping changes are automatically synced by Firefox - break; - default: - console.warn(`Unhandled event type: ${event.type}`); - break; - } - this.#ignoreNextEvents = false; - } - - #getTabId(tab) { - return tab.getAttribute('zen-sync-id'); - } - - #getTabWithId(tabId) { - for (const tab of gZenWorkspaces.allStoredTabs) { - if (this.#getTabId(tab) === tabId) { - return tab; - } - } - return null; - } - - #onTabClose(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToClose = this.#getTabWithId(tabId); - if (tabToClose) { - gBrowser.removeTab(tabToClose); - } - } - - #onTabPinned(event) { - const targetTab = event.target; - if (targetTab.hasAttribute('zen-essential')) { - return this.#onTabAddedToEssentials(event); - } - const tabId = this.#getTabId(targetTab); - const elementIndex = targetTab.elementIndex; - const tabToPin = this.#getTabWithId(tabId); - if (tabToPin) { - gBrowser.pinTab(tabToPin); - gBrowser.moveTabTo(tabToPin, { elementIndex, forceUngrouped: !!targetTab.group }); - } - } - - #onTabUnpinned(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToUnpin = this.#getTabWithId(tabId); - if (tabToUnpin) { - gBrowser.unpinTab(tabToUnpin); - } - } - - #onTabIconChanged(event) { - this.#updateTabIconAndLabel(event); - } - - #onTabLabelChanged(event) { - this.#updateTabIconAndLabel(event); - } - - #updateTabIconAndLabel(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToChange = this.#getTabWithId(tabId); - if (tabToChange && tabToChange.hasAttribute('pending')) { - gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); - gBrowser._setTabLabel(tabToChange, targetTab.label); - } - } - - #onTabAddedToEssentials(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToAdd = this.#getTabWithId(tabId); - if (tabToAdd) { - gZenPinnedTabManager.addToEssentials(tabToAdd); - } - } - - #onTabRemovedFromEssentials(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToRemove = this.#getTabWithId(tabId); - if (tabToRemove) { - gZenPinnedTabManager.removeFromEssentials(tabToRemove); - } - } - - #onTabHide(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToHide = this.#getTabWithId(tabId); - if (tabToHide) { - gBrowser.hideTab(tabToHide); - } - } - - #onTabShow(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToShow = this.#getTabWithId(tabId); - if (tabToShow) { - gBrowser.showTab(tabToShow); - } - } - - #onTabMove(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToMove = this.#getTabWithId(tabId); - const workspaceId = targetTab.getAttribute('zen-workspace-id'); - const isEssential = targetTab.hasAttribute('zen-essential'); - if (tabToMove) { - let tabSibling = targetTab.previousElementSibling; - let isFirst = false; - if (!tabSibling?.hasAttribute('zen-sync-id')) { - isFirst = true; - } - gBrowser.zenHandleTabMove(tabToMove, () => { - if (isFirst) { - let container; - if (isEssential) { - container = gZenWorkspaces.getEssentialsSection(tabToMove); - } else { - const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); - container = tabToMove.pinned - ? workspaceElement.pinnedTabsContainer - : workspaceElement.tabsContainer; - } - container.insertBefore(tabToMove, container.firstChild); - } else { - let relativeTab = gZenWorkspaces.allStoredTabs.find((tab) => { - return this.#getTabId(tab) === this.#getTabId(tabSibling); - }); - if (relativeTab) { - relativeTab.after(tabToMove); - } - } - }); - } - } - - async #onTabOpen(event) { - const targetTab = event.target; - const isPinned = targetTab.pinned; - const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); - if (!this.#getTabId(targetTab) && !targetTab.hasAttribute('zen-empty-tab')) { - const tabId = gZenUIManager.generateUuidv4(); - targetTab.setAttribute('zen-sync-id', tabId); - } - const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, { - createLazyBrowser: true, - essential: isEssential, - pinned: isPinned, - }); - if (!isEssential) { - gZenWorkspaces.moveTabToWorkspace( - duplicatedTab, - targetTab.getAttribute('zen-workspace-id') - ); - } - duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id')); - } - - #onTabGroupCreate(event) { - void event; - //const targetGroup = event.target; - //const isSplitView = targetGroup.classList.contains('zen-split-view'); - //const isFolder = targetGroup.isZenFolder; - } - } - - window.gZenWorkspaceWindowSync = new nsZenWorkspaceWindowSync(); -} From ce986beb2f851e26e6d7b2e19f557dd79406cb43 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Mon, 17 Nov 2025 18:51:14 +0100 Subject: [PATCH 18/54] feat: Run session saver before opening a new window, b=no-bug, c=no-component --- .../base/content/zen-assets.jar.inc.mn | 3 +- .../sessionstore/SessionStore-sys-mjs.patch | 33 +++++++++---------- .../sessionstore/ZenSessionManager.sys.mjs | 15 +++++---- .../ZenSessionStore.mjs | 0 src/zen/sessionstore/ZenWindowSync.sys.mjs | 2 ++ 5 files changed, 29 insertions(+), 24 deletions(-) rename src/zen/{common => sessionstore}/ZenSessionStore.mjs (100%) diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index d42187d8a0..9c1208ecdb 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -8,11 +8,12 @@ content/browser/zen-sets.js (../../zen/common/zen-sets.js) content/browser/ZenUIManager.mjs (../../zen/common/ZenUIManager.mjs) content/browser/zen-components/ZenCommonUtils.mjs (../../zen/common/ZenCommonUtils.mjs) - content/browser/zen-components/ZenSessionStore.mjs (../../zen/common/ZenSessionStore.mjs) content/browser/zen-components/ZenEmojisData.min.mjs (../../zen/common/emojis/ZenEmojisData.min.mjs) content/browser/zen-components/ZenEmojiPicker.mjs (../../zen/common/emojis/ZenEmojiPicker.mjs) content/browser/zen-components/ZenHasPolyfill.mjs (../../zen/common/ZenHasPolyfill.mjs) + content/browser/zen-components/ZenSessionStore.mjs (../../zen/sessionstore/ZenSessionStore.mjs) + * content/browser/zen-styles/zen-theme.css (../../zen/common/styles/zen-theme.css) content/browser/zen-styles/zen-buttons.css (../../zen/common/styles/zen-buttons.css) content/browser/zen-styles/zen-browser-ui.css (../../zen/common/styles/zen-browser-ui.css) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index c778b4fd59..324d80b997 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 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc3315aa98 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb90e3f1092 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -36,18 +36,17 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { +@@ -2215,6 +2219,9 @@ var SessionStoreInternal = { }); this._shouldRestoreLastSession = false; } + else if (!aInitialState && isRegularWindow) { -+ aInitialState = lazy.ZenSessionStore.getNewWindowData(); -+ this.restoreWindows(aWindow, aInitialState, {}); ++ lazy.ZenSessionStore.restoreNewWindow(aWindow, this); + } if (this._restoreLastWindow && aWindow.toolbar.visible) { // always reset (if not a popup window) -@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { +@@ -2384,11 +2391,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -59,7 +58,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { +@@ -3373,7 +3378,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -68,7 +67,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc return; } -@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { +@@ -4089,6 +4094,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -80,7 +79,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4535,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -88,7 +87,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5385,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -97,7 +96,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc removableTabs.push(tab); } } -@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5445,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -106,7 +105,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5636,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -120,7 +119,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5652,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -128,7 +127,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5665,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -137,7 +136,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { +@@ -5764,8 +5777,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -148,7 +147,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { +@@ -5808,6 +5821,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -157,7 +156,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { +@@ -6371,6 +6386,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -183,7 +182,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { +@@ -7289,7 +7323,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 06478075e9..914a3eb6d6 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -157,12 +157,15 @@ class nsZenSessionManager { aWindowData.groups = sidebar.groups; } - getNewWindowData() { - lazy.SessionSaver.run(); - const state = lazy.SessionStore.getCurrentState(true); - const windows = state.windows || {}; - let newWindow = Cu.cloneInto(windows[0], {}); - return { windows: [newWindow] }; + restoreNewWindow(aWindow, SessionStoreInternal) { + lazy.SessionSaver.run().then(() => { + const state = lazy.SessionStore.getCurrentState(true); + const windows = state.windows || {}; + let newWindow = Cu.cloneInto(windows[0], {}); + delete newWindow.selected; + const newState = { windows: [newWindow] }; + SessionStoreInternal.restoreWindows(aWindow, newState, {}); + }); } } diff --git a/src/zen/common/ZenSessionStore.mjs b/src/zen/sessionstore/ZenSessionStore.mjs similarity index 100% rename from src/zen/common/ZenSessionStore.mjs rename to src/zen/sessionstore/ZenSessionStore.mjs diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index a96e096872..21bc6c230a 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -18,6 +18,8 @@ class nsZenWindowSync { Services.obs.removeObserver(this, topic); } } + + observe(aSubject, aTopic) {} } export const ZenWindowSync = new nsZenWindowSync(); From 0adf260ddc12bd68fc4c675b9cbdd2df34c8233a Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Wed, 27 Aug 2025 23:25:07 +0200 Subject: [PATCH 19/54] feat: Full cross-window workspace syncing, b=no-bug, c=workspaces --- src/zen/workspaces/ZenWindowSyncing.mjs | 226 ++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 src/zen/workspaces/ZenWindowSyncing.mjs diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs new file mode 100644 index 0000000000..19fbf09f42 --- /dev/null +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -0,0 +1,226 @@ +// 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 nsZenWorkspaceWindowSync extends nsZenMultiWindowFeature { + #ignoreNextEvents = false; + #waitForPromise = null; + + constructor() { + super(); + if (!window.closed) { + this.init(); + } + } + + async init() { + await gZenWorkspaces.promiseInitialized; + this.#makeSureAllTabsHaveIds(); + this.#setUpEventListeners(); + } + + #makeSureAllTabsHaveIds() { + const allTabs = gZenWorkspaces.allStoredTabs; + for (const tab of allTabs) { + if (!tab.hasAttribute('zen-sync-id')) { + const tabId = gZenUIManager.generateUuidv4(); + tab.setAttribute('zen-sync-id', tabId); + } + } + } + + #setUpEventListeners() { + const kEvents = [ + 'TabClose', + 'TabOpen', + 'TabPinned', + 'TabUnpinned', + 'TabAddedToEssentials', + 'TabRemovedFromEssentials', + 'TabHide', + 'TabShow', + 'TabMove', + ]; + const eventListener = this.#handleEvent.bind(this); + for (const event of kEvents) { + window.addEventListener(event, eventListener); + } + + window.addEventListener('unload', () => { + for (const event of kEvents) { + window.removeEventListener(event, eventListener); + } + }); + } + + #handleEvent(event) { + this.#propagateToOtherWindows(event); + } + + async #propagateToOtherWindows(event) { + if (this.#ignoreNextEvents) { + return; + } + if (this.#waitForPromise) { + await this.#waitForPromise; + } + this.#waitForPromise = new Promise(async (resolve) => { + await this.foreachWindowAsActive(async (browser) => { + if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) { + await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event); + } + }); + resolve(); + }); + } + + async onExternalTabEvent(event) { + this.#ignoreNextEvents = true; + switch (event.type) { + case 'TabClose': + this.#onTabClose(event); + break; + case 'TabOpen': + await this.#onTabOpen(event); + break; + case 'TabPinned': + this.#onTabPinned(event); + break; + case 'TabUnpinned': + this.#onTabUnpinned(event); + break; + case 'TabAddedToEssentials': + this.#onTabAddedToEssentials(event); + break; + case 'TabRemovedFromEssentials': + this.#onTabRemovedFromEssentials(event); + break; + case 'TabHide': + this.#onTabHide(event); + break; + case 'TabShow': + this.#onTabShow(event); + break; + case 'TabMove': + this.#onTabMove(event); + break; + default: + console.warn(`Unhandled event type: ${event.type}`); + break; + } + this.#ignoreNextEvents = false; + } + + #getTabId(tab) { + return tab.getAttribute('zen-sync-id'); + } + + #getTabWithId(tabId) { + for (const tab of gZenWorkspaces.allStoredTabs) { + if (this.#getTabId(tab) === tabId) { + return tab; + } + } + return null; + } + + #onTabClose(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToClose = this.#getTabWithId(tabId); + if (tabToClose) { + gBrowser.removeTab(tabToClose); + } + } + + #onTabPinned(event) { + const targetTab = event.target; + if (targetTab.hasAttribute('zen-essential')) { + return this.#onTabAddedToEssentials(event); + } + const tabId = this.#getTabId(targetTab); + const elementIndex = targetTab.elementIndex; + const tabToPin = this.#getTabWithId(tabId); + if (tabToPin) { + gBrowser.pinTab(tabToPin); + gBrowser.moveTabTo(tabToPin, { elementIndex, forceUngrouped: !!targetTab.group }); + } + } + + #onTabUnpinned(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToUnpin = this.#getTabWithId(tabId); + if (tabToUnpin) { + gBrowser.unpinTab(tabToUnpin); + } + } + + #onTabAddedToEssentials(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToAdd = this.#getTabWithId(tabId); + if (tabToAdd) { + gZenPinnedTabManager.addToEssentials(tabToAdd); + } + } + + #onTabRemovedFromEssentials(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToRemove = this.#getTabWithId(tabId); + if (tabToRemove) { + gZenPinnedTabManager.removeFromEssentials(tabToRemove); + } + } + + #onTabHide(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToHide = this.#getTabWithId(tabId); + if (tabToHide) { + gBrowser.hideTab(tabToHide); + } + } + + #onTabShow(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const tabToShow = this.#getTabWithId(tabId); + if (tabToShow) { + gBrowser.showTab(tabToShow); + } + } + + #onTabMove(event) { + const targetTab = event.target; + const tabId = this.#getTabId(targetTab); + const elementIndex = targetTab.elementIndex; + const tabToMove = this.#getTabWithId(tabId); + if (tabToMove) { + gBrowser.moveTabTo(tabToMove, { elementIndex, forceUngrouped: !!targetTab.group }); + } + } + + async #onTabOpen(event) { + await new Promise((resolve) => { + const targetTab = event.target; + const isPinned = targetTab.pinned; + const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); + const elementIndex = targetTab.elementIndex; + + const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0, true); + if (isEssential) { + gZenPinnedTabManager.addToEssentials(duplicatedTab); + } else if (isPinned) { + gBrowser.pinTab(duplicatedTab); + } + + gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); + resolve(); + }); + } + } + + window.gZenWorkspaceWindowSync = new nsZenWorkspaceWindowSync(); +} From c3b9c3c526cec0aab20e396a6c56f31f66caa813 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Mon, 1 Sep 2025 16:09:59 +0200 Subject: [PATCH 20/54] feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces --- src/zen/tabs/ZenPinnedTabManager.mjs | 2 ++ src/zen/workspaces/ZenWindowSyncing.mjs | 30 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 8598858a4d..2fbfdc3473 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -103,6 +103,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } onTabIconChanged(tab, url = null) { + tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); const iconUrl = url ?? tab.iconImage.src; if (!iconUrl && tab.hasAttribute('zen-pin-id')) { try { @@ -1564,6 +1565,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } async onTabLabelChanged(tab) { + tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } })); if (!this._pinsCache) { return; } diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index 19fbf09f42..bcb63d8f42 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -40,6 +40,8 @@ 'TabHide', 'TabShow', 'TabMove', + 'ZenTabIconChanged', + 'ZenTabLabelChanged', ]; const eventListener = this.#handleEvent.bind(this); for (const event of kEvents) { @@ -104,6 +106,12 @@ case 'TabMove': this.#onTabMove(event); break; + case 'ZenTabIconChanged': + this.#onTabIconChanged(event); + break; + case 'ZenTabLabelChanged': + this.#onTabLabelChanged(event); + break; default: console.warn(`Unhandled event type: ${event.type}`); break; @@ -156,6 +164,26 @@ } } + #onTabIconChanged(event) { + this.#updateTabIconAndLabel(event); + } + + #onTabLabelChanged(event) { + this.#updateTabIconAndLabel(event); + } + + #updateTabIconAndLabel(event) { + const targetTab = event.target; + if (targetTab.hasAttribute("pending")) { + const tabId = this.#getTabId(targetTab); + const tabToChange = this.#getTabWithId(tabId); + if (tabToChange) { + gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); + gBrowser._setTabLabel(tabToChange, targetTab.label); + } + } + } + #onTabAddedToEssentials(event) { const targetTab = event.target; const tabId = this.#getTabId(targetTab); @@ -209,7 +237,7 @@ const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); const elementIndex = targetTab.elementIndex; - const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0, true); + const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0); if (isEssential) { gZenPinnedTabManager.addToEssentials(duplicatedTab); } else if (isPinned) { From acb37089360ecd443b44607bcfe0ce8e30f152cb Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Tue, 2 Sep 2025 16:21:04 +0200 Subject: [PATCH 21/54] feat: Dont session duplicate the tabs, b=no-bug, c=workspaces --- src/zen/workspaces/ZenWindowSyncing.mjs | 55 +++++++++++++------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index bcb63d8f42..da5de683bd 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -66,13 +66,14 @@ if (this.#waitForPromise) { await this.#waitForPromise; } - this.#waitForPromise = new Promise(async (resolve) => { - await this.foreachWindowAsActive(async (browser) => { + this.#waitForPromise = new Promise((resolve) => { + this.foreachWindowAsActive(async (browser) => { if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) { await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event); } + }).then(() => { + resolve(); }); - resolve(); }); } @@ -174,13 +175,11 @@ #updateTabIconAndLabel(event) { const targetTab = event.target; - if (targetTab.hasAttribute("pending")) { - const tabId = this.#getTabId(targetTab); - const tabToChange = this.#getTabWithId(tabId); - if (tabToChange) { - gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); - gBrowser._setTabLabel(tabToChange, targetTab.label); - } + const tabId = this.#getTabId(targetTab); + const tabToChange = this.#getTabWithId(tabId); + if (tabToChange && tabToChange.hasAttribute('pending')) { + gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); + gBrowser._setTabLabel(tabToChange, targetTab.label); } } @@ -223,30 +222,34 @@ #onTabMove(event) { const targetTab = event.target; const tabId = this.#getTabId(targetTab); - const elementIndex = targetTab.elementIndex; + const tabIndex = targetTab._pPos; const tabToMove = this.#getTabWithId(tabId); if (tabToMove) { - gBrowser.moveTabTo(tabToMove, { elementIndex, forceUngrouped: !!targetTab.group }); + gBrowser.moveTabTo(tabToMove, { tabIndex, forceUngrouped: !!targetTab.group }); } } async #onTabOpen(event) { - await new Promise((resolve) => { - const targetTab = event.target; - const isPinned = targetTab.pinned; - const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); - const elementIndex = targetTab.elementIndex; - - const duplicatedTab = SessionStore.duplicateTab(window, targetTab, 0); - if (isEssential) { - gZenPinnedTabManager.addToEssentials(duplicatedTab); - } else if (isPinned) { - gBrowser.pinTab(duplicatedTab); - } + const targetTab = event.target; + const isPinned = targetTab.pinned; + const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); + const elementIndex = targetTab.elementIndex; - gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); - resolve(); + const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, { + createLazyBrowser: true, }); + + duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); + duplicatedTab.setAttribute('zen-tab-id', targetTab.getAttribute('zen-tab-id')); + duplicatedTab.setAttribute('zen-workspace-id', targetTab.getAttribute('zen-workspace-id')); + + if (isEssential) { + gZenPinnedTabManager.addToEssentials(duplicatedTab); + } else if (isPinned) { + gBrowser.pinTab(duplicatedTab); + } + + gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); } } From 1d26040fc61f705cf4e818707f7ab3ab4edd3650 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 28 Sep 2025 23:45:13 +0200 Subject: [PATCH 22/54] feat: Start on new session restore, b=no-bug, c=no-component --- prefs/firefox/browser.yaml | 3 +- src/zen/sessionstore/ZenSessionFile.sys.mjs | 27 ++++++++++ .../sessionstore/ZenSessionManager.sys.mjs | 50 +++++++++++++++++++ src/zen/sessionstore/ZenSessionWindow.sys.mjs | 35 +++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/zen/sessionstore/ZenSessionFile.sys.mjs create mode 100644 src/zen/sessionstore/ZenSessionManager.sys.mjs create mode 100644 src/zen/sessionstore/ZenSessionWindow.sys.mjs diff --git a/prefs/firefox/browser.yaml b/prefs/firefox/browser.yaml index 7c8c8dfcd1..7eadee8231 100644 --- a/prefs/firefox/browser.yaml +++ b/prefs/firefox/browser.yaml @@ -3,7 +3,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. - name: browser.startup.page - value: 3 + value: 0 + locked: true - name: browser.sessionstore.restore_pinned_tabs_on_demand value: true diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs new file mode 100644 index 0000000000..c50960ae10 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionFile.sys.mjs @@ -0,0 +1,27 @@ +// 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 FILE_NAME = 'zen-sessions.jsonlz4'; + +export class nsZenSessionFile { + #path; + + #windows; + + constructor() { + this.#path = PathUtils.join(profileDir, FILE_NAME); + } + + async read() { + try { + return await IOUtils.readJSON(this.#path, { compress: true }); + } catch (e) { + return {}; + } + } + + async write(data) { + await IOUtils.writeJSON(this.#path, data, { compress: true }); + } +} diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs new file mode 100644 index 0000000000..68aa829e40 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -0,0 +1,50 @@ +// 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 { + cancelIdleCallback, + clearTimeout, + requestIdleCallback, + setTimeout, +} from 'resource://gre/modules/Timer.sys.mjs'; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs', + PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', + RunState: 'resource:///modules/sessionstore/RunState.sys.mjs', +}); + +class nsZenSessionManager { + #file; + + constructor() { + this.#file = null; + } + + get file() { + if (!this.#file) { + this.#file = lazy.ZenSessionFile; + } + return this.#file; + } + + /** + * Saves the current session state. Collects data and writes to disk. + * + * @param forceUpdateAllWindows (optional) + * Forces us to recollect data for all windows and will bypass and + * update the corresponding caches. + */ + saveState(forceUpdateAllWindows = false) { + if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Don't save (or even collect) anything in permanent private + // browsing mode + return Promise.resolve(); + } + } +} + +export const ZenSessionStore = new nsZenSessionManager(); diff --git a/src/zen/sessionstore/ZenSessionWindow.sys.mjs b/src/zen/sessionstore/ZenSessionWindow.sys.mjs new file mode 100644 index 0000000000..4600702349 --- /dev/null +++ b/src/zen/sessionstore/ZenSessionWindow.sys.mjs @@ -0,0 +1,35 @@ +// 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/. + +export class ZenSessionWindow { + #id; + #selectedWorkspace; + #selectedTab; + + constructor(id) { + this.#id = id; + this.#selectedWorkspace = null; + this.#selectedTab = null; + } + + get id() { + return this.#id; + } + + get selectedWorkspace() { + return this.#selectedWorkspace; + } + + set selectedWorkspace(workspace) { + this.#selectedWorkspace = workspace; + } + + get selectedTab() { + return this.#selectedTab; + } + + set selectedTab(tab) { + this.#selectedTab = tab; + } +} From c60990c283844f621c34717d763e4ab2ed595278 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 27 Sep 2025 17:54:28 +0200 Subject: [PATCH 23/54] feat: Properly handle tab moves, b=no-bug, c=workspaces --- src/zen/workspaces/ZenWindowSyncing.mjs | 83 ++++++++++++++++++++----- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index da5de683bd..857f9fc9cf 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -22,7 +22,7 @@ #makeSureAllTabsHaveIds() { const allTabs = gZenWorkspaces.allStoredTabs; for (const tab of allTabs) { - if (!tab.hasAttribute('zen-sync-id')) { + if (!tab.hasAttribute('zen-sync-id') && !tab.hasAttribute('zen-empty-tab')) { const tabId = gZenUIManager.generateUuidv4(); tab.setAttribute('zen-sync-id', tabId); } @@ -33,15 +33,25 @@ const kEvents = [ 'TabClose', 'TabOpen', + 'TabMove', + 'TabPinned', 'TabUnpinned', + 'TabAddedToEssentials', 'TabRemovedFromEssentials', + 'TabHide', 'TabShow', - 'TabMove', + 'ZenTabIconChanged', 'ZenTabLabelChanged', + + 'TabGroupCreate', + 'TabGroupRemoved', + 'TabGrouped', + 'TabUngrouped', + 'TabGroupMoved', ]; const eventListener = this.#handleEvent.bind(this); for (const event of kEvents) { @@ -105,6 +115,7 @@ this.#onTabShow(event); break; case 'TabMove': + case 'TabGroupMoved': this.#onTabMove(event); break; case 'ZenTabIconChanged': @@ -113,6 +124,14 @@ case 'ZenTabLabelChanged': this.#onTabLabelChanged(event); break; + case 'TabGroupCreate': + this.#onTabGroupCreate(event); + break; + case 'TabGroupRemoved': + case 'TabGrouped': + case 'TabUngrouped': + // Tab grouping changes are automatically synced by Firefox + break; default: console.warn(`Unhandled event type: ${event.type}`); break; @@ -222,10 +241,36 @@ #onTabMove(event) { const targetTab = event.target; const tabId = this.#getTabId(targetTab); - const tabIndex = targetTab._pPos; const tabToMove = this.#getTabWithId(tabId); + const workspaceId = targetTab.getAttribute('zen-workspace-id'); + const isEssential = targetTab.hasAttribute('zen-essential'); if (tabToMove) { - gBrowser.moveTabTo(tabToMove, { tabIndex, forceUngrouped: !!targetTab.group }); + let tabSibling = targetTab.previousElementSibling; + let isFirst = false; + if (!tabSibling?.hasAttribute('zen-sync-id')) { + isFirst = true; + } + gBrowser.zenHandleTabMove(tabToMove, () => { + if (isFirst) { + let container; + if (isEssential) { + container = gZenWorkspaces.getEssentialsSection(tabToMove); + } else { + const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); + container = tabToMove.pinned + ? workspaceElement.pinnedTabsContainer + : workspaceElement.tabsContainer; + } + container.insertBefore(tabToMove, container.firstChild); + } else { + let relativeTab = gZenWorkspaces.allStoredTabs.find((tab) => { + return this.#getTabId(tab) === this.#getTabId(tabSibling); + }); + if (relativeTab) { + relativeTab.after(tabToMove); + } + } + }); } } @@ -233,23 +278,29 @@ const targetTab = event.target; const isPinned = targetTab.pinned; const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); - const elementIndex = targetTab.elementIndex; - + if (!this.#getTabId(targetTab) && !targetTab.hasAttribute('zen-empty-tab')) { + const tabId = gZenUIManager.generateUuidv4(); + targetTab.setAttribute('zen-sync-id', tabId); + } const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, { createLazyBrowser: true, + essential: isEssential, + pinned: isPinned, }); - - duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); - duplicatedTab.setAttribute('zen-tab-id', targetTab.getAttribute('zen-tab-id')); - duplicatedTab.setAttribute('zen-workspace-id', targetTab.getAttribute('zen-workspace-id')); - - if (isEssential) { - gZenPinnedTabManager.addToEssentials(duplicatedTab); - } else if (isPinned) { - gBrowser.pinTab(duplicatedTab); + if (!isEssential) { + gZenWorkspaces.moveTabToWorkspace( + duplicatedTab, + targetTab.getAttribute('zen-workspace-id') + ); } + duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); + duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id')); + } - gBrowser.moveTabTo(duplicatedTab, { elementIndex, forceUngrouped: !!targetTab.group }); + #onTabGroupCreate(event) { + const targetGroup = event.target; + const isSplitView = targetGroup.classList.contains('zen-split-view'); + const isFolder = targetGroup.isZenFolder; } } From 435762c682d64814f29722151fae28c6cbbf282b Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:52:53 +0200 Subject: [PATCH 24/54] Discard changes to prefs/browser.yaml --- prefs/firefox/browser.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/prefs/firefox/browser.yaml b/prefs/firefox/browser.yaml index 7eadee8231..7c8c8dfcd1 100644 --- a/prefs/firefox/browser.yaml +++ b/prefs/firefox/browser.yaml @@ -3,8 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. - name: browser.startup.page - value: 0 - locked: true + value: 3 - name: browser.sessionstore.restore_pinned_tabs_on_demand value: true From 34c725aad0b8435a8dd548eb2e9b6e249b7832a5 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Wed, 29 Oct 2025 23:57:20 +0100 Subject: [PATCH 25/54] feat: Start doing out own session restore, b=no-bug, c=folders, tabs --- .../sessionstore/SessionFile-sys-mjs.patch | 21 ++ .../sessionstore/SessionStartup-sys-mjs.patch | 21 ++ .../sessionstore/SessionStore-sys-mjs.patch | 51 +++-- src/zen/ZenComponents.manifest | 1 + src/zen/folders/ZenFolder.mjs | 21 ++ src/zen/moz.build | 1 + .../sessionstore/SessionComponents.manifest | 6 + src/zen/sessionstore/ZenSessionFile.sys.mjs | 39 ++-- .../sessionstore/ZenSessionManager.sys.mjs | 137 ++++++++++++- src/zen/sessionstore/ZenSessionWindow.sys.mjs | 35 ---- src/zen/sessionstore/moz.build | 8 + src/zen/tabs/ZenPinnedTabManager.mjs | 191 +----------------- src/zen/zen.globals.js | 1 - 13 files changed, 270 insertions(+), 263 deletions(-) create mode 100644 src/browser/components/sessionstore/SessionFile-sys-mjs.patch create mode 100644 src/browser/components/sessionstore/SessionStartup-sys-mjs.patch create mode 100644 src/zen/sessionstore/SessionComponents.manifest delete mode 100644 src/zen/sessionstore/ZenSessionWindow.sys.mjs create mode 100644 src/zen/sessionstore/moz.build diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch new file mode 100644 index 0000000000..04aa6f0cac --- /dev/null +++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs +index 157c55ab24a418b56690d2e26320582909b919e4..14755f57dc450583e69eee94eb11f16980d5e5cb 100644 +--- a/browser/components/sessionstore/SessionFile.sys.mjs ++++ b/browser/components/sessionstore/SessionFile.sys.mjs +@@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + RunState: "resource:///modules/sessionstore/RunState.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID"; +@@ -364,7 +365,7 @@ var SessionFileInternal = { + this._readOrigin = result.origin; + + result.noFilesFound = noFilesFound; +- ++ await lazy.ZenSessionStore.readFile(); + return result; + }, + diff --git a/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch new file mode 100644 index 0000000000..b106193cf3 --- /dev/null +++ b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs +index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644 +--- a/browser/components/sessionstore/SessionStartup.sys.mjs ++++ b/browser/components/sessionstore/SessionStartup.sys.mjs +@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + StartupPerformance: + "resource:///modules/sessionstore/StartupPerformance.sys.mjs", + sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const STATE_RUNNING_STR = "running"; +@@ -179,6 +180,8 @@ export var SessionStartup = { + this._initialState = parsed; + } + ++ lazy.ZenSessionStore.onFileRead(this._initialState); ++ + if (this._initialState == null) { + // No valid session found. + this._sessionType = this.NO_SESSION; diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 5cf576c24d..1a0ebb806e 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 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0eeba2381c 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa477fa68794 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -11,7 +11,15 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -1911,6 +1913,8 @@ var SessionStoreInternal = { +@@ -195,6 +197,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { +@@ -1904,6 +1907,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -20,7 +28,18 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2151,7 +2155,6 @@ var SessionStoreInternal = { +@@ -2041,6 +2046,10 @@ var SessionStoreInternal = { + // A regular window is not a private window, taskbar tab window, or popup window + let isRegularWindow = + !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; ++ if (!aInitialState && isRegularWindow) { ++ aInitialState = ZenSessionStore.getNewWindowData(this._windows); ++ this.restoreWindows(aWindow, aInitialState, {}); ++ } + + // perform additional initialization when the first window is loading + if (lazy.RunState.isStopped) { +@@ -2139,7 +2148,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -28,7 +47,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2384,11 +2387,9 @@ var SessionStoreInternal = { +@@ -2372,11 +2380,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -40,7 +59,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3374,7 @@ var SessionStoreInternal = { +@@ -3361,7 +3367,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -49,7 +68,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e return; } -@@ -4089,6 +4090,12 @@ var SessionStoreInternal = { +@@ -4073,6 +4079,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -62,7 +81,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4532,7 @@ var SessionStoreInternal = { +@@ -4509,6 +4520,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -70,7 +89,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5382,7 @@ var SessionStoreInternal = { +@@ -5358,7 +5370,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -79,7 +98,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e removableTabs.push(tab); } } -@@ -5434,7 +5442,7 @@ var SessionStoreInternal = { +@@ -5418,7 +5430,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -88,7 +107,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5633,12 @@ var SessionStoreInternal = { +@@ -5609,11 +5621,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -102,7 +121,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5649,7 @@ var SessionStoreInternal = { +@@ -5624,6 +5637,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -110,7 +129,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5662,7 @@ var SessionStoreInternal = { +@@ -5636,7 +5650,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -119,7 +138,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5774,8 @@ var SessionStoreInternal = { +@@ -5748,8 +5762,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -130,7 +149,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5818,8 @@ var SessionStoreInternal = { +@@ -5792,6 +5806,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -139,7 +158,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6383,25 @@ var SessionStoreInternal = { +@@ -6348,6 +6364,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -165,7 +184,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7320,7 @@ var SessionStoreInternal = { +@@ -7263,7 +7298,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/zen/ZenComponents.manifest b/src/zen/ZenComponents.manifest index 74c0232b85..0f38828a14 100644 --- a/src/zen/ZenComponents.manifest +++ b/src/zen/ZenComponents.manifest @@ -13,3 +13,4 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} #include common/Components.manifest +#include sessionstore/SessionComponents.manifest diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index 2209e67138..16746cd26f 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -157,6 +157,17 @@ class ZenFolder extends MozTabbrowserTabGroup { } } } + async unpackTabs() { + this.collapsed = false; + for (let tab of this.allItems.reverse()) { + tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab; + if (tab.hasAttribute('zen-empty-tab')) { + gBrowser.removeTab(tab); + } else { + gBrowser.ungroupTab(tab); + } + } + } async delete() { for (const tab of this.allItemsRecursive) { @@ -169,6 +180,16 @@ class ZenFolder extends MozTabbrowserTabGroup { } await gBrowser.removeTabGroup(this, { isUserTriggered: true }); } + async delete() { + for (const tab of this.allItemsRecursive) { + if (tab.hasAttribute('zen-empty-tab')) { + // Manually remove the empty tabs as removeTabs() inside removeTabGroup + // does ignore them. + gBrowser.removeTab(tab); + } + } + await gBrowser.removeTabGroup(this, { isUserTriggered: true }); + } get allItemsRecursive() { const items = []; diff --git a/src/zen/moz.build b/src/zen/moz.build index 21915a69ab..eb681597f0 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -13,4 +13,5 @@ DIRS += [ "tests", "urlbar", "toolkit", + "sessionstore", ] diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest new file mode 100644 index 0000000000..f8f08d1d79 --- /dev/null +++ b/src/zen/sessionstore/SessionComponents.manifest @@ -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/. + +# Browser global components initializing before UI startup +category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs index c50960ae10..526f7ab047 100644 --- a/src/zen/sessionstore/ZenSessionFile.sys.mjs +++ b/src/zen/sessionstore/ZenSessionFile.sys.mjs @@ -2,26 +2,39 @@ // 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 FILE_NAME = 'zen-sessions.jsonlz4'; +// Note that changing this hidden pref will make the previous session file +// unused, causing a new session file to be created on next write. +const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true); -export class nsZenSessionFile { - #path; - - #windows; +const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json'; - constructor() { - this.#path = PathUtils.join(profileDir, FILE_NAME); - } +export class nsZenSessionFile { + #path = PathUtils.join(PathUtils.profileDir, FILE_NAME); + #sidebar = []; async read() { try { - return await IOUtils.readJSON(this.#path, { compress: true }); - } catch (e) { - return {}; + const data = await IOUtils.readJSON(this.#path, { compress: SHOULD_COMPRESS_FILE }); + this.#sidebar = data.sidebar || []; + } catch { + // File doesn't exist yet, that's fine. } } - async write(data) { - await IOUtils.writeJSON(this.#path, data, { compress: true }); + get sidebar() { + return this.#sidebar; + } + + set sidebar(data) { + this.#sidebar = data; + } + + async #write(data) { + await IOUtils.writeJSON(this.#path, data, { compress: SHOULD_COMPRESS_FILE }); + } + + async store() { + const data = { sidebar: this.#sidebar }; + await this.#write(data); } } diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 68aa829e40..5120319bcd 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -12,23 +12,74 @@ import { const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs', + nsZenSessionFile: 'resource:///modules/zen/ZenSessionFile.sys.mjs', PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', - RunState: 'resource:///modules/sessionstore/RunState.sys.mjs', + BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', + TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', + SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', }); +const TAB_CUSTOM_VALUES = new WeakMap(); +const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes +const OBSERVING = ['sessionstore-state-write-complete', 'browser-window-before-show']; + class nsZenSessionManager { #file; constructor() { - this.#file = null; + this.#file = new lazy.nsZenSessionFile(); + } + + // Called from SessionComponents.manifest on app-startup + init() { + this.#initObservers(); + } + + async readFile() { + await this.#file.read(); } - get file() { - if (!this.#file) { - this.#file = lazy.ZenSessionFile; + onFileRead(initialState) { + for (const winData of initialState.windows || []) { + this.restoreWindowData(winData); } - return this.#file; + } + + #initObservers() { + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + get #sidebar() { + return this.#file.sidebar; + } + + set #sidebar(data) { + this.#file.sidebar = data; + } + + observe(aSubject, aTopic) { + switch (aTopic) { + case 'sessionstore-state-write-complete': { + this.#saveState(true); + break; + } + case 'browser-window-before-show': // catch new windows + this.#onBeforeBrowserWindowShown(aSubject); + break; + default: + break; + } + } + + /** Handles the browser-window-before-show observer notification. */ + #onBeforeBrowserWindowShown(aWindow) { + // TODO: Initialize new window + } + + get #topMostWindow() { + return lazy.BrowserWindowTracker.getTopWindow(); } /** @@ -38,12 +89,82 @@ class nsZenSessionManager { * Forces us to recollect data for all windows and will bypass and * update the corresponding caches. */ - saveState(forceUpdateAllWindows = false) { + async #saveState(forceUpdateAllWindows = false) { if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { // Don't save (or even collect) anything in permanent private // browsing mode return Promise.resolve(); } + // Collect an initial snapshot of window data before we do the flush. + const window = this.#topMostWindow; + // We don't have any normal windows or no windows at all + if (!window) { + return; + } + this.#collectWindowData(this.#topMostWindow, forceUpdateAllWindows); + this.#file.store(); + } + + /** + * Collects session data for a given window. + * + * @param window + * The window to collect data for. + * @param forceUpdate + * Forces us to recollect data and will bypass and update the + * corresponding caches. + * @param zIndex + * The z-index of the window. + */ + #collectWindowData(window, forceUpdate = false, zIndex = 0) { + let sidebarData = this.#sidebar; + if (!sidebarData || forceUpdate) { + sidebarData = {}; + } + + // If it hasn't changed, don't update. + if ( + !forceUpdate && + sidebarData.lastCollected && + Date.now() - sidebarData.lastCollected < LAZY_COLLECT_THRESHOLD + ) { + return; + } + sidebarData.lastCollected = Date.now(); + this.#collectTabsData(window, sidebarData); + this.#sidebar = sidebarData; + } + + /** + * Collects session data for all tabs in a given window. + * + * @param aWindow + * The window to collect tab data for. + * @param winData + * The window data object to populate. + */ + #collectTabsData(aWindow, sidebarData) { + const winData = lazy.SessionStore.getWindowState(aWindow).windows[0]; + if (!winData) return; + sidebarData.tabs = winData.tabs; + sidebarData.folders = winData.folders; + sidebarData.splitViewData = winData.splitViewData; + sidebarData.groups = winData.groups; + } + + restoreWindowData(aWindowData) { + const sidebar = this.#file.sidebar; + if (!sidebar) { + return; + } + aWindowData.tabs = sidebar.tabs || []; + aWindowData.splitViewData = sidebar.splitViewData; + aWindowData.folders = sidebar.folders; + aWindowData.groups = sidebar.groups; + } + + getNewWindowData(aWindows) { + return { windows: [Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {})] }; } } diff --git a/src/zen/sessionstore/ZenSessionWindow.sys.mjs b/src/zen/sessionstore/ZenSessionWindow.sys.mjs deleted file mode 100644 index 4600702349..0000000000 --- a/src/zen/sessionstore/ZenSessionWindow.sys.mjs +++ /dev/null @@ -1,35 +0,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/. - -export class ZenSessionWindow { - #id; - #selectedWorkspace; - #selectedTab; - - constructor(id) { - this.#id = id; - this.#selectedWorkspace = null; - this.#selectedTab = null; - } - - get id() { - return this.#id; - } - - get selectedWorkspace() { - return this.#selectedWorkspace; - } - - set selectedWorkspace(workspace) { - this.#selectedWorkspace = workspace; - } - - get selectedTab() { - return this.#selectedTab; - } - - set selectedTab(tab) { - this.#selectedTab = tab; - } -} diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build new file mode 100644 index 0000000000..af5a7dd3bb --- /dev/null +++ b/src/zen/sessionstore/moz.build @@ -0,0 +1,8 @@ +# 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/. + +EXTRA_JS_MODULES.zen += [ + "ZenSessionFile.sys.mjs", + "ZenSessionManager.sys.mjs", +] diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 2fbfdc3473..62350e00d1 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -212,199 +212,10 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { this.hasInitializedPins = true; } - async #initializePinnedTabs(init = false) { - const pins = this._pinsCache; - if (!pins?.length || !init) { + async #initializePinnedTabs(init = false) { this.#finishedInitializingPins(); - return; } - const pinnedTabsByUUID = new Map(); - const pinsToCreate = new Set(pins.map((p) => p.uuid)); - - // First pass: identify existing tabs and remove those without pins - for (let tab of gZenWorkspaces.allStoredTabs) { - const pinId = tab.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - - if (pinsToCreate.has(pinId)) { - // This is a valid pinned tab that matches a pin - pinnedTabsByUUID.set(pinId, tab); - pinsToCreate.delete(pinId); - - if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) { - this._resetTabToStoredState(tab); - } - } else { - // This is a pinned tab that no longer has a corresponding pin - gBrowser.removeTab(tab); - } - } - - for (const group of gZenWorkspaces.allTabGroups) { - const pinId = group.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - if (pinsToCreate.has(pinId)) { - // This is a valid pinned group that matches a pin - pinsToCreate.delete(pinId); - } - } - - // Second pass: For every existing tab, update its label - // and set 'zen-has-static-label' attribute if it's been edited - for (let pin of pins) { - const tab = pinnedTabsByUUID.get(pin.uuid); - if (!tab) { - continue; - } - - tab.removeAttribute('zen-has-static-label'); // So we can set it again - if (pin.title && pin.editedTitle) { - gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true }); - tab.setAttribute('zen-has-static-label', 'true'); - } - } - - const groups = new Map(); - const pendingTabsInsideGroups = {}; - - // Third pass: create new tabs for pins that don't have tabs - for (let pin of pins) { - try { - if (!pinsToCreate.has(pin.uuid)) { - continue; // Skip pins that already have tabs - } - - if (pin.isGroup) { - const tabs = []; - // If there's already existing tabs, let's use them - for (const [uuid, existingTab] of pinnedTabsByUUID) { - const pinObject = this._pinsCache.find((p) => p.uuid === uuid); - if (pinObject && pinObject.parentUuid === pin.uuid) { - tabs.push(existingTab); - } - } - // We still need to iterate through pending tabs since the database - // query doesn't guarantee the order of insertion - for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) { - if (parentUuid === pin.uuid) { - tabs.push(...folderTabs); - } - } - const group = gZenFolders.createFolder(tabs, { - label: pin.title, - collapsed: pin.isFolderCollapsed, - initialPinId: pin.uuid, - workspaceId: pin.workspaceUuid, - insertAfter: - groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || null, - }); - gZenFolders.setFolderUserIcon(group, pin.folderIcon); - groups.set(pin.uuid, group); - continue; - } - - let params = { - skipAnimation: true, - allowInheritPrincipal: false, - skipBackgroundNotify: true, - userContextId: pin.containerTabId || 0, - createLazyBrowser: true, - skipLoad: true, - noInitialLabel: false, - }; - - // Create and initialize the tab - let newTab = gBrowser.addTrustedTab(pin.url, params); - newTab.setAttribute('zenDefaultUserContextId', true); - - // Set initial label/title - if (pin.title) { - gBrowser.setInitialTabTitle(newTab, pin.title); - } - - // Set the icon if we have it cached - if (pin.iconUrl) { - gBrowser.setIcon(newTab, pin.iconUrl); - } - - newTab.setAttribute('zen-pin-id', pin.uuid); - - if (pin.workspaceUuid) { - newTab.setAttribute('zen-workspace-id', pin.workspaceUuid); - } - - if (pin.isEssential) { - newTab.setAttribute('zen-essential', 'true'); - } - - if (pin.editedTitle) { - newTab.setAttribute('zen-has-static-label', 'true'); - } - - // Initialize browser state if needed - if (!newTab.linkedBrowser._remoteAutoRemoved) { - let state = { - entries: [ - { - url: pin.url, - title: pin.title, - triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, - }, - ], - userContextId: pin.containerTabId || 0, - image: pin.iconUrl, - }; - - SessionStore.setTabState(newTab, state); - } - - this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`); - gBrowser.pinTab(newTab); - - if (pin.parentUuid) { - const parentGroup = groups.get(pin.parentUuid); - if (parentGroup) { - parentGroup.querySelector('.tab-group-container').appendChild(newTab); - } else { - if (pendingTabsInsideGroups[pin.parentUuid]) { - pendingTabsInsideGroups[pin.parentUuid].push(newTab); - } else { - pendingTabsInsideGroups[pin.parentUuid] = [newTab]; - } - } - } else { - if (!pin.isEssential) { - const container = gZenWorkspaces.workspaceElement( - pin.workspaceUuid - )?.pinnedTabsContainer; - if (container) { - container.insertBefore(newTab, container.lastChild); - } - } else { - gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab); - } - } - - gBrowser.tabContainer._invalidateCachedTabs(); - newTab.initialize(); - } catch (ex) { - console.error('Failed to initialize pinned tabs:', ex); - } - } - - setTimeout(() => { - this.#finishedInitializingPins(); - }, 0); - - gBrowser._updateTabBarForPinnedTabs(); - gZenUIManager.updateTabsToolbar(); - } - _onPinnedTabEvent(action, event) { if (!this.enabled) return; const tab = event.target; diff --git a/src/zen/zen.globals.js b/src/zen/zen.globals.js index 197089441a..06e07eaa47 100644 --- a/src/zen/zen.globals.js +++ b/src/zen/zen.globals.js @@ -26,7 +26,6 @@ export default [ 'ZEN_KEYSET_ID', 'gZenPinnedTabManager', - 'ZenPinnedTabsStorage', 'gZenEmojiPicker', 'gZenSessionStore', From 61659c63d927d6b657571f33eb113ed96af41034 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 31 Oct 2025 23:07:38 +0100 Subject: [PATCH 26/54] feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces --- .../sessionstore/SessionStore-sys-mjs.patch | 8 +- .../sessionstore/TabState-sys-mjs.patch | 4 +- .../tabbrowser/content/tab-js.patch | 10 +- .../tabbrowser/content/tabbrowser-js.patch | 20 +- src/zen/common/modules/ZenUIManager.mjs | 8 - src/zen/folders/ZenFolders.mjs | 195 +++--- .../sessionstore/ZenSessionManager.sys.mjs | 16 +- src/zen/tabs/ZenPinnedTabManager.mjs | 596 +++------------- src/zen/tabs/ZenPinnedTabsStorage.mjs | 659 ------------------ src/zen/workspaces/ZenWindowSyncing.mjs | 8 +- src/zen/workspaces/ZenWorkspaces.mjs | 6 - 11 files changed, 195 insertions(+), 1335 deletions(-) delete mode 100644 src/zen/tabs/ZenPinnedTabsStorage.mjs diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 1a0ebb806e..eaa5064a1a 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 eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa477fa68794 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd499c66d7 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -33,7 +33,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa47 let isRegularWindow = !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; + if (!aInitialState && isRegularWindow) { -+ aInitialState = ZenSessionStore.getNewWindowData(this._windows); ++ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); + this.restoreWindows(aWindow, aInitialState, {}); + } @@ -172,8 +172,8 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa47 + if (tabData.zenHasStaticLabel) { + tab.setAttribute("zen-has-static-label", "true"); + } -+ if (tabData.zenPinnedId) { -+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId); ++ if (tabData.zenSyncId) { ++ tab.setAttribute("zen-sync-id", tabData.zenSyncId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch index 2100e23343..cfeea50354 100644 --- a/src/browser/components/sessionstore/TabState-sys-mjs.patch +++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs -index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644 +index 82721356d191055bec0d4b0ca49e481221988801..d1323fe17c995611ebdfe2869b0ccd2d45bcfa11 100644 --- a/browser/components/sessionstore/TabState.sys.mjs +++ b/browser/components/sessionstore/TabState.sys.mjs @@ -85,7 +85,22 @@ class _TabState { @@ -7,7 +7,7 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a } + tabData.zenWorkspace = tab.getAttribute("zen-workspace-id"); -+ tabData.zenPinnedId = tab.getAttribute("zen-pin-id"); ++ tabData.zenSyncId = tab.getAttribute("zen-sync-id"); + tabData.zenEssential = tab.getAttribute("zen-essential"); + tabData.pinned = tabData.pinned || tabData.zenEssential; + tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId"); diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 3452bced0a..66ea04f7b2 100644 --- a/src/browser/components/tabbrowser/content/tab-js.patch +++ b/src/browser/components/tabbrowser/content/tab-js.patch @@ -121,15 +121,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 on_click(event) { if (event.button != 0) { return; -@@ -572,6 +594,7 @@ - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this, true); - gBrowser.removeTab(this, { - animate: true, - triggeringEvent: event, -@@ -584,6 +607,14 @@ +@@ -582,6 +605,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 1bb7700594..77d1b4b06f 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -518,17 +518,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e TabBarVisibility.update(); } -@@ -4635,6 +4768,9 @@ - return; - } - -+ for (let tab of selectedTabs) { -+ gZenPinnedTabManager._removePinnedAttributes(tab, true); -+ } - this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); - } - -@@ -4896,6 +5032,7 @@ +@@ -4814,6 +4944,7 @@ telemetrySource, } = {} ) { @@ -879,11 +869,3 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), -@@ -9863,6 +10050,7 @@ var TabContextMenu = { - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true); - gBrowser.removeTab(this.contextTab, { - animate: true, - ...gBrowser.TabMetrics.userTriggeredContext( diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs index e16fc982f7..780063f0fd 100644 --- a/src/zen/common/modules/ZenUIManager.mjs +++ b/src/zen/common/modules/ZenUIManager.mjs @@ -1310,14 +1310,6 @@ window.gZenVerticalTabsManager = { } else { gBrowser.setTabTitle(this._tabEdited); } - if (this._tabEdited.getAttribute('zen-pin-id')) { - // Update pin title in storage - await gZenPinnedTabManager.updatePinTitle( - this._tabEdited, - this._tabEdited.label, - !!newName - ); - } // Maybe add some confetti here?!? gZenUIManager.motion.animate( diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 8fdf63fec7..8a39c01952 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -505,10 +505,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { gBrowser.pinTab(emptyTab); tabs = [emptyTab, ...filteredTabs]; - const folder = this._createFolderNode(options); - if (options.initialPinId) { - folder.setAttribute('zen-pin-id', options.initialPinId); - } + const folder = this._createFolderNode(options); if (options.insertAfter) { options.insertAfter.after(folder); @@ -932,49 +929,49 @@ class nsZenFolders extends nsZenDOMOperatedFeature { const storedData = []; - for (const folder of allData) { - const parentFolder = folder.parentElement.closest('zen-folder'); - // Skip split-view-group if it's not a zen-folder child - if (!parentFolder && folder.hasAttribute('split-view-group')) continue; - const emptyFolderTabs = folder.tabs - .filter((tab) => tab.hasAttribute('zen-empty-tab')) - .map((tab) => tab.getAttribute('zen-pin-id')); + for (const folder of allData) { + const parentFolder = folder.parentElement.closest('zen-folder'); + // Skip split-view-group if it's not a zen-folder child + if (!parentFolder && folder.hasAttribute('split-view-group')) continue; + const emptyFolderTabs = folder.tabs + .filter((tab) => tab.hasAttribute('zen-empty-tab')) + .map((tab) => tab.getAttribute('zen-sync-id')); let prevSiblingInfo = null; const prevSibling = folder.previousElementSibling; const userIcon = folder?.icon?.querySelector('svg .icon image'); - if (prevSibling) { - if (gBrowser.isTabGroup(prevSibling)) { - prevSiblingInfo = { type: 'group', id: prevSibling.id }; - } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) { - const zenPinId = prevSibling.getAttribute('zen-pin-id'); - prevSiblingInfo = { type: 'tab', id: zenPinId }; - } else { - prevSiblingInfo = { type: 'start', id: null }; + if (prevSibling) { + if (gBrowser.isTabGroup(prevSibling)) { + prevSiblingInfo = { type: 'group', id: prevSibling.id }; + } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-sync-id')) { + const zenPinId = prevSibling.getAttribute('zen-sync-id'); + prevSiblingInfo = { type: 'tab', id: zenPinId }; + } else { + prevSiblingInfo = { type: 'start', id: null }; + } } - } - storedData.push({ - pinned: folder.pinned, - essential: folder.essential, - splitViewGroup: folder.hasAttribute('split-view-group'), - id: folder.id, - name: folder.label, - collapsed: folder.collapsed, - saveOnWindowClose: folder.saveOnWindowClose, - parentId: parentFolder ? parentFolder.id : null, - prevSiblingInfo: prevSiblingInfo, - emptyTabIds: emptyFolderTabs, - userIcon: userIcon?.getAttribute('href'), - pinId: folder.getAttribute('zen-pin-id'), - // note: We shouldn't be using the workspace-id anywhere, we are just - // remembering it for the pinned tabs manager to use it later. - workspaceId: folder.getAttribute('zen-workspace-id'), - }); + storedData.push({ + pinned: folder.pinned, + essential: folder.essential, + splitViewGroup: folder.hasAttribute('split-view-group'), + id: folder.id, + name: folder.label, + collapsed: folder.collapsed, + saveOnWindowClose: folder.saveOnWindowClose, + parentId: parentFolder ? parentFolder.id : null, + prevSiblingInfo: prevSiblingInfo, + emptyTabIds: emptyFolderTabs, + userIcon: userIcon?.getAttribute('href'), + syncId: folder.getAttribute('zen-sync-id'), + // note: We shouldn't be using the workspace-id anywhere, we are just + // remembering it for the pinned tabs manager to use it later. + workspaceId: folder.getAttribute('zen-workspace-id'), + }); + } + return storedData; } - return storedData; - } restoreDataFromSessionStore(data) { if (!data || this._sessionRestoring) { @@ -993,40 +990,40 @@ class nsZenFolders extends nsZenDOMOperatedFeature { }; tabFolderWorkingData.set(folderData.id, workingData); - const oldGroup = document.getElementById(folderData.id); - folderData.emptyTabIds.forEach((zenPinId) => { - oldGroup - ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`) - ?.setAttribute('zen-empty-tab', true); - }); - if (oldGroup) { - if (!folderData.splitViewGroup) { - const folder = this._createFolderNode({ - id: folderData.id, - label: folderData.name, - collapsed: folderData.collapsed, - pinned: folderData.pinned, - saveOnWindowClose: folderData.saveOnWindowClose, - workspaceId: folderData.workspaceId, - }); - folder.setAttribute('zen-pin-id', folderData.pinId); - workingData.node = folder; - oldGroup.before(folder); - } else { - workingData.node = oldGroup; - } - while (oldGroup.tabs.length > 0) { - const tab = oldGroup.tabs[0]; - if (folderData.workspaceId) { - tab.setAttribute('zen-workspace-id', folderData.workspaceId); + const oldGroup = document.getElementById(folderData.id); + folderData.emptyTabIds.forEach((zenSyncId) => { + oldGroup + ?.querySelector(`tab[zen-sync-id="${zenSyncId}"]`) + ?.setAttribute('zen-empty-tab', true); + }); + if (oldGroup) { + if (!folderData.splitViewGroup) { + const folder = this._createFolderNode({ + id: folderData.id, + label: folderData.name, + collapsed: folderData.collapsed, + pinned: folderData.pinned, + saveOnWindowClose: folderData.saveOnWindowClose, + workspaceId: folderData.workspaceId, + }); + folder.setAttribute('zen-sync-id', folderData.syncId); + workingData.node = folder; + oldGroup.before(folder); + } else { + workingData.node = oldGroup; + } + while (oldGroup.tabs.length > 0) { + const tab = oldGroup.tabs[0]; + if (folderData.workspaceId) { + tab.setAttribute('zen-workspace-id', folderData.workspaceId); + } + workingData.containingTabsFragment.appendChild(tab); + } + if (!folderData.splitViewGroup) { + oldGroup.remove(); } - workingData.containingTabsFragment.appendChild(tab); - } - if (!folderData.splitViewGroup) { - oldGroup.remove(); } } - } for (const { node, containingTabsFragment } of tabFolderWorkingData.values()) { if (node) { @@ -1034,41 +1031,41 @@ class nsZenFolders extends nsZenDOMOperatedFeature { } } - // Nesting folders into each other according to parentId. - for (const { stateData, node } of tabFolderWorkingData.values()) { - if (node && stateData.parentId) { - const parentWorkingData = tabFolderWorkingData.get(stateData.parentId); - if (parentWorkingData && parentWorkingData.node) { - switch (stateData?.prevSiblingInfo?.type) { - case 'tab': { - const tab = parentWorkingData.node.querySelector( - `[zen-pin-id="${stateData.prevSiblingInfo.id}"]` - ); - tab.after(node); - break; - } - case 'group': { - const folder = document.getElementById(stateData.prevSiblingInfo.id); - if (folder) { - folder.after(node); + // Nesting folders into each other according to parentId. + for (const { stateData, node } of tabFolderWorkingData.values()) { + if (node && stateData.parentId) { + const parentWorkingData = tabFolderWorkingData.get(stateData.parentId); + if (parentWorkingData && parentWorkingData.node) { + switch (stateData?.prevSiblingInfo?.type) { + case 'tab': { + const tab = parentWorkingData.node.querySelector( + `[zen-sync-id="${stateData.prevSiblingInfo.id}"]` + ); + tab.after(node); break; } - // If we didn't find the group, we should debug it and continue to default case. - console.warn( - `Zen Folders: Could not find previous sibling group with id ${stateData.prevSiblingInfo.id} while restoring session.` - ); - // @eslint-disable-next-line no-fallthrough - } - default: { - // Should insert after zen-empty-tab - const start = - parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling; - start.after(node); + case 'group': { + const folder = document.getElementById(stateData.prevSiblingInfo.id); + if (folder) { + folder.after(node); + break; + } + // If we didn't find the group, we should debug it and continue to default case. + console.warn( + `Zen Folders: Could not find previous sibling group with id ${stateData.prevSiblingInfo.id} while restoring session.` + ); + // @eslint-disable-next-line no-fallthrough + } + default: { + // Should insert after zen-empty-tab + const start = + parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling; + start.after(node); + } } } } } - } // Initialize UI state for all folders. for (const { stateData, node } of tabFolderWorkingData.values()) { diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 5120319bcd..e8cf59114d 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -2,13 +2,6 @@ // 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 { - cancelIdleCallback, - clearTimeout, - requestIdleCallback, - setTimeout, -} from 'resource://gre/modules/Timer.sys.mjs'; - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -19,7 +12,6 @@ ChromeUtils.defineESModuleGetters(lazy, { SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', }); -const TAB_CUSTOM_VALUES = new WeakMap(); const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes const OBSERVING = ['sessionstore-state-write-complete', 'browser-window-before-show']; @@ -76,6 +68,7 @@ class nsZenSessionManager { /** Handles the browser-window-before-show observer notification. */ #onBeforeBrowserWindowShown(aWindow) { // TODO: Initialize new window + void aWindow; } get #topMostWindow() { @@ -113,10 +106,8 @@ class nsZenSessionManager { * @param forceUpdate * Forces us to recollect data and will bypass and update the * corresponding caches. - * @param zIndex - * The z-index of the window. */ - #collectWindowData(window, forceUpdate = false, zIndex = 0) { + #collectWindowData(window, forceUpdate = false) { let sidebarData = this.#sidebar; if (!sidebarData || forceUpdate) { sidebarData = {}; @@ -164,7 +155,8 @@ class nsZenSessionManager { } getNewWindowData(aWindows) { - return { windows: [Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {})] }; + let newWindow = { ...Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {}), ...this.#sidebar }; + return { windows: [newWindow] }; } } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 62350e00d1..19a2c0d562 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -6,24 +6,8 @@ import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components const lazy = {}; -class ZenPinnedTabsObserver { - static ALL_EVENTS = [ - 'TabPinned', - 'TabUnpinned', - 'TabMove', - 'TabGroupCreate', - 'TabGroupRemoved', - 'TabGroupMoved', - 'ZenFolderRenamed', - 'ZenFolderIconChanged', - 'TabGroupCollapse', - 'TabGroupExpand', - 'TabGrouped', - 'TabUngrouped', - 'ZenFolderChangedWorkspace', - 'TabAddedToEssentials', - 'TabRemovedFromEssentials', - ]; + class ZenPinnedTabsObserver { + static ALL_EVENTS = ['TabPinned', 'TabUnpinned']; #listeners = []; @@ -102,26 +86,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - onTabIconChanged(tab, url = null) { - tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); - const iconUrl = url ?? tab.iconImage.src; - if (!iconUrl && tab.hasAttribute('zen-pin-id')) { - try { - setTimeout(async () => { - const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI); - if (favicon) { - gBrowser.setIcon(tab, favicon); - } - }); - } catch { - // Handle error - } - } else { + onTabIconChanged(tab, url = null) { + tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); + const iconUrl = url ?? tab.iconImage.src; if (tab.hasAttribute('zen-essential')) { tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); } } - } _onTabResetPinButton(event, tab) { event.stopPropagation(); @@ -147,316 +118,35 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return !gZenWorkspaces.privateWindowOrDisabled; } - get maxEssentialTabs() { - return lazy.zenTabsEssentialsMax; - } - - async refreshPinnedTabs({ init = false } = {}) { - if (!this.enabled) { - return; - } - await ZenPinnedTabsStorage.promiseInitialized; - await this.#initializePinsCache(); - setTimeout(async () => { - // Execute in a separate task to avoid blocking the main thread - await SessionStore.promiseAllWindowsRestored; - await gZenWorkspaces.promiseInitialized; - await this.#initializePinnedTabs(init); - if (init) { - this._hasFinishedLoading = true; - } - }, 10); - } - - async #initializePinsCache() { - try { - // Get pin data - const pins = await ZenPinnedTabsStorage.getPins(); - - // Enhance pins with favicons - this._pinsCache = await Promise.all( - pins.map(async (pin) => { - try { - if (pin.isGroup) { - return pin; // Skip groups for now - } - const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url)); - return { - ...pin, - iconUrl: image || null, - }; - } catch { - // If favicon fetch fails, continue without icon - return { - ...pin, - iconUrl: null, - }; - } - }) - ); - } catch (ex) { - console.error('Failed to initialize pins cache:', ex); - this._pinsCache = []; - } - - this.log(`Initialized pins cache with ${this._pinsCache.length} pins`); - return this._pinsCache; - } - - #finishedInitializingPins() { - if (this.hasInitializedPins) { - return; + get maxEssentialTabs() { + return lazy.zenTabsEssentialsMax; } - this._resolvePinnedInitializedInternal(); - delete this._resolvePinnedInitializedInternal; - this.hasInitializedPins = true; - } - async #initializePinnedTabs(init = false) { - this.#finishedInitializingPins(); - } - - _onPinnedTabEvent(action, event) { - if (!this.enabled) return; - const tab = event.target; - if (this._ignoreNextTabPinnedEvent) { - delete this._ignoreNextTabPinnedEvent; - return; - } - switch (action) { - case 'TabPinned': - case 'TabAddedToEssentials': - tab._zenClickEventListener = this._zenClickEventListener; - tab.addEventListener('click', tab._zenClickEventListener); - this._setPinnedAttributes(tab); - break; - case 'TabRemovedFromEssentials': - if (tab.pinned) { - this.#onTabMove(tab); - break; - } - // [Fall through] - case 'TabUnpinned': - this._removePinnedAttributes(tab); - if (tab._zenClickEventListener) { - tab.removeEventListener('click', tab._zenClickEventListener); - delete tab._zenClickEventListener; - } - break; - case 'TabMove': - this.#onTabMove(tab); - break; - case 'TabGroupCreate': - this.#onTabGroupCreate(event); - break; - case 'TabGroupRemoved': - this.#onTabGroupRemoved(event); - break; - case 'TabGroupMoved': - this.#onTabGroupMoved(event); - break; - case 'ZenFolderRenamed': - case 'ZenFolderIconChanged': - case 'TabGroupCollapse': - case 'TabGroupExpand': - case 'ZenFolderChangedWorkspace': - this.#updateGroupInfo(event.originalTarget, action); - break; - case 'TabGrouped': - this.#onTabGrouped(event); - break; - case 'TabUngrouped': - this.#onTabUngrouped(event); - break; - default: - console.warn('ZenPinnedTabManager: Unhandled tab event', action); - break; - } - } - - async #onTabGroupCreate(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - if (group.hasAttribute('zen-pin-id')) { - return; // Group already exists in storage - } - const workspaceId = group.getAttribute('zen-workspace-id'); - let id = await ZenPinnedTabsStorage.createGroup( - group.name, - group.iconURL, - group.collapsed, - workspaceId, - group.getAttribute('zen-pin-id'), - group._pPos - ); - group.setAttribute('zen-pin-id', id); - for (const tab of group.tabs) { - // Only add it if the tab is directly under the group - if ( - tab.pinned && - tab.hasAttribute('zen-pin-id') && - tab.group === group && - this.hasInitializedPins - ) { - const tabPinId = tab.getAttribute('zen-pin-id'); - await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos); + _onPinnedTabEvent(action, event) { + if (!this.enabled) return; + const tab = event.target; + if (this._ignoreNextTabPinnedEvent) { + delete this._ignoreNextTabPinnedEvent; + return; } - } - await this.refreshPinnedTabs(); - } - - async #onTabGrouped(event) { - const tab = event.detail; - const group = tab.group; - if (!group.isZenFolder) { - return; - } - const pinId = group.getAttribute('zen-pin-id'); - const tabPinId = tab.getAttribute('zen-pin-id'); - const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId); - if (!tabPin || !tabPin.group) { - return; - } - ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos); - } - - async #onTabUngrouped(event) { - const tab = event.detail; - const group = tab.group; - if (!group?.isZenFolder) { - return; - } - const tabPinId = tab.getAttribute('zen-pin-id'); - const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId); - if (!tabPin) { - return; - } - ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos); - } - - async #updateGroupInfo(group, action) { - if (!group?.isZenFolder) { - return; - } - const pinId = group.getAttribute('zen-pin-id'); - const groupPin = this._pinsCache?.find((p) => p.uuid === pinId); - if (groupPin) { - groupPin.title = group.name; - groupPin.folderIcon = group.iconURL; - groupPin.isFolderCollapsed = group.collapsed; - groupPin.position = group._pPos; - groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null; - groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null; - await this.savePin(groupPin); switch (action) { - case 'ZenFolderRenamed': - case 'ZenFolderIconChanged': - case 'TabGroupCollapse': - case 'TabGroupExpand': + case 'TabPinned': + tab._zenClickEventListener = this._zenClickEventListener; + tab.addEventListener('click', tab._zenClickEventListener); break; - default: - for (const item of group.allItems) { - if (gBrowser.isTabGroup(item)) { - await this.#updateGroupInfo(item, action); - } else { - await this.#onTabMove(item); - } + // [Fall through] + case 'TabUnpinned': + if (tab._zenClickEventListener) { + tab.removeEventListener('click', tab._zenClickEventListener); + delete tab._zenClickEventListener; } - } - } - } - - async #onTabGroupRemoved(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id')); - group.removeAttribute('zen-pin-id'); - } - - async #onTabGroupMoved(event) { - const group = event.originalTarget; - if (!group.isZenFolder) { - return; - } - const newIndex = group._pPos; - const pinId = group.getAttribute('zen-pin-id'); - if (!pinId) { - return; - } - for (const tab of group.allItemsRecursive) { - if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) { - const pin = this._pinsCache.find((p) => p.uuid === pinId); - if (pin) { - pin.position = tab._pPos; - pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null; - pin.workspaceUuid = group.getAttribute('zen-workspace-id'); - await this.savePin(pin, false); - } - break; - } - } - const groupPin = this._pinsCache?.find((p) => p.uuid === pinId); - if (groupPin) { - groupPin.position = newIndex; - groupPin.parentUuid = group.group?.getAttribute('zen-pin-id'); - groupPin.workspaceUuid = group.getAttribute('zen-workspace-id'); - await this.savePin(groupPin); - } - } - - async #onTabMove(tab) { - if (!tab.pinned || !this._pinsCache) { - return; - } - - const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups]; - for (let i = 0; i < allTabs.length; i++) { - const otherTab = allTabs[i]; - if ( - otherTab.pinned && - otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id') - ) { - const actualPin = this._pinsCache.find( - (pin) => pin.uuid === otherTab.getAttribute('zen-pin-id') - ); - if (!actualPin) { - continue; - } - actualPin.position = otherTab._pPos; - actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id'); - actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null; - await this.savePin(actualPin, false); + break; + default: + console.warn('ZenPinnedTabManager: Unhandled tab event', action); + break; } } - const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - - if (!actualPin) { - return; - } - actualPin.position = tab._pPos; - actualPin.isEssential = tab.hasAttribute('zen-essential'); - actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null; - actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null; - - // There was a bug where the title and hasStaticLabel attribute were not being set - // This is a workaround to fix that - if (tab.hasAttribute('zen-has-static-label')) { - actualPin.editedTitle = true; - actualPin.title = tab.label; - } - await this.savePin(actualPin); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabMoved', { - detail: { tab }, - }) - ); - } - async _onTabClick(e) { const tab = e.target?.closest('tab'); if (e.button === 1 && tab) { @@ -484,128 +174,17 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return; } - const browser = tab.linkedBrowser; - - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - - if (!pin) { - return; - } - - const userContextId = tab.getAttribute('usercontextid'); - - pin.title = tab.label || browser.contentTitle; - pin.url = browser.currentURI.spec; - pin.workspaceUuid = tab.getAttribute('zen-workspace-id'); - pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0; - - await this.savePin(pin); - this.resetPinChangedUrl(tab); - await this.refreshPinnedTabs(); - gZenUIManager.showToast('zen-pinned-tab-replaced'); - } - - async _setPinnedAttributes(tab) { - if ( - tab.hasAttribute('zen-pin-id') || - !this._hasFinishedLoading || - tab.hasAttribute('zen-empty-tab') - ) { - return; - } - - this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`); - const browser = tab.linkedBrowser; - - const uuid = gZenUIManager.generateUuidv4(); - const userContextId = tab.getAttribute('usercontextid'); - - let entry = null; - - if (tab.getAttribute('zen-pinned-entry')) { - entry = JSON.parse(tab.getAttribute('zen-pinned-entry')); - } - - await this.savePin({ - uuid, - title: entry?.title || tab.label || browser.contentTitle, - url: entry?.url || browser.currentURI.spec, - containerTabId: userContextId ? parseInt(userContextId, 10) : 0, - workspaceUuid: tab.getAttribute('zen-workspace-id'), - isEssential: tab.getAttribute('zen-essential') === 'true', - parentUuid: tab.group?.getAttribute('zen-pin-id') || null, - position: tab._pPos, - }); - - tab.setAttribute('zen-pin-id', uuid); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabCreated', { - detail: { tab }, - }) - ); - - // This is used while migrating old pins to new system - we don't want to refresh when migrating - if (tab.getAttribute('zen-pinned-entry')) { - tab.removeAttribute('zen-pinned-entry'); - return; - } - this.onLocationChange(browser); - await this.refreshPinnedTabs(); - } - - async _removePinnedAttributes(tab, isClosing = false) { - tab.removeAttribute('zen-has-static-label'); - if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) { - return; - } - - if (Services.startup.shuttingDown || window.skipNextCanClose) { - return; - } - - this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`); - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); - this.resetPinChangedUrl(tab); - - if (!isClosing) { - tab.removeAttribute('zen-pin-id'); - tab.removeAttribute('zen-essential'); // Just in case - - if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) { - const workspace = await gZenWorkspaces.getActiveWorkspace(); - tab.setAttribute('zen-workspace-id', workspace.uuid); - } + this.resetPinChangedUrl(tab); + gZenUIManager.showToast('zen-pinned-tab-replaced'); } - await this.refreshPinnedTabs(); - tab.dispatchEvent( - new CustomEvent('ZenPinnedTabRemoved', { - detail: { tab }, - }) - ); - } _initClosePinnedTabShortcut() { let cmdClose = document.getElementById('cmd_close'); - if (cmdClose) { - cmdClose.addEventListener('command', this.onCloseTabShortcut.bind(this)); - } - } - - async savePin(pin, notifyObservers = true) { - if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) { - return; - } - const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid); - if (existingPin) { - Object.assign(existingPin, pin); - } else { - // We shouldn't need it, but just in case there's - // a race condition while making new pinned tabs. - this._pinsCache.push(pin); + if (cmdClose) { + cmdClose.addEventListener('command', this.onCloseTabShortcut.bind(this)); + } } - await ZenPinnedTabsStorage.savePin(pin, notifyObservers); - } async onCloseTabShortcut( event, @@ -804,67 +383,61 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - addToEssentials(tab) { - const tabs = tab - ? // if it's already an array, dont make it [tab] - tab?.length - ? tab - : [tab] - : TabContextMenu.contextTab.multiselected - ? gBrowser.selectedTabs - : [TabContextMenu.contextTab]; - let movedAll = true; - for (let i = 0; i < tabs.length; i++) { - let tab = tabs[i]; - const section = gZenWorkspaces.getEssentialsSection(tab); - if (!this.canEssentialBeAdded(tab)) { - movedAll = false; - continue; - } - if (tab.hasAttribute('zen-essential')) { - continue; - } - tab.setAttribute('zen-essential', 'true'); - if (tab.hasAttribute('zen-workspace-id')) { - tab.removeAttribute('zen-workspace-id'); - } - if (tab.pinned && tab.hasAttribute('zen-pin-id')) { - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (pin) { - pin.isEssential = true; - pin.workspaceUuid = null; - this.savePin(pin); + addToEssentials(tab) { + const tabs = tab + ? // if it's already an array, dont make it [tab] + tab?.length + ? tab + : [tab] + : TabContextMenu.contextTab.multiselected + ? gBrowser.selectedTabs + : [TabContextMenu.contextTab]; + let movedAll = true; + for (let i = 0; i < tabs.length; i++) { + let tab = tabs[i]; + const section = gZenWorkspaces.getEssentialsSection(tab); + if (!this.canEssentialBeAdded(tab)) { + movedAll = false; + continue; } - gBrowser.zenHandleTabMove(tab, () => { - if (tab.ownerGlobal !== window) { - tab = gBrowser.adoptTab(tab, { - selectTab: tab.selected, - }); - tab.setAttribute('zen-essential', 'true'); - } else { - section.appendChild(tab); - } + if (tab.hasAttribute('zen-essential')) { + continue; + } + tab.setAttribute('zen-essential', 'true'); + if (tab.hasAttribute('zen-workspace-id')) { + tab.removeAttribute('zen-workspace-id'); + } + if (tab.pinned && tab.hasAttribute('zen-pin-id')) { + gBrowser.zenHandleTabMove(tab, () => { + if (tab.ownerGlobal !== window) { + tab = gBrowser.adoptTab(tab, { + selectTab: tab.selected, + }); + tab.setAttribute('zen-essential', 'true'); + } else { + section.appendChild(tab); + } + }); + } else { + gBrowser.pinTab(tab); + this._ignoreNextTabPinnedEvent = true; + } + tab.setAttribute('zenDefaultUserContextId', true); + if (tab.selected) { + gZenWorkspaces.switchTabIfNeeded(tab); + } + this.onTabIconChanged(tab); + // Dispatch the event to update the UI + const event = new CustomEvent('TabAddedToEssentials', { + detail: { tab }, + bubbles: true, + cancelable: false, }); - } else { - gBrowser.pinTab(tab); - this._ignoreNextTabPinnedEvent = true; - } - tab.setAttribute('zenDefaultUserContextId', true); - if (tab.selected) { - gZenWorkspaces.switchTabIfNeeded(tab); + tab.dispatchEvent(event); } - this.onTabIconChanged(tab); - // Dispatch the event to update the UI - const event = new CustomEvent('TabAddedToEssentials', { - detail: { tab }, - bubbles: true, - cancelable: false, - }); - tab.dispatchEvent(event); + gZenUIManager.updateTabsToolbar(); + return movedAll; } - gZenUIManager.updateTabsToolbar(); - return movedAll; - } removeEssentials(tab, unpin = true) { const tabs = tab @@ -1227,11 +800,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true'; } - async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) { - const uuid = tab.getAttribute('zen-pin-id'); - await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers); - - await this.refreshPinnedTabs(); + async updatePinTitle(tab, newTitle, isEdited = true) { + const uuid = tab.getAttribute('zen-pin-id'); const browsers = Services.wm.getEnumerator('navigator:browser'); diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs deleted file mode 100644 index 4847687cf3..0000000000 --- a/src/zen/tabs/ZenPinnedTabsStorage.mjs +++ /dev/null @@ -1,659 +0,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/. - -window.ZenPinnedTabsStorage = { - _saveCache: [], - - async init() { - await this._ensureTable(); - }, - - async _ensureTable() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => { - // Create the pins table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - title TEXT NOT NULL, - url TEXT, - container_id INTEGER, - workspace_uuid TEXT, - position INTEGER NOT NULL DEFAULT 0, - is_essential BOOLEAN NOT NULL DEFAULT 0, - is_group BOOLEAN NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - const columns = await db.execute(`PRAGMA table_info(zen_pins)`); - const columnNames = columns.map((row) => row.getResultByName('name')); - - // Helper function to add column if it doesn't exist - const addColumnIfNotExists = async (columnName, definition) => { - if (!columnNames.includes(columnName)) { - await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`); - } - }; - - await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL'); - await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL'); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) - `); - - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins_changes ( - uuid TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL - ) - `); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid) - `); - - this._resolveInitialized(); - }); - }, - - /** - * Private helper method to notify observers with a list of changed UUIDs. - * @param {string} event - The observer event name. - * @param {Array} uuids - Array of changed workspace UUIDs. - */ - _notifyPinsChanged(event, uuids) { - if (uuids.length === 0) return; // No changes to notify - - // Convert the array of UUIDs to a JSON string - const data = JSON.stringify(uuids); - - Services.obs.notifyObservers(null, event, data); - }, - - async savePin(pin, notifyObservers = true) { - // If we find the exact same pin in the cache, skip saving - const existingIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === pin.uuid); - if (existingIndex !== -1) { - const existingPin = this._saveCache[existingIndex]; - const isSame = Object.keys(pin).every((key) => pin[key] === existingPin[key]); - if (isSame) { - return; // No changes, skip saving - } else { - // Update the cached pin - this._saveCache[existingIndex] = pin; - } - } else { - // Add to cache - this._saveCache.push(pin); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - let newPosition; - if ('position' in pin && Number.isFinite(pin.position)) { - newPosition = pin.position; - } else { - // Get the maximum position within the same parent group (or null for root level) - const maxPositionResult = await db.execute( - ` - SELECT MAX("position") as max_position - FROM zen_pins - WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '') - `, - { folder_parent_uuid: pin.parentUuid || null } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - // Insert or replace the pin - await db.executeCached( - ` - INSERT OR REPLACE INTO zen_pins ( - uuid, title, url, container_id, workspace_uuid, position, - is_essential, is_group, folder_parent_uuid, edited_title, created_at, - updated_at, is_folder_collapsed, folder_icon - ) VALUES ( - :uuid, :title, :url, :container_id, :workspace_uuid, :position, - :is_essential, :is_group, :folder_parent_uuid, :edited_title, - COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), - :now, :is_folder_collapsed, :folder_icon - ) - `, - { - uuid: pin.uuid, - title: pin.title, - url: pin.isGroup ? '' : pin.url, - container_id: pin.containerTabId || null, - workspace_uuid: pin.workspaceUuid || null, - position: newPosition, - is_essential: pin.isEssential || false, - is_group: pin.isGroup || false, - folder_parent_uuid: pin.parentUuid || null, - edited_title: pin.editedTitle || false, - now, - folder_icon: pin.folderIcon || null, - is_folder_collapsed: pin.isFolderCollapsed || false, - } - ); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - - changedUUIDs.add(pin.uuid); - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async getPins() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.executeCached(` - SELECT * FROM zen_pins - ORDER BY position ASC - `); - return rows.map((row) => ({ - uuid: row.getResultByName('uuid'), - title: row.getResultByName('title'), - url: row.getResultByName('url'), - containerTabId: row.getResultByName('container_id'), - workspaceUuid: row.getResultByName('workspace_uuid'), - position: row.getResultByName('position'), - isEssential: Boolean(row.getResultByName('is_essential')), - isGroup: Boolean(row.getResultByName('is_group')), - parentUuid: row.getResultByName('folder_parent_uuid'), - editedTitle: Boolean(row.getResultByName('edited_title')), - folderIcon: row.getResultByName('folder_icon'), - isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), - })); - }, - - /** - * Create a new group - * @param {string} title - The title of the group - * @param {string} workspaceUuid - The workspace UUID (optional) - * @param {string} parentUuid - The parent group UUID (optional, null for root level) - * @param {number} position - The position of the group (optional, will auto-calculate if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - * @returns {Promise} The UUID of the created group - */ - async createGroup( - title, - icon = null, - isCollapsed = false, - workspaceUuid = null, - parentUuid = null, - position = null, - notifyObservers = true - ) { - if (!title || typeof title !== 'string') { - throw new Error('Group title is required and must be a string'); - } - - const groupUuid = gZenUIManager.generateUuidv4(); - - const groupPin = { - uuid: groupUuid, - title, - folderIcon: icon || null, - isFolderCollapsed: isCollapsed || false, - workspaceUuid, - parentUuid, - position, - isGroup: true, - isEssential: false, - editedTitle: true, // Group titles are always considered edited - }; - - await this.savePin(groupPin, notifyObservers); - return groupUuid; - }, - - /** - * Add an existing tab/pin to a group - * @param {string} tabUuid - The UUID of the tab to add to the group - * @param {string} groupUuid - The UUID of the target group - * @param {number} position - The position within the group (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) { - if (!tabUuid || !groupUuid) { - throw new Error('Both tabUuid and groupUuid are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => { - await db.executeTransaction(async () => { - // Verify the group exists and is actually a group - const groupCheck = await db.execute( - `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`, - { groupUuid } - ); - - if (groupCheck.length === 0) { - throw new Error(`Group with UUID ${groupUuid} does not exist`); - } - - if (!groupCheck[0].getResultByName('is_group')) { - throw new Error(`Pin with UUID ${groupUuid} is not a group`); - } - - const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, { - tabUuid, - }); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - const now = Date.now(); - let newPosition; - - if (position !== null && Number.isFinite(position)) { - newPosition = position; - } else { - // Get the maximum position within the group - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`, - { groupUuid } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = :groupUuid, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - groupUuid, - newPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - /** - * Remove a tab from its group (move to root level) - * @param {string} tabUuid - The UUID of the tab to remove from its group - * @param {number} newPosition - The new position at root level (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) { - if (!tabUuid) { - throw new Error('tabUuid is required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.removeTabFromGroup', - async (db) => { - await db.executeTransaction(async () => { - // Verify the tab exists and is in a group - const tabCheck = await db.execute( - `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`, - { tabUuid } - ); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - if (!tabCheck[0].getResultByName('folder_parent_uuid')) { - return; - } - - const now = Date.now(); - let finalPosition; - - if (newPosition !== null && Number.isFinite(newPosition)) { - finalPosition = newPosition; - } else { - // Get the maximum position at root level (where folder_parent_uuid is null) - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL` - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - finalPosition = maxPosition + 1000; - } - - // Update the tab to be at root level - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = NULL, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - newPosition: finalPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async removePin(uuid, notifyObservers = true) { - const cachedIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === uuid); - if (cachedIndex !== -1) { - this._saveCache.splice(cachedIndex, 1); - } - - const changedUUIDs = [uuid]; - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { - await db.executeTransaction(async () => { - // Get all child UUIDs first for change tracking - const children = await db.execute( - `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`, - { - uuid, - } - ); - - // Add child UUIDs to changedUUIDs array - for (const child of children) { - changedUUIDs.push(child.getResultByName('uuid')); - } - - // Delete the pin/group itself - await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid }); - - // Record the changes - const now = Math.floor(Date.now() / 1000); - for (const changedUuid of changedUUIDs) { - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: changedUuid, - timestamp: now, - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-removed', changedUUIDs); - } - }, - - async wipeAllPins() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => { - await db.execute(`DELETE FROM zen_pins`); - await db.execute(`DELETE FROM zen_pins_changes`); - await this.updateLastChangeTimestamp(db); - }); - }, - - async markChanged(uuid) { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - }); - }, - - async getChangedIDs() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.execute(` - SELECT uuid, timestamp FROM zen_pins_changes - `); - const changes = {}; - for (const row of rows) { - changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); - } - return changes; - }, - - async clearChangedIDs() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => { - await db.execute(`DELETE FROM zen_pins_changes`); - }); - }, - - shouldReorderPins(before, current, after) { - const minGap = 1; // Minimum allowed gap between positions - return ( - (before !== null && current - before < minGap) || (after !== null && after - current < minGap) - ); - }, - - async reorderAllPins(db, changedUUIDs) { - const pins = await db.execute(` - SELECT uuid - FROM zen_pins - ORDER BY position ASC - `); - - for (let i = 0; i < pins.length; i++) { - const newPosition = (i + 1) * 1000; // Use large increments - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pins[i].getResultByName('uuid') } - ); - changedUUIDs.add(pins[i].getResultByName('uuid')); - } - }, - - async updateLastChangeTimestamp(db) { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO moz_meta (key, value) - VALUES ('zen_pins_last_change', :now) - `, - { now } - ); - }, - - async getLastChangeTimestamp() { - const db = await PlacesUtils.promiseDBConnection(); - const result = await db.executeCached(` - SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change' - `); - return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; - }, - - async updatePinPositions(pins) { - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.updatePinPositions', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - for (let i = 0; i < pins.length; i++) { - const pin = pins[i]; - const newPosition = (i + 1) * 1000; - - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pin.uuid } - ); - - changedUUIDs.add(pin.uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - }, - - async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) { - if (!uuid || typeof newTitle !== 'string') { - throw new Error('Invalid parameters: uuid and newTitle are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - // Update the pin's title and edited_title flag - const result = await db.execute( - ` - UPDATE zen_pins - SET title = :newTitle, - edited_title = :isEdited, - updated_at = :now - WHERE uuid = :uuid - `, - { - uuid, - newTitle, - isEdited, - now, - } - ); - - // Only proceed with change tracking if a row was actually updated - if (result.rowsAffected > 0) { - changedUUIDs.add(uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - } - }); - }); - - if (notifyObservers && changedUUIDs.size > 0) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async __dropTables() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => { - await db.execute(`DROP TABLE IF EXISTS zen_pins`); - await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`); - }); - }, -}; - -ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => { - ZenPinnedTabsStorage._resolveInitialized = resolve; - ZenPinnedTabsStorage.init(); -}); diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs index 857f9fc9cf..4c06ae0e06 100644 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ b/src/zen/workspaces/ZenWindowSyncing.mjs @@ -293,14 +293,14 @@ targetTab.getAttribute('zen-workspace-id') ); } - duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id')); duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id')); } #onTabGroupCreate(event) { - const targetGroup = event.target; - const isSplitView = targetGroup.classList.contains('zen-split-view'); - const isFolder = targetGroup.isZenFolder; + void event; + //const targetGroup = event.target; + //const isSplitView = targetGroup.classList.contains('zen-split-view'); + //const isFolder = targetGroup.isZenFolder; } } diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index cb08f58d00..296da89e06 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -935,7 +935,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { await this.workspaceBookmarks(); await this.initializeTabsStripSections(); this._initializeEmptyTab(); - await gZenPinnedTabManager.refreshPinnedTabs({ init: true }); await this.changeWorkspace(activeWorkspace, { onInit: true }); this.#fixTabPositions(); this.onWindowResize(); @@ -1474,11 +1473,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { !tab.hasAttribute('zen-empty-tab') && !tab.hasAttribute('zen-essential') ); - for (const tab of tabs) { - if (tab.pinned) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); - } - } gBrowser.removeTabs(tabs, { animate: false, skipSessionStore: true, From 2706e8761d8b37380c4a993e9ad43f0557a2e5a6 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 1 Nov 2025 13:40:14 +0100 Subject: [PATCH 27/54] feat: Dont restore windows that are already initialized, b=no-bug, c=no-component --- .../sessionstore/SessionStore-sys-mjs.patch | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index eaa5064a1a..a68d47d695 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 eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd499c66d7 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03cda47b6f 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -28,18 +28,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2041,6 +2046,10 @@ var SessionStoreInternal = { - // A regular window is not a private window, taskbar tab window, or popup window - let isRegularWindow = - !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; -+ if (!aInitialState && isRegularWindow) { -+ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); -+ this.restoreWindows(aWindow, aInitialState, {}); -+ } - - // perform additional initialization when the first window is loading - if (lazy.RunState.isStopped) { -@@ -2139,7 +2148,6 @@ var SessionStoreInternal = { +@@ -2139,7 +2144,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -47,6 +36,17 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..1ca2e7327e72824805a93c18cb7e3dfd !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. +@@ -2203,6 +2207,10 @@ var SessionStoreInternal = { + }); + this._shouldRestoreLastSession = false; + } ++ else if (!aInitialState && isRegularWindow) { ++ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); ++ this.restoreWindows(aWindow, aInitialState, {}); ++ } + + if (this._restoreLastWindow && aWindow.toolbar.visible) { + // always reset (if not a popup window) @@ -2372,11 +2380,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } From 32d603ced938b7d7cb31f407eb594978b7b366f1 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Thu, 13 Nov 2025 13:56:31 +0100 Subject: [PATCH 28/54] chore: Update patches to ff 145, b=no-bug, c=no-component --- .../sessionstore/SessionStore-sys-mjs.patch | 36 ++-- .../tabbrowser/content/tab-js.patch | 2 +- .../tabbrowser/content/tabbrowser-js.patch | 164 +++++++----------- 3 files changed, 85 insertions(+), 117 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index a68d47d695..94f08855db 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 eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03cda47b6f 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be523e4feb 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -11,7 +11,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -195,6 +197,7 @@ ChromeUtils.defineESModuleGetters(lazy, { +@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, { TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", @@ -19,7 +19,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 }); ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { -@@ -1904,6 +1907,8 @@ var SessionStoreInternal = { +@@ -1911,6 +1914,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -28,7 +28,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2139,7 +2144,6 @@ var SessionStoreInternal = { +@@ -2151,7 +2156,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -36,7 +36,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2203,6 +2207,10 @@ var SessionStoreInternal = { +@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { }); this._shouldRestoreLastSession = false; } @@ -47,7 +47,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 if (this._restoreLastWindow && aWindow.toolbar.visible) { // always reset (if not a popup window) -@@ -2372,11 +2380,9 @@ var SessionStoreInternal = { +@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -59,7 +59,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3361,7 +3367,7 @@ var SessionStoreInternal = { +@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -68,7 +68,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 return; } -@@ -4073,6 +4079,11 @@ var SessionStoreInternal = { +@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -81,7 +81,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4509,6 +4520,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -89,7 +89,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5358,7 +5370,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -98,7 +98,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 removableTabs.push(tab); } } -@@ -5418,7 +5430,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -107,7 +107,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 winData.workspaceID = workspaceID; } }, -@@ -5609,11 +5621,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -121,7 +121,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5624,6 +5637,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -129,7 +129,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5636,7 +5650,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -138,7 +138,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5748,8 +5762,8 @@ var SessionStoreInternal = { +@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -149,7 +149,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5792,6 +5806,8 @@ var SessionStoreInternal = { +@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -158,7 +158,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6348,6 +6364,25 @@ var SessionStoreInternal = { +@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -184,7 +184,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7263,7 +7298,7 @@ var SessionStoreInternal = { +@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 66ea04f7b2..75153b1734 100644 --- a/src/browser/components/tabbrowser/content/tab-js.patch +++ b/src/browser/components/tabbrowser/content/tab-js.patch @@ -121,7 +121,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 on_click(event) { if (event.button != 0) { return; -@@ -582,6 +605,14 @@ +@@ -584,6 +607,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 77d1b4b06f..1ceba0b2fd 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,16 +1,8 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e6923fe5f 100644 +index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc349868d0d0 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js -@@ -386,6 +386,7 @@ - * @type {MozBrowser[]} - */ - get splitViewBrowsers() { -+ return gZenViewSplitter.splitViewBrowsers; - const browsers = []; - if (this.#activeSplitView) { - for (const tab of this.#activeSplitView.tabs) { -@@ -450,15 +451,64 @@ +@@ -450,15 +450,64 @@ return this.tabContainer.visibleTabs; } @@ -77,7 +69,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e set selectedTab(val) { if ( gSharedTabWarning.willShowSharedTabWarning(val) || -@@ -613,6 +663,7 @@ +@@ -613,6 +662,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -85,7 +77,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -898,13 +949,17 @@ +@@ -898,13 +948,17 @@ } this.showTab(aTab); @@ -104,7 +96,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e aTab.setAttribute("pinned", "true"); this._updateTabBarForPinnedTabs(); -@@ -917,11 +972,15 @@ +@@ -917,11 +971,15 @@ } this.#handleTabMove(aTab, () => { @@ -121,7 +113,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e }); aTab.style.marginInlineStart = ""; -@@ -1098,6 +1157,8 @@ +@@ -1098,6 +1156,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -130,7 +122,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if ( aIconURL && !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) -@@ -1107,6 +1168,9 @@ +@@ -1107,6 +1167,9 @@ ); return; } @@ -140,14 +132,6 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1379,7 +1443,6 @@ - - // Preview mode should not reset the owner - if (!this._previewMode && !oldTab.selected) { -- oldTab.owner = null; - } - - let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); @@ -1470,6 +1533,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); @@ -156,6 +140,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { +@@ -1622,6 +1686,9 @@ @@ -1622,6 +1686,9 @@ } @@ -166,6 +151,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); +@@ -1945,7 +2012,8 @@ @@ -1945,7 +2012,8 @@ } @@ -176,6 +162,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e return false; } +@@ -2053,7 +2121,7 @@ @@ -2053,7 +2121,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -185,6 +172,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" +@@ -2328,6 +2396,7 @@ @@ -2328,6 +2396,7 @@ uriIsAboutBlank, userContextId, @@ -193,6 +181,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the +@@ -2401,8 +2470,7 @@ @@ -2401,8 +2470,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); @@ -203,6 +192,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e b.setAttribute("transparent", "true"); } +@@ -2567,7 +2635,7 @@ @@ -2567,7 +2635,7 @@ let panel = this.getPanel(browser); @@ -212,6 +202,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. +@@ -2626,8 +2694,8 @@ @@ -2626,8 +2694,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. @@ -223,6 +214,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } +@@ -2814,7 +2882,6 @@ @@ -2814,7 +2882,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, @@ -231,6 +223,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e focusUrlBar: true, }); resolve(this.selectedBrowser); +@@ -2923,6 +2990,8 @@ @@ -2923,6 +2990,8 @@ schemelessInput, hasValidUserGestureActivation = false, @@ -240,7 +233,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,10 +3002,17 @@ +@@ -2933,6 +3002,12 @@ ); } @@ -253,20 +246,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (!UserInteraction.running("browser.tabs.opening", window)) { UserInteraction.start("browser.tabs.opening", "initting", window); } - -+ if (!gURLBar.hasAttribute("zen-newtab")) { - // If we're opening a foreground tab, set the owner by default. - ownerTab ??= inBackground ? null : this.selectedTab; - -@@ -2944,6 +3020,7 @@ - if (this.selectedTab.owner) { - this.selectedTab.owner = null; - } -+ } - - // Find the tab that opened this one, if any. This is used for - // determining positioning, and inherited attributes such as the -@@ -2996,6 +3073,19 @@ +@@ -2996,6 +3071,19 @@ noInitialLabel, skipBackgroundNotify, }); @@ -286,7 +266,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3094,7 @@ +@@ -3004,6 +3092,7 @@ ownerTab, openerTab, pinned, @@ -294,7 +274,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3113,7 @@ +@@ -3022,6 +3111,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -302,7 +282,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e })); if (focusUrlBar) { -@@ -3146,6 +3238,12 @@ +@@ -3146,6 +3236,12 @@ } } @@ -315,7 +295,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3330,10 +3428,10 @@ +@@ -3330,10 +3426,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -327,7 +307,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } if (!color) { -@@ -3354,9 +3452,14 @@ +@@ -3354,9 +3450,14 @@ label, isAdoptingGroup ); @@ -344,7 +324,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e ); group.addTabs(tabs); -@@ -3477,7 +3580,7 @@ +@@ -3477,7 +3578,7 @@ } this.#handleTabMove(tab, () => @@ -353,7 +333,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e ); } -@@ -3679,6 +3782,7 @@ +@@ -3679,6 +3780,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -361,7 +341,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3748,6 +3852,7 @@ +@@ -3748,6 +3850,7 @@ openWindowInfo, name, skipLoad, @@ -369,7 +349,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e }); } -@@ -3935,7 +4040,7 @@ +@@ -3935,7 +4038,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -378,7 +358,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e let url = "about:blank"; if (tabData.entries?.length) { -@@ -3972,8 +4077,10 @@ +@@ -3972,8 +4075,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -390,7 +370,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (select) { tabToSelect = tab; } -@@ -3985,7 +4092,8 @@ +@@ -3985,7 +4090,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -400,7 +380,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -3999,7 +4107,10 @@ +@@ -3999,7 +4105,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -412,7 +392,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e ); tabsFragment.appendChild(tabGroup.node); } -@@ -4044,9 +4155,23 @@ +@@ -4044,9 +4153,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -436,7 +416,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4237,11 +4362,14 @@ +@@ -4237,11 +4360,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -452,7 +432,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if ( !bulkOrderedOpen && ((openerTab && -@@ -4253,7 +4381,7 @@ +@@ -4253,7 +4379,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -461,7 +441,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e tabGroup = previousTab.group; } if ( -@@ -4264,7 +4392,7 @@ +@@ -4264,7 +4390,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -470,7 +450,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4292,14 +4420,14 @@ +@@ -4292,14 +4418,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -489,7 +469,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4310,7 +4438,7 @@ +@@ -4310,7 +4436,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -498,19 +478,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4338,7 +4466,11 @@ - const tabContainer = pinned - ? this.tabContainer.pinnedTabsContainer - : this.tabContainer; -+ if (itemAfter) { -+ itemAfter.before(tab); -+ } else { - tabContainer.insertBefore(tab, itemAfter); -+ } - } - - this._updateTabsAfterInsert(); -@@ -4346,6 +4478,7 @@ +@@ -4346,6 +4472,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -518,7 +486,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e TabBarVisibility.update(); } -@@ -4814,6 +4944,7 @@ +@@ -4896,6 +5026,7 @@ telemetrySource, } = {} ) { @@ -526,7 +494,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5122,7 @@ +@@ -4985,6 +5116,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -534,7 +502,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } catch (e) { console.error(e); } -@@ -5023,6 +5161,12 @@ +@@ -5023,6 +5155,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -547,7 +515,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5181,9 @@ +@@ -5037,6 +5175,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -557,7 +525,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5232,13 @@ +@@ -5085,7 +5226,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -572,7 +540,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e return; } -@@ -5219,7 +5372,7 @@ +@@ -5219,7 +5366,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -581,7 +549,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5396,7 @@ +@@ -5243,6 +5390,7 @@ newTab = true; } @@ -589,7 +557,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5437,7 @@ +@@ -5283,13 +5431,7 @@ aTab._mouseleave(); if (newTab) { @@ -604,7 +572,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } else { TabBarVisibility.update(); } -@@ -5422,6 +5570,7 @@ +@@ -5422,6 +5564,7 @@ this.tabs[i]._tPos = i; } @@ -612,7 +580,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5792,7 @@ +@@ -5643,6 +5786,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -620,7 +588,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5805,13 @@ +@@ -5655,13 +5799,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -636,7 +604,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5827,7 @@ +@@ -5677,7 +5821,7 @@ } if (tab) { @@ -645,7 +613,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5848,7 @@ +@@ -5698,7 +5842,7 @@ }); } @@ -654,7 +622,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } _blurTab(aTab) { -@@ -6104,10 +6254,10 @@ +@@ -6104,10 +6248,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -667,7 +635,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6166,6 +6316,7 @@ +@@ -6166,6 +6310,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -675,7 +643,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (this.tabs.length == 1) { return null; } -@@ -6299,7 +6450,7 @@ +@@ -6299,7 +6444,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -684,7 +652,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } /** -@@ -6375,8 +6526,8 @@ +@@ -6375,8 +6520,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -695,7 +663,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6402,10 +6553,16 @@ +@@ -6402,10 +6547,16 @@ this.#handleTabMove( element, () => { @@ -714,7 +682,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6463,23 +6620,28 @@ +@@ -6463,23 +6614,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -749,7 +717,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6492,14 +6654,34 @@ +@@ -6492,14 +6648,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -785,7 +753,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6508,7 +6690,7 @@ +@@ -6508,7 +6684,7 @@ element, () => { if (moveBefore) { @@ -794,7 +762,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e } else if (targetElement) { targetElement.after(element); } else { -@@ -6580,10 +6762,10 @@ +@@ -6580,10 +6756,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -807,7 +775,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6613,6 +6795,7 @@ +@@ -6613,6 +6789,7 @@ let state = { tabIndex: tab._tPos, @@ -815,7 +783,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6639,7 +6822,7 @@ +@@ -6639,7 +6816,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -824,7 +792,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6676,6 +6859,10 @@ +@@ -6676,6 +6853,10 @@ moveActionCallback(); @@ -835,7 +803,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7576,7 +7763,7 @@ +@@ -7576,7 +7757,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -844,7 +812,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e window.focus(); aEvent.preventDefault(); break; -@@ -7593,7 +7780,6 @@ +@@ -7593,7 +7774,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -852,7 +820,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e }); break; case "TabGroupCreateByUser": -@@ -8542,6 +8728,7 @@ +@@ -8542,6 +8722,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -860,7 +828,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..cead9e6e52f7354e20b8b64ad06a075e gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9543,7 +9730,7 @@ var TabContextMenu = { +@@ -9543,7 +9724,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; From 00417582fbdd83c64bf302e56a8934521905a8b5 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:57:32 +0100 Subject: [PATCH 29/54] Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch --- .../sessionstore/SessionStore-sys-mjs.patch | 59 +++++++------------ 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 94f08855db..7238d4a487 100644 --- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch @@ -1,25 +1,17 @@ diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs -index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be523e4feb 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d89fb95494 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs -@@ -127,6 +127,8 @@ const TAB_EVENTS = [ +@@ -126,6 +126,8 @@ const TAB_EVENTS = [ + "TabUngrouped", "TabGroupCollapse", "TabGroupExpand", - "TabSplitViewActivate", + "TabAddedToEssentials", + "TabRemovedFromEssentials", ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, { - TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", - TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", - setTimeout: "resource://gre/modules/Timer.sys.mjs", -+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", - }); - - ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { -@@ -1911,6 +1914,8 @@ var SessionStoreInternal = { +@@ -1904,6 +1906,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -28,7 +20,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2151,7 +2156,6 @@ var SessionStoreInternal = { +@@ -2139,7 +2143,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -36,18 +28,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { - }); - this._shouldRestoreLastSession = false; - } -+ else if (!aInitialState && isRegularWindow) { -+ aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows); -+ this.restoreWindows(aWindow, aInitialState, {}); -+ } - - if (this._restoreLastWindow && aWindow.toolbar.visible) { - // always reset (if not a popup window) -@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { +@@ -2372,11 +2375,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -59,7 +40,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { +@@ -3361,7 +3362,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -68,7 +49,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be return; } -@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { +@@ -4073,6 +4074,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -81,7 +62,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { +@@ -4509,6 +4515,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -89,7 +70,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { +@@ -5358,7 +5365,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -98,7 +79,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be removableTabs.push(tab); } } -@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { +@@ -5418,7 +5425,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -107,7 +88,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { +@@ -5609,11 +5616,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -121,7 +102,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { +@@ -5624,6 +5632,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -129,7 +110,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { +@@ -5636,7 +5645,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -138,7 +119,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { +@@ -5748,8 +5757,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -149,7 +130,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { +@@ -5792,6 +5801,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -158,7 +139,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { +@@ -6348,6 +6359,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -172,8 +153,8 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be + if (tabData.zenHasStaticLabel) { + tab.setAttribute("zen-has-static-label", "true"); + } -+ if (tabData.zenSyncId) { -+ tab.setAttribute("zen-sync-id", tabData.zenSyncId); ++ if (tabData.zenPinnedId) { ++ tab.setAttribute("zen-pin-id", tabData.zenPinnedId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); @@ -184,7 +165,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..2a7d429e2ed2f2be5bb554a5fb5fd2be if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { +@@ -7263,7 +7293,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { From c2a50661814eb32eb68f835dd2903283b272ba63 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:57:42 +0100 Subject: [PATCH 30/54] Discard changes to src/browser/components/tabbrowser/content/tab-js.patch --- .../tabbrowser/content/tab-js.patch | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 75153b1734..9970988541 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 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577b5fad08c 100644 +index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362efc001afdd 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -42,7 +42,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 ".tab-label-container": "pinned,selected=visuallyselected,labeldirection", ".tab-label": -@@ -186,7 +189,7 @@ +@@ -184,7 +187,7 @@ } set _visuallySelected(val) { @@ -51,7 +51,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 return; } -@@ -222,11 +225,21 @@ +@@ -220,11 +223,21 @@ } get visible() { @@ -78,7 +78,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } get hidden() { -@@ -297,7 +310,7 @@ +@@ -295,7 +308,7 @@ return false; } @@ -87,7 +87,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } get lastAccessed() { -@@ -374,8 +387,11 @@ +@@ -372,8 +385,11 @@ } get group() { @@ -101,7 +101,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } return null; } -@@ -470,6 +486,8 @@ +@@ -468,6 +484,8 @@ this.style.MozUserFocus = "ignore"; } else if ( event.target.classList.contains("tab-close-button") || @@ -110,7 +110,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 event.target.classList.contains("tab-icon-overlay") || event.target.classList.contains("tab-audio-button") ) { -@@ -524,6 +542,10 @@ +@@ -522,6 +540,10 @@ this.style.MozUserFocus = ""; } @@ -121,7 +121,15 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 on_click(event) { if (event.button != 0) { return; -@@ -584,6 +607,14 @@ +@@ -570,6 +592,7 @@ + ) + ); + } else { ++ gZenPinnedTabManager._removePinnedAttributes(this, true); + gBrowser.removeTab(this, { + animate: true, + triggeringEvent: event, +@@ -582,6 +605,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -136,7 +144,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } on_dblclick(event) { -@@ -607,6 +638,8 @@ +@@ -605,6 +636,8 @@ animate: true, triggeringEvent: event, }); From a61846bf9a78e2c90ded7489c97cae718fb4d736 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:57:55 +0100 Subject: [PATCH 31/54] Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch --- .../tabbrowser/content/tabbrowser-js.patch | 161 +++++++++--------- 1 file changed, 85 insertions(+), 76 deletions(-) diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 1ceba0b2fd..b8655415ac 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,8 +1,8 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc349868d0d0 100644 +index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394b7dbc6a7 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js -@@ -450,15 +450,64 @@ +@@ -432,15 +432,64 @@ return this.tabContainer.visibleTabs; } @@ -69,7 +69,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 set selectedTab(val) { if ( gSharedTabWarning.willShowSharedTabWarning(val) || -@@ -613,6 +662,7 @@ +@@ -588,6 +637,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -77,7 +77,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -898,13 +948,17 @@ +@@ -873,13 +923,17 @@ } this.showTab(aTab); @@ -96,7 +96,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab.setAttribute("pinned", "true"); this._updateTabBarForPinnedTabs(); -@@ -917,11 +971,15 @@ +@@ -892,11 +946,15 @@ } this.#handleTabMove(aTab, () => { @@ -113,7 +113,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }); aTab.style.marginInlineStart = ""; -@@ -1098,6 +1156,8 @@ +@@ -1073,6 +1131,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -122,7 +122,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if ( aIconURL && !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) -@@ -1107,6 +1167,9 @@ +@@ -1082,6 +1142,9 @@ ); return; } @@ -132,7 +132,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1470,6 +1533,7 @@ +@@ -1445,6 +1508,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -140,8 +140,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1622,6 +1686,9 @@ -@@ -1622,6 +1686,9 @@ +@@ -1597,6 +1661,9 @@ } let activeEl = document.activeElement; @@ -151,8 +150,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1945,7 +2012,8 @@ -@@ -1945,7 +2012,8 @@ +@@ -1920,7 +1987,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -162,8 +160,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 return false; } -@@ -2053,7 +2121,7 @@ -@@ -2053,7 +2121,7 @@ +@@ -2028,7 +2096,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -172,8 +169,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2328,6 +2396,7 @@ -@@ -2328,6 +2396,7 @@ +@@ -2303,6 +2371,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -181,8 +177,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2401,8 +2470,7 @@ -@@ -2401,8 +2470,7 @@ +@@ -2376,8 +2445,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -192,8 +187,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 b.setAttribute("transparent", "true"); } -@@ -2567,7 +2635,7 @@ -@@ -2567,7 +2635,7 @@ +@@ -2542,7 +2610,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -202,8 +196,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2626,8 +2694,8 @@ -@@ -2626,8 +2694,8 @@ +@@ -2601,8 +2669,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -214,8 +207,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2814,7 +2882,6 @@ -@@ -2814,7 +2882,6 @@ +@@ -2779,7 +2847,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -223,8 +215,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2990,8 @@ -@@ -2923,6 +2990,8 @@ +@@ -2859,6 +2926,8 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -233,7 +224,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,6 +3002,12 @@ +@@ -2869,6 +2938,12 @@ ); } @@ -246,7 +237,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (!UserInteraction.running("browser.tabs.opening", window)) { UserInteraction.start("browser.tabs.opening", "initting", window); } -@@ -2996,6 +3071,19 @@ +@@ -2932,6 +3007,19 @@ noInitialLabel, skipBackgroundNotify, }); @@ -266,7 +257,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3092,7 @@ +@@ -2940,6 +3028,7 @@ ownerTab, openerTab, pinned, @@ -274,7 +265,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3111,7 @@ +@@ -2958,6 +3047,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -282,7 +273,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 })); if (focusUrlBar) { -@@ -3146,6 +3236,12 @@ +@@ -3078,6 +3168,12 @@ } } @@ -295,7 +286,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3330,10 +3426,10 @@ +@@ -3248,10 +3344,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -307,7 +298,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } if (!color) { -@@ -3354,9 +3450,14 @@ +@@ -3272,9 +3368,14 @@ label, isAdoptingGroup ); @@ -324,7 +315,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); group.addTabs(tabs); -@@ -3477,7 +3578,7 @@ +@@ -3395,7 +3496,7 @@ } this.#handleTabMove(tab, () => @@ -333,7 +324,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); } -@@ -3679,6 +3780,7 @@ +@@ -3597,6 +3698,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -341,7 +332,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3748,6 +3850,7 @@ +@@ -3666,6 +3768,7 @@ openWindowInfo, name, skipLoad, @@ -349,7 +340,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }); } -@@ -3935,7 +4038,7 @@ +@@ -3853,7 +3956,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -358,7 +349,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 let url = "about:blank"; if (tabData.entries?.length) { -@@ -3972,8 +4075,10 @@ +@@ -3890,8 +3993,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -370,7 +361,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (select) { tabToSelect = tab; } -@@ -3985,7 +4090,8 @@ +@@ -3903,7 +4008,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -380,7 +371,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -3999,7 +4105,10 @@ +@@ -3917,7 +4023,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -392,7 +383,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); tabsFragment.appendChild(tabGroup.node); } -@@ -4044,9 +4153,23 @@ +@@ -3962,9 +4071,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -416,7 +407,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4237,11 +4360,14 @@ +@@ -4155,11 +4278,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -432,7 +423,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if ( !bulkOrderedOpen && ((openerTab && -@@ -4253,7 +4379,7 @@ +@@ -4171,7 +4297,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -441,7 +432,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 tabGroup = previousTab.group; } if ( -@@ -4264,7 +4390,7 @@ +@@ -4182,7 +4308,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -450,7 +441,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4292,14 +4418,14 @@ +@@ -4210,14 +4336,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -469,7 +460,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4310,7 +4436,7 @@ +@@ -4228,7 +4354,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -478,7 +469,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4346,6 +4472,7 @@ +@@ -4264,6 +4390,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -486,7 +477,17 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 TabBarVisibility.update(); } -@@ -4896,6 +5026,7 @@ +@@ -4553,6 +4680,9 @@ + return; + } + ++ for (let tab of selectedTabs) { ++ gZenPinnedTabManager._removePinnedAttributes(tab, true); ++ } + this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); + } + +@@ -4814,6 +4944,7 @@ telemetrySource, } = {} ) { @@ -494,7 +495,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5116,7 @@ +@@ -4903,6 +5034,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -502,7 +503,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } catch (e) { console.error(e); } -@@ -5023,6 +5155,12 @@ +@@ -4941,6 +5073,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -515,7 +516,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5175,9 @@ +@@ -4955,6 +5093,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -525,7 +526,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5226,13 @@ +@@ -5003,7 +5144,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -540,7 +541,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 return; } -@@ -5219,7 +5366,7 @@ +@@ -5137,7 +5284,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -549,7 +550,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5390,7 @@ +@@ -5161,6 +5308,7 @@ newTab = true; } @@ -557,7 +558,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5431,7 @@ +@@ -5201,13 +5349,7 @@ aTab._mouseleave(); if (newTab) { @@ -572,7 +573,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else { TabBarVisibility.update(); } -@@ -5422,6 +5564,7 @@ +@@ -5340,6 +5482,7 @@ this.tabs[i]._tPos = i; } @@ -580,7 +581,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5786,7 @@ +@@ -5552,6 +5695,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -588,7 +589,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5799,13 @@ +@@ -5564,13 +5708,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -604,7 +605,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5821,7 @@ +@@ -5586,7 +5730,7 @@ } if (tab) { @@ -613,7 +614,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5842,7 @@ +@@ -5607,7 +5751,7 @@ }); } @@ -622,7 +623,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } _blurTab(aTab) { -@@ -6104,10 +6248,10 @@ +@@ -6013,10 +6157,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -635,7 +636,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6166,6 +6310,7 @@ +@@ -6075,6 +6219,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -643,7 +644,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (this.tabs.length == 1) { return null; } -@@ -6299,7 +6444,7 @@ +@@ -6208,7 +6353,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -652,7 +653,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } /** -@@ -6375,8 +6520,8 @@ +@@ -6284,8 +6429,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -663,7 +664,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6402,10 +6547,16 @@ +@@ -6311,10 +6456,16 @@ this.#handleTabMove( element, () => { @@ -682,7 +683,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6463,23 +6614,28 @@ +@@ -6372,23 +6523,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -717,7 +718,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6492,14 +6648,34 @@ +@@ -6401,14 +6557,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -753,7 +754,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6508,7 +6684,7 @@ +@@ -6417,7 +6593,7 @@ element, () => { if (moveBefore) { @@ -762,7 +763,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 } else if (targetElement) { targetElement.after(element); } else { -@@ -6580,10 +6756,10 @@ +@@ -6489,10 +6665,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -775,7 +776,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6613,6 +6789,7 @@ +@@ -6522,6 +6698,7 @@ let state = { tabIndex: tab._tPos, @@ -783,7 +784,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6639,7 +6816,7 @@ +@@ -6548,7 +6725,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -792,7 +793,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6676,6 +6853,10 @@ +@@ -6585,6 +6762,10 @@ moveActionCallback(); @@ -803,7 +804,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7576,7 +7757,7 @@ +@@ -7486,7 +7667,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -812,7 +813,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 window.focus(); aEvent.preventDefault(); break; -@@ -7593,7 +7774,6 @@ +@@ -7501,7 +7682,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -820,7 +821,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 }); break; case "TabGroupCreateByUser": -@@ -8542,6 +8722,7 @@ +@@ -8442,6 +8622,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -828,7 +829,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9543,7 +9724,7 @@ var TabContextMenu = { +@@ -9443,7 +9624,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; @@ -837,3 +838,11 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..326bf96d9346aba7096d518fbf63cc34 // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), +@@ -9763,6 +9944,7 @@ var TabContextMenu = { + ) + ); + } else { ++ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true); + gBrowser.removeTab(this.contextTab, { + animate: true, + ...gBrowser.TabMetrics.userTriggeredContext( From c5df63711be33b1498a6f44acc28b1f8ed7f5720 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:58:18 +0100 Subject: [PATCH 32/54] Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs --- src/zen/tabs/ZenPinnedTabsStorage.mjs | 635 ++++++++++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 src/zen/tabs/ZenPinnedTabsStorage.mjs diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs new file mode 100644 index 0000000000..425dbf2d11 --- /dev/null +++ b/src/zen/tabs/ZenPinnedTabsStorage.mjs @@ -0,0 +1,635 @@ +// 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/. +var ZenPinnedTabsStorage = { + async init() { + await this._ensureTable(); + }, + + async _ensureTable() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => { + // Create the pins table if it doesn't exist + await db.execute(` + CREATE TABLE IF NOT EXISTS zen_pins ( + id INTEGER PRIMARY KEY, + uuid TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + url TEXT, + container_id INTEGER, + workspace_uuid TEXT, + position INTEGER NOT NULL DEFAULT 0, + is_essential BOOLEAN NOT NULL DEFAULT 0, + is_group BOOLEAN NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `); + + const columns = await db.execute(`PRAGMA table_info(zen_pins)`); + const columnNames = columns.map((row) => row.getResultByName('name')); + + // Helper function to add column if it doesn't exist + const addColumnIfNotExists = async (columnName, definition) => { + if (!columnNames.includes(columnName)) { + await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`); + } + }; + + await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0'); + await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0'); + await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL'); + await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL'); + + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) + `); + + await db.execute(` + CREATE TABLE IF NOT EXISTS zen_pins_changes ( + uuid TEXT PRIMARY KEY, + timestamp INTEGER NOT NULL + ) + `); + + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid) + `); + + this._resolveInitialized(); + }); + }, + + /** + * Private helper method to notify observers with a list of changed UUIDs. + * @param {string} event - The observer event name. + * @param {Array} uuids - Array of changed workspace UUIDs. + */ + _notifyPinsChanged(event, uuids) { + if (uuids.length === 0) return; // No changes to notify + + // Convert the array of UUIDs to a JSON string + const data = JSON.stringify(uuids); + + Services.obs.notifyObservers(null, event, data); + }, + + async savePin(pin, notifyObservers = true) { + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + let newPosition; + if ('position' in pin && Number.isFinite(pin.position)) { + newPosition = pin.position; + } else { + // Get the maximum position within the same parent group (or null for root level) + const maxPositionResult = await db.execute( + ` + SELECT MAX("position") as max_position + FROM zen_pins + WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '') + `, + { folder_parent_uuid: pin.parentUuid || null } + ); + const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; + newPosition = maxPosition + 1000; + } + + // Insert or replace the pin + await db.executeCached( + ` + INSERT OR REPLACE INTO zen_pins ( + uuid, title, url, container_id, workspace_uuid, position, + is_essential, is_group, folder_parent_uuid, edited_title, created_at, + updated_at, is_folder_collapsed, folder_icon + ) VALUES ( + :uuid, :title, :url, :container_id, :workspace_uuid, :position, + :is_essential, :is_group, :folder_parent_uuid, :edited_title, + COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), + :now, :is_folder_collapsed, :folder_icon + ) + `, + { + uuid: pin.uuid, + title: pin.title, + url: pin.isGroup ? '' : pin.url, + container_id: pin.containerTabId || null, + workspace_uuid: pin.workspaceUuid || null, + position: newPosition, + is_essential: pin.isEssential || false, + is_group: pin.isGroup || false, + folder_parent_uuid: pin.parentUuid || null, + edited_title: pin.editedTitle || false, + now, + folder_icon: pin.folderIcon || null, + is_folder_collapsed: pin.isFolderCollapsed || false, + } + ); + + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: pin.uuid, + timestamp: Math.floor(now / 1000), + } + ); + + changedUUIDs.add(pin.uuid); + await this.updateLastChangeTimestamp(db); + }); + }); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + async getPins() { + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.executeCached(` + SELECT * FROM zen_pins + ORDER BY position ASC + `); + return rows.map((row) => ({ + uuid: row.getResultByName('uuid'), + title: row.getResultByName('title'), + url: row.getResultByName('url'), + containerTabId: row.getResultByName('container_id'), + workspaceUuid: row.getResultByName('workspace_uuid'), + position: row.getResultByName('position'), + isEssential: Boolean(row.getResultByName('is_essential')), + isGroup: Boolean(row.getResultByName('is_group')), + parentUuid: row.getResultByName('folder_parent_uuid'), + editedTitle: Boolean(row.getResultByName('edited_title')), + folderIcon: row.getResultByName('folder_icon'), + isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), + })); + }, + + /** + * Create a new group + * @param {string} title - The title of the group + * @param {string} workspaceUuid - The workspace UUID (optional) + * @param {string} parentUuid - The parent group UUID (optional, null for root level) + * @param {number} position - The position of the group (optional, will auto-calculate if not provided) + * @param {boolean} notifyObservers - Whether to notify observers (default: true) + * @returns {Promise} The UUID of the created group + */ + async createGroup( + title, + icon = null, + isCollapsed = false, + workspaceUuid = null, + parentUuid = null, + position = null, + notifyObservers = true + ) { + if (!title || typeof title !== 'string') { + throw new Error('Group title is required and must be a string'); + } + + const groupUuid = gZenUIManager.generateUuidv4(); + + const groupPin = { + uuid: groupUuid, + title, + folderIcon: icon || null, + isFolderCollapsed: isCollapsed || false, + workspaceUuid, + parentUuid, + position, + isGroup: true, + isEssential: false, + editedTitle: true, // Group titles are always considered edited + }; + + await this.savePin(groupPin, notifyObservers); + return groupUuid; + }, + + /** + * Add an existing tab/pin to a group + * @param {string} tabUuid - The UUID of the tab to add to the group + * @param {string} groupUuid - The UUID of the target group + * @param {number} position - The position within the group (optional, will append if not provided) + * @param {boolean} notifyObservers - Whether to notify observers (default: true) + */ + async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) { + if (!tabUuid || !groupUuid) { + throw new Error('Both tabUuid and groupUuid are required'); + } + + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => { + await db.executeTransaction(async () => { + // Verify the group exists and is actually a group + const groupCheck = await db.execute( + `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`, + { groupUuid } + ); + + if (groupCheck.length === 0) { + throw new Error(`Group with UUID ${groupUuid} does not exist`); + } + + if (!groupCheck[0].getResultByName('is_group')) { + throw new Error(`Pin with UUID ${groupUuid} is not a group`); + } + + const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, { + tabUuid, + }); + + if (tabCheck.length === 0) { + throw new Error(`Tab with UUID ${tabUuid} does not exist`); + } + + const now = Date.now(); + let newPosition; + + if (position !== null && Number.isFinite(position)) { + newPosition = position; + } else { + // Get the maximum position within the group + const maxPositionResult = await db.execute( + `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`, + { groupUuid } + ); + const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; + newPosition = maxPosition + 1000; + } + + await db.execute( + ` + UPDATE zen_pins + SET folder_parent_uuid = :groupUuid, + position = :newPosition, + updated_at = :now + WHERE uuid = :tabUuid + `, + { + tabUuid, + groupUuid, + newPosition, + now, + } + ); + + changedUUIDs.add(tabUuid); + + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: tabUuid, + timestamp: Math.floor(now / 1000), + } + ); + + await this.updateLastChangeTimestamp(db); + }); + }); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + /** + * Remove a tab from its group (move to root level) + * @param {string} tabUuid - The UUID of the tab to remove from its group + * @param {number} newPosition - The new position at root level (optional, will append if not provided) + * @param {boolean} notifyObservers - Whether to notify observers (default: true) + */ + async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) { + if (!tabUuid) { + throw new Error('tabUuid is required'); + } + + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper( + 'ZenPinnedTabsStorage.removeTabFromGroup', + async (db) => { + await db.executeTransaction(async () => { + // Verify the tab exists and is in a group + const tabCheck = await db.execute( + `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`, + { tabUuid } + ); + + if (tabCheck.length === 0) { + throw new Error(`Tab with UUID ${tabUuid} does not exist`); + } + + if (!tabCheck[0].getResultByName('folder_parent_uuid')) { + return; + } + + const now = Date.now(); + let finalPosition; + + if (newPosition !== null && Number.isFinite(newPosition)) { + finalPosition = newPosition; + } else { + // Get the maximum position at root level (where folder_parent_uuid is null) + const maxPositionResult = await db.execute( + `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL` + ); + const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; + finalPosition = maxPosition + 1000; + } + + // Update the tab to be at root level + await db.execute( + ` + UPDATE zen_pins + SET folder_parent_uuid = NULL, + position = :newPosition, + updated_at = :now + WHERE uuid = :tabUuid + `, + { + tabUuid, + newPosition: finalPosition, + now, + } + ); + + changedUUIDs.add(tabUuid); + + // Record the change + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: tabUuid, + timestamp: Math.floor(now / 1000), + } + ); + + await this.updateLastChangeTimestamp(db); + }); + } + ); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + async removePin(uuid, notifyObservers = true) { + const changedUUIDs = [uuid]; + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { + await db.executeTransaction(async () => { + // Get all child UUIDs first for change tracking + const children = await db.execute( + `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`, + { + uuid, + } + ); + + // Add child UUIDs to changedUUIDs array + for (const child of children) { + changedUUIDs.push(child.getResultByName('uuid')); + } + + // Delete the pin/group itself + await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid }); + + // Record the changes + const now = Math.floor(Date.now() / 1000); + for (const changedUuid of changedUUIDs) { + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: changedUuid, + timestamp: now, + } + ); + } + + await this.updateLastChangeTimestamp(db); + }); + }); + + if (notifyObservers) { + this._notifyPinsChanged('zen-pin-removed', changedUUIDs); + } + }, + + async wipeAllPins() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => { + await db.execute(`DELETE FROM zen_pins`); + await db.execute(`DELETE FROM zen_pins_changes`); + await this.updateLastChangeTimestamp(db); + }); + }, + + async markChanged(uuid) { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => { + const now = Date.now(); + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid, + timestamp: Math.floor(now / 1000), + } + ); + }); + }, + + async getChangedIDs() { + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.execute(` + SELECT uuid, timestamp FROM zen_pins_changes + `); + const changes = {}; + for (const row of rows) { + changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); + } + return changes; + }, + + async clearChangedIDs() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => { + await db.execute(`DELETE FROM zen_pins_changes`); + }); + }, + + shouldReorderPins(before, current, after) { + const minGap = 1; // Minimum allowed gap between positions + return ( + (before !== null && current - before < minGap) || (after !== null && after - current < minGap) + ); + }, + + async reorderAllPins(db, changedUUIDs) { + const pins = await db.execute(` + SELECT uuid + FROM zen_pins + ORDER BY position ASC + `); + + for (let i = 0; i < pins.length; i++) { + const newPosition = (i + 1) * 1000; // Use large increments + await db.execute( + ` + UPDATE zen_pins + SET position = :newPosition + WHERE uuid = :uuid + `, + { newPosition, uuid: pins[i].getResultByName('uuid') } + ); + changedUUIDs.add(pins[i].getResultByName('uuid')); + } + }, + + async updateLastChangeTimestamp(db) { + const now = Date.now(); + await db.execute( + ` + INSERT OR REPLACE INTO moz_meta (key, value) + VALUES ('zen_pins_last_change', :now) + `, + { now } + ); + }, + + async getLastChangeTimestamp() { + const db = await PlacesUtils.promiseDBConnection(); + const result = await db.executeCached(` + SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change' + `); + return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; + }, + + async updatePinPositions(pins) { + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper( + 'ZenPinnedTabsStorage.updatePinPositions', + async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + for (let i = 0; i < pins.length; i++) { + const pin = pins[i]; + const newPosition = (i + 1) * 1000; + + await db.execute( + ` + UPDATE zen_pins + SET position = :newPosition + WHERE uuid = :uuid + `, + { newPosition, uuid: pin.uuid } + ); + + changedUUIDs.add(pin.uuid); + + // Record the change + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid: pin.uuid, + timestamp: Math.floor(now / 1000), + } + ); + } + + await this.updateLastChangeTimestamp(db); + }); + } + ); + + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + }, + + async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) { + if (!uuid || typeof newTitle !== 'string') { + throw new Error('Invalid parameters: uuid and newTitle are required'); + } + + const changedUUIDs = new Set(); + + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => { + await db.executeTransaction(async () => { + const now = Date.now(); + + // Update the pin's title and edited_title flag + const result = await db.execute( + ` + UPDATE zen_pins + SET title = :newTitle, + edited_title = :isEdited, + updated_at = :now + WHERE uuid = :uuid + `, + { + uuid, + newTitle, + isEdited, + now, + } + ); + + // Only proceed with change tracking if a row was actually updated + if (result.rowsAffected > 0) { + changedUUIDs.add(uuid); + + // Record the change + await db.execute( + ` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, + { + uuid, + timestamp: Math.floor(now / 1000), + } + ); + + await this.updateLastChangeTimestamp(db); + } + }); + }); + + if (notifyObservers && changedUUIDs.size > 0) { + this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); + } + }, + + async __dropTables() { + await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => { + await db.execute(`DROP TABLE IF EXISTS zen_pins`); + await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`); + }); + }, +}; + +ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => { + ZenPinnedTabsStorage._resolveInitialized = resolve; + ZenPinnedTabsStorage.init(); +}); From 6fbba04dabe9093c977a921e9d9b821c54a23410 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Thu, 13 Nov 2025 14:40:52 +0100 Subject: [PATCH 33/54] feat: Run session saver before opening a new winodw, b=no-bug, c=tabs --- .../sessionstore/SessionFile-sys-mjs.patch | 4 +- .../sessionstore/SessionStore-sys-mjs.patch | 55 ++++--- .../tabbrowser/content/tab-js.patch | 14 +- .../tabbrowser/content/tabbrowser-js.patch | 80 +++++------ .../sessionstore/ZenSessionManager.sys.mjs | 10 +- src/zen/tabs/ZenPinnedTabManager.mjs | 135 +++++++++--------- 6 files changed, 148 insertions(+), 150 deletions(-) diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch index 04aa6f0cac..895c4313fa 100644 --- a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs -index 157c55ab24a418b56690d2e26320582909b919e4..14755f57dc450583e69eee94eb11f16980d5e5cb 100644 +index 31140cb8be3b529a0952ca8dc55165690b0e2120..605c9e0aa84da0a2d3171a0573e8cd95e27bd0c4 100644 --- a/browser/components/sessionstore/SessionFile.sys.mjs +++ b/browser/components/sessionstore/SessionFile.sys.mjs @@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, { @@ -10,7 +10,7 @@ index 157c55ab24a418b56690d2e26320582909b919e4..14755f57dc450583e69eee94eb11f169 }); const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID"; -@@ -364,7 +365,7 @@ var SessionFileInternal = { +@@ -380,7 +381,7 @@ var SessionFileInternal = { this._readOrigin = result.origin; result.noFilesFound = noFilesFound; diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 7238d4a487..0362605b1b 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 eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d89fb95494 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc3315aa98 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -126,6 +126,8 @@ const TAB_EVENTS = [ @@ -11,7 +11,15 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -1904,6 +1906,8 @@ var SessionStoreInternal = { +@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { +@@ -1911,6 +1914,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -20,7 +28,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2139,7 +2143,6 @@ var SessionStoreInternal = { +@@ -2151,7 +2156,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -28,7 +36,18 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2372,11 +2375,9 @@ var SessionStoreInternal = { +@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { + }); + this._shouldRestoreLastSession = false; + } ++ else if (!aInitialState && isRegularWindow) { ++ aInitialState = lazy.ZenSessionStore.getNewWindowData(); ++ this.restoreWindows(aWindow, aInitialState, {}); ++ } + + if (this._restoreLastWindow && aWindow.toolbar.visible) { + // always reset (if not a popup window) +@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -40,7 +59,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3361,7 +3362,7 @@ var SessionStoreInternal = { +@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -49,7 +68,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 return; } -@@ -4073,6 +4074,11 @@ var SessionStoreInternal = { +@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -62,7 +81,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4509,6 +4515,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -70,7 +89,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5358,7 +5365,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -79,7 +98,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 removableTabs.push(tab); } } -@@ -5418,7 +5425,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -88,7 +107,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 winData.workspaceID = workspaceID; } }, -@@ -5609,11 +5616,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -102,7 +121,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5624,6 +5632,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -110,7 +129,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5636,7 +5645,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -119,7 +138,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5748,8 +5757,8 @@ var SessionStoreInternal = { +@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -130,7 +149,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5792,6 +5801,8 @@ var SessionStoreInternal = { +@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -139,7 +158,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6348,6 +6359,25 @@ var SessionStoreInternal = { +@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -153,8 +172,8 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 + if (tabData.zenHasStaticLabel) { + tab.setAttribute("zen-has-static-label", "true"); + } -+ if (tabData.zenPinnedId) { -+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId); ++ if (tabData.zenSyncId) { ++ tab.setAttribute("zen-sync-id", tabData.zenSyncId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); @@ -165,7 +184,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7263,7 +7293,7 @@ var SessionStoreInternal = { +@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 9970988541..796fdb7fce 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 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362efc001afdd 100644 +index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..ce54ed0c8a93d5521a436c55c9432c090b0420ac 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -121,15 +121,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef on_click(event) { if (event.button != 0) { return; -@@ -570,6 +592,7 @@ - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this, true); - gBrowser.removeTab(this, { - animate: true, - triggeringEvent: event, -@@ -582,6 +605,14 @@ +@@ -584,6 +606,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -144,7 +136,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef } on_dblclick(event) { -@@ -605,6 +636,8 @@ +@@ -607,6 +637,8 @@ animate: true, triggeringEvent: event, }); diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index b8655415ac..ced7c7b499 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394b7dbc6a7 100644 +index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4cd44d980e 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -432,15 +432,64 @@ @@ -400,9 +400,11 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 + } + } + } ++ } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; } + } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { @@ -477,17 +479,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 TabBarVisibility.update(); } -@@ -4553,6 +4680,9 @@ - return; - } - -+ for (let tab of selectedTabs) { -+ gZenPinnedTabManager._removePinnedAttributes(tab, true); -+ } - this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); - } - -@@ -4814,6 +4944,7 @@ +@@ -4896,6 +5024,7 @@ telemetrySource, } = {} ) { @@ -495,7 +487,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4903,6 +5034,7 @@ +@@ -4985,6 +5114,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -503,7 +495,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } catch (e) { console.error(e); } -@@ -4941,6 +5073,12 @@ +@@ -5023,6 +5153,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -516,7 +508,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -4955,6 +5093,9 @@ +@@ -5037,6 +5173,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -526,7 +518,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5003,7 +5144,13 @@ +@@ -5085,7 +5224,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -541,7 +533,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 return; } -@@ -5137,7 +5284,7 @@ +@@ -5219,7 +5364,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -550,7 +542,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5161,6 +5308,7 @@ +@@ -5243,6 +5388,7 @@ newTab = true; } @@ -558,7 +550,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5201,13 +5349,7 @@ +@@ -5283,13 +5429,7 @@ aTab._mouseleave(); if (newTab) { @@ -573,7 +565,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else { TabBarVisibility.update(); } -@@ -5340,6 +5482,7 @@ +@@ -5422,6 +5562,7 @@ this.tabs[i]._tPos = i; } @@ -581,7 +573,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5552,6 +5695,7 @@ +@@ -5643,6 +5784,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -589,7 +581,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5564,13 +5708,13 @@ +@@ -5655,13 +5797,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -605,7 +597,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5586,7 +5730,7 @@ +@@ -5677,7 +5819,7 @@ } if (tab) { @@ -614,7 +606,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } // If no qualifying visible tab was found, see if there is a tab in -@@ -5607,7 +5751,7 @@ +@@ -5698,7 +5840,7 @@ }); } @@ -623,7 +615,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } _blurTab(aTab) { -@@ -6013,10 +6157,10 @@ +@@ -6104,10 +6246,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -636,7 +628,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6075,6 +6219,7 @@ +@@ -6166,6 +6308,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -644,7 +636,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (this.tabs.length == 1) { return null; } -@@ -6208,7 +6353,7 @@ +@@ -6299,7 +6442,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -653,7 +645,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } /** -@@ -6284,8 +6429,8 @@ +@@ -6375,8 +6518,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -664,7 +656,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6311,10 +6456,16 @@ +@@ -6402,10 +6545,16 @@ this.#handleTabMove( element, () => { @@ -683,7 +675,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6372,23 +6523,28 @@ +@@ -6463,23 +6612,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -718,7 +710,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6401,14 +6557,34 @@ +@@ -6492,14 +6646,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -754,7 +746,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6417,7 +6593,7 @@ +@@ -6508,7 +6682,7 @@ element, () => { if (moveBefore) { @@ -763,7 +755,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 } else if (targetElement) { targetElement.after(element); } else { -@@ -6489,10 +6665,10 @@ +@@ -6580,10 +6754,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -776,7 +768,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6522,6 +6698,7 @@ +@@ -6613,6 +6787,7 @@ let state = { tabIndex: tab._tPos, @@ -784,7 +776,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6548,7 +6725,7 @@ +@@ -6639,7 +6814,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -793,7 +785,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6585,6 +6762,10 @@ +@@ -6676,6 +6851,10 @@ moveActionCallback(); @@ -804,7 +796,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7486,7 +7667,7 @@ +@@ -7576,7 +7755,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -813,7 +805,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 window.focus(); aEvent.preventDefault(); break; -@@ -7501,7 +7682,6 @@ +@@ -7593,7 +7772,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -821,7 +813,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 }); break; case "TabGroupCreateByUser": -@@ -8442,6 +8622,7 @@ +@@ -8542,6 +8720,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -829,7 +821,7 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9443,7 +9624,7 @@ var TabContextMenu = { +@@ -9543,7 +9722,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; @@ -838,11 +830,3 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394 // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), -@@ -9763,6 +9944,7 @@ var TabContextMenu = { - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true); - gBrowser.removeTab(this.contextTab, { - animate: true, - ...gBrowser.TabMetrics.userTriggeredContext( diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index e8cf59114d..89ecc9bbb7 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -10,6 +10,7 @@ ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', + SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs', }); const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes @@ -86,7 +87,7 @@ class nsZenSessionManager { if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { // Don't save (or even collect) anything in permanent private // browsing mode - return Promise.resolve(); + return; } // Collect an initial snapshot of window data before we do the flush. const window = this.#topMostWindow; @@ -154,8 +155,11 @@ class nsZenSessionManager { aWindowData.groups = sidebar.groups; } - getNewWindowData(aWindows) { - let newWindow = { ...Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {}), ...this.#sidebar }; + getNewWindowData() { + lazy.SessionSaver.run(); + const state = lazy.SessionStore.getCurrentState(forceUpdateAllWindows); + const windows = state.windows || {}; + let newWindow = { ...Cu.cloneInto(windows[Object.keys(windows)[0]], {}), ...this.#sidebar }; return { windows: [newWindow] }; } } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 19a2c0d562..8ced9364c4 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -229,87 +229,86 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { behavior = behavior.contains('reset') ? 'reset-unload-switch' : 'unload-switch'; } - switch (behavior) { - case 'close': { - for (const tab of pinnedTabs) { - this._removePinnedAttributes(tab, true); - gBrowser.removeTab(tab, { animate: true }); - } - break; - } - case 'reset-unload-switch': - case 'unload-switch': - case 'reset-switch': - case 'switch': - if (behavior.includes('unload')) { + switch (behavior) { + case 'close': { for (const tab of pinnedTabs) { - if (tab.hasAttribute('glance-id')) { - // We have a glance tab inside the tab we are trying to unload, - // before we used to just ignore it but now we need to fully close - // it as well. - gZenGlanceManager.manageTabClose(tab.glanceTab); - await new Promise((resolve) => { - let hasRan = false; - const onGlanceClose = () => { - hasRan = true; - resolve(); - }; - window.addEventListener('GlanceClose', onGlanceClose, { once: true }); - // Set a timeout to resolve the promise if the event doesn't fire. - // We do this to prevent any future issues where glance woudnt close such as - // glance requering to ask for permit unload. - setTimeout(() => { - if (!hasRan) { - console.warn('GlanceClose event did not fire within 3 seconds'); + gBrowser.removeTab(tab, { animate: true }); + } + break; + } + case 'reset-unload-switch': + case 'unload-switch': + case 'reset-switch': + case 'switch': + if (behavior.includes('unload')) { + for (const tab of pinnedTabs) { + if (tab.hasAttribute('glance-id')) { + // We have a glance tab inside the tab we are trying to unload, + // before we used to just ignore it but now we need to fully close + // it as well. + gZenGlanceManager.manageTabClose(tab.glanceTab); + await new Promise((resolve) => { + let hasRan = false; + const onGlanceClose = () => { + hasRan = true; resolve(); - } - }, 3000); - }); - return; + }; + window.addEventListener('GlanceClose', onGlanceClose, { once: true }); + // Set a timeout to resolve the promise if the event doesn't fire. + // We do this to prevent any future issues where glance woudnt close such as + // glance requering to ask for permit unload. + setTimeout(() => { + if (!hasRan) { + console.warn('GlanceClose event did not fire within 3 seconds'); + resolve(); + } + }, 3000); + }); + return; + } + const isSpltView = tab.group?.hasAttribute('split-view-group'); + const group = isSpltView ? tab.group.group : tab.group; + if (!folderToUnload && tab.hasAttribute('folder-active')) { + await gZenFolders.animateUnload(group, tab); + } } - const isSpltView = tab.group?.hasAttribute('split-view-group'); - const group = isSpltView ? tab.group.group : tab.group; - if (!folderToUnload && tab.hasAttribute('folder-active')) { - await gZenFolders.animateUnload(group, tab); + if (folderToUnload) { + await gZenFolders.animateUnloadAll(folderToUnload); + } + const allAreUnloaded = pinnedTabs.every( + (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential') + ); + for (const tab of pinnedTabs) { + if (allAreUnloaded && closeIfPending) { + return await this.onCloseTabShortcut(event, tab, { behavior: 'close' }); + } + } + await gBrowser.explicitUnloadTabs(pinnedTabs); + for (const tab of pinnedTabs) { + tab.removeAttribute('discarded'); } } - if (folderToUnload) { - await gZenFolders.animateUnloadAll(folderToUnload); + if (selectedTabs.length) { + this._handleTabSwitch(selectedTabs[0]); } - const allAreUnloaded = pinnedTabs.every( - (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential') - ); - for (const tab of pinnedTabs) { - if (allAreUnloaded && closeIfPending) { - return await this.onCloseTabShortcut(event, tab, { behavior: 'close' }); + if (behavior.includes('reset')) { + for (const tab of pinnedTabs) { + this._resetTabToStoredState(tab); } } - await gBrowser.explicitUnloadTabs(pinnedTabs); - for (const tab of pinnedTabs) { - tab.removeAttribute('discarded'); - } - } - if (selectedTabs.length) { - this._handleTabSwitch(selectedTabs[0]); - } - if (behavior.includes('reset')) { + break; + case 'reset': for (const tab of pinnedTabs) { this._resetTabToStoredState(tab); } - } - break; - case 'reset': - for (const tab of pinnedTabs) { - this._resetTabToStoredState(tab); - } - break; - default: - return; + break; + default: + return; + } + } catch (ex) { + console.error('Error handling close tab shortcut for pinned tab:', ex); } - } catch (ex) { - console.error('Error handling close tab shortcut for pinned tab:', ex); } - } _handleTabSwitch(selectedTab) { if (selectedTab !== gBrowser.selectedTab) { From 9c5bf19ae3c04c90f6dc1a6595005b9857a648ff Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Thu, 13 Nov 2025 17:03:27 +0100 Subject: [PATCH 34/54] feat: Clone the previous state, b=no-bug, c=no-component --- src/zen/sessionstore/ZenSessionManager.sys.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 89ecc9bbb7..2a229dacab 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -157,9 +157,9 @@ class nsZenSessionManager { getNewWindowData() { lazy.SessionSaver.run(); - const state = lazy.SessionStore.getCurrentState(forceUpdateAllWindows); + const state = lazy.SessionStore.getCurrentState(true); const windows = state.windows || {}; - let newWindow = { ...Cu.cloneInto(windows[Object.keys(windows)[0]], {}), ...this.#sidebar }; + let newWindow = Cu.cloneInto(windows[0], {}); return { windows: [newWindow] }; } } From 8a963a9257cb0a983b9210d0ffd01d885e3f2d48 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Mon, 17 Nov 2025 13:36:57 +0100 Subject: [PATCH 35/54] feat: Move window sync to its own JS module, b=no-bug, c=workspaces --- .../sessionstore/SessionComponents.manifest | 5 + .../sessionstore/ZenSessionManager.sys.mjs | 16 +- src/zen/sessionstore/ZenWindowSync.sys.mjs | 23 ++ src/zen/sessionstore/moz.build | 1 + src/zen/workspaces/ZenWindowSyncing.mjs | 308 ------------------ 5 files changed, 38 insertions(+), 315 deletions(-) create mode 100644 src/zen/sessionstore/ZenWindowSync.sys.mjs delete mode 100644 src/zen/workspaces/ZenWindowSyncing.mjs diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest index f8f08d1d79..9da9dc1054 100644 --- a/src/zen/sessionstore/SessionComponents.manifest +++ b/src/zen/sessionstore/SessionComponents.manifest @@ -4,3 +4,8 @@ # Browser global components initializing before UI startup category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init +category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init + +# App shutdown consumers +category browser-quit-application-granted resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.uninit +category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 2a229dacab..06478075e9 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -25,7 +25,15 @@ class nsZenSessionManager { // Called from SessionComponents.manifest on app-startup init() { - this.#initObservers(); + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + uninit() { + for (let topic of OBSERVING) { + Services.obs.removeObserver(this, topic); + } } async readFile() { @@ -38,12 +46,6 @@ class nsZenSessionManager { } } - #initObservers() { - for (let topic of OBSERVING) { - Services.obs.addObserver(this, topic); - } - } - get #sidebar() { return this.#file.sidebar; } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs new file mode 100644 index 0000000000..a96e096872 --- /dev/null +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -0,0 +1,23 @@ +// 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 OBSERVING = ['browser-window-before-show']; + +class nsZenWindowSync { + constructor() {} + + init() { + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + uninit() { + for (let topic of OBSERVING) { + Services.obs.removeObserver(this, topic); + } + } +} + +export const ZenWindowSync = new nsZenWindowSync(); diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build index af5a7dd3bb..902ae2a301 100644 --- a/src/zen/sessionstore/moz.build +++ b/src/zen/sessionstore/moz.build @@ -5,4 +5,5 @@ EXTRA_JS_MODULES.zen += [ "ZenSessionFile.sys.mjs", "ZenSessionManager.sys.mjs", + "ZenWindowSync.sys.mjs", ] diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs deleted file mode 100644 index 4c06ae0e06..0000000000 --- a/src/zen/workspaces/ZenWindowSyncing.mjs +++ /dev/null @@ -1,308 +0,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 nsZenWorkspaceWindowSync extends nsZenMultiWindowFeature { - #ignoreNextEvents = false; - #waitForPromise = null; - - constructor() { - super(); - if (!window.closed) { - this.init(); - } - } - - async init() { - await gZenWorkspaces.promiseInitialized; - this.#makeSureAllTabsHaveIds(); - this.#setUpEventListeners(); - } - - #makeSureAllTabsHaveIds() { - const allTabs = gZenWorkspaces.allStoredTabs; - for (const tab of allTabs) { - if (!tab.hasAttribute('zen-sync-id') && !tab.hasAttribute('zen-empty-tab')) { - const tabId = gZenUIManager.generateUuidv4(); - tab.setAttribute('zen-sync-id', tabId); - } - } - } - - #setUpEventListeners() { - const kEvents = [ - 'TabClose', - 'TabOpen', - 'TabMove', - - 'TabPinned', - 'TabUnpinned', - - 'TabAddedToEssentials', - 'TabRemovedFromEssentials', - - 'TabHide', - 'TabShow', - - 'ZenTabIconChanged', - 'ZenTabLabelChanged', - - 'TabGroupCreate', - 'TabGroupRemoved', - 'TabGrouped', - 'TabUngrouped', - 'TabGroupMoved', - ]; - const eventListener = this.#handleEvent.bind(this); - for (const event of kEvents) { - window.addEventListener(event, eventListener); - } - - window.addEventListener('unload', () => { - for (const event of kEvents) { - window.removeEventListener(event, eventListener); - } - }); - } - - #handleEvent(event) { - this.#propagateToOtherWindows(event); - } - - async #propagateToOtherWindows(event) { - if (this.#ignoreNextEvents) { - return; - } - if (this.#waitForPromise) { - await this.#waitForPromise; - } - this.#waitForPromise = new Promise((resolve) => { - this.foreachWindowAsActive(async (browser) => { - if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) { - await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event); - } - }).then(() => { - resolve(); - }); - }); - } - - async onExternalTabEvent(event) { - this.#ignoreNextEvents = true; - switch (event.type) { - case 'TabClose': - this.#onTabClose(event); - break; - case 'TabOpen': - await this.#onTabOpen(event); - break; - case 'TabPinned': - this.#onTabPinned(event); - break; - case 'TabUnpinned': - this.#onTabUnpinned(event); - break; - case 'TabAddedToEssentials': - this.#onTabAddedToEssentials(event); - break; - case 'TabRemovedFromEssentials': - this.#onTabRemovedFromEssentials(event); - break; - case 'TabHide': - this.#onTabHide(event); - break; - case 'TabShow': - this.#onTabShow(event); - break; - case 'TabMove': - case 'TabGroupMoved': - this.#onTabMove(event); - break; - case 'ZenTabIconChanged': - this.#onTabIconChanged(event); - break; - case 'ZenTabLabelChanged': - this.#onTabLabelChanged(event); - break; - case 'TabGroupCreate': - this.#onTabGroupCreate(event); - break; - case 'TabGroupRemoved': - case 'TabGrouped': - case 'TabUngrouped': - // Tab grouping changes are automatically synced by Firefox - break; - default: - console.warn(`Unhandled event type: ${event.type}`); - break; - } - this.#ignoreNextEvents = false; - } - - #getTabId(tab) { - return tab.getAttribute('zen-sync-id'); - } - - #getTabWithId(tabId) { - for (const tab of gZenWorkspaces.allStoredTabs) { - if (this.#getTabId(tab) === tabId) { - return tab; - } - } - return null; - } - - #onTabClose(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToClose = this.#getTabWithId(tabId); - if (tabToClose) { - gBrowser.removeTab(tabToClose); - } - } - - #onTabPinned(event) { - const targetTab = event.target; - if (targetTab.hasAttribute('zen-essential')) { - return this.#onTabAddedToEssentials(event); - } - const tabId = this.#getTabId(targetTab); - const elementIndex = targetTab.elementIndex; - const tabToPin = this.#getTabWithId(tabId); - if (tabToPin) { - gBrowser.pinTab(tabToPin); - gBrowser.moveTabTo(tabToPin, { elementIndex, forceUngrouped: !!targetTab.group }); - } - } - - #onTabUnpinned(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToUnpin = this.#getTabWithId(tabId); - if (tabToUnpin) { - gBrowser.unpinTab(tabToUnpin); - } - } - - #onTabIconChanged(event) { - this.#updateTabIconAndLabel(event); - } - - #onTabLabelChanged(event) { - this.#updateTabIconAndLabel(event); - } - - #updateTabIconAndLabel(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToChange = this.#getTabWithId(tabId); - if (tabToChange && tabToChange.hasAttribute('pending')) { - gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab)); - gBrowser._setTabLabel(tabToChange, targetTab.label); - } - } - - #onTabAddedToEssentials(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToAdd = this.#getTabWithId(tabId); - if (tabToAdd) { - gZenPinnedTabManager.addToEssentials(tabToAdd); - } - } - - #onTabRemovedFromEssentials(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToRemove = this.#getTabWithId(tabId); - if (tabToRemove) { - gZenPinnedTabManager.removeFromEssentials(tabToRemove); - } - } - - #onTabHide(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToHide = this.#getTabWithId(tabId); - if (tabToHide) { - gBrowser.hideTab(tabToHide); - } - } - - #onTabShow(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToShow = this.#getTabWithId(tabId); - if (tabToShow) { - gBrowser.showTab(tabToShow); - } - } - - #onTabMove(event) { - const targetTab = event.target; - const tabId = this.#getTabId(targetTab); - const tabToMove = this.#getTabWithId(tabId); - const workspaceId = targetTab.getAttribute('zen-workspace-id'); - const isEssential = targetTab.hasAttribute('zen-essential'); - if (tabToMove) { - let tabSibling = targetTab.previousElementSibling; - let isFirst = false; - if (!tabSibling?.hasAttribute('zen-sync-id')) { - isFirst = true; - } - gBrowser.zenHandleTabMove(tabToMove, () => { - if (isFirst) { - let container; - if (isEssential) { - container = gZenWorkspaces.getEssentialsSection(tabToMove); - } else { - const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); - container = tabToMove.pinned - ? workspaceElement.pinnedTabsContainer - : workspaceElement.tabsContainer; - } - container.insertBefore(tabToMove, container.firstChild); - } else { - let relativeTab = gZenWorkspaces.allStoredTabs.find((tab) => { - return this.#getTabId(tab) === this.#getTabId(tabSibling); - }); - if (relativeTab) { - relativeTab.after(tabToMove); - } - } - }); - } - } - - async #onTabOpen(event) { - const targetTab = event.target; - const isPinned = targetTab.pinned; - const isEssential = isPinned && targetTab.hasAttribute('zen-essential'); - if (!this.#getTabId(targetTab) && !targetTab.hasAttribute('zen-empty-tab')) { - const tabId = gZenUIManager.generateUuidv4(); - targetTab.setAttribute('zen-sync-id', tabId); - } - const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, { - createLazyBrowser: true, - essential: isEssential, - pinned: isPinned, - }); - if (!isEssential) { - gZenWorkspaces.moveTabToWorkspace( - duplicatedTab, - targetTab.getAttribute('zen-workspace-id') - ); - } - duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id')); - } - - #onTabGroupCreate(event) { - void event; - //const targetGroup = event.target; - //const isSplitView = targetGroup.classList.contains('zen-split-view'); - //const isFolder = targetGroup.isZenFolder; - } - } - - window.gZenWorkspaceWindowSync = new nsZenWorkspaceWindowSync(); -} From 40bc51f9048b635d72c2fb8e24f74cef585fa72a Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Mon, 17 Nov 2025 18:51:14 +0100 Subject: [PATCH 36/54] feat: Run session saver before opening a new window, b=no-bug, c=no-component --- .../sessionstore/SessionStore-sys-mjs.patch | 33 +++++++++---------- .../sessionstore/ZenSessionManager.sys.mjs | 15 +++++---- src/zen/sessionstore/ZenWindowSync.sys.mjs | 2 ++ 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 0362605b1b..9c7cd3bf82 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 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc3315aa98 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb90e3f1092 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -126,6 +126,8 @@ const TAB_EVENTS = [ @@ -36,18 +36,17 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2215,6 +2219,10 @@ var SessionStoreInternal = { +@@ -2215,6 +2219,9 @@ var SessionStoreInternal = { }); this._shouldRestoreLastSession = false; } + else if (!aInitialState && isRegularWindow) { -+ aInitialState = lazy.ZenSessionStore.getNewWindowData(); -+ this.restoreWindows(aWindow, aInitialState, {}); ++ lazy.ZenSessionStore.restoreNewWindow(aWindow, this); + } if (this._restoreLastWindow && aWindow.toolbar.visible) { // always reset (if not a popup window) -@@ -2384,11 +2392,9 @@ var SessionStoreInternal = { +@@ -2384,11 +2391,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -59,7 +58,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3379,7 @@ var SessionStoreInternal = { +@@ -3373,7 +3378,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -68,7 +67,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc return; } -@@ -4089,6 +4095,11 @@ var SessionStoreInternal = { +@@ -4089,6 +4094,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -81,7 +80,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4535,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -89,7 +88,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5385,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -98,7 +97,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc removableTabs.push(tab); } } -@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5445,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -107,7 +106,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5636,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -121,7 +120,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5652,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -129,7 +128,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5665,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -138,7 +137,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { +@@ -5764,8 +5777,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -149,7 +148,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { +@@ -5808,6 +5821,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -158,7 +157,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { +@@ -6371,6 +6386,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -184,7 +183,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..7cd5b92c1b1ddbaea89de5e9627fc5bc if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { +@@ -7289,7 +7323,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 06478075e9..914a3eb6d6 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -157,12 +157,15 @@ class nsZenSessionManager { aWindowData.groups = sidebar.groups; } - getNewWindowData() { - lazy.SessionSaver.run(); - const state = lazy.SessionStore.getCurrentState(true); - const windows = state.windows || {}; - let newWindow = Cu.cloneInto(windows[0], {}); - return { windows: [newWindow] }; + restoreNewWindow(aWindow, SessionStoreInternal) { + lazy.SessionSaver.run().then(() => { + const state = lazy.SessionStore.getCurrentState(true); + const windows = state.windows || {}; + let newWindow = Cu.cloneInto(windows[0], {}); + delete newWindow.selected; + const newState = { windows: [newWindow] }; + SessionStoreInternal.restoreWindows(aWindow, newState, {}); + }); } } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index a96e096872..21bc6c230a 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -18,6 +18,8 @@ class nsZenWindowSync { Services.obs.removeObserver(this, topic); } } + + observe(aSubject, aTopic) {} } export const ZenWindowSync = new nsZenWindowSync(); From c6cd9122623213f02fa1dda23af1a23065f7be45 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 21 Nov 2025 14:29:58 +0100 Subject: [PATCH 37/54] feat: Start making use of IDs instead of sync identifiers, b=no-bug, c=folders --- .../sessionstore/SessionStore-sys-mjs.patch | 30 +-- .../sessionstore/TabState-sys-mjs.patch | 4 +- .../tabbrowser/content/tabbrowser-js.patch | 211 +++++++++--------- src/zen/folders/ZenFolders.mjs | 17 +- 4 files changed, 135 insertions(+), 127 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 9c7cd3bf82..47833210a7 100644 --- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch @@ -1,11 +1,11 @@ diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs -index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb90e3f1092 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..9ef1996a0e8a3ebe55dc25921b8fc8cc0ac8a303 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs -@@ -126,6 +126,8 @@ const TAB_EVENTS = [ - "TabUngrouped", +@@ -127,6 +127,8 @@ const TAB_EVENTS = [ "TabGroupCollapse", "TabGroupExpand", + "TabSplitViewActivate", + "TabAddedToEssentials", + "TabRemovedFromEssentials", ]; @@ -67,7 +67,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 return; } -@@ -4089,6 +4094,11 @@ var SessionStoreInternal = { +@@ -4089,6 +4094,12 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -80,7 +80,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4535,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -88,7 +88,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5385,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -97,7 +97,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 removableTabs.push(tab); } } -@@ -5434,7 +5445,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -106,7 +106,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5636,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -120,7 +120,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5652,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -128,7 +128,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5665,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -137,7 +137,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5777,8 @@ var SessionStoreInternal = { +@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -148,7 +148,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5821,8 @@ var SessionStoreInternal = { +@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -157,7 +157,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6386,25 @@ var SessionStoreInternal = { +@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -172,7 +172,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 + tab.setAttribute("zen-has-static-label", "true"); + } + if (tabData.zenSyncId) { -+ tab.setAttribute("zen-sync-id", tabData.zenSyncId); ++ tab.setAttribute("id", tabData.zenPinnedId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); @@ -183,7 +183,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..ad80ed937f696ba2800e62dfc11fcfb9 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7323,7 @@ var SessionStoreInternal = { +@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch index cfeea50354..82c2f882ff 100644 --- a/src/browser/components/sessionstore/TabState-sys-mjs.patch +++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs -index 82721356d191055bec0d4b0ca49e481221988801..d1323fe17c995611ebdfe2869b0ccd2d45bcfa11 100644 +index 82721356d191055bec0d4b0ca49e481221988801..2d05ba4812e9a73bd896c1aeb007180bbc531a3c 100644 --- a/browser/components/sessionstore/TabState.sys.mjs +++ b/browser/components/sessionstore/TabState.sys.mjs @@ -85,7 +85,22 @@ class _TabState { @@ -7,7 +7,7 @@ index 82721356d191055bec0d4b0ca49e481221988801..d1323fe17c995611ebdfe2869b0ccd2d } + tabData.zenWorkspace = tab.getAttribute("zen-workspace-id"); -+ tabData.zenSyncId = tab.getAttribute("zen-sync-id"); ++ tabData.zenSyncId = tab.getAttribute("id"); + tabData.zenEssential = tab.getAttribute("zen-essential"); + tabData.pinned = tabData.pinned || tabData.zenEssential; + tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId"); diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 3790f98463..d34b618822 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,9 +1,16 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4cd44d980e 100644 -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4cd44d980e 100644 +index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca08b55b76 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js -@@ -432,15 +432,64 @@ +@@ -386,6 +386,7 @@ + * @type {MozBrowser[]} + */ + get splitViewBrowsers() { ++ return gZenViewSplitter.splitViewBrowsers; + const browsers = []; + if (this.#activeSplitView) { + for (const tab of this.#activeSplitView.tabs) { +@@ -450,15 +451,64 @@ return this.tabContainer.visibleTabs; } @@ -70,7 +77,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c set selectedTab(val) { if ( gSharedTabWarning.willShowSharedTabWarning(val) || -@@ -588,6 +637,7 @@ +@@ -613,6 +663,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -78,7 +85,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -873,13 +923,17 @@ +@@ -898,13 +949,17 @@ } this.showTab(aTab); @@ -97,7 +104,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c aTab.setAttribute("pinned", "true"); this._updateTabBarForPinnedTabs(); -@@ -892,11 +946,15 @@ +@@ -917,11 +972,15 @@ } this.#handleTabMove(aTab, () => { @@ -114,7 +121,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c }); aTab.style.marginInlineStart = ""; -@@ -1073,6 +1131,8 @@ +@@ -1098,6 +1157,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -123,7 +130,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if ( aIconURL && !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) -@@ -1082,6 +1142,9 @@ +@@ -1107,6 +1168,9 @@ ); return; } @@ -133,7 +140,15 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1445,6 +1508,7 @@ +@@ -1379,7 +1443,6 @@ + + // Preview mode should not reset the owner + if (!this._previewMode && !oldTab.selected) { +- oldTab.owner = null; + } + + let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); +@@ -1470,6 +1533,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -141,7 +156,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1597,6 +1661,9 @@ +@@ -1622,6 +1686,9 @@ } let activeEl = document.activeElement; @@ -151,7 +166,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1920,7 +1987,8 @@ +@@ -1945,7 +2012,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -161,7 +176,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c return false; } -@@ -2028,7 +2096,7 @@ +@@ -2053,7 +2121,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -170,7 +185,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2303,6 +2371,7 @@ +@@ -2328,6 +2396,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -178,7 +193,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2376,8 +2445,7 @@ +@@ -2401,8 +2470,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -188,7 +203,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c b.setAttribute("transparent", "true"); } -@@ -2542,7 +2610,7 @@ +@@ -2567,7 +2635,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -197,7 +212,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2601,8 +2669,8 @@ +@@ -2626,8 +2694,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -208,7 +223,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2779,7 +2847,6 @@ +@@ -2814,7 +2882,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -216,7 +231,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2859,6 +2926,8 @@ +@@ -2923,6 +2990,8 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -225,7 +240,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2869,6 +2938,12 @@ +@@ -2933,10 +3002,17 @@ ); } @@ -238,7 +253,20 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (!UserInteraction.running("browser.tabs.opening", window)) { UserInteraction.start("browser.tabs.opening", "initting", window); } -@@ -2932,6 +3007,19 @@ + ++ if (!gURLBar.hasAttribute("zen-newtab")) { + // If we're opening a foreground tab, set the owner by default. + ownerTab ??= inBackground ? null : this.selectedTab; + +@@ -2944,6 +3020,7 @@ + if (this.selectedTab.owner) { + this.selectedTab.owner = null; + } ++ } + + // Find the tab that opened this one, if any. This is used for + // determining positioning, and inherited attributes such as the +@@ -2996,6 +3073,19 @@ noInitialLabel, skipBackgroundNotify, }); @@ -258,7 +286,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -2940,6 +3028,7 @@ +@@ -3004,6 +3094,7 @@ ownerTab, openerTab, pinned, @@ -266,7 +294,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -2958,6 +3047,7 @@ +@@ -3022,6 +3113,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -274,7 +302,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c })); if (focusUrlBar) { -@@ -3078,6 +3168,12 @@ +@@ -3146,6 +3238,12 @@ } } @@ -287,7 +315,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3248,10 +3344,10 @@ +@@ -3330,10 +3428,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -299,7 +327,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } if (!color) { -@@ -3272,9 +3368,14 @@ +@@ -3354,9 +3452,14 @@ label, isAdoptingGroup ); @@ -316,7 +344,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c ); group.addTabs(tabs); -@@ -3395,7 +3496,7 @@ +@@ -3477,7 +3580,7 @@ } this.#handleTabMove(tab, () => @@ -325,7 +353,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c ); } -@@ -3597,6 +3698,7 @@ +@@ -3679,6 +3782,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -333,7 +361,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3666,6 +3768,7 @@ +@@ -3748,6 +3852,7 @@ openWindowInfo, name, skipLoad, @@ -341,7 +369,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c }); } -@@ -3853,7 +3956,7 @@ +@@ -3935,7 +4040,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -350,7 +378,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c let url = "about:blank"; if (tabData.entries?.length) { -@@ -3890,8 +3993,10 @@ +@@ -3972,8 +4077,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -362,7 +390,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (select) { tabToSelect = tab; } -@@ -3903,7 +4008,8 @@ +@@ -3985,7 +4092,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -372,7 +400,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -3917,7 +4023,10 @@ +@@ -3999,7 +4107,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -384,7 +412,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c ); tabsFragment.appendChild(tabGroup.node); } -@@ -3962,9 +4071,23 @@ +@@ -4044,9 +4155,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -400,17 +428,15 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c + gZenWorkspaces._initialTab._shouldRemove = true; + } + } -+ } -+ } + } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; - } - } ++ } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4155,11 +4278,14 @@ +@@ -4237,11 +4362,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -426,7 +452,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if ( !bulkOrderedOpen && ((openerTab && -@@ -4171,7 +4297,7 @@ +@@ -4253,7 +4381,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -435,7 +461,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c tabGroup = previousTab.group; } if ( -@@ -4182,7 +4308,7 @@ +@@ -4264,7 +4392,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -444,7 +470,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4210,14 +4336,14 @@ +@@ -4292,14 +4420,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -463,7 +489,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4228,7 +4354,7 @@ +@@ -4310,7 +4438,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -472,7 +498,19 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4264,6 +4390,7 @@ +@@ -4338,7 +4466,11 @@ + const tabContainer = pinned + ? this.tabContainer.pinnedTabsContainer + : this.tabContainer; ++ if (itemAfter) { ++ itemAfter.before(tab); ++ } else { + tabContainer.insertBefore(tab, itemAfter); ++ } + } + + this._updateTabsAfterInsert(); +@@ -4346,6 +4478,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -480,8 +518,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c TabBarVisibility.update(); } -@@ -4896,6 +5024,7 @@ -@@ -4896,6 +5024,7 @@ +@@ -4896,6 +5029,7 @@ telemetrySource, } = {} ) { @@ -489,8 +526,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5114,7 @@ -@@ -4985,6 +5114,7 @@ +@@ -4985,6 +5119,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -498,8 +534,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } catch (e) { console.error(e); } -@@ -5023,6 +5153,12 @@ -@@ -5023,6 +5153,12 @@ +@@ -5023,6 +5158,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -512,8 +547,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5173,9 @@ -@@ -5037,6 +5173,9 @@ +@@ -5037,6 +5178,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -523,8 +557,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5224,13 @@ -@@ -5085,7 +5224,13 @@ +@@ -5085,7 +5229,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -539,8 +572,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c return; } -@@ -5219,7 +5364,7 @@ -@@ -5219,7 +5364,7 @@ +@@ -5219,7 +5369,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -549,8 +581,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5388,7 @@ -@@ -5243,6 +5388,7 @@ +@@ -5243,6 +5393,7 @@ newTab = true; } @@ -558,8 +589,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5429,7 @@ -@@ -5283,13 +5429,7 @@ +@@ -5283,13 +5434,7 @@ aTab._mouseleave(); if (newTab) { @@ -574,8 +604,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } else { TabBarVisibility.update(); } -@@ -5422,6 +5562,7 @@ -@@ -5422,6 +5562,7 @@ +@@ -5422,6 +5567,7 @@ this.tabs[i]._tPos = i; } @@ -583,8 +612,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5784,7 @@ -@@ -5643,6 +5784,7 @@ +@@ -5643,6 +5789,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -592,8 +620,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5797,13 @@ -@@ -5655,13 +5797,13 @@ +@@ -5655,13 +5802,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -609,8 +636,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5819,7 @@ -@@ -5677,7 +5819,7 @@ +@@ -5677,7 +5824,7 @@ } if (tab) { @@ -619,8 +645,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5840,7 @@ -@@ -5698,7 +5840,7 @@ +@@ -5698,7 +5845,7 @@ }); } @@ -629,8 +654,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } _blurTab(aTab) { -@@ -6104,10 +6246,10 @@ -@@ -6104,10 +6246,10 @@ +@@ -6104,10 +6251,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -643,8 +667,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6166,6 +6308,7 @@ -@@ -6166,6 +6308,7 @@ +@@ -6166,6 +6313,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -652,8 +675,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (this.tabs.length == 1) { return null; } -@@ -6299,7 +6442,7 @@ -@@ -6299,7 +6442,7 @@ +@@ -6299,7 +6447,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -662,8 +684,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } /** -@@ -6375,8 +6518,8 @@ -@@ -6375,8 +6518,8 @@ +@@ -6375,8 +6523,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -674,8 +695,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6402,10 +6545,16 @@ -@@ -6402,10 +6545,16 @@ +@@ -6402,10 +6550,16 @@ this.#handleTabMove( element, () => { @@ -694,8 +714,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6463,23 +6612,28 @@ -@@ -6463,23 +6612,28 @@ +@@ -6463,23 +6617,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -730,8 +749,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6492,14 +6646,34 @@ -@@ -6492,14 +6646,34 @@ +@@ -6492,14 +6651,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -767,8 +785,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6508,7 +6682,7 @@ -@@ -6508,7 +6682,7 @@ +@@ -6508,7 +6687,7 @@ element, () => { if (moveBefore) { @@ -777,8 +794,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c } else if (targetElement) { targetElement.after(element); } else { -@@ -6580,10 +6754,10 @@ -@@ -6580,10 +6754,10 @@ +@@ -6580,10 +6759,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -791,8 +807,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6613,6 +6787,7 @@ -@@ -6613,6 +6787,7 @@ +@@ -6613,6 +6792,7 @@ let state = { tabIndex: tab._tPos, @@ -800,8 +815,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6639,7 +6814,7 @@ -@@ -6639,7 +6814,7 @@ +@@ -6639,7 +6819,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -810,8 +824,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6676,6 +6851,10 @@ -@@ -6676,6 +6851,10 @@ +@@ -6676,6 +6856,10 @@ moveActionCallback(); @@ -822,8 +835,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7576,7 +7755,7 @@ -@@ -7576,7 +7755,7 @@ +@@ -7576,7 +7760,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -832,8 +844,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c window.focus(); aEvent.preventDefault(); break; -@@ -7593,7 +7772,6 @@ -@@ -7593,7 +7772,6 @@ +@@ -7593,7 +7777,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -841,8 +852,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c }); break; case "TabGroupCreateByUser": -@@ -8542,6 +8720,7 @@ -@@ -8542,6 +8720,7 @@ +@@ -8542,6 +8725,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -850,8 +860,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..30c8fd7e978eb3036f35b17ae3f6ea4c gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9543,7 +9722,7 @@ var TabContextMenu = { -@@ -9543,7 +9722,7 @@ var TabContextMenu = { +@@ -9543,7 +9727,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 8a39c01952..93b272dfe0 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -935,7 +935,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (!parentFolder && folder.hasAttribute('split-view-group')) continue; const emptyFolderTabs = folder.tabs .filter((tab) => tab.hasAttribute('zen-empty-tab')) - .map((tab) => tab.getAttribute('zen-sync-id')); + .map((tab) => tab.getAttribute('id')); let prevSiblingInfo = null; const prevSibling = folder.previousElementSibling; @@ -944,8 +944,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (prevSibling) { if (gBrowser.isTabGroup(prevSibling)) { prevSiblingInfo = { type: 'group', id: prevSibling.id }; - } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-sync-id')) { - const zenPinId = prevSibling.getAttribute('zen-sync-id'); + } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) { + const zenPinId = prevSibling.getAttribute('id'); prevSiblingInfo = { type: 'tab', id: zenPinId }; } else { prevSiblingInfo = { type: 'start', id: null }; @@ -964,7 +964,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature { prevSiblingInfo: prevSiblingInfo, emptyTabIds: emptyFolderTabs, userIcon: userIcon?.getAttribute('href'), - syncId: folder.getAttribute('zen-sync-id'), // note: We shouldn't be using the workspace-id anywhere, we are just // remembering it for the pinned tabs manager to use it later. workspaceId: folder.getAttribute('zen-workspace-id'), @@ -991,9 +990,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature { tabFolderWorkingData.set(folderData.id, workingData); const oldGroup = document.getElementById(folderData.id); - folderData.emptyTabIds.forEach((zenSyncId) => { + folderData.emptyTabIds.forEach((id) => { oldGroup - ?.querySelector(`tab[zen-sync-id="${zenSyncId}"]`) + ?.querySelector(`tab[id="${id}"]`) ?.setAttribute('zen-empty-tab', true); }); if (oldGroup) { @@ -1006,7 +1005,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { saveOnWindowClose: folderData.saveOnWindowClose, workspaceId: folderData.workspaceId, }); - folder.setAttribute('zen-sync-id', folderData.syncId); + folder.setAttribute('id', folderData.id); workingData.node = folder; oldGroup.before(folder); } else { @@ -1038,8 +1037,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (parentWorkingData && parentWorkingData.node) { switch (stateData?.prevSiblingInfo?.type) { case 'tab': { - const tab = parentWorkingData.node.querySelector( - `[zen-sync-id="${stateData.prevSiblingInfo.id}"]` + const tab = document.getElementById( + stateData.prevSiblingInfo.id ); tab.after(node); break; From 1fcef12d0ad28e1e5430d7eaa0cbd1591c5c1d8a Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 22 Nov 2025 13:58:15 +0100 Subject: [PATCH 38/54] feat: Listen to new tab opens for new sync system, b=no-bug, c=common, folders, tabs --- src/zen/common/modules/ZenSessionStore.mjs | 2 +- src/zen/folders/ZenFolder.mjs | 23 -- src/zen/folders/ZenFolders.mjs | 186 +++++----- src/zen/sessionstore/ZenWindowSync.sys.mjs | 172 ++++++++- src/zen/tabs/ZenPinnedTabManager.mjs | 383 +++++++++------------ 5 files changed, 434 insertions(+), 332 deletions(-) diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs index daa0d0962c..c19233e7d2 100644 --- a/src/zen/common/modules/ZenSessionStore.mjs +++ b/src/zen/common/modules/ZenSessionStore.mjs @@ -18,7 +18,7 @@ class ZenSessionStore extends nsZenPreloadedFeature { tab.setAttribute('zen-workspace-id', tabData.zenWorkspace); } if (tabData.zenPinnedId) { - tab.setAttribute('zen-pin-id', tabData.zenPinnedId); + tab.setAttribute('id', tabData.zenPinnedId); } if (tabData.zenHasStaticLabel) { tab.setAttribute('zen-has-static-label', 'true'); diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index 16746cd26f..6c1d2dbc78 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -150,28 +150,15 @@ class ZenFolder extends MozTabbrowserTabGroup { for (let tab of this.allItems.reverse()) { tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab; if (tab.hasAttribute('zen-empty-tab')) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); gBrowser.removeTab(tab); } else { gBrowser.ungroupTab(tab); } } } - async unpackTabs() { - this.collapsed = false; - for (let tab of this.allItems.reverse()) { - tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab; - if (tab.hasAttribute('zen-empty-tab')) { - gBrowser.removeTab(tab); - } else { - gBrowser.ungroupTab(tab); - } - } - } async delete() { for (const tab of this.allItemsRecursive) { - await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id')); if (tab.hasAttribute('zen-empty-tab')) { // Manually remove the empty tabs as removeTabs() inside removeTabGroup // does ignore them. @@ -180,16 +167,6 @@ class ZenFolder extends MozTabbrowserTabGroup { } await gBrowser.removeTabGroup(this, { isUserTriggered: true }); } - async delete() { - for (const tab of this.allItemsRecursive) { - if (tab.hasAttribute('zen-empty-tab')) { - // Manually remove the empty tabs as removeTabs() inside removeTabGroup - // does ignore them. - gBrowser.removeTab(tab); - } - } - await gBrowser.removeTabGroup(this, { isUserTriggered: true }); - } get allItemsRecursive() { const items = []; diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 93b272dfe0..d5adaa8b01 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -505,7 +505,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { gBrowser.pinTab(emptyTab); tabs = [emptyTab, ...filteredTabs]; - const folder = this._createFolderNode(options); + const folder = this._createFolderNode(options); if (options.insertAfter) { options.insertAfter.after(folder); @@ -929,48 +929,48 @@ class nsZenFolders extends nsZenDOMOperatedFeature { const storedData = []; - for (const folder of allData) { - const parentFolder = folder.parentElement.closest('zen-folder'); - // Skip split-view-group if it's not a zen-folder child - if (!parentFolder && folder.hasAttribute('split-view-group')) continue; - const emptyFolderTabs = folder.tabs - .filter((tab) => tab.hasAttribute('zen-empty-tab')) - .map((tab) => tab.getAttribute('id')); + for (const folder of allData) { + const parentFolder = folder.parentElement.closest('zen-folder'); + // Skip split-view-group if it's not a zen-folder child + if (!parentFolder && folder.hasAttribute('split-view-group')) continue; + const emptyFolderTabs = folder.tabs + .filter((tab) => tab.hasAttribute('zen-empty-tab')) + .map((tab) => tab.getAttribute('id')); let prevSiblingInfo = null; const prevSibling = folder.previousElementSibling; const userIcon = folder?.icon?.querySelector('svg .icon image'); - if (prevSibling) { - if (gBrowser.isTabGroup(prevSibling)) { - prevSiblingInfo = { type: 'group', id: prevSibling.id }; - } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) { - const zenPinId = prevSibling.getAttribute('id'); - prevSiblingInfo = { type: 'tab', id: zenPinId }; - } else { - prevSiblingInfo = { type: 'start', id: null }; - } + if (prevSibling) { + if (gBrowser.isTabGroup(prevSibling)) { + prevSiblingInfo = { type: 'group', id: prevSibling.id }; + } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) { + const zenPinId = prevSibling.getAttribute('id'); + prevSiblingInfo = { type: 'tab', id: zenPinId }; + } else { + prevSiblingInfo = { type: 'start', id: null }; } - - storedData.push({ - pinned: folder.pinned, - essential: folder.essential, - splitViewGroup: folder.hasAttribute('split-view-group'), - id: folder.id, - name: folder.label, - collapsed: folder.collapsed, - saveOnWindowClose: folder.saveOnWindowClose, - parentId: parentFolder ? parentFolder.id : null, - prevSiblingInfo: prevSiblingInfo, - emptyTabIds: emptyFolderTabs, - userIcon: userIcon?.getAttribute('href'), - // note: We shouldn't be using the workspace-id anywhere, we are just - // remembering it for the pinned tabs manager to use it later. - workspaceId: folder.getAttribute('zen-workspace-id'), - }); } - return storedData; + + storedData.push({ + pinned: folder.pinned, + essential: folder.essential, + splitViewGroup: folder.hasAttribute('split-view-group'), + id: folder.id, + name: folder.label, + collapsed: folder.collapsed, + saveOnWindowClose: folder.saveOnWindowClose, + parentId: parentFolder ? parentFolder.id : null, + prevSiblingInfo: prevSiblingInfo, + emptyTabIds: emptyFolderTabs, + userIcon: userIcon?.getAttribute('href'), + // note: We shouldn't be using the workspace-id anywhere, we are just + // remembering it for the pinned tabs manager to use it later. + workspaceId: folder.getAttribute('zen-workspace-id'), + }); } + return storedData; + } restoreDataFromSessionStore(data) { if (!data || this._sessionRestoring) { @@ -989,40 +989,38 @@ class nsZenFolders extends nsZenDOMOperatedFeature { }; tabFolderWorkingData.set(folderData.id, workingData); - const oldGroup = document.getElementById(folderData.id); - folderData.emptyTabIds.forEach((id) => { - oldGroup - ?.querySelector(`tab[id="${id}"]`) - ?.setAttribute('zen-empty-tab', true); - }); - if (oldGroup) { - if (!folderData.splitViewGroup) { - const folder = this._createFolderNode({ - id: folderData.id, - label: folderData.name, - collapsed: folderData.collapsed, - pinned: folderData.pinned, - saveOnWindowClose: folderData.saveOnWindowClose, - workspaceId: folderData.workspaceId, - }); - folder.setAttribute('id', folderData.id); - workingData.node = folder; - oldGroup.before(folder); - } else { - workingData.node = oldGroup; - } - while (oldGroup.tabs.length > 0) { - const tab = oldGroup.tabs[0]; - if (folderData.workspaceId) { - tab.setAttribute('zen-workspace-id', folderData.workspaceId); - } - workingData.containingTabsFragment.appendChild(tab); - } - if (!folderData.splitViewGroup) { - oldGroup.remove(); + const oldGroup = document.getElementById(folderData.id); + folderData.emptyTabIds.forEach((id) => { + oldGroup?.querySelector(`tab[id="${id}"]`)?.setAttribute('zen-empty-tab', true); + }); + if (oldGroup) { + if (!folderData.splitViewGroup) { + const folder = this._createFolderNode({ + id: folderData.id, + label: folderData.name, + collapsed: folderData.collapsed, + pinned: folderData.pinned, + saveOnWindowClose: folderData.saveOnWindowClose, + workspaceId: folderData.workspaceId, + }); + folder.setAttribute('id', folderData.id); + workingData.node = folder; + oldGroup.before(folder); + } else { + workingData.node = oldGroup; + } + while (oldGroup.tabs.length > 0) { + const tab = oldGroup.tabs[0]; + if (folderData.workspaceId) { + tab.setAttribute('zen-workspace-id', folderData.workspaceId); } + workingData.containingTabsFragment.appendChild(tab); + } + if (!folderData.splitViewGroup) { + oldGroup.remove(); } } + } for (const { node, containingTabsFragment } of tabFolderWorkingData.values()) { if (node) { @@ -1030,41 +1028,39 @@ class nsZenFolders extends nsZenDOMOperatedFeature { } } - // Nesting folders into each other according to parentId. - for (const { stateData, node } of tabFolderWorkingData.values()) { - if (node && stateData.parentId) { - const parentWorkingData = tabFolderWorkingData.get(stateData.parentId); - if (parentWorkingData && parentWorkingData.node) { - switch (stateData?.prevSiblingInfo?.type) { - case 'tab': { - const tab = document.getElementById( - stateData.prevSiblingInfo.id - ); - tab.after(node); + // Nesting folders into each other according to parentId. + for (const { stateData, node } of tabFolderWorkingData.values()) { + if (node && stateData.parentId) { + const parentWorkingData = tabFolderWorkingData.get(stateData.parentId); + if (parentWorkingData && parentWorkingData.node) { + switch (stateData?.prevSiblingInfo?.type) { + case 'tab': { + const tab = document.getElementById(stateData.prevSiblingInfo.id); + tab.after(node); + break; + } + case 'group': { + const folder = document.getElementById(stateData.prevSiblingInfo.id); + if (folder) { + folder.after(node); break; } - case 'group': { - const folder = document.getElementById(stateData.prevSiblingInfo.id); - if (folder) { - folder.after(node); - break; - } - // If we didn't find the group, we should debug it and continue to default case. - console.warn( - `Zen Folders: Could not find previous sibling group with id ${stateData.prevSiblingInfo.id} while restoring session.` - ); - // @eslint-disable-next-line no-fallthrough - } - default: { - // Should insert after zen-empty-tab - const start = - parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling; - start.after(node); - } + // If we didn't find the group, we should debug it and continue to default case. + console.warn( + `Zen Folders: Could not find previous sibling group with id ${stateData.prevSiblingInfo.id} while restoring session.` + ); + // @eslint-disable-next-line no-fallthrough + } + default: { + // Should insert after zen-empty-tab + const start = + parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling; + start.after(node); } } } } + } // Initialize UI state for all folders. for (const { stateData, node } of tabFolderWorkingData.values()) { diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 21bc6c230a..42947a9ed4 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -2,15 +2,49 @@ // 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 OBSERVING = ['browser-window-before-show']; +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', + SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', + TabStateFlusher: 'resource:///modules/sessionstore/TabStateFlusher.sys.mjs', +}); + +const OBSERVING = ['browser-window-delayed-startup']; +const EVENTS = ['TabOpen']; class nsZenWindowSync { constructor() {} + /** + * Whether to ignore the next set of events. + * This is used to prevent recursive event handling. + */ + #ignoreNextEvents = false; + + /** + * Iterator that yields all currently opened browser windows. + * (Might miss the most recent one.) + * This list is in focus order, but may include minimized windows + * before non-minimized windows. + */ + #browserWindows = { + *[Symbol.iterator]() { + for (let window of lazy.BrowserWindowTracker.orderedWindows) { + if (window.__SSi && !window.closed) { + yield window; + } + } + }, + }; + init() { for (let topic of OBSERVING) { Services.obs.addObserver(this, topic); } + SessionStore.promiseInitialized.then(() => { + this.#onSessionStoreInitialized(); + }); } uninit() { @@ -19,7 +53,141 @@ class nsZenWindowSync { } } - observe(aSubject, aTopic) {} + /** + * Called when a browser window is about to be shown. + * Adds event listeners for the specified events. + * + * @param {Window} aWindow - The browser window that is about to be shown. + */ + #onWindowBeforeShow(aWindow) { + for (let eventName of EVENTS) { + aWindow.addEventListener(eventName, this); + } + } + + /** * Generates a unique tab ID. + * + * @returns {string} A unique tab ID. + */ + get #newTabSyncId() { + // Note: If this changes, make sure to also update the + // getExtTabGroupIdForInternalTabGroupId implementation in + // browser/components/extensions/parent/ext-browser.js. + // See: Bug 1960104 - Improve tab group ID generation in addTabGroup + // This is implemented from gBrowser.addTabGroup. + return `${Date.now()}-${Math.round(Math.random() * 100)}`; + } + + /** + * Called when the session store has finished initializing for a window. + * + * @param {Window} aWindow - The browser window that has initialized session store. + */ + #onSessionStoreInitialized() { + // For every tab we have in where there's no sync ID, we need to + // assign one and sync it to other windows. + // 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) { + if (!tab.id) { + tab.id = this.#newTabSyncId; + lazy.TabStateFlusher.flush(tab.linkedBrowser); + } + } + }); + } + + /** + * Runs a callback function on all browser windows except the specified one. + * + * @param {Window} aWindow - The browser window to exclude. + * @param {Function} aCallback - The callback function to run on each window. + */ + #runOnAllWindows(aWindow, aCallback) { + this.#ignoreNextEvents = true; + for (let window of this.#browserWindows) { + if (window !== aWindow) { + aCallback(window); + } + } + this.#ignoreNextEvents = false; + } + + observe(aSubject, aTopic) { + switch (aTopic) { + case 'browser-window-delayed-startup': { + this.#onWindowBeforeShow(aSubject); + break; + } + } + } + + handleEvent(aEvent) { + if (this.#ignoreNextEvents) { + return; + } + const handler = `on_${aEvent.type}`; + if (typeof this[handler] === 'function') { + this[handler](aEvent); + } else { + console.warn(`ZenWindowSync: No handler for event type: ${aEvent.type}`); + } + } + + /** + * Synchronizes the icon and label of the target tab with the original tab. + * + * @param {Object} aOriginalTab - The original tab to copy from. + * @param {Object} aTargetTab - The target tab to copy to. + * @param {Window} aWindow - The window containing the tabs. + */ + #syncTabWithOriginal(aOriginalTab, aTargetTab, aWindow) { + const { gBrowser } = aWindow; + gBrowser.setIcon(aTargetTab, gBrowser.getIcon(aOriginalTab)); + gBrowser._setTabLabel(aTargetTab, aOriginalTab.label); + this.#syncTabPosition(aOriginalTab, aTargetTab, aWindow); + } + + /** + * Synchronizes the position of the target tab with the original tab. + * + * @param {Object} aOriginalTab - The original tab to copy from. + * @param {Object} aTargetTab - The target tab to copy to. + * @param {Window} aWindow - The window containing the tabs. + */ + #syncTabPosition(aOriginalTab, aTargetTab, aWindow) { + const { gBrowser, gZenPinnedTabManager } = aWindow; + const originalIsEssential = aOriginalTab.hasAttribute('zen-essential'); + const targetIsEssential = aTargetTab.hasAttribute('zen-essential'); + const originalIsPinned = aOriginalTab.pinned; + const targetIsPinned = aTargetTab.pinned; + + if (originalIsEssential !== targetIsEssential) { + if (originalIsEssential) { + gZenPinnedTabManager.addToEssentials(aTargetTab); + } else { + gZenPinnedTabManager.removeEssentials(aTargetTab, /* unpin= */ !targetIsPinned); + } + } else if (originalIsPinned !== targetIsPinned) { + if (originalIsPinned) { + gBrowser.pinTab(aTargetTab); + } else { + gBrowser.unpinTab(aTargetTab); + } + } + } + + on_TabOpen(aEvent) { + const tab = aEvent.target; + const window = tab.ownerGlobal; + + this.#runOnAllWindows(window, (win) => { + const newTab = win.gBrowser.duplicateTab(tab); + this.#syncTabWithOriginal(tab, newTab, win); + }); + } } export const ZenWindowSync = new nsZenWindowSync(); diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 9d7bc5b364..af06b405e1 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -6,8 +6,8 @@ import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components const lazy = {}; - class ZenPinnedTabsObserver { - static ALL_EVENTS = ['TabPinned', 'TabUnpinned']; +class ZenPinnedTabsObserver { + static ALL_EVENTS = ['TabPinned', 'TabUnpinned']; #listeners = []; @@ -86,16 +86,17 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - onTabIconChanged(tab, url = null) { - tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); - tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); - const iconUrl = url ?? tab.iconImage.src; - if (tab.hasAttribute('zen-essential')) { - tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); + onTabIconChanged(tab, url = null) { + tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); + tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); + const iconUrl = url ?? tab.iconImage.src; + if (tab.hasAttribute('zen-essential')) { + tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); if (tab.hasAttribute('zen-essential')) { tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`); } } + } _onTabResetPinButton(event, tab) { event.stopPropagation(); @@ -121,34 +122,34 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return !gZenWorkspaces.privateWindowOrDisabled; } - get maxEssentialTabs() { - return lazy.zenTabsEssentialsMax; - } + get maxEssentialTabs() { + return lazy.zenTabsEssentialsMax; + } - _onPinnedTabEvent(action, event) { - if (!this.enabled) return; - const tab = event.target; - if (this._ignoreNextTabPinnedEvent) { - delete this._ignoreNextTabPinnedEvent; - return; - } - switch (action) { - case 'TabPinned': - tab._zenClickEventListener = this._zenClickEventListener; - tab.addEventListener('click', tab._zenClickEventListener); - break; - // [Fall through] - case 'TabUnpinned': - if (tab._zenClickEventListener) { - tab.removeEventListener('click', tab._zenClickEventListener); - delete tab._zenClickEventListener; - } - break; - default: - console.warn('ZenPinnedTabManager: Unhandled tab event', action); - break; - } + _onPinnedTabEvent(action, event) { + if (!this.enabled) return; + const tab = event.target; + if (this._ignoreNextTabPinnedEvent) { + delete this._ignoreNextTabPinnedEvent; + return; + } + switch (action) { + case 'TabPinned': + tab._zenClickEventListener = this._zenClickEventListener; + tab.addEventListener('click', tab._zenClickEventListener); + break; + // [Fall through] + case 'TabUnpinned': + if (tab._zenClickEventListener) { + tab.removeEventListener('click', tab._zenClickEventListener); + delete tab._zenClickEventListener; + } + break; + default: + console.warn('ZenPinnedTabManager: Unhandled tab event', action); + break; } + } async _onTabClick(e) { const tab = e.target?.closest('tab'); @@ -159,17 +160,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - async _onTabClick(e) { - const tab = e.target?.closest('tab'); - if (e.button === 1 && tab) { - await this.onCloseTabShortcut(e, tab, { - closeIfPending: Services.prefs.getBoolPref( - 'zen.pinned-tab-manager.wheel-close-if-pending' - ), - }); - } - } - async resetPinnedTab(tab) { if (!tab) { tab = TabContextMenu.contextTab; @@ -188,20 +178,17 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return; } - this.resetPinChangedUrl(tab); - gZenUIManager.showToast('zen-pinned-tab-replaced'); - } + this.resetPinChangedUrl(tab); + gZenUIManager.showToast('zen-pinned-tab-replaced'); + } _initClosePinnedTabShortcut() { let cmdClose = document.getElementById('cmd_close'); - _initClosePinnedTabShortcut() { - let cmdClose = document.getElementById('cmd_close'); - - if (cmdClose) { - cmdClose.addEventListener('command', this.onCloseTabShortcut.bind(this)); - } + if (cmdClose) { + cmdClose.addEventListener('command', this.onCloseTabShortcut.bind(this)); } + } async onCloseTabShortcut( event, @@ -229,32 +216,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { ), ]; - async onCloseTabShortcut( - event, - selectedTab = gBrowser.selectedTab, - { - behavior = lazy.zenPinnedTabCloseShortcutBehavior, - noClose = false, - closeIfPending = false, - alwaysUnload = false, - folderToUnload = null, - } = {} - ) { - try { - const tabs = Array.isArray(selectedTab) ? selectedTab : [selectedTab]; - const pinnedTabs = [ - ...new Set( - tabs - .flatMap((tab) => { - if (tab.group?.hasAttribute('split-view-group')) { - return tab.group.tabs; - } - return tab; - }) - .filter((tab) => tab?.pinned) - ), - ]; - if (!pinnedTabs.length) { return; } @@ -272,86 +233,86 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { behavior = behavior.contains('reset') ? 'reset-unload-switch' : 'unload-switch'; } - switch (behavior) { - case 'close': { - for (const tab of pinnedTabs) { - gBrowser.removeTab(tab, { animate: true }); - } - break; + switch (behavior) { + case 'close': { + for (const tab of pinnedTabs) { + gBrowser.removeTab(tab, { animate: true }); } - case 'reset-unload-switch': - case 'unload-switch': - case 'reset-switch': - case 'switch': - if (behavior.includes('unload')) { - for (const tab of pinnedTabs) { - if (tab.hasAttribute('glance-id')) { - // We have a glance tab inside the tab we are trying to unload, - // before we used to just ignore it but now we need to fully close - // it as well. - gZenGlanceManager.manageTabClose(tab.glanceTab); - await new Promise((resolve) => { - let hasRan = false; - const onGlanceClose = () => { - hasRan = true; + break; + } + case 'reset-unload-switch': + case 'unload-switch': + case 'reset-switch': + case 'switch': + if (behavior.includes('unload')) { + for (const tab of pinnedTabs) { + if (tab.hasAttribute('glance-id')) { + // We have a glance tab inside the tab we are trying to unload, + // before we used to just ignore it but now we need to fully close + // it as well. + gZenGlanceManager.manageTabClose(tab.glanceTab); + await new Promise((resolve) => { + let hasRan = false; + const onGlanceClose = () => { + hasRan = true; + resolve(); + }; + window.addEventListener('GlanceClose', onGlanceClose, { once: true }); + // Set a timeout to resolve the promise if the event doesn't fire. + // We do this to prevent any future issues where glance woudnt close such as + // glance requering to ask for permit unload. + setTimeout(() => { + if (!hasRan) { + console.warn('GlanceClose event did not fire within 3 seconds'); resolve(); - }; - window.addEventListener('GlanceClose', onGlanceClose, { once: true }); - // Set a timeout to resolve the promise if the event doesn't fire. - // We do this to prevent any future issues where glance woudnt close such as - // glance requering to ask for permit unload. - setTimeout(() => { - if (!hasRan) { - console.warn('GlanceClose event did not fire within 3 seconds'); - resolve(); - } - }, 3000); - }); - return; - } - const isSpltView = tab.group?.hasAttribute('split-view-group'); - const group = isSpltView ? tab.group.group : tab.group; - if (!folderToUnload && tab.hasAttribute('folder-active')) { - await gZenFolders.animateUnload(group, tab); - } + } + }, 3000); + }); + return; } - if (folderToUnload) { - await gZenFolders.animateUnloadAll(folderToUnload); - } - const allAreUnloaded = pinnedTabs.every( - (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential') - ); - for (const tab of pinnedTabs) { - if (allAreUnloaded && closeIfPending) { - return await this.onCloseTabShortcut(event, tab, { behavior: 'close' }); - } - } - await gBrowser.explicitUnloadTabs(pinnedTabs); - for (const tab of pinnedTabs) { - tab.removeAttribute('discarded'); + const isSpltView = tab.group?.hasAttribute('split-view-group'); + const group = isSpltView ? tab.group.group : tab.group; + if (!folderToUnload && tab.hasAttribute('folder-active')) { + await gZenFolders.animateUnload(group, tab); } } - if (selectedTabs.length) { - this._handleTabSwitch(selectedTabs[0]); + if (folderToUnload) { + await gZenFolders.animateUnloadAll(folderToUnload); } - if (behavior.includes('reset')) { - for (const tab of pinnedTabs) { - this._resetTabToStoredState(tab); + const allAreUnloaded = pinnedTabs.every( + (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential') + ); + for (const tab of pinnedTabs) { + if (allAreUnloaded && closeIfPending) { + return await this.onCloseTabShortcut(event, tab, { behavior: 'close' }); } } - break; - case 'reset': + await gBrowser.explicitUnloadTabs(pinnedTabs); + for (const tab of pinnedTabs) { + tab.removeAttribute('discarded'); + } + } + if (selectedTabs.length) { + this._handleTabSwitch(selectedTabs[0]); + } + if (behavior.includes('reset')) { for (const tab of pinnedTabs) { this._resetTabToStoredState(tab); } - break; - default: - return; - } - } catch (ex) { - console.error('Error handling close tab shortcut for pinned tab:', ex); + } + break; + case 'reset': + for (const tab of pinnedTabs) { + this._resetTabToStoredState(tab); + } + break; + default: + return; } + } catch (ex) { + console.error('Error handling close tab shortcut for pinned tab:', ex); } + } _handleTabSwitch(selectedTab) { if (selectedTab !== gBrowser.selectedTab) { @@ -425,61 +386,61 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - addToEssentials(tab) { - const tabs = tab - ? // if it's already an array, dont make it [tab] - tab?.length - ? tab - : [tab] - : TabContextMenu.contextTab.multiselected - ? gBrowser.selectedTabs - : [TabContextMenu.contextTab]; - let movedAll = true; - for (let i = 0; i < tabs.length; i++) { - let tab = tabs[i]; - const section = gZenWorkspaces.getEssentialsSection(tab); - if (!this.canEssentialBeAdded(tab)) { - movedAll = false; - continue; - } - if (tab.hasAttribute('zen-essential')) { - continue; - } - tab.setAttribute('zen-essential', 'true'); - if (tab.hasAttribute('zen-workspace-id')) { - tab.removeAttribute('zen-workspace-id'); - } - if (tab.pinned && tab.hasAttribute('zen-pin-id')) { - gBrowser.zenHandleTabMove(tab, () => { - if (tab.ownerGlobal !== window) { - tab = gBrowser.adoptTab(tab, { - selectTab: tab.selected, - }); - tab.setAttribute('zen-essential', 'true'); - } else { - section.appendChild(tab); - } - }); - } else { - gBrowser.pinTab(tab); - this._ignoreNextTabPinnedEvent = true; - } - tab.setAttribute('zenDefaultUserContextId', true); - if (tab.selected) { - gZenWorkspaces.switchTabIfNeeded(tab); - } - this.onTabIconChanged(tab); - // Dispatch the event to update the UI - const event = new CustomEvent('TabAddedToEssentials', { - detail: { tab }, - bubbles: true, - cancelable: false, + addToEssentials(tab) { + const tabs = tab + ? // if it's already an array, dont make it [tab] + tab?.length + ? tab + : [tab] + : TabContextMenu.contextTab.multiselected + ? gBrowser.selectedTabs + : [TabContextMenu.contextTab]; + let movedAll = true; + for (let i = 0; i < tabs.length; i++) { + let tab = tabs[i]; + const section = gZenWorkspaces.getEssentialsSection(tab); + if (!this.canEssentialBeAdded(tab)) { + movedAll = false; + continue; + } + if (tab.hasAttribute('zen-essential')) { + continue; + } + tab.setAttribute('zen-essential', 'true'); + if (tab.hasAttribute('zen-workspace-id')) { + tab.removeAttribute('zen-workspace-id'); + } + if (tab.pinned && tab.hasAttribute('zen-pin-id')) { + gBrowser.zenHandleTabMove(tab, () => { + if (tab.ownerGlobal !== window) { + tab = gBrowser.adoptTab(tab, { + selectTab: tab.selected, + }); + tab.setAttribute('zen-essential', 'true'); + } else { + section.appendChild(tab); + } }); - tab.dispatchEvent(event); + } else { + gBrowser.pinTab(tab); + this._ignoreNextTabPinnedEvent = true; + } + tab.setAttribute('zenDefaultUserContextId', true); + if (tab.selected) { + gZenWorkspaces.switchTabIfNeeded(tab); } - gZenUIManager.updateTabsToolbar(); - return movedAll; + this.onTabIconChanged(tab); + // Dispatch the event to update the UI + const event = new CustomEvent('TabAddedToEssentials', { + detail: { tab }, + bubbles: true, + cancelable: false, + }); + tab.dispatchEvent(event); } + gZenUIManager.updateTabsToolbar(); + return movedAll; + } removeEssentials(tab, unpin = true) { const tabs = tab @@ -842,8 +803,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true'; } - async updatePinTitle(tab, newTitle, isEdited = true) { - const uuid = tab.getAttribute('zen-pin-id'); + async updatePinTitle(tab, newTitle, isEdited = true) { + const uuid = tab.getAttribute('zen-pin-id'); const browsers = Services.wm.getEnumerator('navigator:browser'); @@ -987,16 +948,16 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - async onTabLabelChanged(tab) { - tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } })); - if (!this._pinsCache) { - return; - } - // If our current pin in the cache point to about:blank, we need to update the entry - const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); - if (!pin) { - return; - } + async onTabLabelChanged(tab) { + tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } })); + if (!this._pinsCache) { + return; + } + // If our current pin in the cache point to about:blank, we need to update the entry + const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); + if (!pin) { + return; + } if (pin.url === 'about:blank' && tab.linkedBrowser.currentURI.spec !== 'about:blank') { await this.replacePinnedUrlWithCurrent(tab); From 04ab9a4b4ed2ffdd4727d1cb226b65823816b471 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 22 Nov 2025 18:15:18 +0100 Subject: [PATCH 39/54] feat: Listen for more tab events and properly sync them, b=no-bug, c=common, folders, tabs --- src/browser/base/content/zen-assets.inc.xhtml | 1 - .../sessionstore/SessionStore-sys-mjs.patch | 6 +- src/zen/common/modules/ZenSessionStore.mjs | 4 +- src/zen/common/modules/ZenUIManager.mjs | 1 - src/zen/folders/ZenFolders.mjs | 3 +- .../sessionstore/ZenSessionManager.sys.mjs | 2 +- src/zen/sessionstore/ZenWindowSync.sys.mjs | 232 ++++++- src/zen/tabs/ZenPinnedTabManager.mjs | 4 +- src/zen/tabs/ZenPinnedTabsStorage.mjs | 635 ------------------ src/zen/tabs/jar.inc.mn | 1 - 10 files changed, 223 insertions(+), 666 deletions(-) delete mode 100644 src/zen/tabs/ZenPinnedTabsStorage.mjs diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index 2ab4e638ed..7f3bce1f40 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -47,7 +47,6 @@ - diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 47833210a7..6390c02f9d 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 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..9ef1996a0e8a3ebe55dc25921b8fc8cc0ac8a303 100644 +index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..a7ae5953037bbdab8844c9e400caf233bd61eb0f 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -72,7 +72,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..9ef1996a0e8a3ebe55dc25921b8fc8cc ); tabState.pinned = false; + tabState.zenEssential = false; -+ tabState.zenPinnedId = null; ++ tabState.zenSyncId = null; + tabState.zenIsGlance = false; + tabState.zenGlanceId = null; + tabState.zenHasStaticLabel = false; @@ -172,7 +172,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..9ef1996a0e8a3ebe55dc25921b8fc8cc + tab.setAttribute("zen-has-static-label", "true"); + } + if (tabData.zenSyncId) { -+ tab.setAttribute("id", tabData.zenPinnedId); ++ tab.setAttribute("id", tabData.zenSyncId); + } + if (tabData.zenDefaultUserContextId) { + tab.setAttribute("zenDefaultUserContextId", true); diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs index c19233e7d2..aad335c014 100644 --- a/src/zen/common/modules/ZenSessionStore.mjs +++ b/src/zen/common/modules/ZenSessionStore.mjs @@ -17,8 +17,8 @@ class ZenSessionStore extends nsZenPreloadedFeature { if (tabData.zenWorkspace) { tab.setAttribute('zen-workspace-id', tabData.zenWorkspace); } - if (tabData.zenPinnedId) { - tab.setAttribute('id', tabData.zenPinnedId); + if (tabData.zenSyncId) { + tab.setAttribute('id', tabData.zenSyncId); } if (tabData.zenHasStaticLabel) { tab.setAttribute('zen-has-static-label', 'true'); diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs index 780063f0fd..517696d30b 100644 --- a/src/zen/common/modules/ZenUIManager.mjs +++ b/src/zen/common/modules/ZenUIManager.mjs @@ -811,7 +811,6 @@ window.gZenVerticalTabsManager = { !aItem.isConnected || gZenUIManager.testingEnabled || !gZenStartup.isReady || - !gZenPinnedTabManager.hasInitializedPins || aItem.group?.hasAttribute('split-view-group') ) { return; diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index d5adaa8b01..08b0ad4653 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -945,8 +945,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (gBrowser.isTabGroup(prevSibling)) { prevSiblingInfo = { type: 'group', id: prevSibling.id }; } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) { - const zenPinId = prevSibling.getAttribute('id'); - prevSiblingInfo = { type: 'tab', id: zenPinId }; + prevSiblingInfo = { type: 'tab', id: prevSibling.getAttribute('id') }; } else { prevSiblingInfo = { type: 'start', id: null }; } diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 914a3eb6d6..2d2a630a90 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -164,7 +164,7 @@ class nsZenSessionManager { let newWindow = Cu.cloneInto(windows[0], {}); delete newWindow.selected; const newState = { windows: [newWindow] }; - SessionStoreInternal.restoreWindows(aWindow, newState, {}); + //SessionStoreInternal.restoreWindows(aWindow, newState, {}); }); } } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 42947a9ed4..12b70f64eb 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -10,17 +10,41 @@ ChromeUtils.defineESModuleGetters(lazy, { TabStateFlusher: 'resource:///modules/sessionstore/TabStateFlusher.sys.mjs', }); -const OBSERVING = ['browser-window-delayed-startup']; -const EVENTS = ['TabOpen']; +const OBSERVING = ['browser-window-before-show']; +const EVENTS = [ + 'TabOpen', + 'ZenTabIconChanged', + 'ZenTabLabelChanged', + 'TabMove', + 'TabPinned', + 'TabUnpinned', + 'TabClose', + 'TabAddedToEssentials', + 'TabRemovedFromEssentials', +]; + +// Flags acting as an enum for sync types. +const SYNC_FLAG_LABEL = 1 << 0; +const SYNC_FLAG_ICON = 1 << 1; +const SYNC_FLAG_MOVE = 1 << 2; class nsZenWindowSync { constructor() {} /** - * Whether to ignore the next set of events. - * This is used to prevent recursive event handling. + * Context about the currently handled event. + * Used to avoid re-entrancy issues. + * + * We do still wan't to keep a stack of these in order + * to handle consequtive events properly. For example, + * loading a webpage will call IconChanged and TitleChanged + * events one after another. */ - #ignoreNextEvents = false; + #eventHandlingContext = { + window: null, + eventCount: 0, + lastHandlerPromise: Promise.resolve(), + }; /** * Iterator that yields all currently opened browser windows. @@ -31,7 +55,7 @@ class nsZenWindowSync { #browserWindows = { *[Symbol.iterator]() { for (let window of lazy.BrowserWindowTracker.orderedWindows) { - if (window.__SSi && !window.closed) { + if (window.__SSi && !window.closed && window.gZenStartup.isReady) { yield window; } } @@ -42,7 +66,7 @@ class nsZenWindowSync { for (let topic of OBSERVING) { Services.obs.addObserver(this, topic); } - SessionStore.promiseInitialized.then(() => { + lazy.SessionStore.promiseAllWindowsRestored.then(() => { this.#onSessionStoreInitialized(); }); } @@ -106,18 +130,16 @@ class nsZenWindowSync { * @param {Function} aCallback - The callback function to run on each window. */ #runOnAllWindows(aWindow, aCallback) { - this.#ignoreNextEvents = true; for (let window of this.#browserWindows) { if (window !== aWindow) { aCallback(window); } } - this.#ignoreNextEvents = false; } observe(aSubject, aTopic) { switch (aTopic) { - case 'browser-window-delayed-startup': { + case 'browser-window-before-show': { this.#onWindowBeforeShow(aSubject); break; } @@ -125,9 +147,41 @@ class nsZenWindowSync { } handleEvent(aEvent) { - if (this.#ignoreNextEvents) { + const window = aEvent.currentTarget.ownerGlobal; + if (!window.gZenStartup.isReady) { + return; + } + if (this.#eventHandlingContext.window && this.#eventHandlingContext.window !== window) { + // We're already handling an event for another window. + // To avoid re-entrancy issues, we skip this event. return; } + const lastHandlerPromise = this.#eventHandlingContext.lastHandlerPromise; + this.#eventHandlingContext.eventCount++; + this.#eventHandlingContext.window = window; + let resolveNewPromise; + this.#eventHandlingContext.lastHandlerPromise = new Promise((resolve) => { + resolveNewPromise = resolve; + }); + // Wait for the last handler to finish before processing the next event. + lastHandlerPromise.then(() => { + try { + this.#handleNextEvent(aEvent); + } finally { + if (--this.#eventHandlingContext.eventCount === 0) { + this.#eventHandlingContext.window = null; + } + resolveNewPromise(); + } + }); + } + + /** + * Handles the next event by calling the appropriate handler method. + * + * @param {Event} aEvent - The event to handle. + */ + #handleNextEvent(aEvent) { const handler = `on_${aEvent.type}`; if (typeof this[handler] === 'function') { this[handler](aEvent); @@ -136,18 +190,46 @@ class nsZenWindowSync { } } + /** + * Retrieves a tab element from a window by its ID. + * + * @param {Window} aWindow - The window containing the tab. + * @param {string} aTabId - The ID of the tab to retrieve. + * @returns {Object|null} The tab element if found, otherwise null. + */ + #getTabFromWindow(aWindow, aTabId) { + return aWindow.document.getElementById(aTabId); + } + /** * Synchronizes the icon and label of the target tab with the original tab. * * @param {Object} aOriginalTab - The original tab to copy from. * @param {Object} aTargetTab - The target tab to copy to. * @param {Window} aWindow - The window containing the tabs. + * @param {number} flags - The sync flags indicating what to synchronize. */ - #syncTabWithOriginal(aOriginalTab, aTargetTab, aWindow) { + #syncTabWithOriginal(aOriginalTab, aTargetTab, aWindow, flags = 0) { + if (!aOriginalTab || !aTargetTab) { + return; + } const { gBrowser } = aWindow; - gBrowser.setIcon(aTargetTab, gBrowser.getIcon(aOriginalTab)); - gBrowser._setTabLabel(aTargetTab, aOriginalTab.label); - this.#syncTabPosition(aOriginalTab, aTargetTab, aWindow); + if (flags & SYNC_FLAG_ICON) { + gBrowser.setIcon(aTargetTab, gBrowser.getIcon(aOriginalTab)); + } + if (flags & SYNC_FLAG_LABEL) { + gBrowser._setTabLabel(aTargetTab, aOriginalTab.label); + } + if (flags & SYNC_FLAG_MOVE && !aTargetTab.hasAttribute('zen-empty-tab')) { + const workspaceId = aOriginalTab.getAttribute('zen-workspace-id'); + if (workspaceId) { + aTargetTab.setAttribute('zen-workspace-id', workspaceId); + } else { + aTargetTab.removeAttribute('zen-workspace-id'); + } + this.#syncTabPosition(aOriginalTab, aTargetTab, aWindow); + } + lazy.TabStateFlusher.flush(aTargetTab.linkedBrowser); } /** @@ -177,15 +259,131 @@ class nsZenWindowSync { gBrowser.unpinTab(aTargetTab); } } + + this.#moveTabToMatchOriginal(aOriginalTab, aTargetTab, aWindow, { + isEssential: originalIsEssential, + isPinned: originalIsPinned, + }); } + /** + * Moves the target tab to match the position of the original tab. + * + * @param {Object} aOriginalTab - The original tab to match. + * @param {Object} aTargetTab - The target tab to move. + * @param {Window} aWindow - The window containing the tabs. + */ + #moveTabToMatchOriginal(aOriginalTab, aTargetTab, aWindow, { isEssential, isPinned }) { + const { gBrowser, gZenWorkspaces } = aWindow; + const originalSibling = aOriginalTab.previousElementSibling; + let isFirstTab = true; + if (gBrowser.isTabGroup(originalSibling) || gBrowser.isTab(originalSibling)) { + isFirstTab = !originalSibling.hasAttribute('id'); + } + + gBrowser.zenHandleTabMove(aOriginalTab, () => { + if (isFirstTab) { + let container; + if (isEssential) { + container = gZenWorkspaces.getEssentialsSection(aTargetTab); + } else { + const workspaceId = aTargetTab.getAttribute('zen-workspace-id'); + const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); + container = isPinned + ? workspaceElement.pinnedTabsContainer + : workspaceElement.tabsContainer; + } + if (container) { + container.insertBefore(aTargetTab, container.firstChild); + } + return; + } + const relativeTab = this.#getTabFromWindow(aWindow, originalSibling.id); + if (relativeTab) { + relativeTab.after(aTargetTab); + } + }); + } + + /** + * Synchronizes a tab across all browser windows. + * + * @param {Object} aTab - The tab to synchronize. + * @param {number} flags - The sync flags indicating what to synchronize. + */ + #syncTabForAllWindows(aTab, flags = 0) { + const window = aTab.ownerGlobal; + this.#runOnAllWindows(window, (win) => { + this.#syncTabWithOriginal(aTab, this.#getTabFromWindow(win, aTab.id), win, flags); + }); + } + + /** + * Delegates generic sync events to synchronize tabs across windows. + * + * @param {Event} aEvent - The event to delegate. + * @param {number} flags - The sync flags indicating what to synchronize. + */ + #delegateGenericSyncEvent(aEvent, flags = 0) { + const tab = aEvent.target; + this.#syncTabForAllWindows(tab, flags); + } + + /* Mark: Event Handlers */ + on_TabOpen(aEvent) { const tab = aEvent.target; const window = tab.ownerGlobal; - + tab.id = this.#newTabSyncId; this.#runOnAllWindows(window, (win) => { const newTab = win.gBrowser.duplicateTab(tab); - this.#syncTabWithOriginal(tab, newTab, win); + newTab.id = tab.id; + this.#syncTabWithOriginal( + tab, + newTab, + win, + SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE + ); + win.gZenVerticalTabsManager.animateItemOpen(newTab); + }); + } + + on_ZenTabIconChanged(aEvent) { + return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON); + } + + on_ZenTabLabelChanged(aEvent) { + return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_LABEL); + } + + on_TabMove(aEvent) { + return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_MOVE); + } + + on_TabPinned(aEvent) { + return this.on_TabMove(aEvent); + } + + on_TabUnpinned(aEvent) { + return this.on_TabMove(aEvent); + } + + on_TabAddedToEssentials(aEvent) { + return this.on_TabMove(aEvent); + } + + on_TabRemovedFromEssentials(aEvent) { + return this.on_TabMove(aEvent); + } + + on_TabClose(aEvent) { + const tab = aEvent.target; + const window = tab.ownerGlobal; + this.#runOnAllWindows(window, (win) => { + const targetTab = this.#getTabFromWindow(win, tab.id); + if (targetTab) { + win.gBrowser.removeTab(targetTab, { animate: true }); + } }); } } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index af06b405e1..3ef23aab13 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -60,7 +60,6 @@ class ZenPinnedTabsObserver { } class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { - hasInitializedPins = false; promiseInitializedPinned = new Promise((resolve) => { this._resolvePinnedInitializedInternal = resolve; }); @@ -87,7 +86,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } onTabIconChanged(tab, url = null) { - tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } })); const iconUrl = url ?? tab.iconImage.src; if (tab.hasAttribute('zen-essential')) { @@ -949,7 +947,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } async onTabLabelChanged(tab) { - tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } })); + tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { bubbles: true, detail: { tab } })); if (!this._pinsCache) { return; } diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs deleted file mode 100644 index 425dbf2d11..0000000000 --- a/src/zen/tabs/ZenPinnedTabsStorage.mjs +++ /dev/null @@ -1,635 +0,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/. -var ZenPinnedTabsStorage = { - async init() { - await this._ensureTable(); - }, - - async _ensureTable() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => { - // Create the pins table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - title TEXT NOT NULL, - url TEXT, - container_id INTEGER, - workspace_uuid TEXT, - position INTEGER NOT NULL DEFAULT 0, - is_essential BOOLEAN NOT NULL DEFAULT 0, - is_group BOOLEAN NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - const columns = await db.execute(`PRAGMA table_info(zen_pins)`); - const columnNames = columns.map((row) => row.getResultByName('name')); - - // Helper function to add column if it doesn't exist - const addColumnIfNotExists = async (columnName, definition) => { - if (!columnNames.includes(columnName)) { - await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`); - } - }; - - await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0'); - await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL'); - await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL'); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) - `); - - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_pins_changes ( - uuid TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL - ) - `); - - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid) - `); - - this._resolveInitialized(); - }); - }, - - /** - * Private helper method to notify observers with a list of changed UUIDs. - * @param {string} event - The observer event name. - * @param {Array} uuids - Array of changed workspace UUIDs. - */ - _notifyPinsChanged(event, uuids) { - if (uuids.length === 0) return; // No changes to notify - - // Convert the array of UUIDs to a JSON string - const data = JSON.stringify(uuids); - - Services.obs.notifyObservers(null, event, data); - }, - - async savePin(pin, notifyObservers = true) { - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - let newPosition; - if ('position' in pin && Number.isFinite(pin.position)) { - newPosition = pin.position; - } else { - // Get the maximum position within the same parent group (or null for root level) - const maxPositionResult = await db.execute( - ` - SELECT MAX("position") as max_position - FROM zen_pins - WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '') - `, - { folder_parent_uuid: pin.parentUuid || null } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - // Insert or replace the pin - await db.executeCached( - ` - INSERT OR REPLACE INTO zen_pins ( - uuid, title, url, container_id, workspace_uuid, position, - is_essential, is_group, folder_parent_uuid, edited_title, created_at, - updated_at, is_folder_collapsed, folder_icon - ) VALUES ( - :uuid, :title, :url, :container_id, :workspace_uuid, :position, - :is_essential, :is_group, :folder_parent_uuid, :edited_title, - COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), - :now, :is_folder_collapsed, :folder_icon - ) - `, - { - uuid: pin.uuid, - title: pin.title, - url: pin.isGroup ? '' : pin.url, - container_id: pin.containerTabId || null, - workspace_uuid: pin.workspaceUuid || null, - position: newPosition, - is_essential: pin.isEssential || false, - is_group: pin.isGroup || false, - folder_parent_uuid: pin.parentUuid || null, - edited_title: pin.editedTitle || false, - now, - folder_icon: pin.folderIcon || null, - is_folder_collapsed: pin.isFolderCollapsed || false, - } - ); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - - changedUUIDs.add(pin.uuid); - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async getPins() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.executeCached(` - SELECT * FROM zen_pins - ORDER BY position ASC - `); - return rows.map((row) => ({ - uuid: row.getResultByName('uuid'), - title: row.getResultByName('title'), - url: row.getResultByName('url'), - containerTabId: row.getResultByName('container_id'), - workspaceUuid: row.getResultByName('workspace_uuid'), - position: row.getResultByName('position'), - isEssential: Boolean(row.getResultByName('is_essential')), - isGroup: Boolean(row.getResultByName('is_group')), - parentUuid: row.getResultByName('folder_parent_uuid'), - editedTitle: Boolean(row.getResultByName('edited_title')), - folderIcon: row.getResultByName('folder_icon'), - isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')), - })); - }, - - /** - * Create a new group - * @param {string} title - The title of the group - * @param {string} workspaceUuid - The workspace UUID (optional) - * @param {string} parentUuid - The parent group UUID (optional, null for root level) - * @param {number} position - The position of the group (optional, will auto-calculate if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - * @returns {Promise} The UUID of the created group - */ - async createGroup( - title, - icon = null, - isCollapsed = false, - workspaceUuid = null, - parentUuid = null, - position = null, - notifyObservers = true - ) { - if (!title || typeof title !== 'string') { - throw new Error('Group title is required and must be a string'); - } - - const groupUuid = gZenUIManager.generateUuidv4(); - - const groupPin = { - uuid: groupUuid, - title, - folderIcon: icon || null, - isFolderCollapsed: isCollapsed || false, - workspaceUuid, - parentUuid, - position, - isGroup: true, - isEssential: false, - editedTitle: true, // Group titles are always considered edited - }; - - await this.savePin(groupPin, notifyObservers); - return groupUuid; - }, - - /** - * Add an existing tab/pin to a group - * @param {string} tabUuid - The UUID of the tab to add to the group - * @param {string} groupUuid - The UUID of the target group - * @param {number} position - The position within the group (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) { - if (!tabUuid || !groupUuid) { - throw new Error('Both tabUuid and groupUuid are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => { - await db.executeTransaction(async () => { - // Verify the group exists and is actually a group - const groupCheck = await db.execute( - `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`, - { groupUuid } - ); - - if (groupCheck.length === 0) { - throw new Error(`Group with UUID ${groupUuid} does not exist`); - } - - if (!groupCheck[0].getResultByName('is_group')) { - throw new Error(`Pin with UUID ${groupUuid} is not a group`); - } - - const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, { - tabUuid, - }); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - const now = Date.now(); - let newPosition; - - if (position !== null && Number.isFinite(position)) { - newPosition = position; - } else { - // Get the maximum position within the group - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`, - { groupUuid } - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; - } - - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = :groupUuid, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - groupUuid, - newPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - /** - * Remove a tab from its group (move to root level) - * @param {string} tabUuid - The UUID of the tab to remove from its group - * @param {number} newPosition - The new position at root level (optional, will append if not provided) - * @param {boolean} notifyObservers - Whether to notify observers (default: true) - */ - async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) { - if (!tabUuid) { - throw new Error('tabUuid is required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.removeTabFromGroup', - async (db) => { - await db.executeTransaction(async () => { - // Verify the tab exists and is in a group - const tabCheck = await db.execute( - `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`, - { tabUuid } - ); - - if (tabCheck.length === 0) { - throw new Error(`Tab with UUID ${tabUuid} does not exist`); - } - - if (!tabCheck[0].getResultByName('folder_parent_uuid')) { - return; - } - - const now = Date.now(); - let finalPosition; - - if (newPosition !== null && Number.isFinite(newPosition)) { - finalPosition = newPosition; - } else { - // Get the maximum position at root level (where folder_parent_uuid is null) - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL` - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - finalPosition = maxPosition + 1000; - } - - // Update the tab to be at root level - await db.execute( - ` - UPDATE zen_pins - SET folder_parent_uuid = NULL, - position = :newPosition, - updated_at = :now - WHERE uuid = :tabUuid - `, - { - tabUuid, - newPosition: finalPosition, - now, - } - ); - - changedUUIDs.add(tabUuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: tabUuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async removePin(uuid, notifyObservers = true) { - const changedUUIDs = [uuid]; - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { - await db.executeTransaction(async () => { - // Get all child UUIDs first for change tracking - const children = await db.execute( - `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`, - { - uuid, - } - ); - - // Add child UUIDs to changedUUIDs array - for (const child of children) { - changedUUIDs.push(child.getResultByName('uuid')); - } - - // Delete the pin/group itself - await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid }); - - // Record the changes - const now = Math.floor(Date.now() / 1000); - for (const changedUuid of changedUUIDs) { - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: changedUuid, - timestamp: now, - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - }); - - if (notifyObservers) { - this._notifyPinsChanged('zen-pin-removed', changedUUIDs); - } - }, - - async wipeAllPins() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => { - await db.execute(`DELETE FROM zen_pins`); - await db.execute(`DELETE FROM zen_pins_changes`); - await this.updateLastChangeTimestamp(db); - }); - }, - - async markChanged(uuid) { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - }); - }, - - async getChangedIDs() { - const db = await PlacesUtils.promiseDBConnection(); - const rows = await db.execute(` - SELECT uuid, timestamp FROM zen_pins_changes - `); - const changes = {}; - for (const row of rows) { - changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); - } - return changes; - }, - - async clearChangedIDs() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => { - await db.execute(`DELETE FROM zen_pins_changes`); - }); - }, - - shouldReorderPins(before, current, after) { - const minGap = 1; // Minimum allowed gap between positions - return ( - (before !== null && current - before < minGap) || (after !== null && after - current < minGap) - ); - }, - - async reorderAllPins(db, changedUUIDs) { - const pins = await db.execute(` - SELECT uuid - FROM zen_pins - ORDER BY position ASC - `); - - for (let i = 0; i < pins.length; i++) { - const newPosition = (i + 1) * 1000; // Use large increments - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pins[i].getResultByName('uuid') } - ); - changedUUIDs.add(pins[i].getResultByName('uuid')); - } - }, - - async updateLastChangeTimestamp(db) { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO moz_meta (key, value) - VALUES ('zen_pins_last_change', :now) - `, - { now } - ); - }, - - async getLastChangeTimestamp() { - const db = await PlacesUtils.promiseDBConnection(); - const result = await db.executeCached(` - SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change' - `); - return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; - }, - - async updatePinPositions(pins) { - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper( - 'ZenPinnedTabsStorage.updatePinPositions', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - for (let i = 0; i < pins.length; i++) { - const pin = pins[i]; - const newPosition = (i + 1) * 1000; - - await db.execute( - ` - UPDATE zen_pins - SET position = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: pin.uuid } - ); - - changedUUIDs.add(pin.uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: pin.uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - }, - - async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) { - if (!uuid || typeof newTitle !== 'string') { - throw new Error('Invalid parameters: uuid and newTitle are required'); - } - - const changedUUIDs = new Set(); - - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - // Update the pin's title and edited_title flag - const result = await db.execute( - ` - UPDATE zen_pins - SET title = :newTitle, - edited_title = :isEdited, - updated_at = :now - WHERE uuid = :uuid - `, - { - uuid, - newTitle, - isEdited, - now, - } - ); - - // Only proceed with change tracking if a row was actually updated - if (result.rowsAffected > 0) { - changedUUIDs.add(uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - } - }); - }); - - if (notifyObservers && changedUUIDs.size > 0) { - this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs)); - } - }, - - async __dropTables() { - await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => { - await db.execute(`DROP TABLE IF EXISTS zen_pins`); - await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`); - }); - }, -}; - -ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => { - ZenPinnedTabsStorage._resolveInitialized = resolve; - ZenPinnedTabsStorage.init(); -}); diff --git a/src/zen/tabs/jar.inc.mn b/src/zen/tabs/jar.inc.mn index 2acab4816b..192d0d33d2 100644 --- a/src/zen/tabs/jar.inc.mn +++ b/src/zen/tabs/jar.inc.mn @@ -2,7 +2,6 @@ # 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/. - content/browser/zen-components/ZenPinnedTabsStorage.mjs (../../zen/tabs/ZenPinnedTabsStorage.mjs) content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs) * content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css) content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css) \ No newline at end of file From 57233a18f60891913b84505df071d440b38d2f7c Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 23 Nov 2025 19:08:39 +0100 Subject: [PATCH 40/54] feat: Start moving browser views to the selected windows, b=no-bug, c=no-component --- .../tabbrowser/content/tabbrowser-js.patch | 148 ++++++++++++------ src/zen/sessionstore/ZenWindowSync.sys.mjs | 87 +++++++++- 2 files changed, 182 insertions(+), 53 deletions(-) diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index d34b618822..da60f7df32 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca08b55b76 100644 +index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564caba7ad06f 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -140,7 +140,15 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1379,7 +1443,6 @@ +@@ -1362,6 +1426,7 @@ + + let newTab = this.getTabForBrowser(newBrowser); + ++ newBrowser = gZenWindowSync.onTabSwitchOrWindowFocus(window, newTab); + let timerId; + if (!aForceUpdate) { + timerId = Glean.browserTabswitch.update.start(); +@@ -1379,7 +1444,6 @@ // Preview mode should not reset the owner if (!this._previewMode && !oldTab.selected) { @@ -148,7 +156,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); -@@ -1470,6 +1533,7 @@ +@@ -1470,6 +1534,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -156,7 +164,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1622,6 +1686,9 @@ +@@ -1622,6 +1687,9 @@ } let activeEl = document.activeElement; @@ -166,7 +174,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1945,7 +2012,8 @@ +@@ -1945,7 +2013,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -176,7 +184,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca return false; } -@@ -2053,7 +2121,7 @@ +@@ -2053,7 +2122,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -185,7 +193,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2328,6 +2396,7 @@ +@@ -2328,6 +2397,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -193,7 +201,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2401,8 +2470,7 @@ +@@ -2401,8 +2471,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -203,7 +211,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca b.setAttribute("transparent", "true"); } -@@ -2567,7 +2635,7 @@ +@@ -2567,7 +2636,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -212,7 +220,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2626,8 +2694,8 @@ +@@ -2626,8 +2695,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -223,7 +231,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2814,7 +2882,6 @@ +@@ -2814,7 +2883,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -231,7 +239,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2990,8 @@ +@@ -2923,6 +2991,8 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -240,7 +248,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,10 +3002,17 @@ +@@ -2933,10 +3003,17 @@ ); } @@ -258,7 +266,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca // If we're opening a foreground tab, set the owner by default. ownerTab ??= inBackground ? null : this.selectedTab; -@@ -2944,6 +3020,7 @@ +@@ -2944,6 +3021,7 @@ if (this.selectedTab.owner) { this.selectedTab.owner = null; } @@ -266,7 +274,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca // Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the -@@ -2996,6 +3073,19 @@ +@@ -2996,6 +3074,19 @@ noInitialLabel, skipBackgroundNotify, }); @@ -286,7 +294,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3094,7 @@ +@@ -3004,6 +3095,7 @@ ownerTab, openerTab, pinned, @@ -294,7 +302,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3113,7 @@ +@@ -3022,6 +3114,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -302,7 +310,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca })); if (focusUrlBar) { -@@ -3146,6 +3238,12 @@ +@@ -3146,6 +3239,12 @@ } } @@ -315,7 +323,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3330,10 +3428,10 @@ +@@ -3330,10 +3429,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -327,7 +335,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } if (!color) { -@@ -3354,9 +3452,14 @@ +@@ -3354,9 +3453,14 @@ label, isAdoptingGroup ); @@ -344,7 +352,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca ); group.addTabs(tabs); -@@ -3477,7 +3580,7 @@ +@@ -3477,7 +3581,7 @@ } this.#handleTabMove(tab, () => @@ -353,7 +361,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca ); } -@@ -3679,6 +3782,7 @@ +@@ -3679,6 +3783,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -361,7 +369,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3748,6 +3852,7 @@ +@@ -3748,6 +3853,7 @@ openWindowInfo, name, skipLoad, @@ -369,7 +377,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca }); } -@@ -3935,7 +4040,7 @@ +@@ -3935,7 +4041,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -378,7 +386,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca let url = "about:blank"; if (tabData.entries?.length) { -@@ -3972,8 +4077,10 @@ +@@ -3972,8 +4078,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -390,7 +398,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if (select) { tabToSelect = tab; } -@@ -3985,7 +4092,8 @@ +@@ -3985,7 +4093,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -400,7 +408,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -3999,7 +4107,10 @@ +@@ -3999,7 +4108,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -412,7 +420,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca ); tabsFragment.appendChild(tabGroup.node); } -@@ -4044,9 +4155,23 @@ +@@ -4044,9 +4156,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -428,15 +436,15 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca + gZenWorkspaces._initialTab._shouldRemove = true; + } + } - } ++ } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; -+ } + } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4237,11 +4362,14 @@ +@@ -4237,11 +4363,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -452,7 +460,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if ( !bulkOrderedOpen && ((openerTab && -@@ -4253,7 +4381,7 @@ +@@ -4253,7 +4382,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -461,7 +469,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca tabGroup = previousTab.group; } if ( -@@ -4264,7 +4392,7 @@ +@@ -4264,7 +4393,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -470,7 +478,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4292,14 +4420,14 @@ +@@ -4292,14 +4421,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -489,7 +497,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4310,7 +4438,7 @@ +@@ -4310,7 +4439,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -498,7 +506,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4338,7 +4466,11 @@ +@@ -4338,7 +4467,11 @@ const tabContainer = pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; @@ -510,7 +518,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } this._updateTabsAfterInsert(); -@@ -4346,6 +4478,7 @@ +@@ -4346,6 +4479,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -518,7 +526,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca TabBarVisibility.update(); } -@@ -4896,6 +5029,7 @@ +@@ -4896,6 +5030,7 @@ telemetrySource, } = {} ) { @@ -526,7 +534,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5119,7 @@ +@@ -4985,6 +5120,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -534,7 +542,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } catch (e) { console.error(e); } -@@ -5023,6 +5158,12 @@ +@@ -5023,6 +5159,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -547,7 +555,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5178,9 @@ +@@ -5037,6 +5179,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -557,7 +565,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5229,13 @@ +@@ -5085,7 +5230,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -572,7 +580,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca return; } -@@ -5219,7 +5369,7 @@ +@@ -5219,7 +5370,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -581,7 +589,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5393,7 @@ +@@ -5243,6 +5394,7 @@ newTab = true; } @@ -589,7 +597,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5434,7 @@ +@@ -5283,13 +5435,7 @@ aTab._mouseleave(); if (newTab) { @@ -604,7 +612,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } else { TabBarVisibility.update(); } -@@ -5422,6 +5567,7 @@ +@@ -5422,6 +5568,7 @@ this.tabs[i]._tPos = i; } @@ -612,7 +620,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5789,7 @@ +@@ -5643,6 +5790,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -620,7 +628,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5802,13 @@ +@@ -5655,13 +5803,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -636,7 +644,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5824,7 @@ +@@ -5677,7 +5825,7 @@ } if (tab) { @@ -645,7 +653,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5845,7 @@ +@@ -5698,7 +5846,7 @@ }); } @@ -654,6 +662,46 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..d1c089ba66defc74dbf06b283bc6ddca } _blurTab(aTab) { +@@ -5709,7 +5857,7 @@ + * @returns {boolean} + * False if swapping isn't permitted, true otherwise. + */ +- swapBrowsersAndCloseOther(aOurTab, aOtherTab) { ++ swapBrowsersAndCloseOther(aOurTab, aOtherTab, zenCloseOther = true) { + // Do not allow transfering a private tab to a non-private window + // and vice versa. + if ( +@@ -5763,6 +5911,7 @@ + // fire the beforeunload event in the process. Close the other + // window if this was its last tab. + if ( ++ zenCloseOther && + !remoteBrowser._beginRemoveTab(aOtherTab, { + adoptedByTab: aOurTab, + closeWindowWithLastTab: true, +@@ -5774,7 +5923,7 @@ + // If this is the last tab of the window, hide the window + // immediately without animation before the docshell swap, to avoid + // about:blank being painted. +- let [closeWindow] = aOtherTab._endRemoveArgs; ++ let [closeWindow] = !zenCloseOther ? [false] : aOtherTab._endRemoveArgs; + if (closeWindow) { + let win = aOtherTab.ownerGlobal; + win.windowUtils.suppressAnimation(true); +@@ -5898,11 +6047,13 @@ + } + + // Finish tearing down the tab that's going away. ++ if (zenCloseOther) { + if (closeWindow) { + aOtherTab.ownerGlobal.close(); + } else { + remoteBrowser._endRemoveTab(aOtherTab); + } ++ } + + this.setTabTitle(aOurTab); + @@ -6104,10 +6251,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 12b70f64eb..6d3b89118b 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -13,14 +13,19 @@ ChromeUtils.defineESModuleGetters(lazy, { const OBSERVING = ['browser-window-before-show']; const EVENTS = [ 'TabOpen', + 'TabClose', + 'ZenTabIconChanged', 'ZenTabLabelChanged', + 'TabMove', 'TabPinned', 'TabUnpinned', - 'TabClose', 'TabAddedToEssentials', 'TabRemovedFromEssentials', + + 'focus', + 'unload', ]; // Flags acting as an enum for sync types. @@ -84,6 +89,7 @@ class nsZenWindowSync { * @param {Window} aWindow - The browser window that is about to be shown. */ #onWindowBeforeShow(aWindow) { + aWindow.gZenWindowSync = this; for (let eventName of EVENTS) { aWindow.addEventListener(eventName, this); } @@ -318,6 +324,67 @@ class nsZenWindowSync { }); } + /** + * Swaps the browser docshells between two tabs. + * + * @param {Object} aOurTab - The tab in the current window. + * @param {Object} aOtherTab - The tab in the other window. + */ + #swapBrowserDocShells(aOurTab, aOtherTab) { + try { + aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); + const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; + // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', + // but we need to manually remove some attributes from the other tab. + for (let attr of kAttributesToRemove) { + aOtherTab.removeAttribute(attr); + } + aOtherTab.linkedBrowser.style.opacity = 0; + aOurTab.linkedBrowser.style.opacity = ''; + } catch (e) { + // Handle any errors that may occur during the swapBrowsers operation. + console.error('Error swapping browsers:', e); + } + } + + /** + * Retrieves the active tab, where the web contents are being viewed + * from other windows by its ID. + * + * @param {Window} aWindow - The window to exclude. + * @param {string} aTabId - The ID of the tab to retrieve. + * @returns {Object|null} The active tab from other windows if found, otherwise null. + */ + #getActiveTabFromOtherWindows(aWindow, aTabId) { + for (let window of this.#browserWindows) { + if (window !== aWindow) { + const tab = this.#getTabFromWindow(window, aTabId); + if (tab?._zenContentsVisible) { + return tab; + } + } + } + return null; + } + + /** + * Handles tab switch or window focus events to synchronize tab contents visibility. + * + * @param {Window} aWindow - The window that triggered the event. + */ + onTabSwitchOrWindowFocus(aWindow) { + const selectedTab = aWindow.gBrowser.selectedTab; + if (selectedTab._zenContentsVisible) { + return; + } + const otherSelectedTab = this.#getActiveTabFromOtherWindows(aWindow, selectedTab.id); + selectedTab._zenContentsVisible = true; + if (otherSelectedTab) { + delete otherSelectedTab._zenContentsVisible; + this.#swapBrowserDocShells(selectedTab, otherSelectedTab); + } + } + /** * Delegates generic sync events to synchronize tabs across windows. * @@ -334,9 +401,17 @@ class nsZenWindowSync { on_TabOpen(aEvent) { const tab = aEvent.target; const window = tab.ownerGlobal; + if (tab.id) { + // This tab was opened as part of a sync operation. + return; + } tab.id = this.#newTabSyncId; + if (tab.selected) { + tab._zenContentsVisible = true; + } this.#runOnAllWindows(window, (win) => { - const newTab = win.gBrowser.duplicateTab(tab); + const newTab = win.gBrowser.addTrustedTab('about:blank', { animate: true }); + newTab.setAttribute('zen-workspace-id', tab.getAttribute('zen-workspace-id') || ''); newTab.id = tab.id; this.#syncTabWithOriginal( tab, @@ -344,7 +419,6 @@ class nsZenWindowSync { win, SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE ); - win.gZenVerticalTabsManager.animateItemOpen(newTab); }); } @@ -386,6 +460,13 @@ class nsZenWindowSync { } }); } + + on_focus(aEvent) { + const { ownerGlobal: window } = aEvent.target; + this.onTabSwitchOrWindowFocus(window); + } + + on_unload() {} } export const ZenWindowSync = new nsZenWindowSync(); From 722ffa5e9733d374049c65159930db2578bdc293 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 23 Nov 2025 19:09:45 +0100 Subject: [PATCH 41/54] chore: Remove extra patch, b=no-bug, c=no-component --- .../tabbrowser/content/tabbrowser-js.patch | 144 +++++++++--------- 1 file changed, 68 insertions(+), 76 deletions(-) diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index da60f7df32..889af0a55b 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564caba7ad06f 100644 +index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9ff6eb16c8 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -140,15 +140,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1362,6 +1426,7 @@ - - let newTab = this.getTabForBrowser(newBrowser); - -+ newBrowser = gZenWindowSync.onTabSwitchOrWindowFocus(window, newTab); - let timerId; - if (!aForceUpdate) { - timerId = Glean.browserTabswitch.update.start(); -@@ -1379,7 +1444,6 @@ +@@ -1379,7 +1443,6 @@ // Preview mode should not reset the owner if (!this._previewMode && !oldTab.selected) { @@ -156,7 +148,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); -@@ -1470,6 +1534,7 @@ +@@ -1470,6 +1533,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -164,7 +156,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1622,6 +1687,9 @@ +@@ -1622,6 +1686,9 @@ } let activeEl = document.activeElement; @@ -174,7 +166,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1945,7 +2013,8 @@ +@@ -1945,7 +2012,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -184,7 +176,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca return false; } -@@ -2053,7 +2122,7 @@ +@@ -2053,7 +2121,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -193,7 +185,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2328,6 +2397,7 @@ +@@ -2328,6 +2396,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -201,7 +193,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2401,8 +2471,7 @@ +@@ -2401,8 +2470,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -211,7 +203,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca b.setAttribute("transparent", "true"); } -@@ -2567,7 +2636,7 @@ +@@ -2567,7 +2635,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -220,7 +212,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2626,8 +2695,8 @@ +@@ -2626,8 +2694,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -231,7 +223,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2814,7 +2883,6 @@ +@@ -2814,7 +2882,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -239,7 +231,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2991,8 @@ +@@ -2923,6 +2990,8 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -248,7 +240,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,10 +3003,17 @@ +@@ -2933,10 +3002,17 @@ ); } @@ -266,7 +258,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // If we're opening a foreground tab, set the owner by default. ownerTab ??= inBackground ? null : this.selectedTab; -@@ -2944,6 +3021,7 @@ +@@ -2944,6 +3020,7 @@ if (this.selectedTab.owner) { this.selectedTab.owner = null; } @@ -274,7 +266,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the -@@ -2996,6 +3074,19 @@ +@@ -2996,6 +3073,19 @@ noInitialLabel, skipBackgroundNotify, }); @@ -294,7 +286,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3095,7 @@ +@@ -3004,6 +3094,7 @@ ownerTab, openerTab, pinned, @@ -302,7 +294,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3114,7 @@ +@@ -3022,6 +3113,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -310,7 +302,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca })); if (focusUrlBar) { -@@ -3146,6 +3239,12 @@ +@@ -3146,6 +3238,12 @@ } } @@ -323,7 +315,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3330,10 +3429,10 @@ +@@ -3330,10 +3428,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -335,7 +327,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } if (!color) { -@@ -3354,9 +3453,14 @@ +@@ -3354,9 +3452,14 @@ label, isAdoptingGroup ); @@ -352,7 +344,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca ); group.addTabs(tabs); -@@ -3477,7 +3581,7 @@ +@@ -3477,7 +3580,7 @@ } this.#handleTabMove(tab, () => @@ -361,7 +353,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca ); } -@@ -3679,6 +3783,7 @@ +@@ -3679,6 +3782,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -369,7 +361,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3748,6 +3853,7 @@ +@@ -3748,6 +3852,7 @@ openWindowInfo, name, skipLoad, @@ -377,7 +369,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca }); } -@@ -3935,7 +4041,7 @@ +@@ -3935,7 +4040,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -386,7 +378,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca let url = "about:blank"; if (tabData.entries?.length) { -@@ -3972,8 +4078,10 @@ +@@ -3972,8 +4077,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -398,7 +390,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (select) { tabToSelect = tab; } -@@ -3985,7 +4093,8 @@ +@@ -3985,7 +4092,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -408,7 +400,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -3999,7 +4108,10 @@ +@@ -3999,7 +4107,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -420,7 +412,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca ); tabsFragment.appendChild(tabGroup.node); } -@@ -4044,9 +4156,23 @@ +@@ -4044,9 +4155,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -444,7 +436,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4237,11 +4363,14 @@ +@@ -4237,11 +4362,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -460,7 +452,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if ( !bulkOrderedOpen && ((openerTab && -@@ -4253,7 +4382,7 @@ +@@ -4253,7 +4381,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -469,7 +461,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca tabGroup = previousTab.group; } if ( -@@ -4264,7 +4393,7 @@ +@@ -4264,7 +4392,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -478,7 +470,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4292,14 +4421,14 @@ +@@ -4292,14 +4420,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -497,7 +489,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4310,7 +4439,7 @@ +@@ -4310,7 +4438,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -506,7 +498,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4338,7 +4467,11 @@ +@@ -4338,7 +4466,11 @@ const tabContainer = pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; @@ -518,7 +510,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } this._updateTabsAfterInsert(); -@@ -4346,6 +4479,7 @@ +@@ -4346,6 +4478,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -526,7 +518,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca TabBarVisibility.update(); } -@@ -4896,6 +5030,7 @@ +@@ -4896,6 +5029,7 @@ telemetrySource, } = {} ) { @@ -534,7 +526,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5120,7 @@ +@@ -4985,6 +5119,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -542,7 +534,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } catch (e) { console.error(e); } -@@ -5023,6 +5159,12 @@ +@@ -5023,6 +5158,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -555,7 +547,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5179,9 @@ +@@ -5037,6 +5178,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -565,7 +557,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5230,13 @@ +@@ -5085,7 +5229,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -580,7 +572,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca return; } -@@ -5219,7 +5370,7 @@ +@@ -5219,7 +5369,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -589,7 +581,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5394,7 @@ +@@ -5243,6 +5393,7 @@ newTab = true; } @@ -597,7 +589,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5435,7 @@ +@@ -5283,13 +5434,7 @@ aTab._mouseleave(); if (newTab) { @@ -612,7 +604,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } else { TabBarVisibility.update(); } -@@ -5422,6 +5568,7 @@ +@@ -5422,6 +5567,7 @@ this.tabs[i]._tPos = i; } @@ -620,7 +612,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5790,7 @@ +@@ -5643,6 +5789,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -628,7 +620,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5803,13 @@ +@@ -5655,13 +5802,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -644,7 +636,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5825,7 @@ +@@ -5677,7 +5824,7 @@ } if (tab) { @@ -653,7 +645,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5846,7 @@ +@@ -5698,7 +5845,7 @@ }); } @@ -662,7 +654,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } _blurTab(aTab) { -@@ -5709,7 +5857,7 @@ +@@ -5709,7 +5856,7 @@ * @returns {boolean} * False if swapping isn't permitted, true otherwise. */ @@ -671,7 +663,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // Do not allow transfering a private tab to a non-private window // and vice versa. if ( -@@ -5763,6 +5911,7 @@ +@@ -5763,6 +5910,7 @@ // fire the beforeunload event in the process. Close the other // window if this was its last tab. if ( @@ -679,7 +671,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca !remoteBrowser._beginRemoveTab(aOtherTab, { adoptedByTab: aOurTab, closeWindowWithLastTab: true, -@@ -5774,7 +5923,7 @@ +@@ -5774,7 +5922,7 @@ // If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted. @@ -688,7 +680,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (closeWindow) { let win = aOtherTab.ownerGlobal; win.windowUtils.suppressAnimation(true); -@@ -5898,11 +6047,13 @@ +@@ -5898,11 +6046,13 @@ } // Finish tearing down the tab that's going away. @@ -702,7 +694,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca this.setTabTitle(aOurTab); -@@ -6104,10 +6251,10 @@ +@@ -6104,10 +6250,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -715,7 +707,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6166,6 +6313,7 @@ +@@ -6166,6 +6312,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -723,7 +715,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (this.tabs.length == 1) { return null; } -@@ -6299,7 +6447,7 @@ +@@ -6299,7 +6446,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -732,7 +724,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } /** -@@ -6375,8 +6523,8 @@ +@@ -6375,8 +6522,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -743,7 +735,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6402,10 +6550,16 @@ +@@ -6402,10 +6549,16 @@ this.#handleTabMove( element, () => { @@ -762,7 +754,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6463,23 +6617,28 @@ +@@ -6463,23 +6616,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -797,7 +789,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6492,14 +6651,34 @@ +@@ -6492,14 +6650,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -833,7 +825,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6508,7 +6687,7 @@ +@@ -6508,7 +6686,7 @@ element, () => { if (moveBefore) { @@ -842,7 +834,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca } else if (targetElement) { targetElement.after(element); } else { -@@ -6580,10 +6759,10 @@ +@@ -6580,10 +6758,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -855,7 +847,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6613,6 +6792,7 @@ +@@ -6613,6 +6791,7 @@ let state = { tabIndex: tab._tPos, @@ -863,7 +855,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6639,7 +6819,7 @@ +@@ -6639,7 +6818,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -872,7 +864,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6676,6 +6856,10 @@ +@@ -6676,6 +6855,10 @@ moveActionCallback(); @@ -883,7 +875,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7576,7 +7760,7 @@ +@@ -7576,7 +7759,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -892,7 +884,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca window.focus(); aEvent.preventDefault(); break; -@@ -7593,7 +7777,6 @@ +@@ -7593,7 +7776,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -900,7 +892,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca }); break; case "TabGroupCreateByUser": -@@ -8542,6 +8725,7 @@ +@@ -8542,6 +8724,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -908,7 +900,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..f0a779b987e07d80f47975a8204564ca gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9543,7 +9727,7 @@ var TabContextMenu = { +@@ -9543,7 +9726,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; From 7dd3cf581797801ecfc74f15f47572a0f27ad748 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 28 Nov 2025 13:55:23 +0100 Subject: [PATCH 42/54] feat: Leave a screenshot of the page behind when switching windows or tabs, b=no-bug, c=common --- .../common/styles/zen-browser-container.css | 8 ++ src/zen/sessionstore/ZenWindowSync.sys.mjs | 128 +++++++++++++++++- 2 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/zen/common/styles/zen-browser-container.css b/src/zen/common/styles/zen-browser-container.css index a1715df59b..7763b65d6e 100644 --- a/src/zen/common/styles/zen-browser-container.css +++ b/src/zen/common/styles/zen-browser-container.css @@ -60,3 +60,11 @@ } } } + +.zen-pseudo-browser-image { + position: absolute; + inset: 0; + opacity: 0.4; + pointer-events: none; + z-index: 2; +} diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 6d3b89118b..cfa4ddb52f 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -24,6 +24,8 @@ const EVENTS = [ 'TabAddedToEssentials', 'TabRemovedFromEssentials', + 'TabSelect', + 'focus', 'unload', ]; @@ -51,6 +53,19 @@ class nsZenWindowSync { lastHandlerPromise: Promise.resolve(), }; + /** + * Last focused window. + * Used to determine which window to sync tab contents visibility from. + */ + #lastFocusedWindow = null; + + /** + * Last selected tab. + * Used to determine if we should run another sync operation + * when switching browser views. + */ + #lastSelectedTab = null; + /** * Iterator that yields all currently opened browser windows. * (Might miss the most recent one.) @@ -330,8 +345,9 @@ class nsZenWindowSync { * @param {Object} aOurTab - The tab in the current window. * @param {Object} aOtherTab - The tab in the other window. */ - #swapBrowserDocShells(aOurTab, aOtherTab) { + async #swapBrowserDocShells(aOurTab, aOtherTab) { try { + await this.#styleSwapedBrowsers(aOurTab, aOtherTab); aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', @@ -339,27 +355,99 @@ class nsZenWindowSync { for (let attr of kAttributesToRemove) { aOtherTab.removeAttribute(attr); } - aOtherTab.linkedBrowser.style.opacity = 0; - aOurTab.linkedBrowser.style.opacity = ''; } catch (e) { // Handle any errors that may occur during the swapBrowsers operation. console.error('Error swapping browsers:', e); } } + /** + * Styles the swapped browsers to ensure proper visibility and layout. + * + * @param {Object} aOurTab - The tab in the current window. + * @param {Object} aOtherTab - The tab in the other window. + */ + async #styleSwapedBrowsers(aOurTab, aOtherTab) { + const ourBrowser = aOurTab.linkedBrowser; + const otherBrowser = aOtherTab.linkedBrowser; + + const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob( + aOtherTab.linkedBrowser, + { + fullScale: true, + fullViewport: true, + } + ); + + let mySrc = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(browserBlob); + reader.onloadend = function () { + // result includes identifier 'data:image/png;base64,' plus the base64 data + resolve(reader.result); + }; + reader.onerror = function () { + reject(new Error('Failed to read blob as data URL')); + }; + }); + + const [img, loadPromise] = this.#createPseudoImageForBrowser(otherBrowser, mySrc); + // Run a reflow to ensure the image is rendered before hiding the browser. + void img.getBoundingClientRect(); + await loadPromise; + otherBrowser.style.opacity = 0; + otherBrowser.style.pointerEvents = 'none'; + + this.#maybeRemovePseudoImageForBrowser(ourBrowser); + ourBrowser.style.opacity = ''; + ourBrowser.style.pointerEvents = ''; + } + + /** + * Create and insert a new pseudo image for a browser element. + * + * @param {Object} aBrowser - The browser element to create the pseudo image for. + * @param {string} aSrc - The source URL of the image. + * @returns {Object} The created pseudo image element. + */ + #createPseudoImageForBrowser(aBrowser, aSrc) { + const doc = aBrowser.ownerDocument; + const img = doc.createElement('img'); + img.className = 'zen-pseudo-browser-image'; + aBrowser.after(img); + const loadPromise = new Promise((resolve) => { + img.onload = () => resolve(); + img.src = aSrc; + }); + return [img, loadPromise]; + } + + /** + * Removes the pseudo image element for a browser if it exists. + * + * @param {Object} aBrowser - The browser element to remove the pseudo image for. + */ + #maybeRemovePseudoImageForBrowser(aBrowser) { + const elements = aBrowser.parentNode?.querySelectorAll('.zen-pseudo-browser-image'); + if (elements) { + elements.forEach((element) => element.remove()); + } + } + /** * Retrieves the active tab, where the web contents are being viewed * from other windows by its ID. * * @param {Window} aWindow - The window to exclude. * @param {string} aTabId - The ID of the tab to retrieve. + * @param {Function} filter - A function to filter the tabs. * @returns {Object|null} The active tab from other windows if found, otherwise null. */ - #getActiveTabFromOtherWindows(aWindow, aTabId) { + #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) { for (let window of this.#browserWindows) { if (window !== aWindow) { const tab = this.#getTabFromWindow(window, aTabId); - if (tab?._zenContentsVisible) { + if (filter(tab)) { return tab; } } @@ -371,9 +459,22 @@ class nsZenWindowSync { * Handles tab switch or window focus events to synchronize tab contents visibility. * * @param {Window} aWindow - The window that triggered the event. + * @param {Object} aPreviousTab - The previously selected tab. */ - onTabSwitchOrWindowFocus(aWindow) { + onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) { const selectedTab = aWindow.gBrowser.selectedTab; + if (aPreviousTab?._zenContentsVisible) { + const otherTabToShow = this.#getActiveTabFromOtherWindows( + aWindow, + aPreviousTab.id, + (tab) => tab?.selected + ); + if (otherTabToShow) { + otherTabToShow._zenContentsVisible = true; + delete aPreviousTab._zenContentsVisible; + this.#swapBrowserDocShells(otherTabToShow, aPreviousTab); + } + } if (selectedTab._zenContentsVisible) { return; } @@ -463,9 +564,24 @@ class nsZenWindowSync { on_focus(aEvent) { const { ownerGlobal: window } = aEvent.target; + if (this.#lastFocusedWindow?.deref() === window) { + return; + } + this.#lastFocusedWindow = new WeakRef(window); + this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab); this.onTabSwitchOrWindowFocus(window); } + on_TabSelect(aEvent) { + const tab = aEvent.target; + if (this.#lastSelectedTab?.deref() === tab) { + return; + } + this.#lastSelectedTab = new WeakRef(tab); + const previousTab = aEvent.detail.previousTab; + this.onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab); + } + on_unload() {} } From ae62f5c41d0bb5f3f7112151b67a578b3be11b65 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 28 Nov 2025 17:39:54 +0100 Subject: [PATCH 43/54] feat: Run session saves right before writing and quiting, b=no-bug, c=common --- .../sessionstore/SessionSaver-sys-mjs.patch | 20 +++++ .../sessionstore/TabState-sys-mjs.patch | 5 +- src/zen/common/modules/ZenSessionStore.mjs | 5 +- .../sessionstore/ZenSessionManager.sys.mjs | 83 ++++++++----------- 4 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 src/browser/components/sessionstore/SessionSaver-sys-mjs.patch diff --git a/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch new file mode 100644 index 0000000000..7ec6a4e122 --- /dev/null +++ b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch @@ -0,0 +1,20 @@ +diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs +index 9595712e4cac3e238c19d278879f9dc4b03d36ef..52fc78bf833c3d384eea7657ed8b15d23d09201a 100644 +--- a/browser/components/sessionstore/SessionSaver.sys.mjs ++++ b/browser/components/sessionstore/SessionSaver.sys.mjs +@@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + /* +@@ -305,6 +306,7 @@ var SessionSaverInternal = { + this._maybeClearCookiesAndStorage(state); + + Glean.sessionRestore.collectData.stopAndAccumulate(timerId); ++ lazy.ZenSessionStore.saveState(state); + return this._writeState(state); + }, + diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch index 82c2f882ff..0a7c56bc42 100644 --- a/src/browser/components/sessionstore/TabState-sys-mjs.patch +++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch @@ -1,8 +1,8 @@ diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs -index 82721356d191055bec0d4b0ca49e481221988801..2d05ba4812e9a73bd896c1aeb007180bbc531a3c 100644 +index 82721356d191055bec0d4b0ca49e481221988801..e1d2c5ca0cbe5431df01f8d6411f88e3325e0ee9 100644 --- a/browser/components/sessionstore/TabState.sys.mjs +++ b/browser/components/sessionstore/TabState.sys.mjs -@@ -85,7 +85,22 @@ class _TabState { +@@ -85,7 +85,23 @@ class _TabState { tabData.groupId = tab.group.id; } @@ -17,6 +17,7 @@ index 82721356d191055bec0d4b0ca49e481221988801..2d05ba4812e9a73bd896c1aeb007180b + tabData.zenHasStaticLabel = tab.hasAttribute("zen-has-static-label"); + tabData.zenGlanceId = tab.getAttribute("glance-id"); + tabData.zenIsGlance = tab.hasAttribute("zen-glance-tab"); ++ tabData._zenIsActiveTab = tab._zenContentsVisible; + tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true); + if (tabData.searchMode?.source === tab.ownerGlobal.UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS) { diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs index aad335c014..77aed2ebf2 100644 --- a/src/zen/common/modules/ZenSessionStore.mjs +++ b/src/zen/common/modules/ZenSessionStore.mjs @@ -17,8 +17,9 @@ class ZenSessionStore extends nsZenPreloadedFeature { if (tabData.zenWorkspace) { tab.setAttribute('zen-workspace-id', tabData.zenWorkspace); } - if (tabData.zenSyncId) { - tab.setAttribute('id', tabData.zenSyncId); + // Keep for now, for backward compatibility for window sync to work. + if (tabData.zenSyncId || tabData.zenPinnedId) { + tab.setAttribute('id', tabData.zenSyncId || tabData.zenPinnedId); } if (tabData.zenHasStaticLabel) { tab.setAttribute('zen-has-static-label', 'true'); diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 2d2a630a90..2a61ebb728 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -13,8 +13,7 @@ ChromeUtils.defineESModuleGetters(lazy, { SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs', }); -const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes -const OBSERVING = ['sessionstore-state-write-complete', 'browser-window-before-show']; +const OBSERVING = ['browser-window-before-show']; class nsZenSessionManager { #file; @@ -56,10 +55,6 @@ class nsZenSessionManager { observe(aSubject, aTopic) { switch (aTopic) { - case 'sessionstore-state-write-complete': { - this.#saveState(true); - break; - } case 'browser-window-before-show': // catch new windows this.#onBeforeBrowserWindowShown(aSubject); break; @@ -74,76 +69,68 @@ class nsZenSessionManager { void aWindow; } - get #topMostWindow() { - return lazy.BrowserWindowTracker.getTopWindow(); - } - /** * Saves the current session state. Collects data and writes to disk. * - * @param forceUpdateAllWindows (optional) - * Forces us to recollect data for all windows and will bypass and - * update the corresponding caches. + * @param state + * The current session state. */ - async #saveState(forceUpdateAllWindows = false) { + saveState(state) { if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { // Don't save (or even collect) anything in permanent private // browsing mode return; } - // Collect an initial snapshot of window data before we do the flush. - const window = this.#topMostWindow; - // We don't have any normal windows or no windows at all - if (!window) { - return; - } - this.#collectWindowData(this.#topMostWindow, forceUpdateAllWindows); + this.#collectWindowData(state); this.#file.store(); } /** * Collects session data for a given window. * - * @param window - * The window to collect data for. - * @param forceUpdate - * Forces us to recollect data and will bypass and update the - * corresponding caches. + * @param state + * The current session state. */ - #collectWindowData(window, forceUpdate = false) { + #collectWindowData(state) { let sidebarData = this.#sidebar; - if (!sidebarData || forceUpdate) { + if (!sidebarData) { sidebarData = {}; } - // If it hasn't changed, don't update. - if ( - !forceUpdate && - sidebarData.lastCollected && - Date.now() - sidebarData.lastCollected < LAZY_COLLECT_THRESHOLD - ) { - return; - } sidebarData.lastCollected = Date.now(); - this.#collectTabsData(window, sidebarData); + this.#collectTabsData(sidebarData, state); this.#sidebar = sidebarData; } /** * Collects session data for all tabs in a given window. * - * @param aWindow - * The window to collect tab data for. - * @param winData - * The window data object to populate. + * @param sidebarData + * The sidebar data object to populate. + * @param state + * The current session state. */ - #collectTabsData(aWindow, sidebarData) { - const winData = lazy.SessionStore.getWindowState(aWindow).windows[0]; - if (!winData) return; - sidebarData.tabs = winData.tabs; - sidebarData.folders = winData.folders; - sidebarData.splitViewData = winData.splitViewData; - sidebarData.groups = winData.groups; + #collectTabsData(sidebarData, state) { + if (!state?.windows?.length) return; + + const tabIdRelationMap = new Map(); + for (const window of state.windows) { + // Only accept the tabs with `_zenIsActiveTab` set to true from + // every window. We do this to avoid collecting tabs with invalid + // state when multiple windows are open. Note that if we a tab without + // this flag set in any other window, we just add it anyway. + for (const tabData of window.tabs) { + if (!tabIdRelationMap.has(tabData.zenSyncId) || tabData._zenIsActiveTab) { + tabIdRelationMap.set(tabData.zenSyncId, tabData); + } + } + } + + sidebarData.tabs = Array.from(tabIdRelationMap.values()); + + sidebarData.folders = state.windows[0].folders; + sidebarData.splitViewData = state.windows[0].splitViewData; + sidebarData.groups = state.windows[0].groups; } restoreWindowData(aWindowData) { From a84bd5b0ce2eb3e894f113d0a470955e8573af33 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Tue, 2 Dec 2025 14:32:16 +0100 Subject: [PATCH 44/54] fix: Fixed going back to a different window not allowing to type on inputs, b=no-bug, c=no-component --- src/zen/sessionstore/ZenWindowSync.sys.mjs | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index cfa4ddb52f..dc195c2ea5 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -346,19 +346,19 @@ class nsZenWindowSync { * @param {Object} aOtherTab - The tab in the other window. */ async #swapBrowserDocShells(aOurTab, aOtherTab) { - try { - await this.#styleSwapedBrowsers(aOurTab, aOtherTab); - aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); - const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; - // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', - // but we need to manually remove some attributes from the other tab. - for (let attr of kAttributesToRemove) { - aOtherTab.removeAttribute(attr); - } - } catch (e) { - // Handle any errors that may occur during the swapBrowsers operation. - console.error('Error swapping browsers:', e); + await this.#styleSwapedBrowsers(aOurTab, aOtherTab); + aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); + const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; + // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', + // but we need to manually remove some attributes from the other tab. + for (let attr of kAttributesToRemove) { + aOtherTab.removeAttribute(attr); } + + // Recalculate the focus in order to allow the user to continue typing + // inside the web contentx area without having to click outside and back in. + aOurTab.linkedBrowser.blur(); + aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); } /** From 8bf790df4cd9640dcda65b1ac8a56cb7af822d5b Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Tue, 2 Dec 2025 17:33:39 +0100 Subject: [PATCH 45/54] feat: Start syncing folders as well, b=no-bug, c=folders --- src/zen/folders/ZenFolders.mjs | 2 +- src/zen/sessionstore/ZenWindowSync.sys.mjs | 220 ++++++++++++++------- 2 files changed, 153 insertions(+), 69 deletions(-) diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 08b0ad4653..22ff89845d 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -857,7 +857,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { .open(group.icon, { onlySvgIcons: true }) .then((icon) => { this.setFolderUserIcon(group, icon); - group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true })); + group.dispatchEvent(new CustomEvent('TabGroupUpdate', { bubbles: true })); }) .catch((err) => { console.error(err); diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index dc195c2ea5..bfe9f384a7 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -24,6 +24,11 @@ const EVENTS = [ 'TabAddedToEssentials', 'TabRemovedFromEssentials', + 'TabGroupUpdate', + 'TabGroupCreate', + 'TabGroupRemoved', + 'TabGroupMoved', + 'TabSelect', 'focus', @@ -212,14 +217,14 @@ class nsZenWindowSync { } /** - * Retrieves a tab element from a window by its ID. + * Retrieves a item element from a window by its ID. * - * @param {Window} aWindow - The window containing the tab. - * @param {string} aTabId - The ID of the tab to retrieve. - * @returns {Object|null} The tab element if found, otherwise null. + * @param {Window} aWindow - The window containing the item. + * @param {string} aItemId - The ID of the item to retrieve. + * @returns {MozTabbrowserTab|MozTabbrowserTabGroup|null} The item element if found, otherwise null. */ - #getTabFromWindow(aWindow, aTabId) { - return aWindow.document.getElementById(aTabId); + #getItemFromWindow(aWindow, aItemId) { + return aWindow.document.getElementById(aItemId); } /** @@ -230,112 +235,142 @@ class nsZenWindowSync { * @param {Window} aWindow - The window containing the tabs. * @param {number} flags - The sync flags indicating what to synchronize. */ - #syncTabWithOriginal(aOriginalTab, aTargetTab, aWindow, flags = 0) { - if (!aOriginalTab || !aTargetTab) { + #syncItemWithOriginal(aOriginalItem, aTargetItem, aWindow, flags = 0) { + if (!aOriginalItem || !aTargetItem) { return; } - const { gBrowser } = aWindow; + const { gBrowser, gZenFolders } = aWindow; if (flags & SYNC_FLAG_ICON) { - gBrowser.setIcon(aTargetTab, gBrowser.getIcon(aOriginalTab)); + if (gBrowser.isTab(aOriginalItem)) { + gBrowser.setIcon(aTargetItem, gBrowser.getIcon(aOriginalItem)); + } else if (aOriginalItem.isZenFolder) { + // Icons are a zen-only feature for tab groups. + gZenFolders.setFolderUserIcon(aTargetItem, aOriginalItem.iconURL); + } } if (flags & SYNC_FLAG_LABEL) { - gBrowser._setTabLabel(aTargetTab, aOriginalTab.label); + if (gBrowser.isTab(aOriginalItem)) { + gBrowser._setTabLabel(aTargetItem, aOriginalItem.label); + } else if (gBrowser.isTabGroup(aOriginalItem)) { + aTargetItem.label = aOriginalItem.label; + } } - if (flags & SYNC_FLAG_MOVE && !aTargetTab.hasAttribute('zen-empty-tab')) { - const workspaceId = aOriginalTab.getAttribute('zen-workspace-id'); + if (flags & SYNC_FLAG_MOVE && !aTargetItem.hasAttribute('zen-empty-tab')) { + const workspaceId = aOriginalItem.getAttribute('zen-workspace-id'); if (workspaceId) { - aTargetTab.setAttribute('zen-workspace-id', workspaceId); + aTargetItem.setAttribute('zen-workspace-id', workspaceId); } else { - aTargetTab.removeAttribute('zen-workspace-id'); + aTargetItem.removeAttribute('zen-workspace-id'); } - this.#syncTabPosition(aOriginalTab, aTargetTab, aWindow); + this.#syncItemPosition(aOriginalItem, aTargetItem, aWindow); + } + if (gBrowser.isTab(aTargetItem)) { + lazy.TabStateFlusher.flush(aTargetItem.linkedBrowser); } - lazy.TabStateFlusher.flush(aTargetTab.linkedBrowser); } /** - * Synchronizes the position of the target tab with the original tab. + * Synchronizes the position of the target item with the original item. * - * @param {Object} aOriginalTab - The original tab to copy from. - * @param {Object} aTargetTab - The target tab to copy to. - * @param {Window} aWindow - The window containing the tabs. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to copy from. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to copy to. + * @param {Window} aWindow - The window containing the items. */ - #syncTabPosition(aOriginalTab, aTargetTab, aWindow) { + #syncItemPosition(aOriginalItem, aTargetItem, aWindow) { const { gBrowser, gZenPinnedTabManager } = aWindow; - const originalIsEssential = aOriginalTab.hasAttribute('zen-essential'); - const targetIsEssential = aTargetTab.hasAttribute('zen-essential'); - const originalIsPinned = aOriginalTab.pinned; - const targetIsPinned = aTargetTab.pinned; - - if (originalIsEssential !== targetIsEssential) { - if (originalIsEssential) { - gZenPinnedTabManager.addToEssentials(aTargetTab); - } else { - gZenPinnedTabManager.removeEssentials(aTargetTab, /* unpin= */ !targetIsPinned); - } - } else if (originalIsPinned !== targetIsPinned) { - if (originalIsPinned) { - gBrowser.pinTab(aTargetTab); - } else { - gBrowser.unpinTab(aTargetTab); + const originalIsEssential = aOriginalItem.hasAttribute('zen-essential'); + const targetIsEssential = aTargetItem.hasAttribute('zen-essential'); + const originalIsPinned = aOriginalItem.pinned; + const targetIsPinned = aTargetItem.pinned; + + const isGroup = gBrowser.isTabGroup(aOriginalItem); + const isTab = !isGroup; + + if (isTab) { + if (originalIsEssential !== targetIsEssential) { + if (originalIsEssential) { + gZenPinnedTabManager.addToEssentials(aTargetItem); + } else { + gZenPinnedTabManager.removeEssentials(aTargetItem, /* unpin= */ !targetIsPinned); + } + } else if (originalIsPinned !== targetIsPinned) { + if (originalIsPinned) { + gBrowser.pinTab(aTargetItem); + } else { + gBrowser.unpinTab(aTargetItem); + } } } - this.#moveTabToMatchOriginal(aOriginalTab, aTargetTab, aWindow, { + this.#moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, { isEssential: originalIsEssential, isPinned: originalIsPinned, }); } /** - * Moves the target tab to match the position of the original tab. + * Moves the target item to match the position of the original item. * - * @param {Object} aOriginalTab - The original tab to match. - * @param {Object} aTargetTab - The target tab to move. - * @param {Window} aWindow - The window containing the tabs. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to match. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to move. + * @param {Window} aWindow - The window containing the items. */ - #moveTabToMatchOriginal(aOriginalTab, aTargetTab, aWindow, { isEssential, isPinned }) { + #moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, { isEssential, isPinned }) { const { gBrowser, gZenWorkspaces } = aWindow; - const originalSibling = aOriginalTab.previousElementSibling; + const originalSibling = aOriginalItem.previousElementSibling; let isFirstTab = true; if (gBrowser.isTabGroup(originalSibling) || gBrowser.isTab(originalSibling)) { - isFirstTab = !originalSibling.hasAttribute('id'); + isFirstTab = + !originalSibling.hasAttribute('id') || originalSibling.hasAttribute('zen-empty-tab'); } - gBrowser.zenHandleTabMove(aOriginalTab, () => { + gBrowser.zenHandleTabMove(aOriginalItem, () => { if (isFirstTab) { let container; + const parentGroup = aOriginalItem.group; + if (parentGroup?.hasAttribute('id')) { + container = this.#getItemFromWindow(aWindow, parentGroup.getAttribute('id')); + if (container) { + if (container?.tabs?.length) { + // First tab in folders is the empty tab placeholder. + container.tabs[0].after(aTargetItem); + } else { + container.appendChild(aTargetItem); + } + return; + } + } if (isEssential) { - container = gZenWorkspaces.getEssentialsSection(aTargetTab); + container = gZenWorkspaces.getEssentialsSection(aTargetItem); } else { - const workspaceId = aTargetTab.getAttribute('zen-workspace-id'); + const workspaceId = aTargetItem.getAttribute('zen-workspace-id'); const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId); container = isPinned - ? workspaceElement.pinnedTabsContainer - : workspaceElement.tabsContainer; + ? workspaceElement?.pinnedTabsContainer + : workspaceElement?.tabsContainer; } if (container) { - container.insertBefore(aTargetTab, container.firstChild); + container.insertBefore(aTargetItem, container.firstChild); } return; } - const relativeTab = this.#getTabFromWindow(aWindow, originalSibling.id); + const relativeTab = this.#getItemFromWindow(aWindow, originalSibling.id); if (relativeTab) { - relativeTab.after(aTargetTab); + relativeTab.after(aTargetItem); } }); } /** - * Synchronizes a tab across all browser windows. + * Synchronizes a item across all browser windows. * - * @param {Object} aTab - The tab to synchronize. + * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aItem - The item to synchronize. * @param {number} flags - The sync flags indicating what to synchronize. */ - #syncTabForAllWindows(aTab, flags = 0) { - const window = aTab.ownerGlobal; + #syncItemForAllWindows(aItem, flags = 0) { + const window = aItem.ownerGlobal; this.#runOnAllWindows(window, (win) => { - this.#syncTabWithOriginal(aTab, this.#getTabFromWindow(win, aTab.id), win, flags); + this.#syncItemWithOriginal(aItem, this.#getItemFromWindow(win, aItem.id), win, flags); }); } @@ -354,7 +389,6 @@ class nsZenWindowSync { for (let attr of kAttributesToRemove) { aOtherTab.removeAttribute(attr); } - // Recalculate the focus in order to allow the user to continue typing // inside the web contentx area without having to click outside and back in. aOurTab.linkedBrowser.blur(); @@ -446,7 +480,7 @@ class nsZenWindowSync { #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) { for (let window of this.#browserWindows) { if (window !== aWindow) { - const tab = this.#getTabFromWindow(window, aTabId); + const tab = this.#getItemFromWindow(window, aTabId); if (filter(tab)) { return tab; } @@ -493,8 +527,8 @@ class nsZenWindowSync { * @param {number} flags - The sync flags indicating what to synchronize. */ #delegateGenericSyncEvent(aEvent, flags = 0) { - const tab = aEvent.target; - this.#syncTabForAllWindows(tab, flags); + const item = aEvent.target; + this.#syncItemForAllWindows(item, flags); } /* Mark: Event Handlers */ @@ -502,19 +536,22 @@ class nsZenWindowSync { on_TabOpen(aEvent) { const tab = aEvent.target; const window = tab.ownerGlobal; + if (tab.selected) { + tab._zenContentsVisible = true; + } if (tab.id) { // This tab was opened as part of a sync operation. return; } tab.id = this.#newTabSyncId; - if (tab.selected) { - tab._zenContentsVisible = true; - } this.#runOnAllWindows(window, (win) => { const newTab = win.gBrowser.addTrustedTab('about:blank', { animate: true }); newTab.setAttribute('zen-workspace-id', tab.getAttribute('zen-workspace-id') || ''); newTab.id = tab.id; - this.#syncTabWithOriginal( + if (tab.hasAttribute('zen-empty-tab')) { + newTab.setAttribute('zen-empty-tab', 'true'); + } + this.#syncItemWithOriginal( tab, newTab, win, @@ -555,7 +592,7 @@ class nsZenWindowSync { const tab = aEvent.target; const window = tab.ownerGlobal; this.#runOnAllWindows(window, (win) => { - const targetTab = this.#getTabFromWindow(win, tab.id); + const targetTab = this.#getItemFromWindow(win, tab.id); if (targetTab) { win.gBrowser.removeTab(targetTab, { animate: true }); } @@ -583,6 +620,53 @@ class nsZenWindowSync { } on_unload() {} + + on_TabGroupCreate(aEvent) { + const tabGroup = aEvent.target; + if (tabGroup.id) { + // This tab group was opened as part of a sync operation. + console.log('Duplicate!'); + } + const window = tabGroup.ownerGlobal; + const isFolder = tabGroup.isZenFolder; + const isSplitView = tabGroup.hasAttribute('split-view-group'); + // Tab groups already have an ID upon creation. + this.#runOnAllWindows(window, (win) => { + const newGroup = isFolder + ? win.gZenFolders.createFolder([], {}) + : win.gBrowser.addTabGroup({ splitView: isSplitView }); + newGroup.id = tabGroup.id; + this.#syncItemWithOriginal( + tabGroup, + newGroup, + win, + SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE + ); + }); + } + + on_TabGroupRemoved(aEvent) { + const tabGroup = aEvent.target; + const window = tabGroup.ownerGlobal; + this.#runOnAllWindows(window, (win) => { + const targetGroup = this.#getItemFromWindow(win, tabGroup.id); + if (targetGroup) { + if (targetGroup.isZenFolder) { + targetGroup.delete(); + } else { + win.gBrowser.removeTabGroup(targetGroup, { isUserTriggered: true }); + } + } + }); + } + + on_TabGroupMoved(aEvent) { + return this.on_TabMove(aEvent); + } + + on_TabGroupUpdate(aEvent) { + return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON | SYNC_FLAG_LABEL); + } } export const ZenWindowSync = new nsZenWindowSync(); From a0268ace63e729b67fbb0353743ba7cb7558c67b Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:47:45 +0100 Subject: [PATCH 46/54] Discard changes to src/browser/components/tabbrowser/content/tab-js.patch --- .../tabbrowser/content/tab-js.patch | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 06e5b50ad7..3f27154b7f 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 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577b5fad08c 100644 +index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbfd6214f77 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -42,7 +42,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 ".tab-label-container": "pinned,selected=visuallyselected,labeldirection", ".tab-label": -@@ -184,7 +187,7 @@ +@@ -186,7 +189,7 @@ } set _visuallySelected(val) { @@ -51,7 +51,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 return; } -@@ -220,11 +223,21 @@ +@@ -222,11 +225,21 @@ } get visible() { @@ -78,7 +78,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } get hidden() { -@@ -297,7 +310,7 @@ +@@ -305,7 +318,7 @@ return false; } @@ -87,7 +87,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } get lastAccessed() { -@@ -374,8 +387,11 @@ +@@ -382,7 +395,12 @@ } get group() { @@ -99,7 +99,9 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 + return this.parentElement.parentElement; + } } -@@ -470,6 +486,8 @@ + + get splitview() { +@@ -473,6 +491,8 @@ this.style.MozUserFocus = "ignore"; } else if ( event.target.classList.contains("tab-close-button") || @@ -108,7 +110,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 event.target.classList.contains("tab-icon-overlay") || event.target.classList.contains("tab-audio-button") ) { -@@ -524,6 +542,10 @@ +@@ -527,6 +547,10 @@ this.style.MozUserFocus = ""; } @@ -119,7 +121,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 on_click(event) { if (event.button != 0) { return; -@@ -572,6 +594,7 @@ +@@ -575,6 +599,7 @@ ) ); } else { @@ -127,7 +129,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 gBrowser.removeTab(this, { animate: true, triggeringEvent: event, -@@ -584,6 +607,14 @@ +@@ -587,6 +612,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -142,7 +144,7 @@ index 4c1a48424316b29d27ae2bc8b64004df41c87bb6..f1ff9bf0947127a8e9115357cedac577 } on_dblclick(event) { -@@ -607,6 +638,8 @@ +@@ -610,6 +643,8 @@ animate: true, triggeringEvent: event, }); From 5d964511aaf045fae445b674bc7e2e8c857da804 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Wed, 3 Dec 2025 16:49:37 +0100 Subject: [PATCH 47/54] chore: Update patches to ff 146, b=no-bug, c=no-component --- .../sessionstore/SessionSaver-sys-mjs.patch | 2 +- .../sessionstore/SessionStore-sys-mjs.patch | 12 ++-- .../tabbrowser/content/tab-js.patch | 14 +--- .../tabbrowser/content/tabbrowser-js.patch | 68 +++++++++---------- 4 files changed, 44 insertions(+), 52 deletions(-) diff --git a/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch index 7ec6a4e122..e6956ea61b 100644 --- a/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs -index 9595712e4cac3e238c19d278879f9dc4b03d36ef..52fc78bf833c3d384eea7657ed8b15d23d09201a 100644 +index 9141793550f7c7ff6aa63d4c85bf571b4499e2d0..f00314ebf75ac826e1c9cca8af264ff8aae106c0 100644 --- a/browser/components/sessionstore/SessionSaver.sys.mjs +++ b/browser/components/sessionstore/SessionSaver.sys.mjs @@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, { diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 7e7d9a98f9..a928cdc705 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 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..a7ae5953037bbdab8844c9e400caf233bd61eb0f 100644 +index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4d5afeafa 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -137,7 +137,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..a7ae5953037bbdab8844c9e400caf233 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5778,8 @@ var SessionStoreInternal = { +@@ -5765,8 +5779,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -148,7 +148,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..a7ae5953037bbdab8844c9e400caf233 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5822,8 @@ var SessionStoreInternal = { +@@ -5809,6 +5823,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -157,7 +157,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..a7ae5953037bbdab8844c9e400caf233 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6387,25 @@ var SessionStoreInternal = { +@@ -6372,6 +6388,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -183,7 +183,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..a7ae5953037bbdab8844c9e400caf233 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7324,7 @@ var SessionStoreInternal = { +@@ -7290,7 +7325,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { @@ -192,7 +192,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..a7ae5953037bbdab8844c9e400caf233 // Adjust window.selected if (tIndex + 1 < window.selected) { window.selected -= 1; -@@ -7305,7 +7337,7 @@ var SessionStoreInternal = { +@@ -7305,7 +7340,7 @@ var SessionStoreInternal = { ); // We don't want to increment tIndex here. continue; diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 3f27154b7f..dd0edd05f5 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..02d70e9b0261f92917d274759838cfbfd6214f77 100644 +index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -121,15 +121,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf on_click(event) { if (event.button != 0) { return; -@@ -575,6 +599,7 @@ - ) - ); - } else { -+ gZenPinnedTabManager._removePinnedAttributes(this, true); - gBrowser.removeTab(this, { - animate: true, - triggeringEvent: event, -@@ -587,6 +612,14 @@ +@@ -587,6 +611,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -144,7 +136,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf } on_dblclick(event) { -@@ -610,6 +643,8 @@ +@@ -610,6 +642,8 @@ animate: true, triggeringEvent: event, }); diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 34331a0e81..a38f001305 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9ff6eb16c8 100644 +index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186f0d2bce5 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -518,7 +518,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f TabBarVisibility.update(); } -@@ -4896,6 +5029,7 @@ +@@ -4916,6 +5049,7 @@ telemetrySource, } = {} ) { @@ -526,7 +526,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -4985,6 +5119,7 @@ +@@ -5005,6 +5139,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -534,7 +534,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } catch (e) { console.error(e); } -@@ -5023,6 +5158,12 @@ +@@ -5043,6 +5178,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -547,7 +547,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5037,6 +5178,9 @@ +@@ -5057,6 +5198,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -557,7 +557,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5085,7 +5229,13 @@ +@@ -5105,7 +5249,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -572,7 +572,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f return; } -@@ -5219,7 +5369,7 @@ +@@ -5239,7 +5389,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -581,7 +581,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5243,6 +5393,7 @@ +@@ -5263,6 +5413,7 @@ newTab = true; } @@ -589,7 +589,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5283,13 +5434,7 @@ +@@ -5303,13 +5454,7 @@ aTab._mouseleave(); if (newTab) { @@ -604,7 +604,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } else { TabBarVisibility.update(); } -@@ -5422,6 +5567,7 @@ +@@ -5442,6 +5587,7 @@ this.tabs[i]._tPos = i; } @@ -612,7 +612,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5643,6 +5789,7 @@ +@@ -5663,6 +5809,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -620,7 +620,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5655,13 +5802,13 @@ +@@ -5675,13 +5822,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -636,7 +636,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5677,7 +5824,7 @@ +@@ -5697,7 +5844,7 @@ } if (tab) { @@ -645,7 +645,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } // If no qualifying visible tab was found, see if there is a tab in -@@ -5698,7 +5845,7 @@ +@@ -5718,7 +5865,7 @@ }); } @@ -654,7 +654,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } _blurTab(aTab) { -@@ -5709,7 +5856,7 @@ +@@ -5729,7 +5876,7 @@ * @returns {boolean} * False if swapping isn't permitted, true otherwise. */ @@ -663,7 +663,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f // Do not allow transfering a private tab to a non-private window // and vice versa. if ( -@@ -5763,6 +5910,7 @@ +@@ -5783,6 +5930,7 @@ // fire the beforeunload event in the process. Close the other // window if this was its last tab. if ( @@ -671,7 +671,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f !remoteBrowser._beginRemoveTab(aOtherTab, { adoptedByTab: aOurTab, closeWindowWithLastTab: true, -@@ -5774,7 +5922,7 @@ +@@ -5794,7 +5942,7 @@ // If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted. @@ -680,7 +680,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f if (closeWindow) { let win = aOtherTab.ownerGlobal; win.windowUtils.suppressAnimation(true); -@@ -5898,11 +6046,13 @@ +@@ -5918,11 +6066,13 @@ } // Finish tearing down the tab that's going away. @@ -694,7 +694,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f this.setTabTitle(aOurTab); -@@ -6104,10 +6250,10 @@ +@@ -6124,10 +6274,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -707,7 +707,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6166,6 +6312,7 @@ +@@ -6186,6 +6336,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -715,7 +715,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f if (this.tabs.length == 1) { return null; } -@@ -6299,7 +6446,7 @@ +@@ -6319,7 +6470,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -724,7 +724,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } /** -@@ -6375,8 +6522,8 @@ +@@ -6404,8 +6555,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -735,7 +735,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6402,10 +6549,16 @@ +@@ -6431,10 +6582,16 @@ this.#handleTabMove( element, () => { @@ -754,7 +754,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6463,23 +6616,28 @@ +@@ -6492,23 +6649,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -789,7 +789,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6492,14 +6650,34 @@ +@@ -6521,14 +6683,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -825,7 +825,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6508,7 +6686,7 @@ +@@ -6537,7 +6719,7 @@ element, () => { if (moveBefore) { @@ -834,7 +834,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f } else if (targetElement) { targetElement.after(element); } else { -@@ -6580,10 +6758,10 @@ +@@ -6607,10 +6789,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -847,7 +847,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6613,6 +6791,7 @@ +@@ -6656,6 +6838,7 @@ let state = { tabIndex: tab._tPos, @@ -855,7 +855,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6639,7 +6818,7 @@ +@@ -6682,7 +6865,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -864,7 +864,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6676,6 +6855,10 @@ +@@ -6723,6 +6906,10 @@ moveActionCallback(); @@ -875,7 +875,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7576,7 +7759,7 @@ +@@ -7623,7 +7810,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -884,7 +884,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f window.focus(); aEvent.preventDefault(); break; -@@ -7593,7 +7776,6 @@ +@@ -7640,7 +7827,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -892,7 +892,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f }); break; case "TabGroupCreateByUser": -@@ -8542,6 +8724,7 @@ +@@ -8589,6 +8775,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -900,7 +900,7 @@ index c0eafd4faf8d57b8486c5bf8917375850ec8147e..118194bcf51ae7cea21e268e8ccf6b9f gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9543,7 +9726,7 @@ var TabContextMenu = { +@@ -9623,7 +9810,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; From 22f2b1f29b85287e6a3011e3005408fce7f85685 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Wed, 3 Dec 2025 17:57:46 +0100 Subject: [PATCH 48/54] feat: Early support for unsynced windoiws, b=no-bug, c=workspaces --- .../tabbrowser/content/tabbrowser-js.patch | 40 ++++++++++++------- .../base/BrowsingContextWebProgress-cpp.patch | 22 ++++++++++ .../sessionstore/ZenSessionManager.sys.mjs | 8 +++- src/zen/sessionstore/ZenWindowSync.sys.mjs | 33 +++++++++++++-- src/zen/workspaces/ZenWorkspaces.mjs | 29 +++++++++----- 5 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 src/docshell/base/BrowsingContextWebProgress-cpp.patch diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index a38f001305..69592a03b6 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186f0d2bce5 100644 +index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b04ef78d6 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -715,7 +715,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 if (this.tabs.length == 1) { return null; } -@@ -6319,7 +6470,7 @@ +@@ -6213,7 +6364,8 @@ + AppConstants.BROWSER_CHROME_URL, + "_blank", + options, +- aTab ++ aTab, ++ "zen-unsynced" + ); + } + +@@ -6319,7 +6471,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -724,7 +734,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 } /** -@@ -6404,8 +6555,8 @@ +@@ -6404,8 +6556,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -735,7 +745,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6431,10 +6582,16 @@ +@@ -6431,10 +6583,16 @@ this.#handleTabMove( element, () => { @@ -754,7 +764,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6492,23 +6649,28 @@ +@@ -6492,23 +6650,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -789,7 +799,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6521,14 +6683,34 @@ +@@ -6521,14 +6684,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -825,7 +835,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6537,7 +6719,7 @@ +@@ -6537,7 +6720,7 @@ element, () => { if (moveBefore) { @@ -834,7 +844,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 } else if (targetElement) { targetElement.after(element); } else { -@@ -6607,10 +6789,10 @@ +@@ -6607,10 +6790,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -847,7 +857,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6656,6 +6838,7 @@ +@@ -6656,6 +6839,7 @@ let state = { tabIndex: tab._tPos, @@ -855,7 +865,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6682,7 +6865,7 @@ +@@ -6682,7 +6866,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -864,7 +874,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6723,6 +6906,10 @@ +@@ -6723,6 +6907,10 @@ moveActionCallback(); @@ -875,7 +885,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7623,7 +7810,7 @@ +@@ -7623,7 +7811,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -884,7 +894,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 window.focus(); aEvent.preventDefault(); break; -@@ -7640,7 +7827,6 @@ +@@ -7640,7 +7828,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -892,7 +902,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 }); break; case "TabGroupCreateByUser": -@@ -8589,6 +8775,7 @@ +@@ -8589,6 +8776,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -900,7 +910,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..3acfbc6d434340f0a629eec7edd9e186 gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9623,7 +9810,7 @@ var TabContextMenu = { +@@ -9623,7 +9811,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; diff --git a/src/docshell/base/BrowsingContextWebProgress-cpp.patch b/src/docshell/base/BrowsingContextWebProgress-cpp.patch new file mode 100644 index 0000000000..9ec052d943 --- /dev/null +++ b/src/docshell/base/BrowsingContextWebProgress-cpp.patch @@ -0,0 +1,22 @@ +diff --git a/docshell/base/BrowsingContextWebProgress.cpp b/docshell/base/BrowsingContextWebProgress.cpp +index cc5bf6de7bf4875acf0c572549dc30ff06ac7f6f..4e0f3648f551fe9507a654d19f0a99e455418aa9 100644 +--- a/docshell/base/BrowsingContextWebProgress.cpp ++++ b/docshell/base/BrowsingContextWebProgress.cpp +@@ -54,7 +54,7 @@ NS_IMETHODIMP BrowsingContextWebProgress::AddProgressListener( + + if (mListenerInfoList.Contains(listener)) { + // The listener is already registered! +- return NS_ERROR_FAILURE; ++ return NS_OK; + } + + mListenerInfoList.AppendElement(ListenerInfo(listener, aNotifyMask)); +@@ -68,7 +68,7 @@ NS_IMETHODIMP BrowsingContextWebProgress::RemoveProgressListener( + return NS_ERROR_INVALID_ARG; + } + +- return mListenerInfoList.RemoveElement(listener) ? NS_OK : NS_ERROR_FAILURE; ++ return mListenerInfoList.RemoveElement(listener) ? NS_OK : NS_OK; + } + + NS_IMETHODIMP BrowsingContextWebProgress::GetBrowsingContextXPCOM( diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 2a61ebb728..84570e4651 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -145,13 +145,19 @@ class nsZenSessionManager { } restoreNewWindow(aWindow, SessionStoreInternal) { + if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) { + return; + } lazy.SessionSaver.run().then(() => { const state = lazy.SessionStore.getCurrentState(true); const windows = state.windows || {}; let newWindow = Cu.cloneInto(windows[0], {}); delete newWindow.selected; const newState = { windows: [newWindow] }; - //SessionStoreInternal.restoreWindows(aWindow, newState, {}); + aWindow._zenRestorePromise = new Promise((resolve) => { + SessionStoreInternal.restoreWindows(aWindow, newState, {}); + resolve(); + }); }); } } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index bfe9f384a7..fff488df0a 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -80,7 +80,12 @@ class nsZenWindowSync { #browserWindows = { *[Symbol.iterator]() { for (let window of lazy.BrowserWindowTracker.orderedWindows) { - if (window.__SSi && !window.closed && window.gZenStartup.isReady) { + if ( + window.__SSi && + !window.closed && + window.gZenStartup.isReady && + !window.gZenWorkspaces?.privateWindowOrDisabled + ) { yield window; } } @@ -109,9 +114,23 @@ class nsZenWindowSync { * @param {Window} aWindow - The browser window that is about to be shown. */ #onWindowBeforeShow(aWindow) { + // There are 2 possibilities to know if we are trying to open + // a new *unsynced* window: + // 1. We are passing `zen-unsynced` in the window arguments. + // 2. We are trying to open a link in a new window where other synced + // windows already exist + if ( + aWindow.arguments.some((arg) => arg === 'zen-unsynced') || + (typeof aWindow.arguments[0] === 'string' && + aWindow.arguments.length > 1 && + [...this.#browserWindows].length > 0) + ) { + aWindow.document.documentElement.setAttribute('zen-unsynced-window', 'true'); + return; + } aWindow.gZenWindowSync = this; for (let eventName of EVENTS) { - aWindow.addEventListener(eventName, this); + aWindow.addEventListener(eventName, this, true); } } @@ -174,7 +193,7 @@ class nsZenWindowSync { handleEvent(aEvent) { const window = aEvent.currentTarget.ownerGlobal; - if (!window.gZenStartup.isReady) { + if (!window.gZenStartup.isReady || window.gZenWorkspaces?.privateWindowOrDisabled) { return; } if (this.#eventHandlingContext.window && this.#eventHandlingContext.window !== window) { @@ -619,7 +638,13 @@ class nsZenWindowSync { this.onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab); } - on_unload() {} + on_unload(aEvent) { + const window = aEvent.target.ownerGlobal; + for (let eventName of EVENTS) { + window.removeEventListener(eventName, this); + } + delete window.gZenWindowSync; + } on_TabGroupCreate(aEvent) { const tabGroup = aEvent.target; diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 83507f6dae..a66d4c30a2 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -55,6 +55,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.promiseDBInitialized, this.promisePinnedInitialized, SessionStore.promiseAllWindowsRestored, + window._zenRestorePromise, ]); } @@ -835,8 +836,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return PrivateBrowsingUtils.isWindowPrivate(window); } + get currentWindowIsSyncing() { + return !document.documentElement.hasAttribute('zen-unsynced-window') && !this.isPrivateWindow; + } + get privateWindowOrDisabled() { - return this.isPrivateWindow || !this.shouldHaveWorkspaces; + return !this.shouldHaveWorkspaces || !this.currentWindowIsSyncing; } get workspaceEnabled() { @@ -865,12 +870,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return this._workspaceCache; } - if (this.isPrivateWindow) { + if (!this.currentWindowIsSyncing) { this._workspaceCache = { - workspaces: this._privateWorkspace ? [this._privateWorkspace] : [], + workspaces: this._tempWorkspace ? [this._tempWorkspace] : [], lastChangeTimestamp: 0, }; - this._activeWorkspace = this._privateWorkspace?.uuid; + this._activeWorkspace = this._tempWorkspace?.uuid; return this._workspaceCache; } @@ -1111,7 +1116,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { shouldCloseWindow() { return ( - !window.toolbar.visible || Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab') + !window.toolbar.visible || + Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab') || + this.privateWindowOrDisabled ); } @@ -1388,7 +1395,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async _propagateWorkspaceData({ ignoreStrip = false, clearCache = true, onInit = false } = {}) { - const currentWindowIsPrivate = this.isPrivateWindow; + const currentWindowIsPrivate = !this.currentWindowIsSyncing; if (onInit) { if (currentWindowIsPrivate) return; return await this._propagateWorkspaceDataForWindow(this.ownerWindow, { @@ -1401,7 +1408,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // For example, when the window is in private browsing mode. if ( !browser.gZenWorkspaces.workspaceEnabled || - browser.gZenWorkspaces.isPrivateWindow !== currentWindowIsPrivate + !browser.gZenWorkspaces.currentWindowIsSyncing !== currentWindowIsPrivate ) { return; } @@ -2450,8 +2457,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (!this.workspaceEnabled) { return; } - if (this.isPrivateWindow) { - name = 'Private ' + name; + if (!this.currentWindowIsSyncing) { + name = this.isPrivateWindow ? 'Private ' + name : gZenUIManager.generateUuidv4(); } // get extra tabs remaning (e.g. on new profiles) and just move them to the new workspace const extraTabs = Array.from(gBrowser.tabContainer.arrowScrollbox.children).filter( @@ -2468,8 +2475,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { !dontChange, containerTabId ); - if (this.isPrivateWindow) { - this._privateWorkspace = workspaceData; + if (!this.currentWindowIsSyncing) { + this._tempWorkspace = workspaceData; } else { await this.saveWorkspace(workspaceData, dontChange); } From 899d590329fad0240642cd4d52637631e6459e9c Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Wed, 3 Dec 2025 23:43:23 +0100 Subject: [PATCH 49/54] fix: Move back active views when closing a window, b=no-bug, c=no-component --- src/zen/sessionstore/ZenWindowSync.sys.mjs | 108 ++++++++++++++------- 1 file changed, 75 insertions(+), 33 deletions(-) diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index fff488df0a..8bb05b9170 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -399,8 +399,18 @@ class nsZenWindowSync { * @param {Object} aOurTab - The tab in the current window. * @param {Object} aOtherTab - The tab in the other window. */ - async #swapBrowserDocShells(aOurTab, aOtherTab) { + async #swapBrowserDocShellsAsync(aOurTab, aOtherTab) { await this.#styleSwapedBrowsers(aOurTab, aOtherTab); + this.#swapBrowserDocSheellsInner(aOurTab, aOtherTab); + } + + /** + * Swaps the browser docshells between two tabs. + * + * @param {Object} aOurTab - The tab in the current window. + * @param {Object} aOtherTab - The tab in the other window. + */ + #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true) { aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', @@ -408,10 +418,12 @@ class nsZenWindowSync { for (let attr of kAttributesToRemove) { aOtherTab.removeAttribute(attr); } - // Recalculate the focus in order to allow the user to continue typing - // inside the web contentx area without having to click outside and back in. - aOurTab.linkedBrowser.blur(); - aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); + if (focus) { + // Recalculate the focus in order to allow the user to continue typing + // inside the web contentx area without having to click outside and back in. + aOurTab.linkedBrowser.blur(); + aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); + } } /** @@ -419,37 +431,40 @@ class nsZenWindowSync { * * @param {Object} aOurTab - The tab in the current window. * @param {Object} aOtherTab - The tab in the other window. + * @param {boolean} onClose - Indicates if the styling is done during a tab close operation. */ - async #styleSwapedBrowsers(aOurTab, aOtherTab) { + async #styleSwapedBrowsers(aOurTab, aOtherTab, onClose = false) { const ourBrowser = aOurTab.linkedBrowser; const otherBrowser = aOtherTab.linkedBrowser; - const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob( - aOtherTab.linkedBrowser, - { - fullScale: true, - fullViewport: true, - } - ); - - let mySrc = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(browserBlob); - reader.onloadend = function () { - // result includes identifier 'data:image/png;base64,' plus the base64 data - resolve(reader.result); - }; - reader.onerror = function () { - reject(new Error('Failed to read blob as data URL')); - }; - }); + if (!onClose) { + const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob( + aOtherTab.linkedBrowser, + { + fullScale: true, + fullViewport: true, + } + ); - const [img, loadPromise] = this.#createPseudoImageForBrowser(otherBrowser, mySrc); - // Run a reflow to ensure the image is rendered before hiding the browser. - void img.getBoundingClientRect(); - await loadPromise; - otherBrowser.style.opacity = 0; - otherBrowser.style.pointerEvents = 'none'; + let mySrc = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(browserBlob); + reader.onloadend = function () { + // result includes identifier 'data:image/png;base64,' plus the base64 data + resolve(reader.result); + }; + reader.onerror = function () { + reject(new Error('Failed to read blob as data URL')); + }; + }); + + const [img, loadPromise] = this.#createPseudoImageForBrowser(otherBrowser, mySrc); + // Run a reflow to ensure the image is rendered before hiding the browser. + void img.getBoundingClientRect(); + await loadPromise; + otherBrowser.style.opacity = 0; + otherBrowser.style.pointerEvents = 'none'; + } this.#maybeRemovePseudoImageForBrowser(ourBrowser); ourBrowser.style.opacity = ''; @@ -508,6 +523,32 @@ class nsZenWindowSync { return null; } + /** + * Moves all active tabs from the specified window to other windows. + * + * @param {Window} aWindow - The window to move active tabs from. + */ + #moveAllActiveTabsToOtherWindows(aWindow) { + const mostRecentWindow = [...this.#browserWindows].find((win) => win !== aWindow); + if (!mostRecentWindow || !aWindow.gZenWorkspaces) { + return; + } + const activeTabsOnClosedWindow = aWindow.gZenWorkspaces.allStoredTabs.filter( + (tab) => tab._zenContentsVisible + ); + for (let tab of activeTabsOnClosedWindow) { + const targetTab = this.#getItemFromWindow(mostRecentWindow, tab.id); + if (targetTab) { + targetTab._zenContentsVisible = true; + this.#swapBrowserDocSheellsInner(targetTab, tab, targetTab.selected); + // We can animate later, whats important is to always stay on the same + // process and avoid async operations here to avoid the closed window + // being unloaded before the swap is done. + this.#styleSwapedBrowsers(targetTab, tab, /* onClose =*/ true); + } + } + } + /** * Handles tab switch or window focus events to synchronize tab contents visibility. * @@ -525,7 +566,7 @@ class nsZenWindowSync { if (otherTabToShow) { otherTabToShow._zenContentsVisible = true; delete aPreviousTab._zenContentsVisible; - this.#swapBrowserDocShells(otherTabToShow, aPreviousTab); + this.#swapBrowserDocShellsAsync(otherTabToShow, aPreviousTab); } } if (selectedTab._zenContentsVisible) { @@ -535,7 +576,7 @@ class nsZenWindowSync { selectedTab._zenContentsVisible = true; if (otherSelectedTab) { delete otherSelectedTab._zenContentsVisible; - this.#swapBrowserDocShells(selectedTab, otherSelectedTab); + this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab); } } @@ -644,6 +685,7 @@ class nsZenWindowSync { window.removeEventListener(eventName, this); } delete window.gZenWindowSync; + this.#moveAllActiveTabsToOtherWindows(window); } on_TabGroupCreate(aEvent) { From 73f408c9e18cbd8a637eeb62df079811297d21d6 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Thu, 4 Dec 2025 17:05:39 +0100 Subject: [PATCH 50/54] feat: Stop rendering sub-layers when swaping browsers, b=no-bug, c=common --- prefs/zen/window-sync.yaml | 6 ++ .../common/styles/zen-browser-container.css | 4 ++ src/zen/sessionstore/ZenWindowSync.sys.mjs | 72 ++++++++++++++----- 3 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 prefs/zen/window-sync.yaml diff --git a/prefs/zen/window-sync.yaml b/prefs/zen/window-sync.yaml new file mode 100644 index 0000000000..f7ba5b071f --- /dev/null +++ b/prefs/zen/window-sync.yaml @@ -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/. + +- name: zen.window-sync.enabled + value: true diff --git a/src/zen/common/styles/zen-browser-container.css b/src/zen/common/styles/zen-browser-container.css index 7763b65d6e..c75e00d2e2 100644 --- a/src/zen/common/styles/zen-browser-container.css +++ b/src/zen/common/styles/zen-browser-container.css @@ -68,3 +68,7 @@ pointer-events: none; z-index: 2; } + +browser[zen-pseudo-hidden='true'] { + -moz-subtree-hidden-only-visually: 1 !important; +} diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 8bb05b9170..a10b172f88 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -2,14 +2,19 @@ // 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 { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; + const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', TabStateFlusher: 'resource:///modules/sessionstore/TabStateFlusher.sys.mjs', + ZenSessionStore: 'resource:///modules/zen/ZenSessionManager.sys.mjs', }); +XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gWindowSyncEnabled', 'zen.window-sync.enabled'); + const OBSERVING = ['browser-window-before-show']; const EVENTS = [ 'TabOpen', @@ -93,6 +98,9 @@ class nsZenWindowSync { }; init() { + if (!lazy.gWindowSyncEnabled) { + return; + } for (let topic of OBSERVING) { Services.obs.addObserver(this, topic); } @@ -173,13 +181,18 @@ class nsZenWindowSync { * * @param {Window} aWindow - The browser window to exclude. * @param {Function} aCallback - The callback function to run on each window. + * @returns {any} The value returned by the callback function, if any. */ #runOnAllWindows(aWindow, aCallback) { for (let window of this.#browserWindows) { if (window !== aWindow) { - aCallback(window); + let value = aCallback(window); + if (value) { + return value; + } } } + return null; } observe(aSubject, aTopic) { @@ -411,6 +424,12 @@ class nsZenWindowSync { * @param {Object} aOtherTab - The tab in the other window. */ #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true) { + // Load about:blank + if (aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') { + aOurTab.linkedBrowser.loadURI(Services.io.newURI('about:blank'), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', @@ -424,6 +443,10 @@ class nsZenWindowSync { aOurTab.linkedBrowser.blur(); aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); } + for (const tab of [aOurTab, aOtherTab]) { + // Ensure the tab's state is flushed after the swap. + lazy.TabStateFlusher.flush(tab.linkedBrowser); + } } /** @@ -462,13 +485,11 @@ class nsZenWindowSync { // Run a reflow to ensure the image is rendered before hiding the browser. void img.getBoundingClientRect(); await loadPromise; - otherBrowser.style.opacity = 0; - otherBrowser.style.pointerEvents = 'none'; + otherBrowser.setAttribute('zen-pseudo-hidden', 'true'); } this.#maybeRemovePseudoImageForBrowser(ourBrowser); - ourBrowser.style.opacity = ''; - ourBrowser.style.pointerEvents = ''; + ourBrowser.removeAttribute('zen-pseudo-hidden'); } /** @@ -512,15 +533,12 @@ class nsZenWindowSync { * @returns {Object|null} The active tab from other windows if found, otherwise null. */ #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) { - for (let window of this.#browserWindows) { - if (window !== aWindow) { - const tab = this.#getItemFromWindow(window, aTabId); - if (filter(tab)) { - return tab; - } + return this.#runOnAllWindows(aWindow, (win) => { + const tab = this.#getItemFromWindow(win, aTabId); + if (filter(tab)) { + return tab; } - } - return null; + }); } /** @@ -555,7 +573,7 @@ class nsZenWindowSync { * @param {Window} aWindow - The window that triggered the event. * @param {Object} aPreviousTab - The previously selected tab. */ - onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) { + #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) { const selectedTab = aWindow.gBrowser.selectedTab; if (aPreviousTab?._zenContentsVisible) { const otherTabToShow = this.#getActiveTabFromOtherWindows( @@ -591,6 +609,26 @@ class nsZenWindowSync { this.#syncItemForAllWindows(item, flags); } + /* Mark: Public API */ + + shouldLoadTab(aTab) { + if (!lazy.gWindowSyncEnabled) { + // Since we are never going to sync the tab, we can always load it. + return true; + } + if (aTab._zenContentsVisible) { + // This tab is already active in this window. + return true; + } + // We don't want to trigger a new browser kick-off if there's + // another window where this tab is already active. + return !this.#getActiveTabFromOtherWindows( + aTab.ownerGlobal, + aTab.id, + (tab) => tab?._zenContentsVisible + ); + } + /* Mark: Event Handlers */ on_TabOpen(aEvent) { @@ -661,12 +699,12 @@ class nsZenWindowSync { on_focus(aEvent) { const { ownerGlobal: window } = aEvent.target; - if (this.#lastFocusedWindow?.deref() === window) { + if (!window.gBrowser || this.#lastFocusedWindow?.deref() === window) { return; } this.#lastFocusedWindow = new WeakRef(window); this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab); - this.onTabSwitchOrWindowFocus(window); + this.#onTabSwitchOrWindowFocus(window); } on_TabSelect(aEvent) { @@ -676,7 +714,7 @@ class nsZenWindowSync { } this.#lastSelectedTab = new WeakRef(tab); const previousTab = aEvent.detail.previousTab; - this.onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab); + this.#onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab); } on_unload(aEvent) { From aecea1f5658ebe772952c7ccb300aecd490f1573 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 5 Dec 2025 09:35:22 +0100 Subject: [PATCH 51/54] feat: Improved support for unsynced windows support, b=no-bug, c=workspaces, folders --- .../en-US/browser/browser/zen-workspaces.ftl | 4 + .../base/content/zen-panels/popups.inc | 5 + .../sessionstore/SessionStore-sys-mjs.patch | 72 ++++++++++---- src/zen/folders/ZenFolders.mjs | 15 +-- src/zen/sessionstore/ZenWindowSync.sys.mjs | 12 ++- src/zen/workspaces/ZenWorkspace.mjs | 52 ++++++++++- src/zen/workspaces/ZenWorkspaceIcons.mjs | 2 +- src/zen/workspaces/ZenWorkspaces.mjs | 93 ++++++++++--------- src/zen/workspaces/zen-workspaces.css | 32 +++++-- 9 files changed, 197 insertions(+), 90 deletions(-) diff --git a/locales/en-US/browser/browser/zen-workspaces.ftl b/locales/en-US/browser/browser/zen-workspaces.ftl index 67f43f7fe6..cc8d4601fb 100644 --- a/locales/en-US/browser/browser/zen-workspaces.ftl +++ b/locales/en-US/browser/browser/zen-workspaces.ftl @@ -66,6 +66,10 @@ zen-panel-ui-gradient-click-to-add = Click to add a color zen-workspace-creation-name = .placeholder = Space Name +zen-move-tab-to-workspace-button = + .label = Move Tabs + .tooltiptext = Move all tabs in this window to a Space + zen-workspaces-panel-context-reorder = .label = Reorder Spaces diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc index d94f76ae0c..b4ecd5e149 100644 --- a/src/browser/base/content/zen-panels/popups.inc +++ b/src/browser/base/content/zen-panels/popups.inc @@ -59,3 +59,8 @@ + + +# Popup to move tabs to a synced workspace. +# This would be automatically populated with the list of available synced workspaces. + diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index a928cdc705..009fed3002 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..c4a1d14b1eb4e2b26dd903c462fb5cf4d5afeafa 100644 +index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961ff1220f29 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -28,7 +28,18 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2151,7 +2156,6 @@ var SessionStoreInternal = { +@@ -2020,6 +2025,10 @@ var SessionStoreInternal = { + this._windows[aWindow.__SSi].isTaskbarTab = true; + } + ++ if (aWindow.document.documentElement.hasAttribute("zen-unsynced-window")) { ++ this._windows[aWindow.__SSi].isZenUnsynced = true; ++ } ++ + let tabbrowser = aWindow.gBrowser; + + // add tab change listeners to all already existing tabs +@@ -2151,7 +2160,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -36,7 +47,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2215,6 +2219,9 @@ var SessionStoreInternal = { +@@ -2215,6 +2223,9 @@ var SessionStoreInternal = { }); this._shouldRestoreLastSession = false; } @@ -46,7 +57,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 if (this._restoreLastWindow && aWindow.toolbar.visible) { // always reset (if not a popup window) -@@ -2384,11 +2391,9 @@ var SessionStoreInternal = { +@@ -2384,11 +2395,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -58,7 +69,25 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3378,7 @@ var SessionStoreInternal = { +@@ -2465,7 +2474,7 @@ var SessionStoreInternal = { + // 2) Flush the window. + // 3) When the flush is complete, revisit our decision to store the window + // in _closedWindows, and add/remove as necessary. +- if (!winData.isPrivate && !winData.isTaskbarTab) { ++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) { + this.maybeSaveClosedWindow(winData, isLastWindow); + } + +@@ -2486,7 +2495,7 @@ var SessionStoreInternal = { + + // Save non-private windows if they have at + // least one saveable tab or are the last window. +- if (!winData.isPrivate && !winData.isTaskbarTab) { ++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) { + this.maybeSaveClosedWindow(winData, isLastWindow); + + if (!isLastWindow && winData.closedId > -1) { +@@ -3373,7 +3382,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -67,7 +96,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 return; } -@@ -4089,6 +4094,12 @@ var SessionStoreInternal = { +@@ -4089,6 +4098,12 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -80,7 +109,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4536,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4540,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -88,7 +117,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5386,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5390,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -97,7 +126,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 removableTabs.push(tab); } } -@@ -5434,7 +5446,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5450,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -106,7 +135,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5637,12 @@ var SessionStoreInternal = { +@@ -5483,7 +5499,7 @@ var SessionStoreInternal = { + + // collect the data for all windows + for (ix in this._windows) { +- if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) { ++ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab || this._windows[ix].isZenUnsynced) { + // window data is still in _statesToRestore + continue; + } +@@ -5625,11 +5641,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -120,7 +158,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5653,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5657,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -128,7 +166,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5666,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5670,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -137,7 +175,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5765,8 +5779,8 @@ var SessionStoreInternal = { +@@ -5765,8 +5783,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -148,7 +186,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5809,6 +5823,8 @@ var SessionStoreInternal = { +@@ -5809,6 +5827,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -157,7 +195,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6372,6 +6388,25 @@ var SessionStoreInternal = { +@@ -6372,6 +6392,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -183,7 +221,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7290,7 +7325,7 @@ var SessionStoreInternal = { +@@ -7290,7 +7329,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { @@ -192,7 +230,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..c4a1d14b1eb4e2b26dd903c462fb5cf4 // Adjust window.selected if (tIndex + 1 < window.selected) { window.selected -= 1; -@@ -7305,7 +7340,7 @@ var SessionStoreInternal = { +@@ -7305,7 +7344,7 @@ var SessionStoreInternal = { ); // We don't want to increment tIndex here. continue; diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 22ff89845d..80535e6931 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -101,20 +101,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { .querySelector('menupopup'); changeFolderSpace.innerHTML = ''; for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) { - const item = document.createXULElement('menuitem'); - item.className = 'zen-workspace-context-menu-item'; - item.setAttribute('zen-workspace-id', workspace.uuid); - item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace); - let name = workspace.name; - const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg'); - if (workspace.icon && workspace.icon !== '' && !iconIsSvg) { - name = `${workspace.icon} ${name}`; - } - item.setAttribute('label', name); - if (iconIsSvg) { - item.setAttribute('image', workspace.icon); - item.classList.add('zen-workspace-context-icon'); - } + const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace); item.addEventListener('command', (event) => { if (!this.#lastFolderContextMenu) return; this.changeFolderToSpace( diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index a10b172f88..5bbe1a0f2d 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -428,9 +428,11 @@ class nsZenWindowSync { if (aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') { aOurTab.linkedBrowser.loadURI(Services.io.newURI('about:blank'), { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, }); } aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); + aOtherTab.permanentKey = aOurTab.permanentKey; const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', // but we need to manually remove some attributes from the other tab. @@ -443,10 +445,12 @@ class nsZenWindowSync { aOurTab.linkedBrowser.blur(); aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); } - for (const tab of [aOurTab, aOtherTab]) { - // Ensure the tab's state is flushed after the swap. - lazy.TabStateFlusher.flush(tab.linkedBrowser); - } + // Ensure the tab's state is flushed after the swap. By doing this, + // we can re-schedule another session store delayed process to fire. + // It's also important to note that if we don't flush the state here, + // we would start recieving invalid history changes from the the incorrect + // browser view that was just swapped out. + lazy.TabStateFlusher.flush(aOurTab.linkedBrowser); } /** diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs index ce6baa122b..921ae42391 100644 --- a/src/zen/workspaces/ZenWorkspace.mjs +++ b/src/zen/workspaces/ZenWorkspace.mjs @@ -37,6 +37,14 @@ class nsZenWorkspace extends MozXULElement { `; } + static get moveTabToButtonMarkup() { + return ` + + `; + } + static get inheritedAttributes() { return { '.zen-workspace-tabs-section': 'zen-workspace-id=id', @@ -88,6 +96,20 @@ class nsZenWorkspace extends MozXULElement { gZenWorkspaces.changeWorkspaceIcon(); }); + if (!gZenWorkspaces.currentWindowIsSyncing) { + let actionsButton = this.indicator.querySelector('.zen-workspaces-actions'); + const moveTabToFragment = window.MozXULElement.parseXULToFragment( + nsZenWorkspace.moveTabToButtonMarkup + ); + actionsButton.after(moveTabToFragment); + actionsButton.setAttribute('hidden', 'true'); + actionsButton = actionsButton.nextElementSibling; + actionsButton.addEventListener('command', (event) => { + event.stopPropagation(); + this.#openMoveTabsToWorkspacePanel(event.target); + }); + } + this.scrollbox._getScrollableElements = () => { const children = [...this.pinnedTabsContainer.children, ...this.tabsContainer.children]; if (Services.prefs.getBoolPref('zen.view.show-newtab-button-top', false)) { @@ -209,7 +231,7 @@ class nsZenWorkspace extends MozXULElement { if (newName === '') { return; } - let workspaces = (await gZenWorkspaces._workspaces()).workspaces; + let workspaces = (await gZenWorkspaces.getWorkspaces()).workspaces; let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid); workspaceData.name = newName; await gZenWorkspaces.saveWorkspace(workspaceData); @@ -256,6 +278,34 @@ class nsZenWorkspace extends MozXULElement { this.style.removeProperty('--toolbox-textcolor'); this.style.removeProperty('--zen-primary-color'); } + + #openMoveTabsToWorkspacePanel(button) { + button = button.closest('toolbarbutton'); + if (!button) return; + + const popup = document.getElementById('zenMoveTabsToSyncedWorkspacePopup'); + popup.innerHTML = ''; + + gZenWorkspaces.getWorkspaces(true).then((workspaces) => { + for (const workspace of workspaces.workspaces) { + const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace); + item.addEventListener('command', async () => { + console.log('TODO: Move tab to workspace', workspace); + }); + popup.appendChild(item); + } + + button.setAttribute('open', 'true'); + popup.addEventListener( + 'popuphidden', + () => { + button.removeAttribute('open'); + }, + { once: true } + ); + popup.openPopup(button, 'after_start', 0, 0, true /* isContextMenu */); + }); + } } customElements.define('zen-workspace', nsZenWorkspace); diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs index f9186c43f2..d56f6ecea5 100644 --- a/src/zen/workspaces/ZenWorkspaceIcons.mjs +++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs @@ -126,7 +126,7 @@ class nsZenWorkspaceIcons extends MozXULElement { } async #updateIcons() { - const workspaces = await gZenWorkspaces._workspaces(); + const workspaces = await gZenWorkspaces.getWorkspaces(); this.innerHTML = ''; for (const workspace of workspaces.workspaces) { const button = this.#createWorkspaceIcon(workspace); diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index a66d4c30a2..dc1e145486 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -129,7 +129,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.addPopupListeners(); await this.#waitForPromises(); - await this._workspaces(); + await this.getWorkspaces(); await this.afterLoadInit(); } @@ -319,7 +319,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async _createDefaultWorkspaceIfNeeded() { - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); if (!workspaces.workspaces.length) { await this.createAndSaveWorkspace('Space', null, true); this._workspaceCache = null; @@ -414,7 +414,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { await new Promise((resolve) => { setTimeout(async () => { const tabs = gBrowser.tabContainer.allTabs; - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); for (const workspace of workspaces.workspaces) { await this._createWorkspaceTabsSection(workspace, tabs); } @@ -865,12 +865,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } } - async _workspaces() { - if (this._workspaceCache) { + async getWorkspaces(lieToMe = false) { + if (this._workspaceCache && !lieToMe) { return this._workspaceCache; } - if (!this.currentWindowIsSyncing) { + if (!this.currentWindowIsSyncing && !lieToMe) { this._workspaceCache = { workspaces: this._tempWorkspace ? [this._tempWorkspace] : [], lastChangeTimestamp: 0, @@ -884,7 +884,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ZenWorkspacesStorage.getLastChangeTimestamp(), ]); - this._workspaceCache = { workspaces, lastChangeTimestamp }; + const workspaceCache = { workspaces, lastChangeTimestamp }; // Get the active workspace ID from preferences const activeWorkspaceId = this.activeWorkspace; @@ -892,18 +892,20 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId); // Set the active workspace ID to the first one if the one with selected id doesn't exist if (!activeWorkspace) { - this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid; + this.activeWorkspace = workspaceCache.workspaces[0]?.uuid; } } else { // Set the active workspace ID to the first one if active workspace doesn't exist - this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid; + this.activeWorkspace = workspaceCache.workspaces[0]?.uuid; } // sort by position - this._workspaceCache.workspaces.sort( - (a, b) => (a.position ?? Infinity) - (b.position ?? Infinity) - ); + workspaceCache.workspaces.sort((a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)); - return this._workspaceCache; + if (!lieToMe) { + // We don't want our cache to remain for the next calls + this._workspaceCache = workspaceCache; + } + return workspaceCache; } async workspaceBookmarks() { @@ -1124,7 +1126,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async #clearAnyZombieTabs() { const tabs = this.allStoredTabs; - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); for (let tab of tabs) { const workspaceID = tab.getAttribute('zen-workspace-id'); if ( @@ -1212,6 +1214,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ); } + generateMenuItemForWorkspace(workspace) { + const item = document.createXULElement('menuitem'); + item.className = 'zen-workspace-context-menu-item'; + item.setAttribute('zen-workspace-id', workspace.uuid); + item.setAttribute('disabled', workspace.uuid === this.activeWorkspace); + let name = workspace.name; + const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg'); + if (workspace.icon && workspace.icon !== '' && !iconIsSvg) { + name = `${workspace.icon} ${name}`; + } + item.setAttribute('label', name); + if (iconIsSvg) { + item.setAttribute('image', workspace.icon); + item.classList.add('zen-workspace-context-icon'); + } + return item; + } + #contextMenuData = null; updateWorkspaceActionsMenu(event) { if (event.target.id !== 'zenWorkspaceMoreActions') { @@ -1255,20 +1275,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (!this.#contextMenuData.workspaceId) { separator.hidden = false; for (const workspace of [...this._workspaceCache.workspaces].reverse()) { - const item = document.createXULElement('menuitem'); - item.className = 'zen-workspace-context-menu-item'; - item.setAttribute('zen-workspace-id', workspace.uuid); - item.setAttribute('disabled', workspace.uuid === this.activeWorkspace); - let name = workspace.name; - const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg'); - if (workspace.icon && workspace.icon !== '' && !iconIsSvg) { - name = `${workspace.icon} ${name}`; - } - item.setAttribute('label', name); - if (iconIsSvg) { - item.setAttribute('image', workspace.icon); - item.classList.add('zen-workspace-context-icon'); - } + const item = this.generateMenuItemForWorkspace(workspace); item.addEventListener('command', (e) => { this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id')); }); @@ -1313,7 +1320,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async removeWorkspace(windowID) { - let workspacesData = await this._workspaces(); + let workspacesData = await this.getWorkspaces(); await this.changeWorkspace( workspacesData.workspaces.find((workspace) => workspace.uuid !== windowID) ); @@ -1336,7 +1343,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async getActiveWorkspace() { - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); return ( workspaces.workspaces.find((workspace) => workspace.uuid === this.activeWorkspace) ?? workspaces.workspaces[0] @@ -1370,7 +1377,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { browser.gZenWorkspaces._workspaceCache = null; browser.gZenWorkspaces._workspaceBookmarksCache = null; } - let workspaces = await browser.gZenWorkspaces._workspaces(); + let workspaces = await browser.gZenWorkspaces.getWorkspaces(); browser.document .getElementById('cmd_zenCtxDeleteWorkspace') .setAttribute('disabled', workspaces.workspaces.length <= 1); @@ -1423,7 +1430,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (this.privateWindowOrDisabled) { return; } - const workspaces = (await this._workspaces()).workspaces; + const workspaces = (await this.getWorkspaces()).workspaces; const workspace = workspaces.find((w) => w.uuid === id); if (!workspace) { console.warn(`Workspace with ID ${id} not found for reordering.`); @@ -1449,7 +1456,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async moveWorkspace(draggedWorkspaceId, targetWorkspaceId) { - const workspaces = (await this._workspaces()).workspaces; + const workspaces = (await this.getWorkspaces()).workspaces; const draggedIndex = workspaces.findIndex((w) => w.uuid === draggedWorkspaceId); const draggedWorkspace = workspaces.splice(draggedIndex, 1)[0]; const targetIndex = workspaces.findIndex((w) => w.uuid === targetWorkspaceId); @@ -1662,7 +1669,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); gZenFolders.cancelPopupTimer(); // Refresh tab cache @@ -1778,7 +1785,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } this._organizingWorkspaceStrip = true; - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); let workspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === workspace.uuid); if (!justMove) { this._fixIndicatorsNames(workspaces); @@ -1912,7 +1919,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const kGlobalAnimationDuration = 0.2; this._animatingChange = true; const animations = []; - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid); const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex; const clonedEssentials = []; @@ -2215,7 +2222,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { tab, currentWorkspace.uuid, currentWorkspace.containerTabId, - await this._workspaces() + await this.getWorkspaces() ); } @@ -2274,7 +2281,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const lastSelectedTab = this._lastSelectedWorkspaceTabs[workspace.uuid]; const containerId = workspace.containerTabId?.toString(); - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); // Save current tab as last selected for old workspace if it shouldn't be visible in new workspace if (oldWorkspaceId && oldWorkspaceId !== workspace.uuid) { @@ -2407,7 +2414,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async _updateWorkspacesChangeContextMenu() { if (gZenWorkspaces.privateWindowOrDisabled) return; - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup'); if (!menuPopup) { @@ -2711,7 +2718,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // Context menu management async contextChangeContainerTab(event) { this._organizingWorkspaceStrip = true; - let workspaces = await this._workspaces(); + let workspaces = await this.getWorkspaces(); let workspace = workspaces.workspaces.find( (workspace) => workspace.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace) ); @@ -2765,7 +2772,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async changeWorkspaceShortcut(offset = 1, whileScrolling = false) { // Cycle through workspaces - let workspaces = await this._workspaces(); + let workspaces = await this.getWorkspaces(); let activeWorkspace = await this.getActiveWorkspace(); let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace); @@ -2829,7 +2836,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent( tabs[tabs.length - 1] ); - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); await this.changeWorkspace( workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID) ); @@ -2888,7 +2895,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async shortcutSwitchTo(index) { - const workspaces = await this._workspaces(); + const workspaces = await this.getWorkspaces(); // The index may be out of bounds, if it doesnt exist, don't do anything if (index >= workspaces.workspaces.length || index < 0) { return; diff --git a/src/zen/workspaces/zen-workspaces.css b/src/zen/workspaces/zen-workspaces.css index 674a9cd44e..8c6767c22c 100644 --- a/src/zen/workspaces/zen-workspaces.css +++ b/src/zen/workspaces/zen-workspaces.css @@ -181,13 +181,8 @@ height: calc(100% - var(--zen-toolbox-padding) * 2); } - :root:not([zen-private-window]) & { - &:hover, - &[open='true'] { - &::before { - background: var(--tab-hover-background-color); - } - } + :root[zen-private-window] & { + pointer-events: none; } & .zen-current-workspace-indicator-icon { @@ -228,16 +223,23 @@ font-weight: 600; align-items: center; margin: 0; + + :root[zen-unsynced-window] & { + pointer-events: none; + } } .zen-workspaces-actions { margin-left: auto !important; - opacity: 0; - visibility: collapse; transition: opacity 0.1s; order: 5; --toolbarbutton-inner-padding: 6px !important; + :root:not([zen-unsynced-window]) & { + opacity: 0; + visibility: collapse; + } + & image { border-radius: max(calc(var(--border-radius-medium) - 4px), 4px) !important; width: 26px; @@ -248,9 +250,19 @@ :root[zen-renaming-tab='true'] & { display: none; } + + :root[zen-unsynced-window] & { + .toolbarbutton-text { + display: flex; + } + + .toolbarbutton-icon { + display: none; + } + } } - :root:not([zen-private-window]) &:hover .zen-workspaces-actions, + :root:not([zen-unsynced-window]) &:hover .zen-workspaces-actions, & .zen-workspaces-actions[open='true'] { visibility: visible; pointer-events: auto; From cde32ca1e0076088480c528441659448a6dfd5db Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 5 Dec 2025 14:02:09 +0100 Subject: [PATCH 52/54] feat: Implemented 'Move To...' Button for unsynced windows, b=no-bug, c=workspaces, common --- .../en-US/browser/browser/zen-workspaces.ftl | 2 +- .../tabbrowser/content/tabbrowser-js.patch | 129 +++++++++--------- src/zen/common/styles/zen-theme.css | 7 + src/zen/common/sys/ZenCustomizableUI.sys.mjs | 3 + src/zen/sessionstore/ZenWindowSync.sys.mjs | 76 +++++++++-- src/zen/workspaces/ZenWorkspace.mjs | 5 +- src/zen/workspaces/ZenWorkspaces.mjs | 34 ++--- src/zen/workspaces/create-workspace-form.css | 2 +- src/zen/workspaces/zen-workspaces.css | 21 ++- 9 files changed, 180 insertions(+), 99 deletions(-) diff --git a/locales/en-US/browser/browser/zen-workspaces.ftl b/locales/en-US/browser/browser/zen-workspaces.ftl index cc8d4601fb..9eea8c1be1 100644 --- a/locales/en-US/browser/browser/zen-workspaces.ftl +++ b/locales/en-US/browser/browser/zen-workspaces.ftl @@ -67,7 +67,7 @@ zen-workspace-creation-name = .placeholder = Space Name zen-move-tab-to-workspace-button = - .label = Move Tabs + .label = Move To... .tooltiptext = Move all tabs in this window to a Space zen-workspaces-panel-context-reorder = diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 69592a03b6..6be50106e4 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b04ef78d6 100644 +index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d706c5590 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -231,16 +231,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2990,8 @@ +@@ -2923,6 +2990,9 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, + _forZenEmptyTab, + essential, ++ zenWorkspaceId, } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,10 +3002,17 @@ +@@ -2933,10 +3003,17 @@ ); } @@ -258,7 +259,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // If we're opening a foreground tab, set the owner by default. ownerTab ??= inBackground ? null : this.selectedTab; -@@ -2944,6 +3020,7 @@ +@@ -2944,6 +3021,7 @@ if (this.selectedTab.owner) { this.selectedTab.owner = null; } @@ -266,14 +267,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the -@@ -2996,6 +3073,19 @@ +@@ -2996,6 +3074,21 @@ noInitialLabel, skipBackgroundNotify, }); + if (hasZenDefaultUserContextId) { + t.setAttribute("zenDefaultUserContextId", "true"); + } -+ if (zenForcedWorkspaceId !== undefined) { ++ if (zenWorkspaceId) { ++ t.setAttribute("zen-workspace-id", zenWorkspaceId); ++ } else if (zenForcedWorkspaceId !== undefined) { + t.setAttribute("zen-workspace-id", zenForcedWorkspaceId); + t.setAttribute("change-workspace", "") + } @@ -286,7 +289,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3094,7 @@ +@@ -3004,6 +3097,7 @@ ownerTab, openerTab, pinned, @@ -294,7 +297,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3113,7 @@ +@@ -3022,6 +3116,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -302,7 +305,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b })); if (focusUrlBar) { -@@ -3146,6 +3238,12 @@ +@@ -3146,6 +3241,12 @@ } } @@ -315,7 +318,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3349,10 +3447,10 @@ +@@ -3349,10 +3450,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -327,7 +330,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } if (!color) { -@@ -3373,9 +3471,14 @@ +@@ -3373,9 +3474,14 @@ label, isAdoptingGroup ); @@ -344,7 +347,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b ); group.addTabs(tabs); -@@ -3496,7 +3599,7 @@ +@@ -3496,7 +3602,7 @@ } this.#handleTabMove(tab, () => @@ -353,7 +356,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b ); } -@@ -3698,6 +3801,7 @@ +@@ -3698,6 +3804,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -361,7 +364,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3767,6 +3871,7 @@ +@@ -3767,6 +3874,7 @@ openWindowInfo, name, skipLoad, @@ -369,7 +372,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b }); } -@@ -3955,7 +4060,7 @@ +@@ -3955,7 +4063,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -378,7 +381,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b let url = "about:blank"; if (tabData.entries?.length) { -@@ -3992,8 +4097,10 @@ +@@ -3992,8 +4100,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -390,7 +393,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (select) { tabToSelect = tab; } -@@ -4005,7 +4112,8 @@ +@@ -4005,7 +4115,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -400,7 +403,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -4019,7 +4127,10 @@ +@@ -4019,7 +4130,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -412,7 +415,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b ); tabsFragment.appendChild(tabGroup.node); } -@@ -4064,9 +4175,23 @@ +@@ -4064,9 +4178,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -436,7 +439,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4257,11 +4382,14 @@ +@@ -4257,11 +4385,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -452,7 +455,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if ( !bulkOrderedOpen && ((openerTab && -@@ -4273,7 +4401,7 @@ +@@ -4273,7 +4404,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -461,7 +464,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b tabGroup = previousTab.group; } if ( -@@ -4284,7 +4412,7 @@ +@@ -4284,7 +4415,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -470,7 +473,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4312,14 +4440,14 @@ +@@ -4312,14 +4443,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -489,7 +492,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4330,7 +4458,7 @@ +@@ -4330,7 +4461,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -498,7 +501,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4358,7 +4486,11 @@ +@@ -4358,7 +4489,11 @@ const tabContainer = pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; @@ -510,7 +513,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } this._updateTabsAfterInsert(); -@@ -4366,6 +4498,7 @@ +@@ -4366,6 +4501,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -518,7 +521,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b TabBarVisibility.update(); } -@@ -4916,6 +5049,7 @@ +@@ -4916,6 +5052,7 @@ telemetrySource, } = {} ) { @@ -526,7 +529,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -5005,6 +5139,7 @@ +@@ -5005,6 +5142,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -534,7 +537,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } catch (e) { console.error(e); } -@@ -5043,6 +5178,12 @@ +@@ -5043,6 +5181,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -547,7 +550,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5057,6 +5198,9 @@ +@@ -5057,6 +5201,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -557,7 +560,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5105,7 +5249,13 @@ +@@ -5105,7 +5252,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -572,7 +575,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b return; } -@@ -5239,7 +5389,7 @@ +@@ -5239,7 +5392,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -581,7 +584,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5263,6 +5413,7 @@ +@@ -5263,6 +5416,7 @@ newTab = true; } @@ -589,7 +592,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5303,13 +5454,7 @@ +@@ -5303,13 +5457,7 @@ aTab._mouseleave(); if (newTab) { @@ -604,7 +607,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } else { TabBarVisibility.update(); } -@@ -5442,6 +5587,7 @@ +@@ -5442,6 +5590,7 @@ this.tabs[i]._tPos = i; } @@ -612,7 +615,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5663,6 +5809,7 @@ +@@ -5663,6 +5812,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -620,7 +623,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5675,13 +5822,13 @@ +@@ -5675,13 +5825,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -636,7 +639,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5697,7 +5844,7 @@ +@@ -5697,7 +5847,7 @@ } if (tab) { @@ -645,7 +648,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } // If no qualifying visible tab was found, see if there is a tab in -@@ -5718,7 +5865,7 @@ +@@ -5718,7 +5868,7 @@ }); } @@ -654,7 +657,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } _blurTab(aTab) { -@@ -5729,7 +5876,7 @@ +@@ -5729,7 +5879,7 @@ * @returns {boolean} * False if swapping isn't permitted, true otherwise. */ @@ -663,7 +666,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // Do not allow transfering a private tab to a non-private window // and vice versa. if ( -@@ -5783,6 +5930,7 @@ +@@ -5783,6 +5933,7 @@ // fire the beforeunload event in the process. Close the other // window if this was its last tab. if ( @@ -671,7 +674,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b !remoteBrowser._beginRemoveTab(aOtherTab, { adoptedByTab: aOurTab, closeWindowWithLastTab: true, -@@ -5794,7 +5942,7 @@ +@@ -5794,7 +5945,7 @@ // If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted. @@ -680,7 +683,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (closeWindow) { let win = aOtherTab.ownerGlobal; win.windowUtils.suppressAnimation(true); -@@ -5918,11 +6066,13 @@ +@@ -5918,11 +6069,13 @@ } // Finish tearing down the tab that's going away. @@ -694,7 +697,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b this.setTabTitle(aOurTab); -@@ -6124,10 +6274,10 @@ +@@ -6124,10 +6277,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -707,25 +710,27 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6186,6 +6336,7 @@ +@@ -6185,7 +6338,8 @@ + * * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ - replaceTabWithWindow(aTab, aOptions) { +- replaceTabWithWindow(aTab, aOptions) { ++ replaceTabWithWindow(aTab, aOptions, zenForceSync = false) { + if (!this.isTab(aTab)) return; // TODO: Handle tab groups if (this.tabs.length == 1) { return null; } -@@ -6213,7 +6364,8 @@ +@@ -6213,7 +6367,8 @@ AppConstants.BROWSER_CHROME_URL, "_blank", options, - aTab + aTab, -+ "zen-unsynced" ++ zenForceSync ? "zen-synced" : "zen-unsynced" ); } -@@ -6319,7 +6471,7 @@ +@@ -6319,7 +6474,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -734,7 +739,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } /** -@@ -6404,8 +6556,8 @@ +@@ -6404,8 +6559,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -745,7 +750,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6431,10 +6583,16 @@ +@@ -6431,10 +6586,16 @@ this.#handleTabMove( element, () => { @@ -764,7 +769,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6492,23 +6650,28 @@ +@@ -6492,23 +6653,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -799,7 +804,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6521,14 +6684,34 @@ +@@ -6521,14 +6687,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -835,7 +840,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6537,7 +6720,7 @@ +@@ -6537,7 +6723,7 @@ element, () => { if (moveBefore) { @@ -844,7 +849,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b } else if (targetElement) { targetElement.after(element); } else { -@@ -6607,10 +6790,10 @@ +@@ -6607,10 +6793,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -857,7 +862,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6656,6 +6839,7 @@ +@@ -6656,6 +6842,7 @@ let state = { tabIndex: tab._tPos, @@ -865,7 +870,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6682,7 +6866,7 @@ +@@ -6682,7 +6869,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -874,7 +879,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6723,6 +6907,10 @@ +@@ -6723,6 +6910,10 @@ moveActionCallback(); @@ -885,7 +890,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7623,7 +7811,7 @@ +@@ -7623,7 +7814,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -894,7 +899,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b window.focus(); aEvent.preventDefault(); break; -@@ -7640,7 +7828,6 @@ +@@ -7640,7 +7831,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -902,7 +907,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b }); break; case "TabGroupCreateByUser": -@@ -8589,6 +8776,7 @@ +@@ -8589,6 +8779,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -910,7 +915,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a932324e780e3c86c504c8b7aa8d433b gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9623,7 +9811,7 @@ var TabContextMenu = { +@@ -9623,7 +9814,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css index 93f4f8808d..c1f40a074d 100644 --- a/src/zen/common/styles/zen-theme.css +++ b/src/zen/common/styles/zen-theme.css @@ -231,6 +231,13 @@ --toolbox-textcolor: currentColor !important; } + &[zen-unsynced-window='true'] { + --zen-main-browser-background: linear-gradient(130deg, light-dark(rgb(240, 230, 200), rgb(30, 25, 20)) 0%, light-dark(rgb(220, 200, 150), rgb(50, 45, 40)) 100%); + --zen-main-browser-background-toolbar: var(--zen-main-browser-background); + --zen-primary-color: light-dark(rgb(200, 100, 20), rgb(220, 120, 30)) !important; + --toolbox-textcolor: currentColor !important; + } + --toolbar-field-background-color: var(--zen-colors-input-bg) !important; --arrowpanel-background: var(--zen-dialog-background) !important; diff --git a/src/zen/common/sys/ZenCustomizableUI.sys.mjs b/src/zen/common/sys/ZenCustomizableUI.sys.mjs index bd7efc760d..42fd818dab 100644 --- a/src/zen/common/sys/ZenCustomizableUI.sys.mjs +++ b/src/zen/common/sys/ZenCustomizableUI.sys.mjs @@ -116,6 +116,9 @@ export const ZenCustomizableUI = new (class { #initCreateNewButton(window) { const button = window.document.getElementById('zen-create-new-button'); button.addEventListener('command', (event) => { + if (window.gZenWorkspaces.privateWindowOrDisabled) { + return window.document.getElementById('cmd_newNavigatorTab').doCommand(); + } if (button.hasAttribute('open')) { return; } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 5bbe1a0f2d..9f8f76c29f 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -127,11 +127,21 @@ class nsZenWindowSync { // 1. We are passing `zen-unsynced` in the window arguments. // 2. We are trying to open a link in a new window where other synced // windows already exist + let forcedSync = false; + let hasUnsyncedArg = false; + for (let arg of aWindow.arguments) { + if (arg === 'zen-synced') { + forcedSync = true; + } else if (arg === 'zen-unsynced') { + hasUnsyncedArg = true; + } + } if ( - aWindow.arguments.some((arg) => arg === 'zen-unsynced') || - (typeof aWindow.arguments[0] === 'string' && - aWindow.arguments.length > 1 && - [...this.#browserWindows].length > 0) + !forcedSync && + (hasUnsyncedArg || + (typeof aWindow.arguments[0] === 'string' && + aWindow.arguments.length > 1 && + [...this.#browserWindows].length > 0)) ) { aWindow.document.documentElement.setAttribute('zen-unsynced-window', 'true'); return; @@ -633,26 +643,68 @@ class nsZenWindowSync { ); } + moveTabsToSyncedWorkspace(aWindow, aWorkspaceId) { + const tabsToMove = aWindow.gZenWorkspaces.allStoredTabs.filter( + (tab) => !tab.hasAttribute('zen-empty-tab') + ); + const selectedTab = aWindow.gBrowser.selectedTab; + let win = [...this.#browserWindows][0]; + const moveAllTabsToWindow = (allowSelected = false) => { + const { gBrowser, gZenWorkspaces } = win; + win.focus(); + let tabIndex = 0; + let success = true; + for (const tab of tabsToMove) { + if (tab !== selectedTab || allowSelected) { + const newTab = gBrowser.adoptTab(tab, { tabIndex }); + if (!newTab) { + // The adoption failed. Restore "fadein" and don't increase the index. + tab.setAttribute('fadein', 'true'); + success = false; + continue; + } + gZenWorkspaces.moveTabToWorkspace(newTab, aWorkspaceId); + ++tabIndex; + } + } + if (success) { + aWindow.close(); + } + }; + if (!win) { + win = this.replaceTabWithWindow(selectedTab, {}, /* zenForceSync = */ true); + win.addEventListener( + 'before-initial-tab-adopted', + () => { + moveAllTabsToWindow(); + }, + { once: true } + ); + return; + } + moveAllTabsToWindow(true); + } + /* Mark: Event Handlers */ on_TabOpen(aEvent) { const tab = aEvent.target; const window = tab.ownerGlobal; - if (tab.selected) { - tab._zenContentsVisible = true; - } + // TODO: Should we only set this flag if the tab is selected? + tab._zenContentsVisible = true; if (tab.id) { // This tab was opened as part of a sync operation. return; } tab.id = this.#newTabSyncId; this.#runOnAllWindows(window, (win) => { - const newTab = win.gBrowser.addTrustedTab('about:blank', { animate: true }); - newTab.setAttribute('zen-workspace-id', tab.getAttribute('zen-workspace-id') || ''); + const newTab = win.gBrowser.addTrustedTab('about:blank', { + animate: true, + createLazyBrowser: true, + zenWorkspaceId: tab.getAttribute('zen-workspace-id') || '', + _forZenEmptyTab: tab.hasAttribute('zen-empty-tab'), + }); newTab.id = tab.id; - if (tab.hasAttribute('zen-empty-tab')) { - newTab.setAttribute('zen-empty-tab', 'true'); - } this.#syncItemWithOriginal( tab, newTab, diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs index 921ae42391..05078a0b01 100644 --- a/src/zen/workspaces/ZenWorkspace.mjs +++ b/src/zen/workspaces/ZenWorkspace.mjs @@ -290,7 +290,10 @@ class nsZenWorkspace extends MozXULElement { for (const workspace of workspaces.workspaces) { const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace); item.addEventListener('command', async () => { - console.log('TODO: Move tab to workspace', workspace); + const { ZenWindowSync } = ChromeUtils.importESModule( + 'resource:///modules/zen/ZenWindowSync.sys.mjs' + ); + ZenWindowSync.moveTabsToSyncedWorkspace(window, workspace.uuid); }); popup.appendChild(item); } diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index dc1e145486..555b7881bf 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -884,28 +884,28 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ZenWorkspacesStorage.getLastChangeTimestamp(), ]); - const workspaceCache = { workspaces, lastChangeTimestamp }; + this._workspaceCache = { workspaces, lastChangeTimestamp }; // Get the active workspace ID from preferences const activeWorkspaceId = this.activeWorkspace; - if (activeWorkspaceId) { - const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId); - // Set the active workspace ID to the first one if the one with selected id doesn't exist - if (!activeWorkspace) { - this.activeWorkspace = workspaceCache.workspaces[0]?.uuid; + if (!lieToMe) { + if (activeWorkspaceId) { + const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId); + // Set the active workspace ID to the first one if the one with selected id doesn't exist + if (!activeWorkspace) { + this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid; + } + } else { + // Set the active workspace ID to the first one if active workspace doesn't exist + this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid; } - } else { - // Set the active workspace ID to the first one if active workspace doesn't exist - this.activeWorkspace = workspaceCache.workspaces[0]?.uuid; } - // sort by position - workspaceCache.workspaces.sort((a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)); - if (!lieToMe) { - // We don't want our cache to remain for the next calls - this._workspaceCache = workspaceCache; - } - return workspaceCache; + // sort by position + this._workspaceCache.workspaces.sort( + (a, b) => (a.position ?? Infinity) - (b.position ?? Infinity) + ); + return this._workspaceCache; } async workspaceBookmarks() { @@ -2465,7 +2465,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } if (!this.currentWindowIsSyncing) { - name = this.isPrivateWindow ? 'Private ' + name : gZenUIManager.generateUuidv4(); + name = this.isPrivateWindow ? 'Private ' + name : 'Temporary'; } // get extra tabs remaning (e.g. on new profiles) and just move them to the new workspace const extraTabs = Array.from(gBrowser.tabContainer.arrowScrollbox.children).filter( diff --git a/src/zen/workspaces/create-workspace-form.css b/src/zen/workspaces/create-workspace-form.css index 33d79eca7b..35d14e0215 100644 --- a/src/zen/workspaces/create-workspace-form.css +++ b/src/zen/workspaces/create-workspace-form.css @@ -33,7 +33,7 @@ zen-workspace-creation { width: calc(100% - 10px); margin: auto; gap: 3.2rem; - margin-top: 1.2rem; + margin-top: 0.6rem; height: 100%; & .zen-workspace-creation-form { diff --git a/src/zen/workspaces/zen-workspaces.css b/src/zen/workspaces/zen-workspaces.css index 8c6767c22c..10da559c14 100644 --- a/src/zen/workspaces/zen-workspaces.css +++ b/src/zen/workspaces/zen-workspaces.css @@ -185,6 +185,15 @@ pointer-events: none; } + :root:not([zen-unsynced-window]) & { + &:hover, + &[open='true'] { + &::before { + background: var(--tab-hover-background-color); + } + } + } + & .zen-current-workspace-indicator-icon { position: relative; width: 16px; @@ -234,11 +243,8 @@ transition: opacity 0.1s; order: 5; --toolbarbutton-inner-padding: 6px !important; - - :root:not([zen-unsynced-window]) & { - opacity: 0; - visibility: collapse; - } + opacity: 0; + visibility: collapse; & image { border-radius: max(calc(var(--border-radius-medium) - 4px), 4px) !important; @@ -254,6 +260,8 @@ :root[zen-unsynced-window] & { .toolbarbutton-text { display: flex; + font-size: 10px; + min-height: 22px; } .toolbarbutton-icon { @@ -262,6 +270,9 @@ } } + :root[zen-unsynced-window='true'] + #navigator-toolbox[zen-has-implicit-hover='true'] + & .zen-workspaces-actions, :root:not([zen-unsynced-window]) &:hover .zen-workspaces-actions, & .zen-workspaces-actions[open='true'] { visibility: visible; From f720dce237d5e5b9dd036c14c6a06f01113e9596 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 6 Dec 2025 18:49:26 +0100 Subject: [PATCH 53/54] feat: Make sure to properly flush all windows when making a new one and fix removing progress listeners, b=no-bug, c=workspaces --- .../sessionstore/SessionStore-sys-mjs.patch | 46 +++--- .../tabbrowser/content/tabbrowser-js.patch | 152 +++++++++--------- .../base/BrowsingContextWebProgress-cpp.patch | 22 --- .../sessionstore/ZenSessionManager.sys.mjs | 40 +++-- src/zen/sessionstore/ZenWindowSync.sys.mjs | 75 +++++++-- src/zen/workspaces/ZenWorkspaces.mjs | 1 - 6 files changed, 192 insertions(+), 144 deletions(-) delete mode 100644 src/docshell/base/BrowsingContextWebProgress-cpp.patch diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 009fed3002..d6dc199ea0 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..ddeb16347bd279934ad3b8fcd158961ff1220f29 100644 +index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..1d06c69e3aebe54aac82ff20b82681812db1ecbb 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -47,17 +47,23 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2215,6 +2223,9 @@ var SessionStoreInternal = { +@@ -2215,6 +2223,15 @@ var SessionStoreInternal = { }); this._shouldRestoreLastSession = false; } + else if (!aInitialState && isRegularWindow) { -+ lazy.ZenSessionStore.restoreNewWindow(aWindow, this); ++ let windowPromises = []; ++ for (let window of this._browserWindows) { ++ windowPromises.push(lazy.TabStateFlusher.flushWindow(window)); ++ } ++ Promise.all(windowPromises).then(() => { ++ lazy.ZenSessionStore.restoreNewWindow(aWindow, this); ++ }); + } if (this._restoreLastWindow && aWindow.toolbar.visible) { // always reset (if not a popup window) -@@ -2384,11 +2395,9 @@ var SessionStoreInternal = { +@@ -2384,11 +2401,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -69,7 +75,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -2465,7 +2474,7 @@ var SessionStoreInternal = { +@@ -2465,7 +2480,7 @@ var SessionStoreInternal = { // 2) Flush the window. // 3) When the flush is complete, revisit our decision to store the window // in _closedWindows, and add/remove as necessary. @@ -78,7 +84,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f this.maybeSaveClosedWindow(winData, isLastWindow); } -@@ -2486,7 +2495,7 @@ var SessionStoreInternal = { +@@ -2486,7 +2501,7 @@ var SessionStoreInternal = { // Save non-private windows if they have at // least one saveable tab or are the last window. @@ -87,7 +93,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f this.maybeSaveClosedWindow(winData, isLastWindow); if (!isLastWindow && winData.closedId > -1) { -@@ -3373,7 +3382,7 @@ var SessionStoreInternal = { +@@ -3373,7 +3388,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -96,7 +102,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f return; } -@@ -4089,6 +4098,12 @@ var SessionStoreInternal = { +@@ -4089,6 +4104,12 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -109,7 +115,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4540,7 @@ var SessionStoreInternal = { +@@ -4525,6 +4546,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -117,7 +123,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5390,7 @@ var SessionStoreInternal = { +@@ -5374,7 +5396,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -126,7 +132,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f removableTabs.push(tab); } } -@@ -5434,7 +5450,7 @@ var SessionStoreInternal = { +@@ -5434,7 +5456,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -135,7 +141,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f winData.workspaceID = workspaceID; } }, -@@ -5483,7 +5499,7 @@ var SessionStoreInternal = { +@@ -5483,7 +5505,7 @@ var SessionStoreInternal = { // collect the data for all windows for (ix in this._windows) { @@ -144,7 +150,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f // window data is still in _statesToRestore continue; } -@@ -5625,11 +5641,12 @@ var SessionStoreInternal = { +@@ -5625,11 +5647,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -158,7 +164,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5657,7 @@ var SessionStoreInternal = { +@@ -5640,6 +5663,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -166,7 +172,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5670,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5676,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -175,7 +181,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5765,8 +5783,8 @@ var SessionStoreInternal = { +@@ -5765,8 +5789,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -186,7 +192,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5809,6 +5827,8 @@ var SessionStoreInternal = { +@@ -5809,6 +5833,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -195,7 +201,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6372,6 +6392,25 @@ var SessionStoreInternal = { +@@ -6372,6 +6398,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -221,7 +227,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7290,7 +7329,7 @@ var SessionStoreInternal = { +@@ -7290,7 +7335,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { @@ -230,7 +236,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..ddeb16347bd279934ad3b8fcd158961f // Adjust window.selected if (tIndex + 1 < window.selected) { window.selected -= 1; -@@ -7305,7 +7344,7 @@ var SessionStoreInternal = { +@@ -7305,7 +7350,7 @@ var SessionStoreInternal = { ); // We don't want to increment tIndex here. continue; diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 6be50106e4..483128e807 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d706c5590 100644 +index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c7385d4a3 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -10,7 +10,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d const browsers = []; if (this.#activeSplitView) { for (const tab of this.#activeSplitView.tabs) { -@@ -450,15 +451,64 @@ +@@ -450,15 +451,66 @@ return this.tabContainer.visibleTabs; } @@ -18,6 +18,8 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d + return this.#handleTabMove(...args); + } + ++ get zenTabProgressListener() { return TabProgressListener; } ++ + get _numVisiblePinTabsWithoutCollapsed() { + let i = 0; + for (let item of this.tabContainer.ariaFocusableItems) { @@ -77,7 +79,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d set selectedTab(val) { if ( gSharedTabWarning.willShowSharedTabWarning(val) || -@@ -613,6 +663,7 @@ +@@ -613,6 +665,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -85,7 +87,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -898,13 +949,17 @@ +@@ -898,13 +951,17 @@ } this.showTab(aTab); @@ -104,7 +106,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d aTab.setAttribute("pinned", "true"); this._updateTabBarForPinnedTabs(); -@@ -917,11 +972,15 @@ +@@ -917,11 +974,15 @@ } this.#handleTabMove(aTab, () => { @@ -121,7 +123,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d }); aTab.style.marginInlineStart = ""; -@@ -1098,6 +1157,8 @@ +@@ -1098,6 +1159,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -130,7 +132,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if ( aIconURL && !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) -@@ -1107,6 +1168,9 @@ +@@ -1107,6 +1170,9 @@ ); return; } @@ -140,7 +142,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1379,7 +1443,6 @@ +@@ -1379,7 +1445,6 @@ // Preview mode should not reset the owner if (!this._previewMode && !oldTab.selected) { @@ -148,7 +150,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); -@@ -1470,6 +1533,7 @@ +@@ -1470,6 +1535,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -156,7 +158,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1622,6 +1686,9 @@ +@@ -1622,6 +1688,9 @@ } let activeEl = document.activeElement; @@ -166,7 +168,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1945,7 +2012,8 @@ +@@ -1945,7 +2014,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -176,7 +178,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d return false; } -@@ -2053,7 +2121,7 @@ +@@ -2053,7 +2123,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -185,7 +187,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2328,6 +2396,7 @@ +@@ -2328,6 +2398,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -193,7 +195,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2401,8 +2470,7 @@ +@@ -2401,8 +2472,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -203,7 +205,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d b.setAttribute("transparent", "true"); } -@@ -2567,7 +2635,7 @@ +@@ -2567,7 +2637,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -212,7 +214,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2626,8 +2694,8 @@ +@@ -2626,8 +2696,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -223,7 +225,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2814,7 +2882,6 @@ +@@ -2814,7 +2884,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -231,7 +233,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2990,9 @@ +@@ -2923,6 +2992,9 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -241,7 +243,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,10 +3003,17 @@ +@@ -2933,10 +3005,17 @@ ); } @@ -259,7 +261,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // If we're opening a foreground tab, set the owner by default. ownerTab ??= inBackground ? null : this.selectedTab; -@@ -2944,6 +3021,7 @@ +@@ -2944,6 +3023,7 @@ if (this.selectedTab.owner) { this.selectedTab.owner = null; } @@ -267,7 +269,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the -@@ -2996,6 +3074,21 @@ +@@ -2996,6 +3076,21 @@ noInitialLabel, skipBackgroundNotify, }); @@ -289,7 +291,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3097,7 @@ +@@ -3004,6 +3099,7 @@ ownerTab, openerTab, pinned, @@ -297,7 +299,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3116,7 @@ +@@ -3022,6 +3118,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -305,7 +307,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d })); if (focusUrlBar) { -@@ -3146,6 +3241,12 @@ +@@ -3146,6 +3243,12 @@ } } @@ -318,7 +320,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3349,10 +3450,10 @@ +@@ -3349,10 +3452,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -330,7 +332,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } if (!color) { -@@ -3373,9 +3474,14 @@ +@@ -3373,9 +3476,14 @@ label, isAdoptingGroup ); @@ -347,7 +349,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d ); group.addTabs(tabs); -@@ -3496,7 +3602,7 @@ +@@ -3496,7 +3604,7 @@ } this.#handleTabMove(tab, () => @@ -356,7 +358,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d ); } -@@ -3698,6 +3804,7 @@ +@@ -3698,6 +3806,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -364,7 +366,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3767,6 +3874,7 @@ +@@ -3767,6 +3876,7 @@ openWindowInfo, name, skipLoad, @@ -372,7 +374,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d }); } -@@ -3955,7 +4063,7 @@ +@@ -3955,7 +4065,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -381,7 +383,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d let url = "about:blank"; if (tabData.entries?.length) { -@@ -3992,8 +4100,10 @@ +@@ -3992,8 +4102,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -393,7 +395,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (select) { tabToSelect = tab; } -@@ -4005,7 +4115,8 @@ +@@ -4005,7 +4117,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -403,7 +405,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -4019,7 +4130,10 @@ +@@ -4019,7 +4132,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -415,7 +417,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d ); tabsFragment.appendChild(tabGroup.node); } -@@ -4064,9 +4178,23 @@ +@@ -4064,9 +4180,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -439,7 +441,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4257,11 +4385,14 @@ +@@ -4257,11 +4387,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -455,7 +457,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if ( !bulkOrderedOpen && ((openerTab && -@@ -4273,7 +4404,7 @@ +@@ -4273,7 +4406,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -464,7 +466,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d tabGroup = previousTab.group; } if ( -@@ -4284,7 +4415,7 @@ +@@ -4284,7 +4417,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -473,7 +475,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4312,14 +4443,14 @@ +@@ -4312,14 +4445,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -492,7 +494,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4330,7 +4461,7 @@ +@@ -4330,7 +4463,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -501,7 +503,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4358,7 +4489,11 @@ +@@ -4358,7 +4491,11 @@ const tabContainer = pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; @@ -513,7 +515,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } this._updateTabsAfterInsert(); -@@ -4366,6 +4501,7 @@ +@@ -4366,6 +4503,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -521,7 +523,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d TabBarVisibility.update(); } -@@ -4916,6 +5052,7 @@ +@@ -4916,6 +5054,7 @@ telemetrySource, } = {} ) { @@ -529,7 +531,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -5005,6 +5142,7 @@ +@@ -5005,6 +5144,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -537,7 +539,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } catch (e) { console.error(e); } -@@ -5043,6 +5181,12 @@ +@@ -5043,6 +5183,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -550,7 +552,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5057,6 +5201,9 @@ +@@ -5057,6 +5203,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -560,7 +562,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5105,7 +5252,13 @@ +@@ -5105,7 +5254,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -575,7 +577,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d return; } -@@ -5239,7 +5392,7 @@ +@@ -5239,7 +5394,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -584,7 +586,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5263,6 +5416,7 @@ +@@ -5263,6 +5418,7 @@ newTab = true; } @@ -592,7 +594,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5303,13 +5457,7 @@ +@@ -5303,13 +5459,7 @@ aTab._mouseleave(); if (newTab) { @@ -607,7 +609,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } else { TabBarVisibility.update(); } -@@ -5442,6 +5590,7 @@ +@@ -5442,6 +5592,7 @@ this.tabs[i]._tPos = i; } @@ -615,7 +617,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5663,6 +5812,7 @@ +@@ -5663,6 +5814,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -623,7 +625,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5675,13 +5825,13 @@ +@@ -5675,13 +5827,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -639,7 +641,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5697,7 +5847,7 @@ +@@ -5697,7 +5849,7 @@ } if (tab) { @@ -648,7 +650,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } // If no qualifying visible tab was found, see if there is a tab in -@@ -5718,7 +5868,7 @@ +@@ -5718,7 +5870,7 @@ }); } @@ -657,7 +659,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } _blurTab(aTab) { -@@ -5729,7 +5879,7 @@ +@@ -5729,7 +5881,7 @@ * @returns {boolean} * False if swapping isn't permitted, true otherwise. */ @@ -666,7 +668,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // Do not allow transfering a private tab to a non-private window // and vice versa. if ( -@@ -5783,6 +5933,7 @@ +@@ -5783,6 +5935,7 @@ // fire the beforeunload event in the process. Close the other // window if this was its last tab. if ( @@ -674,7 +676,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d !remoteBrowser._beginRemoveTab(aOtherTab, { adoptedByTab: aOurTab, closeWindowWithLastTab: true, -@@ -5794,7 +5945,7 @@ +@@ -5794,7 +5947,7 @@ // If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted. @@ -683,7 +685,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (closeWindow) { let win = aOtherTab.ownerGlobal; win.windowUtils.suppressAnimation(true); -@@ -5918,11 +6069,13 @@ +@@ -5918,11 +6071,13 @@ } // Finish tearing down the tab that's going away. @@ -697,7 +699,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d this.setTabTitle(aOurTab); -@@ -6124,10 +6277,10 @@ +@@ -6124,10 +6279,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -710,7 +712,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6185,7 +6338,8 @@ +@@ -6185,7 +6340,8 @@ * * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ @@ -720,7 +722,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (this.tabs.length == 1) { return null; } -@@ -6213,7 +6367,8 @@ +@@ -6213,7 +6369,8 @@ AppConstants.BROWSER_CHROME_URL, "_blank", options, @@ -730,7 +732,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d ); } -@@ -6319,7 +6474,7 @@ +@@ -6319,7 +6476,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -739,7 +741,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } /** -@@ -6404,8 +6559,8 @@ +@@ -6404,8 +6561,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -750,7 +752,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6431,10 +6586,16 @@ +@@ -6431,10 +6588,16 @@ this.#handleTabMove( element, () => { @@ -769,7 +771,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6492,23 +6653,28 @@ +@@ -6492,23 +6655,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -804,7 +806,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6521,14 +6687,34 @@ +@@ -6521,14 +6689,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -840,7 +842,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6537,7 +6723,7 @@ +@@ -6537,7 +6725,7 @@ element, () => { if (moveBefore) { @@ -849,7 +851,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d } else if (targetElement) { targetElement.after(element); } else { -@@ -6607,10 +6793,10 @@ +@@ -6607,10 +6795,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -862,7 +864,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6656,6 +6842,7 @@ +@@ -6656,6 +6844,7 @@ let state = { tabIndex: tab._tPos, @@ -870,7 +872,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6682,7 +6869,7 @@ +@@ -6682,7 +6871,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -879,7 +881,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6723,6 +6910,10 @@ +@@ -6723,6 +6912,10 @@ moveActionCallback(); @@ -890,7 +892,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7623,7 +7814,7 @@ +@@ -7623,7 +7816,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -899,7 +901,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d window.focus(); aEvent.preventDefault(); break; -@@ -7640,7 +7831,6 @@ +@@ -7640,7 +7833,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -907,7 +909,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d }); break; case "TabGroupCreateByUser": -@@ -8589,6 +8779,7 @@ +@@ -8589,6 +8781,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -915,7 +917,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..91bd99772d2ef1eb6cb896b445369e1d gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9623,7 +9814,7 @@ var TabContextMenu = { +@@ -9623,7 +9816,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; diff --git a/src/docshell/base/BrowsingContextWebProgress-cpp.patch b/src/docshell/base/BrowsingContextWebProgress-cpp.patch deleted file mode 100644 index 9ec052d943..0000000000 --- a/src/docshell/base/BrowsingContextWebProgress-cpp.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/docshell/base/BrowsingContextWebProgress.cpp b/docshell/base/BrowsingContextWebProgress.cpp -index cc5bf6de7bf4875acf0c572549dc30ff06ac7f6f..4e0f3648f551fe9507a654d19f0a99e455418aa9 100644 ---- a/docshell/base/BrowsingContextWebProgress.cpp -+++ b/docshell/base/BrowsingContextWebProgress.cpp -@@ -54,7 +54,7 @@ NS_IMETHODIMP BrowsingContextWebProgress::AddProgressListener( - - if (mListenerInfoList.Contains(listener)) { - // The listener is already registered! -- return NS_ERROR_FAILURE; -+ return NS_OK; - } - - mListenerInfoList.AppendElement(ListenerInfo(listener, aNotifyMask)); -@@ -68,7 +68,7 @@ NS_IMETHODIMP BrowsingContextWebProgress::RemoveProgressListener( - return NS_ERROR_INVALID_ARG; - } - -- return mListenerInfoList.RemoveElement(listener) ? NS_OK : NS_ERROR_FAILURE; -+ return mListenerInfoList.RemoveElement(listener) ? NS_OK : NS_OK; - } - - NS_IMETHODIMP BrowsingContextWebProgress::GetBrowsingContextXPCOM( diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 84570e4651..c140cdea2a 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -11,8 +11,10 @@ ChromeUtils.defineESModuleGetters(lazy, { TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs', + setTimeout: 'resource://gre/modules/Timer.sys.mjs', }); +const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore'; const OBSERVING = ['browser-window-before-show']; class nsZenSessionManager { @@ -40,6 +42,17 @@ class nsZenSessionManager { } onFileRead(initialState) { + // For the first time after migration, we restore the tabs + // That where going to be restored by SessionStore. The sidebar + // object will always be empty after migration because we haven't + // gotten the opportunity to save the session yet. + if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) { + Services.prefs.setBoolPref(MIGRATION_PREF, true); + return; + } + // Restore all windows with the same sidebar object, this will + // guarantee that all tabs, groups, folders and split view data + // are properly synced across all windows. for (const winData of initialState.windows || []) { this.restoreWindowData(winData); } @@ -76,9 +89,9 @@ class nsZenSessionManager { * The current session state. */ saveState(state) { - if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { + if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || !state?.windows?.length) { // Don't save (or even collect) anything in permanent private - // browsing mode + // browsing mode. We also don't want to save if there are no windows. return; } this.#collectWindowData(state); @@ -111,8 +124,6 @@ class nsZenSessionManager { * The current session state. */ #collectTabsData(sidebarData, state) { - if (!state?.windows?.length) return; - const tabIdRelationMap = new Map(); for (const window of state.windows) { // Only accept the tabs with `_zenIsActiveTab` set to true from @@ -149,14 +160,19 @@ class nsZenSessionManager { return; } lazy.SessionSaver.run().then(() => { - const state = lazy.SessionStore.getCurrentState(true); - const windows = state.windows || {}; - let newWindow = Cu.cloneInto(windows[0], {}); - delete newWindow.selected; - const newState = { windows: [newWindow] }; - aWindow._zenRestorePromise = new Promise((resolve) => { - SessionStoreInternal.restoreWindows(aWindow, newState, {}); - resolve(); + lazy.setTimeout(() => { + const state = lazy.SessionStore.getCurrentState(true); + const windows = state.windows || []; + let windowToClone = windows[0]; + if (!windowToClone) { + this.restoreWindowData((windowToClone = {})); + } + let newWindow = Cu.cloneInto(windowToClone, {}); + delete newWindow.selected; + const newState = { windows: [newWindow] }; + SessionStoreInternal.restoreWindows(aWindow, newState, { + firstWindow: true, + }); }); }); } diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 9f8f76c29f..3ac9e23b1c 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -427,21 +427,63 @@ class nsZenWindowSync { this.#swapBrowserDocSheellsInner(aOurTab, aOtherTab); } + /** + * Restores the tab progress listener for a given tab. + * + * @param {Object} aTab - The tab to restore the progress listener for. + * @param {Function} callback - The callback function to execute while the listener is removed. + * @param {boolean} onClose - Indicates if the swap is done during a tab close operation. + */ + #withRestoreTabProgressListener(aTab, callback, onClose = false) { + const otherTabBrowser = aTab.ownerGlobal.gBrowser; + const otherBrowser = aTab.linkedBrowser; + + // We aren't closing the other tab so, we also need to swap its tablisteners. + let filter = otherTabBrowser._tabFilters.get(aTab); + let tabListener = otherTabBrowser._tabListeners.get(aTab); + otherBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(tabListener); + + try { + callback(); + } catch (e) { + console.error(e); + } + + // Restore the listeners for the swapped in tab. + if (!onClose) { + tabListener = new otherTabBrowser.zenTabProgressListener(aTab, otherBrowser, false, false); + otherTabBrowser._tabListeners.set(aTab, tabListener); + + const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; + filter.addProgressListener(tabListener, notifyAll); + otherBrowser.webProgress.addProgressListener(filter, notifyAll); + } + } + /** * Swaps the browser docshells between two tabs. * * @param {Object} aOurTab - The tab in the current window. * @param {Object} aOtherTab - The tab in the other window. + * @param {boolean} focus - Indicates if the tab should be focused after the swap. + * @param {boolean} onClose - Indicates if the swap is done during a tab close operation. */ - #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true) { + #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true, onClose = false) { // Load about:blank - if (aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') { + if (!onClose && aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') { aOurTab.linkedBrowser.loadURI(Services.io.newURI('about:blank'), { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, }); } - aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); + this.#withRestoreTabProgressListener( + aOtherTab, + () => { + aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); + }, + onClose + ); aOtherTab.permanentKey = aOurTab.permanentKey; const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', @@ -572,7 +614,7 @@ class nsZenWindowSync { const targetTab = this.#getItemFromWindow(mostRecentWindow, tab.id); if (targetTab) { targetTab._zenContentsVisible = true; - this.#swapBrowserDocSheellsInner(targetTab, tab, targetTab.selected); + this.#swapBrowserDocSheellsInner(targetTab, tab, targetTab.selected, /* onClose =*/ true); // We can animate later, whats important is to always stay on the same // process and avoid async operations here to avoid the closed window // being unloaded before the swap is done. @@ -672,14 +714,10 @@ class nsZenWindowSync { } }; if (!win) { - win = this.replaceTabWithWindow(selectedTab, {}, /* zenForceSync = */ true); - win.addEventListener( - 'before-initial-tab-adopted', - () => { - moveAllTabsToWindow(); - }, - { once: true } - ); + win = aWindow.gBrowser.replaceTabWithWindow(selectedTab, {}, /* zenForceSync = */ true); + win.gZenWorkspaces.promiseInitialized.then(() => { + moveAllTabsToWindow(); + }); return; } moveAllTabsToWindow(true); @@ -690,8 +728,9 @@ class nsZenWindowSync { on_TabOpen(aEvent) { const tab = aEvent.target; const window = tab.ownerGlobal; - // TODO: Should we only set this flag if the tab is selected? - tab._zenContentsVisible = true; + if (tab.selected) { + tab._zenContentsVisible = true; + } if (tab.id) { // This tab was opened as part of a sync operation. return; @@ -715,10 +754,18 @@ class nsZenWindowSync { } on_ZenTabIconChanged(aEvent) { + if (!aEvent.target?._zenContentsVisible) { + // No need to sync icon changes for tabs that aren't active in this window. + return; + } return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON); } on_ZenTabLabelChanged(aEvent) { + if (!aEvent.target?._zenContentsVisible) { + // No need to sync label changes for tabs that aren't active in this window. + return; + } return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_LABEL); } diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 555b7881bf..c26949567c 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -55,7 +55,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.promiseDBInitialized, this.promisePinnedInitialized, SessionStore.promiseAllWindowsRestored, - window._zenRestorePromise, ]); } From 307e8fc0ef8ad0e8cefdefb878454c8efb59346e Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 7 Dec 2025 18:58:04 +0100 Subject: [PATCH 54/54] feat: Make sure to not lose any tabs when opening from a private window, b=no-bug, c=workspaces --- .../sessionstore/SessionStore-sys-mjs.patch | 4 +- .../tabbrowser/content/tabbrowser-js.patch | 139 +++++++++--------- .../sessionstore/ZenSessionManager.sys.mjs | 41 ++++-- src/zen/sessionstore/ZenWindowSync.sys.mjs | 19 ++- src/zen/workspaces/ZenWorkspaces.mjs | 2 + 5 files changed, 117 insertions(+), 88 deletions(-) diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index d6dc199ea0..b1c2074d47 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..1d06c69e3aebe54aac82ff20b82681812db1ecbb 100644 +index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..e1d95e97819718d70fab23850a8810dd216db7a8 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -56,7 +56,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..1d06c69e3aebe54aac82ff20b8268181 + for (let window of this._browserWindows) { + windowPromises.push(lazy.TabStateFlusher.flushWindow(window)); + } -+ Promise.all(windowPromises).then(() => { ++ Promise.all(windowPromises).finally(() => { + lazy.ZenSessionStore.restoreNewWindow(aWindow, this); + }); + } diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 483128e807..0950c539de 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c7385d4a3 100644 +index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..9a619171c7f1f7ba43c2c661bdad370f83733bf0 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -386,6 +386,7 @@ @@ -168,17 +168,20 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1945,7 +2014,8 @@ +@@ -1945,7 +2014,11 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { - if (!aLabel || aLabel.includes("about:reader?")) { ++ if (!aTab._zenContentsVisible && !aTab._labelIsInitialTitle) { ++ return false; ++ } + gZenPinnedTabManager.onTabLabelChanged(aTab); + if (!aLabel || aLabel.includes("about:reader?") || aTab.hasAttribute("zen-has-static-label")) { return false; } -@@ -2053,7 +2123,7 @@ +@@ -2053,7 +2126,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -187,7 +190,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2328,6 +2398,7 @@ +@@ -2328,6 +2401,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -195,7 +198,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2401,8 +2472,7 @@ +@@ -2401,8 +2475,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -205,7 +208,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c b.setAttribute("transparent", "true"); } -@@ -2567,7 +2637,7 @@ +@@ -2567,7 +2640,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -214,7 +217,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2626,8 +2696,8 @@ +@@ -2626,8 +2699,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -225,7 +228,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2814,7 +2884,6 @@ +@@ -2814,7 +2887,6 @@ this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { tabIndex: tab._tPos + 1, userContextId: tab.userContextId, @@ -233,7 +236,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -2923,6 +2992,9 @@ +@@ -2923,6 +2995,9 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -243,7 +246,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2933,10 +3005,17 @@ +@@ -2933,10 +3008,17 @@ ); } @@ -261,7 +264,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // If we're opening a foreground tab, set the owner by default. ownerTab ??= inBackground ? null : this.selectedTab; -@@ -2944,6 +3023,7 @@ +@@ -2944,6 +3026,7 @@ if (this.selectedTab.owner) { this.selectedTab.owner = null; } @@ -269,7 +272,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the -@@ -2996,6 +3076,21 @@ +@@ -2996,6 +3079,21 @@ noInitialLabel, skipBackgroundNotify, }); @@ -291,7 +294,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3004,6 +3099,7 @@ +@@ -3004,6 +3102,7 @@ ownerTab, openerTab, pinned, @@ -299,7 +302,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3022,6 +3118,7 @@ +@@ -3022,6 +3121,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -307,7 +310,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c })); if (focusUrlBar) { -@@ -3146,6 +3243,12 @@ +@@ -3146,6 +3246,12 @@ } } @@ -320,7 +323,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3349,10 +3452,10 @@ +@@ -3349,10 +3455,10 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -332,7 +335,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } if (!color) { -@@ -3373,9 +3476,14 @@ +@@ -3373,9 +3479,14 @@ label, isAdoptingGroup ); @@ -349,7 +352,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c ); group.addTabs(tabs); -@@ -3496,7 +3604,7 @@ +@@ -3496,7 +3607,7 @@ } this.#handleTabMove(tab, () => @@ -358,7 +361,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c ); } -@@ -3698,6 +3806,7 @@ +@@ -3698,6 +3809,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -366,7 +369,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -3767,6 +3876,7 @@ +@@ -3767,6 +3879,7 @@ openWindowInfo, name, skipLoad, @@ -374,7 +377,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c }); } -@@ -3955,7 +4065,7 @@ +@@ -3955,7 +4068,7 @@ // Add a new tab if needed. if (!tab) { let createLazyBrowser = @@ -383,7 +386,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c let url = "about:blank"; if (tabData.entries?.length) { -@@ -3992,8 +4102,10 @@ +@@ -3992,8 +4105,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -395,7 +398,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (select) { tabToSelect = tab; } -@@ -4005,7 +4117,8 @@ +@@ -4005,7 +4120,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -405,7 +408,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -4019,7 +4132,10 @@ +@@ -4019,7 +4135,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -417,7 +420,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c ); tabsFragment.appendChild(tabGroup.node); } -@@ -4064,9 +4180,23 @@ +@@ -4064,9 +4183,23 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -433,15 +436,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c + gZenWorkspaces._initialTab._shouldRemove = true; + } + } -+ } + } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; - } ++ } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4257,11 +4387,14 @@ +@@ -4257,11 +4390,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -457,7 +460,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if ( !bulkOrderedOpen && ((openerTab && -@@ -4273,7 +4406,7 @@ +@@ -4273,7 +4409,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -466,7 +469,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c tabGroup = previousTab.group; } if ( -@@ -4284,7 +4417,7 @@ +@@ -4284,7 +4420,7 @@ ) { elementIndex = Infinity; } else if (previousTab.visible) { @@ -475,7 +478,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4312,14 +4445,14 @@ +@@ -4312,14 +4448,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -494,7 +497,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4330,7 +4463,7 @@ +@@ -4330,7 +4466,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -503,7 +506,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (this.isTab(itemAfter) && itemAfter.group == tabGroup) { // Place at the front of, or between tabs in, the same tab group this.tabContainer.insertBefore(tab, itemAfter); -@@ -4358,7 +4491,11 @@ +@@ -4358,7 +4494,11 @@ const tabContainer = pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; @@ -515,7 +518,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } this._updateTabsAfterInsert(); -@@ -4366,6 +4503,7 @@ +@@ -4366,6 +4506,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -523,7 +526,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c TabBarVisibility.update(); } -@@ -4916,6 +5054,7 @@ +@@ -4916,6 +5057,7 @@ telemetrySource, } = {} ) { @@ -531,7 +534,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -5005,6 +5144,7 @@ +@@ -5005,6 +5147,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -539,7 +542,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } catch (e) { console.error(e); } -@@ -5043,6 +5183,12 @@ +@@ -5043,6 +5186,12 @@ aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); } @@ -552,7 +555,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // Handle requests for synchronously removing an already // asynchronously closing tab. if (!animate && aTab.closing) { -@@ -5057,6 +5203,9 @@ +@@ -5057,6 +5206,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -562,7 +565,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5105,7 +5254,13 @@ +@@ -5105,7 +5257,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -577,7 +580,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c return; } -@@ -5239,7 +5394,7 @@ +@@ -5239,7 +5397,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -586,7 +589,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5263,6 +5418,7 @@ +@@ -5263,6 +5421,7 @@ newTab = true; } @@ -594,7 +597,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5303,13 +5459,7 @@ +@@ -5303,13 +5462,7 @@ aTab._mouseleave(); if (newTab) { @@ -609,7 +612,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } else { TabBarVisibility.update(); } -@@ -5442,6 +5592,7 @@ +@@ -5442,6 +5595,7 @@ this.tabs[i]._tPos = i; } @@ -617,7 +620,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5663,6 +5814,7 @@ +@@ -5663,6 +5817,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -625,7 +628,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5675,13 +5827,13 @@ +@@ -5675,13 +5830,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -641,7 +644,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5697,7 +5849,7 @@ +@@ -5697,7 +5852,7 @@ } if (tab) { @@ -650,7 +653,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } // If no qualifying visible tab was found, see if there is a tab in -@@ -5718,7 +5870,7 @@ +@@ -5718,7 +5873,7 @@ }); } @@ -659,7 +662,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } _blurTab(aTab) { -@@ -5729,7 +5881,7 @@ +@@ -5729,7 +5884,7 @@ * @returns {boolean} * False if swapping isn't permitted, true otherwise. */ @@ -668,7 +671,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // Do not allow transfering a private tab to a non-private window // and vice versa. if ( -@@ -5783,6 +5935,7 @@ +@@ -5783,6 +5938,7 @@ // fire the beforeunload event in the process. Close the other // window if this was its last tab. if ( @@ -676,7 +679,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c !remoteBrowser._beginRemoveTab(aOtherTab, { adoptedByTab: aOurTab, closeWindowWithLastTab: true, -@@ -5794,7 +5947,7 @@ +@@ -5794,7 +5950,7 @@ // If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted. @@ -685,7 +688,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (closeWindow) { let win = aOtherTab.ownerGlobal; win.windowUtils.suppressAnimation(true); -@@ -5918,11 +6071,13 @@ +@@ -5918,11 +6074,13 @@ } // Finish tearing down the tab that's going away. @@ -699,7 +702,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c this.setTabTitle(aOurTab); -@@ -6124,10 +6279,10 @@ +@@ -6124,10 +6282,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -712,7 +715,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6185,7 +6340,8 @@ +@@ -6185,7 +6343,8 @@ * * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ @@ -722,7 +725,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (this.tabs.length == 1) { return null; } -@@ -6213,7 +6369,8 @@ +@@ -6213,7 +6372,8 @@ AppConstants.BROWSER_CHROME_URL, "_blank", options, @@ -732,7 +735,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c ); } -@@ -6319,7 +6476,7 @@ +@@ -6319,7 +6479,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -741,7 +744,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } /** -@@ -6404,8 +6561,8 @@ +@@ -6404,8 +6564,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -752,7 +755,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6431,10 +6588,16 @@ +@@ -6431,10 +6591,16 @@ this.#handleTabMove( element, () => { @@ -771,7 +774,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6492,23 +6655,28 @@ +@@ -6492,23 +6658,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -806,7 +809,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6521,14 +6689,34 @@ +@@ -6521,14 +6692,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -842,7 +845,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6537,7 +6725,7 @@ +@@ -6537,7 +6728,7 @@ element, () => { if (moveBefore) { @@ -851,7 +854,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c } else if (targetElement) { targetElement.after(element); } else { -@@ -6607,10 +6795,10 @@ +@@ -6607,10 +6798,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -864,7 +867,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6656,6 +6844,7 @@ +@@ -6656,6 +6847,7 @@ let state = { tabIndex: tab._tPos, @@ -872,7 +875,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6682,7 +6871,7 @@ +@@ -6682,7 +6874,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -881,7 +884,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6723,6 +6912,10 @@ +@@ -6723,6 +6915,10 @@ moveActionCallback(); @@ -892,7 +895,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7623,7 +7816,7 @@ +@@ -7623,7 +7819,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -901,7 +904,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c window.focus(); aEvent.preventDefault(); break; -@@ -7640,7 +7833,6 @@ +@@ -7640,7 +7836,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -909,7 +912,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c }); break; case "TabGroupCreateByUser": -@@ -8589,6 +8781,7 @@ +@@ -8589,6 +8784,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -917,7 +920,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..86358619b637442eb21c7262b0d1436c gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9623,7 +9816,7 @@ var TabContextMenu = { +@@ -9623,7 +9819,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index c140cdea2a..8ebb5c49d9 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -115,6 +115,14 @@ class nsZenSessionManager { this.#sidebar = sidebarData; } + #filterUnusedTabs(tabs) { + return tabs.filter((tab) => { + // We need to ignore empty tabs with no group association + // as they are not useful to restore. + return !(tab.zenIsEmpty && !tab.groupId); + }); + } + /** * Collects session data for all tabs in a given window. * @@ -137,7 +145,7 @@ class nsZenSessionManager { } } - sidebarData.tabs = Array.from(tabIdRelationMap.values()); + sidebarData.tabs = this.#filterUnusedTabs(Array.from(tabIdRelationMap.values())); sidebarData.folders = state.windows[0].folders; sidebarData.splitViewData = state.windows[0].splitViewData; @@ -159,19 +167,24 @@ class nsZenSessionManager { if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) { return; } - lazy.SessionSaver.run().then(() => { - lazy.setTimeout(() => { - const state = lazy.SessionStore.getCurrentState(true); - const windows = state.windows || []; - let windowToClone = windows[0]; - if (!windowToClone) { - this.restoreWindowData((windowToClone = {})); - } - let newWindow = Cu.cloneInto(windowToClone, {}); - delete newWindow.selected; - const newState = { windows: [newWindow] }; - SessionStoreInternal.restoreWindows(aWindow, newState, { - firstWindow: true, + aWindow._zenPromiseNewWindowRestored = new Promise((resolve) => { + lazy.SessionSaver.run().then(() => { + lazy.setTimeout(() => { + const state = lazy.SessionStore.getCurrentState(true); + const windows = state.windows || []; + let windowToClone = + windows.find( + (win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced + ) || {}; + let newWindow = Cu.cloneInto(windowToClone, {}); + this.restoreWindowData(newWindow); + newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []); + delete newWindow.selected; + const newState = { windows: [newWindow] }; + SessionStoreInternal.restoreWindows(aWindow, newState, { + firstWindow: true, + }); + resolve(); }); }); }); diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 3ac9e23b1c..c026562651 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -441,8 +441,12 @@ class nsZenWindowSync { // We aren't closing the other tab so, we also need to swap its tablisteners. let filter = otherTabBrowser._tabFilters.get(aTab); let tabListener = otherTabBrowser._tabListeners.get(aTab); - otherBrowser.webProgress.removeProgressListener(filter); - filter.removeProgressListener(tabListener); + try { + otherBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(tabListener); + } catch { + /* ignore errors, we might have already removed them */ + } try { callback(); @@ -470,13 +474,21 @@ class nsZenWindowSync { * @param {boolean} onClose - Indicates if the swap is done during a tab close operation. */ #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true, onClose = false) { - // Load about:blank + // Load about:blank if by any chance we loaded the previous tab's URL. + // TODO: We should maybe start using a singular about:blank preloaded view + // to avoid loading a full blank page each time and wasting resources. + // We do need to do this though instead of just unloading the browser because + // firefox doesn't expect an unloaded + selected tab, so we need to get + // around this limitation somehow. if (!onClose && aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') { aOurTab.linkedBrowser.loadURI(Services.io.newURI('about:blank'), { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, }); } + // Running `swapBrowsersAndCloseOther` doesn't expect us to use the tab after + // the operation, so it doesn't really care about cleaning up the other tab. + // We need to make a new tab progress listener for the other tab after the swap. this.#withRestoreTabProgressListener( aOtherTab, () => { @@ -484,7 +496,6 @@ class nsZenWindowSync { }, onClose ); - aOtherTab.permanentKey = aOurTab.permanentKey; const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', // but we need to manually remove some attributes from the other tab. diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index c26949567c..e24ddcac60 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -934,6 +934,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { let activeWorkspace = await this.getActiveWorkspace(); this.activeWorkspace = activeWorkspace?.uuid; await gZenSessionStore.promiseInitialized; + await window._zenPromiseNewWindowRestored; + delete window._zenPromiseNewWindowRestored; try { if (activeWorkspace) { window.gZenThemePicker = new nsZenThemePicker();