From 55c079d4ba416e602f218d3851936ca388707062 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:06:09 +0100 Subject: [PATCH] feat: Add "Separate from pinned tab" when resetting pinned tab with CMD, p=#12710 --- .../browser/browser/zen-vertical-tabs.ftl | 1 + .../tabbrowser/content/tab-js.patch | 64 ++++++++- src/zen/tabs/ZenPinnedTabManager.mjs | 42 +++++- src/zen/tests/pinned/browser.toml | 2 + .../pinned/browser_pinned_reset_button.js | 130 ++++++++++++++++++ 5 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 src/zen/tests/pinned/browser_pinned_reset_button.js diff --git a/locales/en-US/browser/browser/zen-vertical-tabs.ftl b/locales/en-US/browser/browser/zen-vertical-tabs.ftl index 9f22dd01a..f54a0d3b0 100644 --- a/locales/en-US/browser/browser/zen-vertical-tabs.ftl +++ b/locales/en-US/browser/browser/zen-vertical-tabs.ftl @@ -46,5 +46,6 @@ tabbrowser-reset-pin-button = zen-tab-sublabel = { $tabSubtitle -> [zen-default-pinned] Back to pinned url + [zen-default-pinned-cmd] Separate from pinned tab *[other] { $tabSubtitle } } diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 10781b429..340265cd8 100644 --- a/src/browser/components/tabbrowser/content/tab-js.patch +++ b/src/browser/components/tabbrowser/content/tab-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js -index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb894fa1d4 100644 +index 836bee14d2b63604688ebe477a5d915a5e99b305..5f60aa3bedd4f80b887ea3e050fd86a21a6b280a 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -110,7 +110,26 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb } get splitview() { -@@ -489,6 +515,8 @@ +@@ -444,6 +470,10 @@ + : this; + gBrowser.warmupTab(tabToWarm); + ++ if (event.target.classList.contains("tab-reset-pin-button")) { ++ gZenPinnedTabManager.onResetPinButtonMouseOver(this, event); ++ } ++ + // If the previous target wasn't part of this tab then this is a mouseenter event. + if (!this.contains(event.relatedTarget)) { + this._mouseenter(); +@@ -455,6 +485,7 @@ + if (!this.contains(event.relatedTarget)) { + this._mouseleave(); + } ++ gZenPinnedTabManager.onResetPinButtonMouseOut(this); + } + + on_dragstart(event) { +@@ -489,6 +520,8 @@ this.style.MozUserFocus = "ignore"; } else if ( event.target.classList.contains("tab-close-button") || @@ -119,7 +138,7 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb event.target.classList.contains("tab-icon-overlay") || event.target.classList.contains("tab-audio-button") ) { -@@ -543,6 +571,10 @@ +@@ -543,16 +576,21 @@ this.style.MozUserFocus = ""; } @@ -130,7 +149,40 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb on_click(event) { if (event.button != 0) { return; -@@ -603,6 +635,14 @@ + } + +- if (event.getModifierState("Accel") || event.shiftKey) { ++ if (event.shiftKey) { + return; + } + + if ( ++ !event.getModifierState("Accel") && + gBrowser.multiSelectedTabsCount > 0 && + !event.target.classList.contains("tab-close-button") && + !event.target.classList.contains("tab-icon-overlay") && +@@ -564,8 +602,9 @@ + } + + if ( +- event.target.classList.contains("tab-icon-overlay") || +- event.target.classList.contains("tab-audio-button") ++ !event.getModifierState("Accel") && ++ (event.target.classList.contains("tab-icon-overlay") || ++ event.target.classList.contains("tab-audio-button")) + ) { + if (this.activeMediaBlocked) { + if (this.multiselected) { +@@ -583,7 +622,7 @@ + return; + } + +- if (event.target.classList.contains("tab-close-button")) { ++ if (!event.getModifierState("Accel") && event.target.classList.contains("tab-close-button")) { + if (this.multiselected) { + gBrowser.removeMultiSelectedTabs( + lazy.TabMetrics.userTriggeredContext( +@@ -603,6 +642,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -138,14 +190,14 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb + if (event.target.classList.contains("tab-reset-pin-button")) { + gZenPinnedTabManager._onTabResetPinButton(event, this, 'reset'); + gBrowser.tabContainer._blockDblClick = true; -+ } else if (event.target.classList.contains("tab-reset-button")) { ++ } else if (!event.getModifierState("Accel") && event.target.classList.contains("tab-reset-button")) { + gZenPinnedTabManager.onCloseTabShortcut(event, this); + gBrowser.tabContainer._blockDblClick = true; + } } on_dblclick(event) { -@@ -626,6 +666,8 @@ +@@ -626,6 +673,8 @@ animate: true, triggeringEvent: event, }); diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index e25742191..28dcb887b 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -115,7 +115,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { _onTabResetPinButton(event, tab) { event.stopPropagation(); - this._resetTabToStoredState(tab); + if (event.getModifierState("Accel")) { + let newTab = gBrowser.duplicateTab(tab, true); + newTab.addEventListener("SSTabRestored", () => { + this._resetTabToStoredState(tab); + }, { once: true }); + } else { + this._resetTabToStoredState(tab); + } gBrowser.selectedTab = tab; } @@ -170,6 +177,37 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } + _onAccelKeyChange(e) { + let tab = this._tabWithResetPinButtonHovered; + if (!tab) { + return; + } + 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); + window.addEventListener(nextEvent, handler, { once: true }); + } + + _setResetPinSublabel(tab, accelHeld) { + let label = tab.querySelector(".zen-tab-sublabel"); + document.l10n.setArgs(label, { + tabSubtitle: accelHeld ? "zen-default-pinned-cmd" : "zen-default-pinned", + }); + } + + onResetPinButtonMouseOver(tab, event) { + this._tabWithResetPinButtonHovered = tab; + this._onAccelKeyChange(event); + } + + onResetPinButtonMouseOut(tab) { + this._setResetPinSublabel(tab, false); + delete this._tabWithResetPinButtonHovered; + } + resetPinnedTab(tab) { if (!tab) { tab = TabContextMenu.contextTab; @@ -812,7 +850,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { tab.removeAttribute("zen-show-sublabel"); const label = tab.querySelector(".zen-tab-sublabel"); - window.document.l10n.setArgs(label, { + document.l10n.setArgs(label, { tabSubtitle: "zen-default-pinned", }); } diff --git a/src/zen/tests/pinned/browser.toml b/src/zen/tests/pinned/browser.toml index bd5d6e5bf..fb11e91bb 100644 --- a/src/zen/tests/pinned/browser.toml +++ b/src/zen/tests/pinned/browser.toml @@ -15,6 +15,8 @@ prefs = ["zen.workspaces.separate-essentials=false"] ["browser_pinned_nounload_reset.js"] +["browser_pinned_reset_button.js"] + ["browser_pinned_reset_noswitch.js"] ["browser_pinned_switch.js"] diff --git a/src/zen/tests/pinned/browser_pinned_reset_button.js b/src/zen/tests/pinned/browser_pinned_reset_button.js new file mode 100644 index 000000000..98b47878c --- /dev/null +++ b/src/zen/tests/pinned/browser_pinned_reset_button.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +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)); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, navigateTo); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, navigateTo); + return tab; +} + +add_task(async function test_ResetPinButton_SelectsTab() { + 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"); + + // Simulate clicking the reset pin button (without Accel key) + gZenPinnedTabManager._onTabResetPinButton( + { stopPropagation() {}, getModifierState() { return false; } }, + tab + ); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + 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"); + + gBrowser.removeTab(otherTab); + gBrowser.removeTab(tab); +}); + +add_task(async function test_ResetPinButton_CmdClick_DuplicatesAndResets() { + const originalUrl = "https://example.com/1"; + const navigatedUrl = "https://example.com/2"; + const tab = await pinAndNavigateTab(originalUrl, navigatedUrl); + const tabCountBefore = gBrowser.tabs.length; + + // Simulate CMD+click on the reset pin button + gZenPinnedTabManager._onTabResetPinButton( + { stopPropagation() {}, getModifierState() { return true; } }, + tab + ); + + // Wait for the duplicate tab to be restored + 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)); + + Assert.equal(gBrowser.tabs.length, tabCountBefore + 1, "A new tab should be created from the duplicate"); + Assert.equal( + newTab.linkedBrowser.currentURI.spec, + navigatedUrl, + "The duplicated tab should have the navigated URL" + ); + 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.equal( + tab.linkedBrowser.currentURI.spec, + originalUrl, + "The pinned tab should be reset to the original URL" + ); + + gBrowser.removeTab(newTab); + gBrowser.removeTab(tab); +}); + +add_task(async function test_Hover_SublabelChangesWithAccelKey() { + const tab = await pinAndNavigateTab("https://example.com/1", "https://example.com/2"); + + // Track calls to document.l10n.setArgs to verify sublabel updates + const sublabelArgs = []; + const label = tab.querySelector(".zen-tab-sublabel"); + const origSetArgs = document.l10n.setArgs; + document.l10n.setArgs = (el, args) => { + if (el === label) { + sublabelArgs.push(args.tabSubtitle); + } + origSetArgs.call(document.l10n, el, args); + }; + + try { + // Simulate hovering with no modifier key held + gZenPinnedTabManager.onResetPinButtonMouseOver(tab, { + getModifierState() { return false; }, + metaKey: false, + type: "mouseover", + }); + + 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; }, + metaKey: true, + type: "keydown", + }); + + 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; }, + metaKey: false, + type: "keyup", + }); + + 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"); + } finally { + document.l10n.setArgs = origSetArgs; + } + + gBrowser.removeTab(tab); +});