mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-10 00:38:04 +00:00
feat: Listen to new tab opens for new sync system, b=no-bug, c=common, folders, tabs
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user