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>
This commit is contained in:
Afeefur
2026-03-14 13:14:45 +04:00
committed by GitHub
parent 55c079d4ba
commit 7dbe5b414d
7 changed files with 127 additions and 29 deletions

View File

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

View File

@@ -40,6 +40,7 @@
<command id="cmd_zenCtxDeleteWorkspace" />
<command id="cmd_zenUnloadWorkspace" />
<command id="cmd_zenUnloadAllOtherWorkspace" />
<command id="cmd_zenChangeWorkspaceName" />
<command id="cmd_zenChangeWorkspaceIcon" />
<command id="cmd_zenReorderWorkspaces" />

View File

@@ -42,10 +42,11 @@
hide-if-usercontext-disabled="true">
<menupopup />
</menu>
<menuitem id="context_zenUnloadWorkspace" data-l10n-id="zen-workspaces-panel-unload" command="cmd_zenUnloadWorkspace"/>
<menuseparator id="context_zenWorkspacesSeparator"/>
<menuseparator/>
<menuitem id="context_zenReorderWorkspaces" data-l10n-id="zen-workspaces-panel-context-reorder" command="cmd_zenReorderWorkspaces"/>
<menuitem id="context_zenUnloadWorkspace" data-l10n-id="zen-workspaces-panel-unload" command="cmd_zenUnloadWorkspace"/>
<menuitem id="context_zenUnloadAllOtherWorkspace" data-l10n-id="zen-workspaces-panel-unload-others" command="cmd_zenUnloadAllOtherWorkspace"/>
<menuseparator/>
<menuitem data-l10n-id="zen-panel-ui-workspaces-create" command="cmd_zenOpenWorkspaceCreation"/>
<menuitem id="context_zenDeleteWorkspace" data-l10n-id="zen-workspaces-panel-context-delete" command="cmd_zenCtxDeleteWorkspace"/>

View File

@@ -129,6 +129,10 @@ document.addEventListener(
gZenWorkspaces.unloadWorkspace();
break;
}
case "cmd_zenUnloadAllOtherWorkspace": {
gZenWorkspaces.unloadAllOtherWorkspaces();
break;
}
case "cmd_zenNewNavigatorUnsynced":
OpenBrowserWindow({ zenSyncedWindow: false });
break;

View File

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

View File

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

View File

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