Skip to content

Commit 249d793

Browse files
junnjieemr-cheffy
andauthored
feat: Ctrl+Tab cycling to keep within essentials/regular tabs only, p=#11242
* feat: ctrl+tab cycles within essential/workspace tabs only * add test for ctrl+tab cycling by attribute * move settings UI related code to /src/zen, revert patches for main.inc.xhtml and main.js, use simple tab filter when current tab is hidden * chore: Cleanup and add extra pref, b=no-bug, c=tests, tabs --------- Co-authored-by: mr. m <[email protected]>
1 parent 8b64925 commit 249d793

File tree

7 files changed

+208
-8
lines changed

7 files changed

+208
-8
lines changed

locales/en-US/browser/browser/preferences/zen-preferences.ftl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ zen-tabs-unloader-enabled =
5959
zen-tabs-close-on-back-with-no-history =
6060
.label = Close tab and switch to its owner tab (or most recently used tab) when going back with no history
6161
62+
zen-tabs-cycle-by-attribute =
63+
.label = Ctrl+Tab cycles within Essential or Workspace tabs only
64+
zen-tabs-cycle-ignore-pending-tabs =
65+
.label = Ignore Pending tabs when cycling with Ctrl+Tab
66+
zen-tabs-cycle-by-attribute-warning = Ctrl+Tab will cycle by recently used order, as it is enabled
67+
6268
zen-look-and-feel-compact-toolbar-themed =
6369
.label = Use themed background for compact toolbar
6470
@@ -150,7 +156,7 @@ zen-theme-marketplace-input-default-placeholder =
150156
.placeholder = Type something...
151157
pane-zen-marketplace-title = Zen Mods
152158
zen-themes-auto-update =
153-
.label = Automatically update installed mods on startup
159+
.label = Automatically update installed mods on startup
154160
155161
zen-settings-workspaces-force-container-tabs-to-workspace =
156162
.label = Switch to workspace where container is set as default when opening container tabs

prefs/zen/zen.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@
3737

3838
- name: zen.tabs.close-on-back-with-no-history
3939
value: true
40+
41+
- name: zen.tabs.ctrl-tab.ignore-essential-tabs
42+
value: false
43+
44+
- name: zen.tabs.ctrl-tab.ignore-pending-tabs
45+
value: false

src/browser/components/preferences/zen-settings.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,13 +707,39 @@ var gZenWorkspacesSettings = {
707707
}
708708
},
709709
};
710+
711+
let toggleZenCycleByAttrWarning = {
712+
observe() {
713+
const warning = document.getElementById('zenTabsCycleByAttributeWarning');
714+
warning.hidden = !(
715+
Services.prefs.getBoolPref('zen.tabs.ctrl-tab.ignore-essential-tabs', false) &&
716+
Services.prefs.getBoolPref('browser.ctrlTab.sortByRecentlyUsed', false)
717+
);
718+
},
719+
};
720+
721+
toggleZenCycleByAttrWarning.observe(); // call it once on initial load
722+
710723
Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs
711724
Services.prefs.addObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
712725
Services.prefs.addObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
726+
Services.prefs.addObserver(
727+
'zen.tabs.ctrl-tab.ignore-essential-tabs',
728+
toggleZenCycleByAttrWarning
729+
);
730+
Services.prefs.addObserver('browser.ctrlTab.sortByRecentlyUsed', toggleZenCycleByAttrWarning);
713731
window.addEventListener('unload', () => {
714732
Services.prefs.removeObserver('zen.glance.enabled', tabsUnloaderPrefListener);
715733
Services.prefs.removeObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
716734
Services.prefs.removeObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
735+
Services.prefs.removeObserver(
736+
'zen.tabs.ctrl-tab.ignore-essential-tabs',
737+
toggleZenCycleByAttrWarning
738+
);
739+
Services.prefs.removeObserver(
740+
'browser.ctrlTab.sortByRecentlyUsed',
741+
toggleZenCycleByAttrWarning
742+
);
717743
});
718744
},
719745
};
@@ -1135,6 +1161,21 @@ Preferences.addAll([
11351161
type: 'bool',
11361162
default: true,
11371163
},
1164+
{
1165+
id: 'zen.tabs.ctrl-tab.ignore-essential-tabs',
1166+
type: 'bool',
1167+
default: false,
1168+
},
1169+
{
1170+
id: 'zen.tabs.ctrl-tab.ignore-pending-tabs',
1171+
type: 'bool',
1172+
default: false,
1173+
},
1174+
{
1175+
id: 'zen.tabs.close-on-back-with-no-history',
1176+
type: 'bool',
1177+
default: false,
1178+
},
11381179
]);
11391180

11401181
Preferences.addSetting({

src/browser/components/preferences/zenTabsManagement.inc.xhtml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
<checkbox id="zenTabsCloseOnBackWithNoHistory"
3030
data-l10n-id="zen-tabs-close-on-back-with-no-history"
3131
preference="zen.tabs.close-on-back-with-no-history"/>
32+
<checkbox data-l10n-id="zen-tabs-cycle-ignore-pending-tabs"
33+
preference="zen.tabs.ctrl-tab.ignore-pending-tabs"/>
34+
<checkbox data-l10n-id="zen-tabs-cycle-by-attribute"
35+
preference="zen.tabs.ctrl-tab.ignore-essential-tabs"/>
36+
<description id="zenTabsCycleByAttributeWarning"
37+
class="description-deemphasized"
38+
data-l10n-id="zen-tabs-cycle-by-attribute-warning"
39+
hidden="true"/>
3240
</groupbox>
3341

3442
<hbox id="zenTabsUnloadCategory"

src/toolkit/content/widgets/tabbox-js.patch

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js
2-
index cfe2da6e199667bd668f117cc8972212c7f57da2..bdd7dcc26139202e6e31afde47dc4d877f3db3c5 100644
2+
index cfe2da6e199667bd668f117cc8972212c7f57da2..470033466eae0e853855e21b86a0722627f9ed4b 100644
33
--- a/toolkit/content/widgets/tabbox.js
44
+++ b/toolkit/content/widgets/tabbox.js
5-
@@ -213,7 +213,7 @@
5+
@@ -11,6 +11,23 @@
6+
"resource://gre/modules/AppConstants.sys.mjs"
7+
);
8+
9+
+ const { XPCOMUtils } = ChromeUtils.importESModule(
10+
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
11+
+ );
12+
+ const lazyZenPrefs = {};
13+
+ XPCOMUtils.defineLazyPreferenceGetter(
14+
+ lazyZenPrefs,
15+
+ "cycleByAttribute",
16+
+ "zen.tabs.ctrl-tab.ignore-essential-tabs",
17+
+ false
18+
+ );
19+
+ XPCOMUtils.defineLazyPreferenceGetter(
20+
+ lazyZenPrefs,
21+
+ "ignorePendingTabs",
22+
+ "zen.tabs.ctrl-tab.ignore-pending-tabs",
23+
+ false
24+
+ );
25+
+
26+
let imports = {};
27+
ChromeUtils.defineESModuleGetters(imports, {
28+
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
29+
@@ -213,7 +230,7 @@
630
) {
731
this._inAsyncOperation = false;
832
if (oldPanel != this._selectedPanel) {
@@ -11,7 +35,7 @@ index cfe2da6e199667bd668f117cc8972212c7f57da2..bdd7dcc26139202e6e31afde47dc4d87
1135
this._selectedPanel?.classList.add("deck-selected");
1236
}
1337
this.setAttribute("selectedIndex", val);
14-
@@ -697,7 +697,7 @@
38+
@@ -697,7 +714,7 @@
1539
if (!tab) {
1640
return;
1741
}
@@ -20,15 +44,15 @@ index cfe2da6e199667bd668f117cc8972212c7f57da2..bdd7dcc26139202e6e31afde47dc4d87
2044
if (otherTab != tab && otherTab.selected) {
2145
otherTab._selected = false;
2246
}
23-
@@ -733,6 +733,7 @@
47+
@@ -733,6 +750,7 @@
2448
* @param {MozTab|null} [val]
2549
*/
2650
set selectedItem(val) {
2751
+ val = window.gZenGlanceManager?.getTabOrGlanceChild(val) || val;
2852
if (val && !val.selected) {
2953
// The selectedIndex setter ignores invalid values
3054
// such as -1 if |val| isn't one of our child nodes.
31-
@@ -910,7 +911,7 @@
55+
@@ -910,7 +928,7 @@
3256
if (tab == startTab) {
3357
return null;
3458
}
@@ -37,7 +61,7 @@ index cfe2da6e199667bd668f117cc8972212c7f57da2..bdd7dcc26139202e6e31afde47dc4d87
3761
return tab;
3862
}
3963
}
40-
@@ -972,10 +973,11 @@
64+
@@ -972,13 +990,30 @@
4165
* @param {boolean} [aWrap]
4266
*/
4367
advanceSelectedTab(aDir, aWrap) {
@@ -50,3 +74,31 @@ index cfe2da6e199667bd668f117cc8972212c7f57da2..bdd7dcc26139202e6e31afde47dc4d87
5074
}
5175
let newTab = null;
5276

77+
+ const tabFilter = tab => {
78+
+ if (!tab.visible) {
79+
+ return false
80+
+ }
81+
+ if (lazyZenPrefs.ignorePendingTabs && tab.hasAttribute("pending")) {
82+
+ return false
83+
+ }
84+
+ if (!lazyZenPrefs.cycleByAttribute) {
85+
+ return true
86+
+ }
87+
+ if (startTab.hasAttribute("zen-essential")) {
88+
+ return tab.hasAttribute("zen-essential")
89+
+ }
90+
+ return !tab.hasAttribute("zen-essential")
91+
+ }
92+
+
93+
// Handle keyboard navigation for a hidden tab that can be selected, like the Firefox View tab,
94+
// which has a random placement in this.allTabs.
95+
if (startTab.hidden) {
96+
@@ -991,7 +1026,7 @@
97+
newTab = this.findNextTab(startTab, {
98+
direction: aDir,
99+
wrap: aWrap,
100+
- filter: tab => tab.visible,
101+
+ filter: tabFilter,
102+
});
103+
}
104+

src/zen/tests/tabs/browser.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ support-files = [
99

1010
["browser_tabs_empty_checks.js"]
1111
["browser_tabs_fetch_checks.js"]
12+
["browser_tabs_cycle_by_attribute.js"]
1213
["browser_drag_drop_vertical.js"]
1314
tags = [
1415
"drag-drop",
1516
"vertical-tabs"
16-
]
17+
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict';
2+
3+
const URL1 = 'data:text/plain,tab1';
4+
const URL2 = 'data:text/plain,tab2';
5+
const URL3 = 'data:text/plain,tab3';
6+
const URL4 = 'data:text/plain,tab4';
7+
const URL5 = 'data:text/plain,tab5';
8+
const URL6 = 'data:text/plain,tab6';
9+
10+
/**
11+
* ensures that tab select action is completed
12+
*/
13+
async function selectTab(tab) {
14+
const onSelect = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, 'TabSelect');
15+
gBrowser.selectedTab = tab;
16+
await onSelect;
17+
}
18+
19+
add_setup(async () => {
20+
// remove default new tab
21+
const tabToRemove = gBrowser.selectedTab;
22+
BrowserTestUtils.removeTab(tabToRemove);
23+
24+
const tabs = await Promise.all([
25+
addTabTo(gBrowser, URL1),
26+
addTabTo(gBrowser, URL2),
27+
addTabTo(gBrowser, URL3),
28+
addTabTo(gBrowser, URL4),
29+
addTabTo(gBrowser, URL5),
30+
addTabTo(gBrowser, URL6),
31+
]);
32+
33+
gZenPinnedTabManager.addToEssentials(tabs.slice(0, 3));
34+
await BrowserTestUtils.waitForCondition(
35+
() => tabs.slice(0, 3).every((tab) => tab.hasAttribute('zen-essential')),
36+
'all essentials ready'
37+
);
38+
39+
const essentialTabs = gBrowser.tabs.filter((tab) => tab.hasAttribute('zen-essential'));
40+
Assert.equal(essentialTabs.length, 3, '3 essential tabs created');
41+
42+
const workspaceTabs = gBrowser.tabs.filter(
43+
(tab) => !tab.hasAttribute('zen-essential') && !tab.hasAttribute('zen-empty-tab')
44+
);
45+
Assert.equal(workspaceTabs.length, 3, '3 workspace tabs created, excluding empty tab');
46+
47+
registerCleanupFunction(async () => {
48+
// replace the default new tab in the test window
49+
addTabTo(gBrowser, 'about:blank');
50+
tabs.forEach((element) => {
51+
BrowserTestUtils.removeTab(element);
52+
});
53+
await SpecialPowers.popPrefEnv();
54+
});
55+
});
56+
57+
add_task(async function cycleTabsByAttribute() {
58+
await SpecialPowers.pushPrefEnv({
59+
set: [['zen.tabs.ctrl-tab.ignore-essential-tabs', true]],
60+
});
61+
62+
const essentialTabs = gBrowser.tabs.filter((tab) => tab.hasAttribute('zen-essential'));
63+
await selectTab(essentialTabs[0]);
64+
65+
gBrowser.tabContainer.advanceSelectedTab(1, true);
66+
gBrowser.tabContainer.advanceSelectedTab(1, true);
67+
gBrowser.tabContainer.advanceSelectedTab(1, true);
68+
ok(
69+
gBrowser.selectedTab === essentialTabs[0],
70+
'tab cycling applies within essential tabs only, as the starting tab is a essential tab'
71+
);
72+
73+
const workspaceTabs = gBrowser.tabs.filter(
74+
(tab) => !tab.hasAttribute('zen-essential') && !tab.hasAttribute('zen-empty-tab')
75+
);
76+
await selectTab(workspaceTabs[0]);
77+
78+
gBrowser.tabContainer.advanceSelectedTab(1, true);
79+
gBrowser.tabContainer.advanceSelectedTab(1, true);
80+
gBrowser.tabContainer.advanceSelectedTab(1, true);
81+
82+
ok(
83+
gBrowser.selectedTab === workspaceTabs[0],
84+
'tab cycling applies within workspace tabs only, as the starting tab is a workspace tab'
85+
);
86+
});

0 commit comments

Comments
 (0)