Bug 1891797 - Close duplicate tabs from the context menu. r=tabbrowser-reviewers,fluent-reviewers,dao,flod

Differential Revision: https://phabricator.services.mozilla.com/D207633
This commit is contained in:
Emilio Cobos Álvarez 2024-04-19 13:31:38 +00:00
parent ce44df6deb
commit 96c9460ed6
9 changed files with 228 additions and 19 deletions

View file

@ -2228,6 +2228,13 @@ pref("privacy.exposeContentTitleInWindow.pbm", true);
// Run media transport in a separate process? // Run media transport in a separate process?
pref("media.peerconnection.mtransport_process", true); pref("media.peerconnection.mtransport_process", true);
// Whether the "Close duplicate tabs" tab context menu is enabled.
#ifdef NIGHTLY_BUILD
pref("browser.tabs.context.close-duplicate.enabled", true);
#else
pref("browser.tabs.context.close-duplicate.enabled", false);
#endif
// For speculatively warming up tabs to improve perceived // For speculatively warming up tabs to improve perceived
// performance while using the async tab switcher. // performance while using the async tab switcher.
pref("browser.tabs.remote.warmup.enabled", true); pref("browser.tabs.remote.warmup.enabled", true);

View file

@ -9030,15 +9030,14 @@ var ConfirmationHint = {
* - event (DOM event): The event that triggered the feedback * - event (DOM event): The event that triggered the feedback
* - descriptionId (string): message ID of the description text * - descriptionId (string): message ID of the description text
* - position (string): position of the panel relative to the anchor. * - position (string): position of the panel relative to the anchor.
* * - l10nArgs (object): l10n arguments for the messageId.
*/ */
show(anchor, messageId, options = {}) { show(anchor, messageId, options = {}) {
this._reset(); this._reset();
MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl"); MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
MozXULElement.insertFTLIfNeeded("browser/confirmationHints.ftl"); MozXULElement.insertFTLIfNeeded("browser/confirmationHints.ftl");
document.l10n.setAttributes(this._message, messageId); document.l10n.setAttributes(this._message, messageId, options.l10nArgs);
if (options.descriptionId) { if (options.descriptionId) {
document.l10n.setAttributes(this._description, options.descriptionId); document.l10n.setAttributes(this._description, options.descriptionId);
this._description.hidden = false; this._description.hidden = false;

View file

@ -77,13 +77,16 @@
data-lazy-l10n-id="tab-context-close-n-tabs" data-lazy-l10n-id="tab-context-close-n-tabs"
data-l10n-args='{"tabCount": 1}' data-l10n-args='{"tabCount": 1}'
oncommand="TabContextMenu.closeContextTabs();"/> oncommand="TabContextMenu.closeContextTabs();"/>
<menuitem id="context_closeDuplicateTabs"
data-lazy-l10n-id="tab-context-close-duplicate-tabs"
oncommand="gBrowser.removeDuplicateTabs(TabContextMenu.contextTab);"/>
<menu id="context_closeTabOptions" <menu id="context_closeTabOptions"
data-lazy-l10n-id="tab-context-close-multiple-tabs"> data-lazy-l10n-id="tab-context-close-multiple-tabs">
<menupopup id="closeTabOptions"> <menupopup id="closeTabOptions">
<menuitem id="context_closeTabsToTheStart" data-lazy-l10n-id="close-tabs-to-the-start" <menuitem id="context_closeTabsToTheStart" data-lazy-l10n-id="close-tabs-to-the-start"
oncommand="gBrowser.removeTabsToTheStartFrom(TabContextMenu.contextTab, {animate: true});"/> oncommand="gBrowser.removeTabsToTheStartFrom(TabContextMenu.contextTab);"/>
<menuitem id="context_closeTabsToTheEnd" data-lazy-l10n-id="close-tabs-to-the-end" <menuitem id="context_closeTabsToTheEnd" data-lazy-l10n-id="close-tabs-to-the-end"
oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab, {animate: true});"/> oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab);"/>
<menuitem id="context_closeOtherTabs" data-lazy-l10n-id="close-other-tabs" <menuitem id="context_closeOtherTabs" data-lazy-l10n-id="close-other-tabs"
oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/> oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/>
</menupopup> </menupopup>

View file

@ -161,6 +161,7 @@
TO_START: 2, TO_START: 2,
TO_END: 3, TO_END: 3,
MULTI_SELECTED: 4, MULTI_SELECTED: 4,
DUPLICATES: 6,
}, },
_lastRelatedTabMap: new WeakMap(), _lastRelatedTabMap: new WeakMap(),
@ -348,6 +349,46 @@
return this.tabContainer._getVisibleTabs(); return this.tabContainer._getVisibleTabs();
}, },
getDuplicateTabsToClose(aTab) {
// One would think that a set is better, but it would need to copy all
// the strings instead of just keeping references to the nsIURI objects,
// and the array is presumed to be small anyways.
let urisToLookFor = [];
if (aTab.multiselected) {
for (let tab of this.selectedTabs) {
let uri = tab.linkedBrowser?.currentURI;
if (uri) {
urisToLookFor.push(uri);
}
}
} else {
let uri = aTab.linkedBrowser?.currentURI;
if (uri) {
urisToLookFor.push(uri);
}
}
if (!urisToLookFor.length) {
return [];
}
let duplicateTabs = [];
for (let tab of this.tabs) {
if (tab == aTab || tab.pinned) {
continue;
}
if (aTab.multiselected && tab.multiselected) {
continue;
}
let uri = tab.linkedBrowser?.currentURI;
if (uri && urisToLookFor.some(u => u.equals(uri))) {
duplicateTabs.push(tab);
}
}
return duplicateTabs;
},
get _numPinnedTabs() { get _numPinnedTabs() {
for (var i = 0; i < this.tabs.length; i++) { for (var i = 0; i < this.tabs.length; i++) {
if (!this.tabs[i].pinned) { if (!this.tabs[i].pinned) {
@ -3509,6 +3550,28 @@
return tabsToEnd; return tabsToEnd;
}, },
removeDuplicateTabs(aTab) {
let tabs = this.getDuplicateTabsToClose(aTab);
if (!tabs.length) {
return;
}
if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.DUPLICATES)
) {
return;
}
this.removeTabs(tabs);
if (tabs.length) {
ConfirmationHint.show(aTab, "confirmation-hint-duplicate-tabs-closed", {
l10nArgs: {
tabCount: tabs.length,
},
});
}
},
/** /**
* In a multi-select context, the tabs (except pinned tabs) that are located to the * In a multi-select context, the tabs (except pinned tabs) that are located to the
* left of the leftmost selected tab will be removed. * left of the leftmost selected tab will be removed.
@ -7554,9 +7617,8 @@ var TabContextMenu = {
tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs]; tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent; contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
if (this.contextTab.hasAttribute("customizemode")) { document.getElementById("context_openTabInWindow").disabled =
document.getElementById("context_openTabInWindow").disabled = true; this.contextTab.hasAttribute("customizemode");
}
// Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible. // Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
document.getElementById("context_duplicateTab").hidden = document.getElementById("context_duplicateTab").hidden =
@ -7590,6 +7652,17 @@ var TabContextMenu = {
.getElementById("context_closeTab") .getElementById("context_closeTab")
.setAttribute("data-l10n-args", tabCountInfo); .setAttribute("data-l10n-args", tabCountInfo);
let closeDuplicateEnabled = Services.prefs.getBoolPref(
"browser.tabs.context.close-duplicate.enabled"
);
let closeDuplicateTabsItem = document.getElementById(
"context_closeDuplicateTabs"
);
closeDuplicateTabsItem.hidden = !closeDuplicateEnabled;
closeDuplicateTabsItem.disabled =
!closeDuplicateEnabled ||
!gBrowser.getDuplicateTabsToClose(this.contextTab).length;
// Disable "Close Multiple Tabs" if all sub menuitems are disabled // Disable "Close Multiple Tabs" if all sub menuitems are disabled
document.getElementById("context_closeTabOptions").disabled = document.getElementById("context_closeTabOptions").disabled =
closeTabsToTheStartItem.disabled && closeTabsToTheStartItem.disabled &&

View file

@ -142,6 +142,8 @@ support-files = [
["browser_multiselect_tabs_close.js"] ["browser_multiselect_tabs_close.js"]
["browser_multiselect_tabs_close_duplicate_tabs.js"]
["browser_multiselect_tabs_close_other_tabs.js"] ["browser_multiselect_tabs_close_other_tabs.js"]
["browser_multiselect_tabs_close_tabs_to_the_left.js"] ["browser_multiselect_tabs_close_tabs_to_the_left.js"]

View file

@ -0,0 +1,118 @@
const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
add_task(async function setPref() {
await SpecialPowers.pushPrefEnv({
set: [[PREF_WARN_ON_CLOSE, false]],
});
});
add_task(async function withAMultiSelectedTab() {
let initialTab = gBrowser.selectedTab;
let tab1 = await addTab();
let tab2 = await addTab();
let tab3 = await addTab();
let tab4 = await addTab();
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
await triggerClickOn(tab1, { ctrlKey: true });
let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
gBrowser.pinTab(tab4);
await tab4Pinned;
ok(initialTab.multiselected, "InitialTab is multiselected");
ok(tab1.multiselected, "Tab1 is multiselected");
ok(!tab2.multiselected, "Tab2 is not multiselected");
ok(!tab3.multiselected, "Tab3 is not multiselected");
ok(!tab4.multiselected, "Tab4 is not multiselected");
ok(tab4.pinned, "Tab4 is pinned");
is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab");
let closingTabs = [tab2, tab3];
let tabClosingPromises = [];
for (let tab of closingTabs) {
tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
}
gBrowser.removeDuplicateTabs(tab1);
await Promise.all(tabClosingPromises);
ok(!initialTab.closing, "InitialTab is not closing");
ok(!tab1.closing, "Tab1 is not closing");
ok(tab2.closing, "Tab2 is closing");
ok(tab3.closing, "Tab3 is closing");
ok(!tab4.closing, "Tab4 is not closing");
is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
is(gBrowser.selectedTab, initialTab, "InitialTab is still the active tab");
gBrowser.clearMultiSelectedTabs();
BrowserTestUtils.removeTab(tab1);
BrowserTestUtils.removeTab(tab4);
});
add_task(async function withNotAMultiSelectedTab() {
let initialTab = gBrowser.selectedTab;
let tab1 = await addTab();
let tab2 = await addTab();
let tab3 = await addTab();
let tab4 = await addTab();
let tab5 = await addTab();
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
await BrowserTestUtils.switchTab(gBrowser, tab1);
await triggerClickOn(tab2, { ctrlKey: true });
await triggerClickOn(tab5, { ctrlKey: true });
let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
gBrowser.pinTab(tab4);
await tab4Pinned;
let tab5Pinned = BrowserTestUtils.waitForEvent(tab5, "TabPinned");
gBrowser.pinTab(tab5);
await tab5Pinned;
ok(!initialTab.multiselected, "InitialTab is not multiselected");
ok(tab1.multiselected, "Tab1 is multiselected");
ok(tab2.multiselected, "Tab2 is multiselected");
ok(!tab3.multiselected, "Tab3 is not multiselected");
ok(!tab4.multiselected, "Tab4 is not multiselected");
ok(tab4.pinned, "Tab4 is pinned");
ok(tab5.multiselected, "Tab5 is multiselected");
ok(tab5.pinned, "Tab5 is pinned");
is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
is(gBrowser.selectedTab, tab1, "Tab1 is the active tab");
let closingTabs = [tab1, tab2];
let tabClosingPromises = [];
for (let tab of closingTabs) {
tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
}
await BrowserTestUtils.switchTab(
gBrowser,
gBrowser.removeDuplicateTabs(tab3)
);
await Promise.all(tabClosingPromises);
ok(!initialTab.closing, "InitialTab is not closing");
ok(tab1.closing, "Tab1 is closing");
ok(tab2.closing, "Tab2 is closing");
ok(!tab3.closing, "Tab3 is not closing");
ok(!tab4.closing, "Tab4 is not closing");
ok(!tab5.closing, "Tab5 is not closing");
is(
gBrowser.multiSelectedTabsCount,
0,
"Zero multiselected tabs, selection is cleared"
);
is(gBrowser.selectedTab, tab3, "tab3 is the active tab now");
for (let tab of [tab3, tab4, tab5]) {
BrowserTestUtils.removeTab(tab);
}
});

View file

@ -2,11 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const remoteClientsFixture = [
{ id: 1, name: "Foo" },
{ id: 2, name: "Bar" },
];
add_task(async function test() { add_task(async function test() {
// There should be one tab when we start the test // There should be one tab when we start the test
let [origTab] = gBrowser.visibleTabs; let [origTab] = gBrowser.visibleTabs;
@ -16,9 +11,8 @@ add_task(async function test() {
// Check the context menu with two tabs // Check the context menu with two tabs
updateTabContextMenu(origTab); updateTabContextMenu(origTab);
is( ok(
document.getElementById("context_closeTab").disabled, !document.getElementById("context_closeTab").disabled,
false,
"Close Tab is enabled" "Close Tab is enabled"
); );
@ -29,11 +23,14 @@ add_task(async function test() {
// Check the context menu with one tab. // Check the context menu with one tab.
updateTabContextMenu(testTab); updateTabContextMenu(testTab);
is( ok(
document.getElementById("context_closeTab").disabled, !document.getElementById("context_closeTab").disabled,
false,
"Close Tab is enabled when more than one tab exists" "Close Tab is enabled when more than one tab exists"
); );
ok(
!document.getElementById("context_closeDuplicateTabs").disabled,
"Close duplicate tabs is enabled when more than one tab with the same URL exists"
);
// Add a tab that will get pinned // Add a tab that will get pinned
// So now there's one pinned tab, one visible unpinned tab, and one hidden tab // So now there's one pinned tab, one visible unpinned tab, and one hidden tab

View file

@ -18,3 +18,10 @@ confirmation-hint-send-to-device = Sent!
confirmation-hint-firefox-relay-mask-created = New mask created! confirmation-hint-firefox-relay-mask-created = New mask created!
confirmation-hint-firefox-relay-mask-reused = Existing mask reused! confirmation-hint-firefox-relay-mask-reused = Existing mask reused!
confirmation-hint-screenshot-copied = Screenshot copied! confirmation-hint-screenshot-copied = Screenshot copied!
# Variables:
# $tabCount (Number): The number of duplicate tabs closed, at least 1.
confirmation-hint-duplicate-tabs-closed =
{ $tabCount ->
[one] Closed { $tabCount } tab
*[other] Closed { $tabCount } tabs
}

View file

@ -72,6 +72,9 @@ move-to-new-window =
tab-context-close-multiple-tabs = tab-context-close-multiple-tabs =
.label = Close Multiple Tabs .label = Close Multiple Tabs
.accesskey = M .accesskey = M
tab-context-close-duplicate-tabs =
.label = Close Duplicate Tabs
.accesskey = u
tab-context-share-url = tab-context-share-url =
.label = Share .label = Share
.accesskey = h .accesskey = h