diff --git a/locales/en-US/browser/browser/preferences/zen-preferences.ftl b/locales/en-US/browser/browser/preferences/zen-preferences.ftl
index d053124e2..2941a5157 100644
--- a/locales/en-US/browser/browser/preferences/zen-preferences.ftl
+++ b/locales/en-US/browser/browser/preferences/zen-preferences.ftl
@@ -59,6 +59,12 @@ zen-tabs-unloader-enabled =
zen-tabs-close-on-back-with-no-history =
.label = Close tab and switch to its owner tab (or most recently used tab) when going back with no history
+zen-tabs-cycle-by-attribute =
+ .label = Ctrl+Tab cycles within Essential or Workspace tabs only
+zen-tabs-cycle-ignore-pending-tabs =
+ .label = Ignore Pending tabs when cycling with Ctrl+Tab
+zen-tabs-cycle-by-attribute-warning = Ctrl+Tab will cycle by recently used order, as it is enabled
+
zen-look-and-feel-compact-toolbar-themed =
.label = Use themed background for compact toolbar
@@ -150,7 +156,7 @@ zen-theme-marketplace-input-default-placeholder =
.placeholder = Type something...
pane-zen-marketplace-title = Zen Mods
zen-themes-auto-update =
- .label = Automatically update installed mods on startup
+ .label = Automatically update installed mods on startup
zen-settings-workspaces-force-container-tabs-to-workspace =
.label = Switch to workspace where container is set as default when opening container tabs
diff --git a/prefs/zen/zen.yaml b/prefs/zen/zen.yaml
index c44a0e626..9c6e9d787 100644
--- a/prefs/zen/zen.yaml
+++ b/prefs/zen/zen.yaml
@@ -37,3 +37,9 @@
- name: zen.tabs.close-on-back-with-no-history
value: true
+
+- name: zen.tabs.ctrl-tab.ignore-essential-tabs
+ value: false
+
+- name: zen.tabs.ctrl-tab.ignore-pending-tabs
+ value: false
diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js
index a126c8a38..35fbccbf7 100644
--- a/src/browser/components/preferences/zen-settings.js
+++ b/src/browser/components/preferences/zen-settings.js
@@ -707,13 +707,39 @@ var gZenWorkspacesSettings = {
}
},
};
+
+ let toggleZenCycleByAttrWarning = {
+ observe() {
+ const warning = document.getElementById('zenTabsCycleByAttributeWarning');
+ warning.hidden = !(
+ Services.prefs.getBoolPref('zen.tabs.ctrl-tab.ignore-essential-tabs', false) &&
+ Services.prefs.getBoolPref('browser.ctrlTab.sortByRecentlyUsed', false)
+ );
+ },
+ };
+
+ toggleZenCycleByAttrWarning.observe(); // call it once on initial load
+
Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs
Services.prefs.addObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
Services.prefs.addObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
+ Services.prefs.addObserver(
+ 'zen.tabs.ctrl-tab.ignore-essential-tabs',
+ toggleZenCycleByAttrWarning
+ );
+ Services.prefs.addObserver('browser.ctrlTab.sortByRecentlyUsed', toggleZenCycleByAttrWarning);
window.addEventListener('unload', () => {
Services.prefs.removeObserver('zen.glance.enabled', tabsUnloaderPrefListener);
Services.prefs.removeObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
Services.prefs.removeObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
+ Services.prefs.removeObserver(
+ 'zen.tabs.ctrl-tab.ignore-essential-tabs',
+ toggleZenCycleByAttrWarning
+ );
+ Services.prefs.removeObserver(
+ 'browser.ctrlTab.sortByRecentlyUsed',
+ toggleZenCycleByAttrWarning
+ );
});
},
};
@@ -1135,6 +1161,21 @@ Preferences.addAll([
type: 'bool',
default: true,
},
+ {
+ id: 'zen.tabs.ctrl-tab.ignore-essential-tabs',
+ type: 'bool',
+ default: false,
+ },
+ {
+ id: 'zen.tabs.ctrl-tab.ignore-pending-tabs',
+ type: 'bool',
+ default: false,
+ },
+ {
+ id: 'zen.tabs.close-on-back-with-no-history',
+ type: 'bool',
+ default: false,
+ },
]);
Preferences.addSetting({
diff --git a/src/browser/components/preferences/zenTabsManagement.inc.xhtml b/src/browser/components/preferences/zenTabsManagement.inc.xhtml
index 2943b863b..6effbd1bd 100644
--- a/src/browser/components/preferences/zenTabsManagement.inc.xhtml
+++ b/src/browser/components/preferences/zenTabsManagement.inc.xhtml
@@ -29,6 +29,14 @@
+
+
+
{
++ if (!tab.visible) {
++ return false
++ }
++ if (lazyZenPrefs.ignorePendingTabs && tab.hasAttribute("pending")) {
++ return false
++ }
++ if (!lazyZenPrefs.cycleByAttribute) {
++ return true
++ }
++ if (startTab.hasAttribute("zen-essential")) {
++ return tab.hasAttribute("zen-essential")
++ }
++ return !tab.hasAttribute("zen-essential")
++ }
++
+ // Handle keyboard navigation for a hidden tab that can be selected, like the Firefox View tab,
+ // which has a random placement in this.allTabs.
+ if (startTab.hidden) {
+@@ -991,7 +1026,7 @@
+ newTab = this.findNextTab(startTab, {
+ direction: aDir,
+ wrap: aWrap,
+- filter: tab => tab.visible,
++ filter: tabFilter,
+ });
+ }
+
diff --git a/src/zen/tests/tabs/browser.toml b/src/zen/tests/tabs/browser.toml
index 0d6efd5dd..eee3f05d4 100644
--- a/src/zen/tests/tabs/browser.toml
+++ b/src/zen/tests/tabs/browser.toml
@@ -9,8 +9,9 @@ support-files = [
["browser_tabs_empty_checks.js"]
["browser_tabs_fetch_checks.js"]
+["browser_tabs_cycle_by_attribute.js"]
["browser_drag_drop_vertical.js"]
tags = [
"drag-drop",
"vertical-tabs"
-]
\ No newline at end of file
+]
diff --git a/src/zen/tests/tabs/browser_tabs_cycle_by_attribute.js b/src/zen/tests/tabs/browser_tabs_cycle_by_attribute.js
new file mode 100644
index 000000000..3dba19ffa
--- /dev/null
+++ b/src/zen/tests/tabs/browser_tabs_cycle_by_attribute.js
@@ -0,0 +1,86 @@
+'use strict';
+
+const URL1 = 'data:text/plain,tab1';
+const URL2 = 'data:text/plain,tab2';
+const URL3 = 'data:text/plain,tab3';
+const URL4 = 'data:text/plain,tab4';
+const URL5 = 'data:text/plain,tab5';
+const URL6 = 'data:text/plain,tab6';
+
+/**
+ * ensures that tab select action is completed
+ */
+async function selectTab(tab) {
+ const onSelect = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, 'TabSelect');
+ gBrowser.selectedTab = tab;
+ await onSelect;
+}
+
+add_setup(async () => {
+ // remove default new tab
+ const tabToRemove = gBrowser.selectedTab;
+ BrowserTestUtils.removeTab(tabToRemove);
+
+ const tabs = await Promise.all([
+ addTabTo(gBrowser, URL1),
+ addTabTo(gBrowser, URL2),
+ addTabTo(gBrowser, URL3),
+ addTabTo(gBrowser, URL4),
+ addTabTo(gBrowser, URL5),
+ addTabTo(gBrowser, URL6),
+ ]);
+
+ gZenPinnedTabManager.addToEssentials(tabs.slice(0, 3));
+ await BrowserTestUtils.waitForCondition(
+ () => tabs.slice(0, 3).every((tab) => tab.hasAttribute('zen-essential')),
+ 'all essentials ready'
+ );
+
+ const essentialTabs = gBrowser.tabs.filter((tab) => tab.hasAttribute('zen-essential'));
+ Assert.equal(essentialTabs.length, 3, '3 essential tabs created');
+
+ const workspaceTabs = gBrowser.tabs.filter(
+ (tab) => !tab.hasAttribute('zen-essential') && !tab.hasAttribute('zen-empty-tab')
+ );
+ Assert.equal(workspaceTabs.length, 3, '3 workspace tabs created, excluding empty tab');
+
+ registerCleanupFunction(async () => {
+ // replace the default new tab in the test window
+ addTabTo(gBrowser, 'about:blank');
+ tabs.forEach((element) => {
+ BrowserTestUtils.removeTab(element);
+ });
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function cycleTabsByAttribute() {
+ await SpecialPowers.pushPrefEnv({
+ set: [['zen.tabs.ctrl-tab.ignore-essential-tabs', true]],
+ });
+
+ const essentialTabs = gBrowser.tabs.filter((tab) => tab.hasAttribute('zen-essential'));
+ await selectTab(essentialTabs[0]);
+
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ ok(
+ gBrowser.selectedTab === essentialTabs[0],
+ 'tab cycling applies within essential tabs only, as the starting tab is a essential tab'
+ );
+
+ const workspaceTabs = gBrowser.tabs.filter(
+ (tab) => !tab.hasAttribute('zen-essential') && !tab.hasAttribute('zen-empty-tab')
+ );
+ await selectTab(workspaceTabs[0]);
+
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+
+ ok(
+ gBrowser.selectedTab === workspaceTabs[0],
+ 'tab cycling applies within workspace tabs only, as the starting tab is a workspace tab'
+ );
+});