// 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 lazy = {}; class ZenPinnedTabsObserver { static ALL_EVENTS = [ 'TabPinned', 'TabUnpinned', 'TabMove', 'TabGroupCreate', 'TabGroupRemoved', 'TabGroupMoved', 'ZenFolderRenamed', 'ZenFolderIconChanged', 'TabGroupCollapse', 'TabGroupExpand', 'TabGrouped', 'TabUngrouped', 'ZenFolderChangedWorkspace', 'TabAddedToEssentials', 'TabRemovedFromEssentials', ]; #listeners = []; constructor() { XPCOMUtils.defineLazyPreferenceGetter( lazy, 'zenPinnedTabRestorePinnedTabsToPinnedUrl', 'zen.pinned-tab-manager.restore-pinned-tabs-to-pinned-url', false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, 'zenPinnedTabCloseShortcutBehavior', 'zen.pinned-tab-manager.close-shortcut-behavior', 'switch' ); XPCOMUtils.defineLazyPreferenceGetter( lazy, 'zenTabsEssentialsMax', 'zen.tabs.essentials.max', 12 ); ChromeUtils.defineESModuleGetters(lazy, { E10SUtils: 'resource://gre/modules/E10SUtils.sys.mjs', }); this.#listenPinnedTabEvents(); } #listenPinnedTabEvents() { const eventListener = this.#eventListener.bind(this); for (const event of ZenPinnedTabsObserver.ALL_EVENTS) { window.addEventListener(event, eventListener); } window.addEventListener('unload', () => { for (const event of ZenPinnedTabsObserver.ALL_EVENTS) { window.removeEventListener(event, eventListener); } }); } #eventListener(event) { for (const listener of this.#listeners) { listener(event.type, event); } } addPinnedTabListener(listener) { this.#listeners.push(listener); } } class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { hasInitializedPins = false; promiseInitializedPinned = new Promise((resolve) => { this._resolvePinnedInitializedInternal = resolve; }); async init() { if (!this.enabled) { return; } this._canLog = Services.prefs.getBoolPref('zen.pinned-tab-manager.debug', false); this.observer = new ZenPinnedTabsObserver(); this._initClosePinnedTabShortcut(); this._insertItemsIntoTabContextMenu(); this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this)); this._zenClickEventListener = this._onTabClick.bind(this); gZenWorkspaces._resolvePinnedInitialized(); } log(message) { if (this._canLog) { console.log(`[ZenPinnedTabManager] ${message}`); } } onTabIconChanged(tab, url = null) { 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})`); } } } _onTabResetPinButton(event, tab) { event.stopPropagation(); const pin = this._pinsCache?.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id')); if (!pin) { return; } let userContextId; if (tab.hasAttribute('usercontextid')) { userContextId = tab.getAttribute('usercontextid'); } const pinnedUrl = Services.io.newURI(pin.url); const browser = tab.linkedBrowser; browser.loadURI(pinnedUrl, { triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({ userContextId, }), }); this.resetPinChangedUrl(tab); } get enabled() { 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; } this._resolvePinnedInitializedInternal(); delete this._resolvePinnedInitializedInternal; this.hasInitializedPins = true; } 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(); } _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); 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) { 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; } if (!tab || !tab.pinned) { return; } await this._resetTabToStoredState(tab); } async replacePinnedUrlWithCurrent(tab = undefined) { tab ??= TabContextMenu.contextTab; if (!tab || !tab.pinned || !tab.getAttribute('zen-pin-id')) { 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'); 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); } await ZenPinnedTabsStorage.savePin(pin, notifyObservers); } 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; } const selectedTabs = pinnedTabs.filter((tab) => tab.selected); event.stopPropagation(); event.preventDefault(); if (noClose && behavior === 'close') { behavior = 'unload-switch'; } if (alwaysUnload && ['close', 'reset', 'switch', 'reset-switch'].includes(behavior)) { 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')) { 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(); } }, 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); } } 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 (selectedTabs.length) { this._handleTabSwitch(selectedTabs[0]); } if (behavior.includes('reset')) { for (const tab of pinnedTabs) { this._resetTabToStoredState(tab); } } 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) { return; } const findNextTab = (direction) => gBrowser.tabContainer.findNextTab(selectedTab, { direction, filter: (tab) => !tab.hidden && !tab.pinned, }); let nextTab = findNextTab(1) || findNextTab(-1); if (!nextTab) { gZenWorkspaces.selectEmptyTab(); return; } if (nextTab) { gBrowser.selectedTab = nextTab; } } _resetTabToStoredState(tab) { const id = tab.getAttribute('zen-pin-id'); if (!id) { return; } const pin = this._pinsCache.find((pin) => pin.uuid === id); if (!pin) { return; } const tabState = SessionStore.getTabState(tab); const state = JSON.parse(tabState); const foundEntryIndex = state.entries?.findIndex((entry) => entry.url === pin.url); if (foundEntryIndex === -1) { state.entries = [ { url: pin.url, title: pin.title, triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, }, ]; } else { // Remove everything except the entry we want to keep const existingEntry = state.entries[foundEntryIndex]; existingEntry.title = pin.title; state.entries = [existingEntry]; } state.image = pin.iconUrl || state.image; state.index = 0; SessionStore.setTabState(tab, state); this.resetPinChangedUrl(tab); } async getFaviconAsBase64(pageUrl) { try { const faviconData = await PlacesUtils.favicons.getFaviconForPage(pageUrl); if (!faviconData) { // empty favicon return null; } return faviconData.dataURI; } catch (ex) { console.error('Failed to get favicon:', ex); return null; } } 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); } 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 ? [tab] : TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab]; for (let i = 0; i < tabs.length; i++) { const tab = tabs[i]; tab.removeAttribute('zen-essential'); if (gZenWorkspaces.workspaceEnabled && gZenWorkspaces.getActiveWorkspaceFromCache().uuid) { tab.setAttribute('zen-workspace-id', gZenWorkspaces.getActiveWorkspaceFromCache().uuid); } if (unpin) { gBrowser.unpinTab(tab); } else { gBrowser.zenHandleTabMove(tab, () => { const pinContainer = gZenWorkspaces.pinnedTabsContainer; pinContainer.prepend(tab); }); } // Dispatch the event to update the UI const event = new CustomEvent('TabRemovedFromEssentials', { detail: { tab }, bubbles: true, cancelable: false, }); tab.dispatchEvent(event); } gZenUIManager.updateTabsToolbar(); } _insertItemsIntoTabContextMenu() { if (!this.enabled) { return; } const elements = window.MozXULElement.parseXULToFragment(`