forked from mirrors/gecko-dev
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:
parent
ce44df6deb
commit
96c9460ed6
9 changed files with 228 additions and 19 deletions
|
|
@ -2228,6 +2228,13 @@ pref("privacy.exposeContentTitleInWindow.pbm", true);
|
|||
// Run media transport in a separate process?
|
||||
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
|
||||
// performance while using the async tab switcher.
|
||||
pref("browser.tabs.remote.warmup.enabled", true);
|
||||
|
|
|
|||
|
|
@ -9030,15 +9030,14 @@ var ConfirmationHint = {
|
|||
* - event (DOM event): The event that triggered the feedback
|
||||
* - descriptionId (string): message ID of the description text
|
||||
* - position (string): position of the panel relative to the anchor.
|
||||
*
|
||||
* - l10nArgs (object): l10n arguments for the messageId.
|
||||
*/
|
||||
show(anchor, messageId, options = {}) {
|
||||
this._reset();
|
||||
|
||||
MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
|
||||
MozXULElement.insertFTLIfNeeded("browser/confirmationHints.ftl");
|
||||
document.l10n.setAttributes(this._message, messageId);
|
||||
|
||||
document.l10n.setAttributes(this._message, messageId, options.l10nArgs);
|
||||
if (options.descriptionId) {
|
||||
document.l10n.setAttributes(this._description, options.descriptionId);
|
||||
this._description.hidden = false;
|
||||
|
|
|
|||
|
|
@ -77,13 +77,16 @@
|
|||
data-lazy-l10n-id="tab-context-close-n-tabs"
|
||||
data-l10n-args='{"tabCount": 1}'
|
||||
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"
|
||||
data-lazy-l10n-id="tab-context-close-multiple-tabs">
|
||||
<menupopup id="closeTabOptions">
|
||||
<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"
|
||||
oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab, {animate: true});"/>
|
||||
oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab);"/>
|
||||
<menuitem id="context_closeOtherTabs" data-lazy-l10n-id="close-other-tabs"
|
||||
oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/>
|
||||
</menupopup>
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@
|
|||
TO_START: 2,
|
||||
TO_END: 3,
|
||||
MULTI_SELECTED: 4,
|
||||
DUPLICATES: 6,
|
||||
},
|
||||
|
||||
_lastRelatedTabMap: new WeakMap(),
|
||||
|
|
@ -348,6 +349,46 @@
|
|||
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() {
|
||||
for (var i = 0; i < this.tabs.length; i++) {
|
||||
if (!this.tabs[i].pinned) {
|
||||
|
|
@ -3509,6 +3550,28 @@
|
|||
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
|
||||
* left of the leftmost selected tab will be removed.
|
||||
|
|
@ -7554,9 +7617,8 @@ var TabContextMenu = {
|
|||
tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
|
||||
contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
|
||||
|
||||
if (this.contextTab.hasAttribute("customizemode")) {
|
||||
document.getElementById("context_openTabInWindow").disabled = true;
|
||||
}
|
||||
document.getElementById("context_openTabInWindow").disabled =
|
||||
this.contextTab.hasAttribute("customizemode");
|
||||
|
||||
// Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
|
||||
document.getElementById("context_duplicateTab").hidden =
|
||||
|
|
@ -7590,6 +7652,17 @@ var TabContextMenu = {
|
|||
.getElementById("context_closeTab")
|
||||
.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
|
||||
document.getElementById("context_closeTabOptions").disabled =
|
||||
closeTabsToTheStartItem.disabled &&
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ support-files = [
|
|||
|
||||
["browser_multiselect_tabs_close.js"]
|
||||
|
||||
["browser_multiselect_tabs_close_duplicate_tabs.js"]
|
||||
|
||||
["browser_multiselect_tabs_close_other_tabs.js"]
|
||||
|
||||
["browser_multiselect_tabs_close_tabs_to_the_left.js"]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
@ -2,11 +2,6 @@
|
|||
* 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 remoteClientsFixture = [
|
||||
{ id: 1, name: "Foo" },
|
||||
{ id: 2, name: "Bar" },
|
||||
];
|
||||
|
||||
add_task(async function test() {
|
||||
// There should be one tab when we start the test
|
||||
let [origTab] = gBrowser.visibleTabs;
|
||||
|
|
@ -16,9 +11,8 @@ add_task(async function test() {
|
|||
|
||||
// Check the context menu with two tabs
|
||||
updateTabContextMenu(origTab);
|
||||
is(
|
||||
document.getElementById("context_closeTab").disabled,
|
||||
false,
|
||||
ok(
|
||||
!document.getElementById("context_closeTab").disabled,
|
||||
"Close Tab is enabled"
|
||||
);
|
||||
|
||||
|
|
@ -29,11 +23,14 @@ add_task(async function test() {
|
|||
|
||||
// Check the context menu with one tab.
|
||||
updateTabContextMenu(testTab);
|
||||
is(
|
||||
document.getElementById("context_closeTab").disabled,
|
||||
false,
|
||||
ok(
|
||||
!document.getElementById("context_closeTab").disabled,
|
||||
"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
|
||||
// So now there's one pinned tab, one visible unpinned tab, and one hidden tab
|
||||
|
|
|
|||
|
|
@ -18,3 +18,10 @@ confirmation-hint-send-to-device = Sent!
|
|||
confirmation-hint-firefox-relay-mask-created = New mask created!
|
||||
confirmation-hint-firefox-relay-mask-reused = Existing mask reused!
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ move-to-new-window =
|
|||
tab-context-close-multiple-tabs =
|
||||
.label = Close Multiple Tabs
|
||||
.accesskey = M
|
||||
tab-context-close-duplicate-tabs =
|
||||
.label = Close Duplicate Tabs
|
||||
.accesskey = u
|
||||
tab-context-share-url =
|
||||
.label = Share
|
||||
.accesskey = h
|
||||
|
|
|
|||
Loading…
Reference in a new issue