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?
|
// 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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue