From 1fcef12d0ad28e1e5430d7eaa0cbd1591c5c1d8a Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 22 Nov 2025 13:58:15 +0100 Subject: [PATCH] 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 | 385 +++++++++------------ 5 files changed, 435 insertions(+), 333 deletions(-) diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs index daa0d0962..c19233e7d 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 16746cd26..6c1d2dbc7 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 93b272dfe..d5adaa8b0 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 21bc6c230..42947a9ed 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 9d7bc5b36..af06b405e 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, - }); - tab.dispatchEvent(event); + 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; } - gZenUIManager.updateTabsToolbar(); - return movedAll; + 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, + }); + 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);