From 7dbe5b414dbfb6e35f4d2815a5fdc7dc8b62f21c Mon Sep 17 00:00:00 2001 From: Afeefur <152735100+AfeefurR@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:14:45 +0400 Subject: [PATCH] feat: Added 'unload all other spaces' option in spaces context menu, p=#12751 Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> --- .../en-US/browser/browser/zen-workspaces.ftl | 3 + .../base/content/zen-commands.inc.xhtml | 1 + .../base/content/zen-panels/popups.inc | 3 +- src/zen/common/zen-sets.js | 4 + src/zen/spaces/ZenSpaceManager.mjs | 15 +++ src/zen/tabs/ZenPinnedTabManager.mjs | 15 ++- .../pinned/browser_pinned_reset_button.js | 115 ++++++++++++++---- 7 files changed, 127 insertions(+), 29 deletions(-) diff --git a/locales/en-US/browser/browser/zen-workspaces.ftl b/locales/en-US/browser/browser/zen-workspaces.ftl index 5a4125163..d57b1e0da 100644 --- a/locales/en-US/browser/browser/zen-workspaces.ftl +++ b/locales/en-US/browser/browser/zen-workspaces.ftl @@ -35,6 +35,9 @@ zen-workspaces-panel-context-default-profile = zen-workspaces-panel-unload = .label = Unload Space +zen-workspaces-panel-unload-others = + .label = Unload All Other Spaces + zen-workspaces-how-to-reorder-title = How to reorder spaces zen-workspaces-how-to-reorder-desc = Drag the space icons at the bottom of the sidebar to reorder them diff --git a/src/browser/base/content/zen-commands.inc.xhtml b/src/browser/base/content/zen-commands.inc.xhtml index 670e8725d..de2cec15d 100644 --- a/src/browser/base/content/zen-commands.inc.xhtml +++ b/src/browser/base/content/zen-commands.inc.xhtml @@ -40,6 +40,7 @@ + diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc index 42dcbe14d..c79bf726b 100644 --- a/src/browser/base/content/zen-panels/popups.inc +++ b/src/browser/base/content/zen-panels/popups.inc @@ -42,10 +42,11 @@ hide-if-usercontext-disabled="true"> - + + diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index c2959fd16..6d539836c 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -129,6 +129,10 @@ document.addEventListener( gZenWorkspaces.unloadWorkspace(); break; } + case "cmd_zenUnloadAllOtherWorkspace": { + gZenWorkspaces.unloadAllOtherWorkspaces(); + break; + } case "cmd_zenNewNavigatorUnsynced": OpenBrowserWindow({ zenSyncedWindow: false }); break; diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index b5e2506d8..54e5013d1 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -1493,6 +1493,21 @@ class nsZenWorkspaces { await gBrowser.explicitUnloadTabs(tabsToUnload); // TODO: unit test this } + async unloadAllOtherWorkspaces() { + const workspaceId = + this.#contextMenuData?.workspaceId || this.activeWorkspace; + + const tabsToUnload = this.allStoredTabs.filter( + tab => + tab.getAttribute("zen-workspace-id") !== workspaceId && + !tab.hasAttribute("zen-empty-tab") && + !tab.hasAttribute("zen-essential") && + !tab.hasAttribute("pending") + ); + + await gBrowser.explicitUnloadTabs(tabsToUnload); // TODO: unit test this + } + moveTabToWorkspace(tab, workspaceID) { return this.moveTabsToWorkspace([tab], workspaceID); } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 28dcb887b..b2df35b68 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -117,9 +117,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { event.stopPropagation(); if (event.getModifierState("Accel")) { let newTab = gBrowser.duplicateTab(tab, true); - newTab.addEventListener("SSTabRestored", () => { - this._resetTabToStoredState(tab); - }, { once: true }); + newTab.addEventListener( + "SSTabRestored", + () => { + this._resetTabToStoredState(tab); + }, + { once: true } + ); } else { this._resetTabToStoredState(tab); } @@ -182,12 +186,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { if (!tab) { return; } - let accelHeld = e.getModifierState("Accel") || (e.metaKey && e.type == "keydown"); + let accelHeld = + e.getModifierState("Accel") || (e.metaKey && e.type == "keydown"); this._setResetPinSublabel(tab, accelHeld); // Up <-> down events until the mouse leaves the button. // When hovered with accelHeld, we should listen to the next keyup event let nextEvent = accelHeld ? "keyup" : "keydown"; - let handler = (nextE) => this._onAccelKeyChange(nextE); + let handler = nextE => this._onAccelKeyChange(nextE); window.addEventListener(nextEvent, handler, { once: true }); } diff --git a/src/zen/tests/pinned/browser_pinned_reset_button.js b/src/zen/tests/pinned/browser_pinned_reset_button.js index 98b47878c..4691c0980 100644 --- a/src/zen/tests/pinned/browser_pinned_reset_button.js +++ b/src/zen/tests/pinned/browser_pinned_reset_button.js @@ -7,7 +7,7 @@ async function pinAndNavigateTab(url, navigateTo) { const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); gBrowser.pinTab(tab); await gBrowser.TabStateFlusher.flush(tab.linkedBrowser); - await new Promise((r) => setTimeout(r, 500)); + await new Promise(r => setTimeout(r, 500)); BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, navigateTo); await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, navigateTo); @@ -15,23 +15,45 @@ async function pinAndNavigateTab(url, navigateTo) { } add_task(async function test_ResetPinButton_SelectsTab() { - const tab = await pinAndNavigateTab("https://example.com/1", "https://example.com/2"); + const tab = await pinAndNavigateTab( + "https://example.com/1", + "https://example.com/2" + ); // Open another tab and select it - const otherTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/other"); - Assert.notEqual(gBrowser.selectedTab, tab, "The pinned tab should not be selected initially"); + const otherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/other" + ); + Assert.notEqual( + gBrowser.selectedTab, + tab, + "The pinned tab should not be selected initially" + ); // Simulate clicking the reset pin button (without Accel key) gZenPinnedTabManager._onTabResetPinButton( - { stopPropagation() {}, getModifierState() { return false; } }, + { + stopPropagation() {}, + getModifierState() { + return false; + }, + }, tab ); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout - await new Promise((r) => setTimeout(r, 100)); + await new Promise(r => setTimeout(r, 100)); - Assert.strictEqual(gBrowser.selectedTab, tab, "The pinned tab should be selected after reset"); - ok(!tab.hasAttribute("zen-pinned-changed"), "zen-pinned-changed should be removed after reset"); + Assert.strictEqual( + gBrowser.selectedTab, + tab, + "The pinned tab should be selected after reset" + ); + ok( + !tab.hasAttribute("zen-pinned-changed"), + "zen-pinned-changed should be removed after reset" + ); gBrowser.removeTab(otherTab); gBrowser.removeTab(tab); @@ -45,17 +67,29 @@ add_task(async function test_ResetPinButton_CmdClick_DuplicatesAndResets() { // Simulate CMD+click on the reset pin button gZenPinnedTabManager._onTabResetPinButton( - { stopPropagation() {}, getModifierState() { return true; } }, + { + stopPropagation() {}, + getModifierState() { + return true; + }, + }, tab ); // Wait for the duplicate tab to be restored - const restoredEvent = await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"); + const restoredEvent = await BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "SSTabRestored" + ); const newTab = restoredEvent.target; // eslint-disable-next-line mozilla/no-arbitrary-setTimeout - await new Promise((r) => setTimeout(r, 100)); + await new Promise(r => setTimeout(r, 100)); - Assert.equal(gBrowser.tabs.length, tabCountBefore + 1, "A new tab should be created from the duplicate"); + Assert.equal( + gBrowser.tabs.length, + tabCountBefore + 1, + "A new tab should be created from the duplicate" + ); Assert.equal( newTab.linkedBrowser.currentURI.spec, navigatedUrl, @@ -63,8 +97,15 @@ add_task(async function test_ResetPinButton_CmdClick_DuplicatesAndResets() { ); ok(!newTab.pinned, "The duplicated tab should not be pinned"); - Assert.strictEqual(gBrowser.selectedTab, tab, "The pinned tab should be selected after CMD+click reset"); - ok(!tab.hasAttribute("zen-pinned-changed"), "zen-pinned-changed should be removed after reset"); + Assert.strictEqual( + gBrowser.selectedTab, + tab, + "The pinned tab should be selected after CMD+click reset" + ); + ok( + !tab.hasAttribute("zen-pinned-changed"), + "zen-pinned-changed should be removed after reset" + ); Assert.equal( tab.linkedBrowser.currentURI.spec, originalUrl, @@ -76,7 +117,10 @@ add_task(async function test_ResetPinButton_CmdClick_DuplicatesAndResets() { }); add_task(async function test_Hover_SublabelChangesWithAccelKey() { - const tab = await pinAndNavigateTab("https://example.com/1", "https://example.com/2"); + const tab = await pinAndNavigateTab( + "https://example.com/1", + "https://example.com/2" + ); // Track calls to document.l10n.setArgs to verify sublabel updates const sublabelArgs = []; @@ -92,36 +136,61 @@ add_task(async function test_Hover_SublabelChangesWithAccelKey() { try { // Simulate hovering with no modifier key held gZenPinnedTabManager.onResetPinButtonMouseOver(tab, { - getModifierState() { return false; }, + getModifierState() { + return false; + }, metaKey: false, type: "mouseover", }); - Assert.equal(sublabelArgs.at(-1), "zen-default-pinned", "Sublabel should show default text on hover without Accel"); + Assert.equal( + sublabelArgs.at(-1), + "zen-default-pinned", + "Sublabel should show default text on hover without Accel" + ); // Simulate pressing CMD while hovering gZenPinnedTabManager._onAccelKeyChange({ - getModifierState() { return true; }, + getModifierState() { + return true; + }, metaKey: true, type: "keydown", }); - Assert.equal(sublabelArgs.at(-1), "zen-default-pinned-cmd", "Sublabel should show CMD text when Accel key is pressed"); + Assert.equal( + sublabelArgs.at(-1), + "zen-default-pinned-cmd", + "Sublabel should show CMD text when Accel key is pressed" + ); // Simulate releasing CMD while still hovering gZenPinnedTabManager._onAccelKeyChange({ - getModifierState() { return false; }, + getModifierState() { + return false; + }, metaKey: false, type: "keyup", }); - Assert.equal(sublabelArgs.at(-1), "zen-default-pinned", "Sublabel should revert to default text when Accel key is released"); + Assert.equal( + sublabelArgs.at(-1), + "zen-default-pinned", + "Sublabel should revert to default text when Accel key is released" + ); // Simulate mouse out gZenPinnedTabManager.onResetPinButtonMouseOut(tab); - Assert.equal(sublabelArgs.at(-1), "zen-default-pinned", "Sublabel should show default text after mouse out"); - ok(!gZenPinnedTabManager._tabWithResetPinButtonHovered, "Hovered tab reference should be cleared after mouse out"); + Assert.equal( + sublabelArgs.at(-1), + "zen-default-pinned", + "Sublabel should show default text after mouse out" + ); + ok( + !gZenPinnedTabManager._tabWithResetPinButtonHovered, + "Hovered tab reference should be cleared after mouse out" + ); } finally { document.l10n.setArgs = origSetArgs; }