Bug 1458013 - Add ability to select a range of tabs using Shift. r?jaws
MozReview-Commit-ID: DQxhkTEyRyq
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -131,16 +131,18 @@ window._gBrowser = {
"userTypedValue", "userTypedClear",
"didStartLoadSinceLastUserTyping", "audioMuted"
],
_removingTabs: [],
_multiSelectedTabsMap: new WeakMap(),
+ _lastMultiSelectedTabRef: null,
+
/**
* Tab close requests are ignored if the window is closing anyway,
* e.g. when holding Ctrl+W.
*/
_windowIsClosing: false,
/**
* This defines a proxy which allows us to access browsers by
@@ -3612,16 +3614,42 @@ window._gBrowser = {
if (aTab.multiselected) {
return;
}
aTab.setAttribute("multiselected", "true");
this._multiSelectedTabsMap.set(aTab, null);
},
+ /**
+ * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
+ */
+ addRangeToMultiSelectedTabs(aTab1, aTab2) {
+ // Let's avoid going through all the heavy process below when the same
+ // tab is given as params.
+ if (aTab1 == aTab2) {
+ this.addToMultiSelectedTabs(aTab1);
+ return;
+ }
+
+ const tabs = [...this.tabs];
+ const indexOfTab1 = tabs.indexOf(aTab1);
+ const indexOfTab2 = tabs.indexOf(aTab2);
+
+ const [lowerIndex, higherIndex] = indexOfTab1 < indexOfTab2 ?
+ [indexOfTab1, indexOfTab2] : [indexOfTab2, indexOfTab1];
+
+ for (let i = lowerIndex; i <= higherIndex; i++) {
+ let tab = tabs[i];
+ if (!tab.hidden) {
+ this.addToMultiSelectedTabs(tab);
+ }
+ }
+ },
+
removeFromMultiSelectedTabs(aTab) {
if (!aTab.multiselected) {
return;
}
aTab.removeAttribute("multiselected");
this._multiSelectedTabsMap.delete(aTab);
},
@@ -3630,22 +3658,34 @@ window._gBrowser = {
for (let tab of selectedTabs) {
if (tab.isConnected && tab.multiselected) {
tab.removeAttribute("multiselected");
}
}
this._multiSelectedTabsMap = new WeakMap();
},
- multiSelectedTabsCount() {
+ get multiSelectedTabsCount() {
return ChromeUtils.nondeterministicGetWeakMapKeys(this._multiSelectedTabsMap)
.filter(tab => tab.isConnected)
.length;
},
+ get lastMultiSelectedTab() {
+ let tab = this._lastMultiSelectedTabRef ? this._lastMultiSelectedTabRef.get() : null;
+ if (tab && tab.isConnected && this._multiSelectedTabsMap.has(tab)) {
+ return tab;
+ }
+ return gBrowser.selectedTab;
+ },
+
+ set lastMultiSelectedTab(aTab) {
+ this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
+ },
+
activateBrowserForPrintPreview(aBrowser) {
this._printPreviewBrowsers.add(aBrowser);
if (this._switcher) {
this._switcher.activateBrowserForPrintPreview(aBrowser);
}
aBrowser.docShellIsActive = true;
},
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1962,40 +1962,48 @@
}
if (this.selected) {
this.style.MozUserFocus = "ignore";
} else {
// When browser.tabs.multiselect config is set to false,
// then we ignore the state of multi-selection keys (Ctrl/Cmd).
const tabSelectionToggled = Services.prefs.getBoolPref("browser.tabs.multiselect") &&
- event.getModifierState("Accel");
+ (event.getModifierState("Accel") || event.shiftKey);
if (this.mOverCloseButton || this._overPlayingIcon || tabSelectionToggled) {
// Prevent tabbox.xml from selecting the tab.
event.stopPropagation();
}
}
]]>
</handler>
<handler event="mouseup">
this.style.MozUserFocus = "";
</handler>
<handler event="click" button="0"><![CDATA[
if (Services.prefs.getBoolPref("browser.tabs.multiselect")) {
- const tabSelectionToggled = event.getModifierState("Accel");
- if (tabSelectionToggled) {
+ if (event.shiftKey) {
+ const lastSelectedTab = gBrowser.lastMultiSelectedTab || gBrowser.selectedTab;
+ gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
+ gBrowser.lastMultiSelectedTab = this;
+ return;
+ }
+ if (event.getModifierState("Accel")) {
+ // Ctrl (Cmd for mac) key is pressed
if (this.multiselected) {
gBrowser.removeFromMultiSelectedTabs(this);
} else {
gBrowser.addToMultiSelectedTabs(this);
+ gBrowser.lastMultiSelectedTab = this;
}
return;
- } else if (gBrowser.multiSelectedTabsCount() > 0) {
+ }
+ if (gBrowser.multiSelectedTabsCount > 0) {
// Tabs were previously multi-selected and user clicks on a tab
// without holding Ctrl/Cmd Key
gBrowser.clearMultiSelectedTabs();
}
}
if (this._overPlayingIcon) {
this.toggleMuteAudio();
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -38,8 +38,9 @@ skip-if = (debug && os == 'mac') || (deb
[browser_tabReorder_overflow.js]
[browser_tabswitch_updatecommands.js]
[browser_viewsource_of_data_URI_in_file_process.js]
[browser_visibleTabs_bookmarkAllTabs.js]
[browser_visibleTabs_contextMenu.js]
[browser_open_newtab_start_observer_notification.js]
[browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
[browser_multiselect_tabs_using_Ctrl.js]
+[browser_multiselect_tabs_using_Shift.js]
--- a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js
@@ -1,26 +1,10 @@
const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
-function triggerClickOn(target, options) {
- let promise = BrowserTestUtils.waitForEvent(target, "click");
- if (AppConstants.platform == "macosx") {
- options = { metaKey: options.ctrlKey };
- }
- EventUtils.synthesizeMouseAtCenter(target, options);
- return promise;
-}
-
-async function addTab() {
- const tab = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/", {skipAnimation: true});
- const browser = gBrowser.getBrowserForTab(tab);
- await BrowserTestUtils.browserLoaded(browser);
- return tab;
-}
-
add_task(async function clickWithoutPrefSet() {
let tab = await addTab();
let mSelectedTabs = gBrowser._multiSelectedTabsMap;
isnot(gBrowser.selectedTab, tab, "Tab doesn't have focus");
// We make sure that the tab-switch is completely done before executing checks
await BrowserTestUtils.switchTab(gBrowser, () => {
@@ -50,46 +34,46 @@ add_task(async function clickWithPrefSet
const tab = await addTab();
await triggerClickOn(tab, { ctrlKey: true });
ok(tab.multiselected && mSelectedTabs.has(tab), "Tab should be (multi) selected after click");
isnot(gBrowser.selectedTab, tab, "Multi-selected tab is not focused");
is(gBrowser.selectedTab, initialFocusedTab, "Focused tab doesn't change");
await triggerClickOn(tab, { ctrlKey: true });
- ok(!tab.multiselected && !mSelectedTabs.has(tab), "Tab is not selected anymore");
+ ok(!tab.multiselected && !mSelectedTabs.has(tab), "Tab is not (multi) selected anymore");
is(gBrowser.selectedTab, initialFocusedTab, "Focused tab still doesn't change");
BrowserTestUtils.removeTab(tab);
});
add_task(async function clearSelection() {
await SpecialPowers.pushPrefEnv({
set: [
[PREF_MULTISELECT_TABS, true]
]
});
const tab1 = await addTab();
const tab2 = await addTab();
const tab3 = await addTab();
- info("We select tab1 and tab2 with ctrl key down");
+ info("We multi-select tab1 and tab2 with ctrl key down");
await triggerClickOn(tab1, { ctrlKey: true });
await triggerClickOn(tab2, { ctrlKey: true });
ok(tab1.multiselected && gBrowser._multiSelectedTabsMap.has(tab1), "Tab1 is (multi) selected");
ok(tab2.multiselected && gBrowser._multiSelectedTabsMap.has(tab2), "Tab2 is (multi) selected");
- is(gBrowser.multiSelectedTabsCount(), 2, "Two tabs selected");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two tabs (multi) selected");
isnot(tab3, gBrowser.selectedTab, "Tab3 doesn't have focus");
info("We select tab3 with Ctrl key up");
await triggerClickOn(tab3, { ctrlKey: false });
- ok(!tab1.multiselected, "Tab1 is unselected");
- ok(!tab2.multiselected, "Tab2 is unselected");
- is(gBrowser.multiSelectedTabsCount(), 0, "Selection is cleared");
+ ok(!tab1.multiselected, "Tab1 is not (multi) selected");
+ ok(!tab2.multiselected, "Tab2 is not (multi) selected");
+ is(gBrowser.multiSelectedTabsCount, 0, "Multi-selection is cleared");
is(tab3, gBrowser.selectedTab, "Tab3 has focus");
BrowserTestUtils.removeTab(tab1);
BrowserTestUtils.removeTab(tab2);
BrowserTestUtils.removeTab(tab3);
});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js
@@ -0,0 +1,125 @@
+const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+
+add_task(async function prefNotSet() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+
+ let mSelectedTabs = gBrowser._multiSelectedTabsMap;
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is mutli-selected");
+
+ info("Click on tab3 while holding shift key");
+ await BrowserTestUtils.switchTab(gBrowser, () => {
+ triggerClickOn(tab3, { shiftKey: true });
+ });
+
+ ok(!tab1.multiselected && !mSelectedTabs.has(tab1), "Tab1 is not multi-selected");
+ ok(!tab2.multiselected && !mSelectedTabs.has(tab2), "Tab2 is not multi-selected");
+ ok(!tab3.multiselected && !mSelectedTabs.has(tab3), "Tab3 is not multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 0, "There is still no multi-selected tab");
+ is(gBrowser.selectedTab, tab3, "Tab3 has focus now");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_MULTISELECT_TABS, true]
+ ]
+ });
+});
+
+add_task(async function noItemsInTheCollectionBeforeShiftClicking() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+ let mSelectedTabs = gBrowser._multiSelectedTabsMap;
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+ gBrowser.hideTab(tab3);
+ ok(tab3.hidden, "Tab3 is hidden");
+
+ info("Click on tab4 while holding shift key");
+ await triggerClickOn(tab4, { shiftKey: true });
+
+ ok(tab1.multiselected && mSelectedTabs.has(tab1), "Tab1 is multi-selected");
+ ok(tab2.multiselected && mSelectedTabs.has(tab2), "Tab2 is multi-selected");
+ ok(!tab3.multiselected && !mSelectedTabs.has(tab3), "Hidden tab3 is not multi-selected");
+ ok(tab4.multiselected && mSelectedTabs.has(tab4), "Tab4 is multi-selected");
+ ok(!tab5.multiselected && !mSelectedTabs.has(tab5), "Tab5 is not multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 3, "three multi-selected tabs");
+ is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+ BrowserTestUtils.removeTab(tab5);
+});
+
+add_task(async function itemsInTheCollectionBeforeShiftClicking() {
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+ let tab5 = await addTab();
+
+ let mSelectedTabs = gBrowser._multiSelectedTabsMap;
+
+ await BrowserTestUtils.switchTab(gBrowser, () => triggerClickOn(tab1, {}));
+
+ is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+ await triggerClickOn(tab3, { ctrlKey: true });
+ is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+ is(gBrowser.multiSelectedTabsCount, 1, "One tab is multi-selected");
+ ok(tab3.multiselected && mSelectedTabs.has(tab3), "Tab3 is multi-selected");
+
+ info("Click on tab5 while holding Shift key");
+ await triggerClickOn(tab5, { shiftKey: true });
+
+ is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+ ok(!tab1.multiselected && !mSelectedTabs.has(tab1), "Tab1 is not multi-selected");
+ ok(!tab2.multiselected && !mSelectedTabs.has(tab2), "Tab2 is not multi-selected ");
+ ok(tab3.multiselected && mSelectedTabs.has(tab3), "Tab3 is multi-selected");
+ ok(tab4.multiselected && mSelectedTabs.has(tab4), "Tab4 is multi-selected");
+ ok(tab5.multiselected && mSelectedTabs.has(tab5), "Tab5 is multi-selected");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+ BrowserTestUtils.removeTab(tab5);
+});
+
+add_task(async function shiftHasHigherPrecOverCtrl() {
+ const tab1 = await addTab();
+ const tab2 = await addTab();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+ info("Click on tab2 with both Ctrl/Cmd and Shift down");
+ await triggerClickOn(tab2, { ctrlKey: true, shiftKey: true });
+
+ is(gBrowser.multiSelectedTabsCount, 2, "Both tab1 and tab2 are multi-selected");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
--- a/browser/base/content/test/tabs/head.js
+++ b/browser/base/content/test/tabs/head.js
@@ -4,8 +4,27 @@ function updateTabContextMenu(tab) {
tab = gBrowser.selectedTab;
var evt = new Event("");
tab.dispatchEvent(evt);
menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
menu.hidePopup();
}
+function triggerClickOn(target, options) {
+ let promise = BrowserTestUtils.waitForEvent(target, "click");
+ if (AppConstants.platform == "macosx") {
+ options = {
+ metaKey: options.ctrlKey,
+ shiftKey: options.shiftKey
+ };
+ }
+ EventUtils.synthesizeMouseAtCenter(target, options);
+ return promise;
+}
+
+async function addTab() {
+ const tab = BrowserTestUtils.addTab(gBrowser,
+ "http://mochi.test:8888/", { skipAnimation: true });
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return tab;
+}