feat: Listen to new tab opens for new sync system, b=no-bug, c=common, folders, tabs

This commit is contained in:
Mr. M
2025-11-22 13:58:15 +01:00
parent c6cd912262
commit 1fcef12d0a
5 changed files with 435 additions and 333 deletions

View File

@@ -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');

View File

@@ -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 = [];

View File

@@ -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()) {

View File

@@ -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();

View File

@@ -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);