From 49035757e27c7eb52fb7622352b5bf1fdd234fe1 Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 9 Jul 2025 02:31:49 -0300 Subject: [PATCH 01/16] feat: add new shortcuts --- .../app/profile/features/workspaces.inc | 4 ++ .../base/content/zen-keysets.inc.xhtml | 4 ++ .../components/preferences/zen-settings.js | 9 ++++ src/zen/kbs/ZenKeyboardShortcuts.mjs | 42 +++++++++++++++++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/browser/app/profile/features/workspaces.inc b/src/browser/app/profile/features/workspaces.inc index b517c738ea..e0e6fe51aa 100644 --- a/src/browser/app/profile/features/workspaces.inc +++ b/src/browser/app/profile/features/workspaces.inc @@ -23,3 +23,7 @@ pref('zen.workspaces.separate-essentials', true); pref('zen.pinned-tab-manager.debug', false); pref('zen.pinned-tab-manager.restore-pinned-tabs-to-pinned-url', false); pref('zen.pinned-tab-manager.close-shortcut-behavior', 'reset-unload-switch'); + +# Section: Tab Navigation +pref('zen.tabs.unloaded-navigation-mode', 'always'); + diff --git a/src/browser/base/content/zen-keysets.inc.xhtml b/src/browser/base/content/zen-keysets.inc.xhtml index c12d9820e6..aaf8bc9937 100644 --- a/src/browser/base/content/zen-keysets.inc.xhtml +++ b/src/browser/base/content/zen-keysets.inc.xhtml @@ -49,6 +49,10 @@ + + + + diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 09bbf767f5..dbb5dab9ea 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -774,6 +774,10 @@ var zenMissingKeyboardShortcutL10n = { key_wrToggleCaptureSequenceCmd: 'zen-key-wr-toggle-capture-sequence-cmd', key_undoCloseWindow: 'zen-key-undo-close-window', + key_zenTabNext: 'zen-key-tab-next-shortcut', + key_zenTabPrevious: 'zen-key-tab-prev-shortcut', + key_toggleUnloadedCycling: 'zen-toggle-unloaded-cycling-shortcut', + key_selectTab1: 'zen-key-select-tab-1', key_selectTab2: 'zen-key-select-tab-2', key_selectTab3: 'zen-key-select-tab-3', @@ -1181,4 +1185,9 @@ Preferences.addAll([ type: 'bool', default: true, }, + { + id: 'zen.tabs.unloaded-navigation-mode', + type: 'string', + default: 'always', + }, ]); diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 9bc7145d4d..3941c57da0 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -984,6 +984,43 @@ class nsZenKeyboardShortcutsVersioner { } } } + if (version < 10) { + // Migrate from version 9 to 10 + // In this new version, we add customizable shortcuts for switching to the next/previous tab. + data.push( + new KeyShortcut( + 'zen-tab-next-shortcut', + '', + 'VK_TAB', + 'windowAndTabManagement', + nsKeyShortcutModifiers.fromObject({ accel: true }), + 'cmd_zenTabNext', + 'zen-tab-next-shortcut' + ) + ); + data.push( + new KeyShortcut( + 'zen-tab-prev-shortcut', + '', + 'VK_TAB', + 'windowAndTabManagement', + nsKeyShortcutModifiers.fromObject({ accel: true, shift: true }), + 'cmd_zenTabPrev', + 'zen-tab-prev-shortcut' + ) + ); + data.push( + new KeyShortcut( + 'zen-toggle-unloaded-cycling-shortcut', + '', + '', + 'windowAndTabManagement', + nsKeyShortcutModifiers.fromObject({ alt: true }), + 'cmd_zenToggleUnloadedCycling', + 'zen-toggle-unloaded-cycling-shortcut' + ) + ); + } return data; } } @@ -1014,10 +1051,9 @@ var gZenKeyboardShortcutsManager = { async init() { if (this.inBrowserView) { const loadedShortcuts = await this._loadSaved(); - - this._currentShortcutList = this.versioner.fixedKeyboardShortcuts(loadedShortcuts); + this.versioner = new nsZenKeyboardShortcutsVersioner(loadedShortcuts); + this._currentShortcutList = this.versioner.fixedKeyboardShortcuts(loadedShortcuts) || []; this._applyShortcuts(); - await this._saveShortcuts(); } }, From b1253bfea100a94ce08f13dba5fb169aa25058de Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 9 Jul 2025 02:34:18 -0300 Subject: [PATCH 02/16] patch: remove mozilla's ctrl+tab navigation --- src/toolkit/content/widgets/tabbox-js.patch | 61 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/toolkit/content/widgets/tabbox-js.patch b/src/toolkit/content/widgets/tabbox-js.patch index f38c620616..adea3d6065 100644 --- a/src/toolkit/content/widgets/tabbox-js.patch +++ b/src/toolkit/content/widgets/tabbox-js.patch @@ -1,7 +1,66 @@ diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js -index 6775a7635c6cdbb276b3a912d0bba07840acb28f..861640d12c6118e11acb3f51723a79098dbbba10 100644 +index 70afbfc87d543971e6f8a0661a44b682920a7bc4..1678c1d34873df2cb97d75d8d3a6cc4959633d5e 100644 --- a/toolkit/content/widgets/tabbox.js +++ b/toolkit/content/widgets/tabbox.js +@@ -126,32 +126,32 @@ + const { ShortcutUtils } = imports; + + switch (ShortcutUtils.getSystemActionForEvent(event)) { +- case ShortcutUtils.CYCLE_TABS: +- Glean.browserUiInteraction.keyboard["ctrl-tab"].add(1); +- Services.prefs.setBoolPref( +- "browser.engagement.ctrlTab.has-used", +- true +- ); +- if (this.tabs && this.handleCtrlTab) { +- this.tabs.advanceSelectedTab( +- event.shiftKey ? DIRECTION_BACKWARD : DIRECTION_FORWARD, +- true +- ); +- event.preventDefault(); +- } +- break; +- case ShortcutUtils.PREVIOUS_TAB: +- if (this.tabs) { +- this.tabs.advanceSelectedTab(DIRECTION_BACKWARD, true); +- event.preventDefault(); +- } +- break; +- case ShortcutUtils.NEXT_TAB: +- if (this.tabs) { +- this.tabs.advanceSelectedTab(DIRECTION_FORWARD, true); +- event.preventDefault(); +- } +- break; ++ // case ShortcutUtils.CYCLE_TABS: ++ // Glean.browserUiInteraction.keyboard["ctrl-tab"].add(1); ++ // Services.prefs.setBoolPref( ++ // "browser.engagement.ctrlTab.has-used", ++ // true ++ // ); ++ // if (this.tabs && this.handleCtrlTab) { ++ // this.tabs.advanceSelectedTab( ++ // event.shiftKey ? DIRECTION_BACKWARD : DIRECTION_FORWARD, ++ // true ++ // ); ++ // event.preventDefault(); ++ // } ++ // break; ++ // case ShortcutUtils.PREVIOUS_TAB: ++ // if (this.tabs) { ++ // this.tabs.advanceSelectedTab(DIRECTION_BACKWARD, true); ++ // event.preventDefault(); ++ // } ++ // break; ++ // case ShortcutUtils.NEXT_TAB: ++ // if (this.tabs) { ++ // this.tabs.advanceSelectedTab(DIRECTION_FORWARD, true); ++ // event.preventDefault(); ++ // } ++ // break; + } + } + } @@ -213,7 +213,7 @@ ) { this._inAsyncOperation = false; From c6769bfe58eae24eabd96f2036a93f6e1e189603 Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 9 Jul 2025 02:35:10 -0300 Subject: [PATCH 03/16] feat: add menu & radio options & shortcuts logic --- .../preferences/zenTabsManagement.inc.xhtml | 29 ++++- src/zen/workspaces/ZenWorkspaces.mjs | 122 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/browser/components/preferences/zenTabsManagement.inc.xhtml b/src/browser/components/preferences/zenTabsManagement.inc.xhtml index 46a921c8f9..0959b4b2c1 100644 --- a/src/browser/components/preferences/zenTabsManagement.inc.xhtml +++ b/src/browser/components/preferences/zenTabsManagement.inc.xhtml @@ -64,6 +64,33 @@ - + + + + + + \ No newline at end of file diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index acac8c510c..8cc270063c 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -120,6 +120,14 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature { window.addEventListener('resize', this.onWindowResize.bind(this)); this.addPopupListeners(); + if (document.readyState === "complete") { + this.initializeTabNavigationCommands(); + } else { + window.addEventListener("load", () => { + this.initializeTabNavigationCommands(); + }, { once: true }); + } + await this.#waitForPromises(); await this._workspaces(); @@ -526,6 +534,40 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature { this._setupSidebarHandlers(); } + initializeTabNavigationCommands() { + const tabNextNode = document.getElementById("cmd_zenTabNext"); + if (tabNextNode) { + tabNextNode.addEventListener("command", () => zenNavigateTab(1)); + } + + const tabPrevNode = document.getElementById("cmd_zenTabPrev"); + if (tabPrevNode) { + tabPrevNode.addEventListener("command", () => zenNavigateTab(-1)); + } + + const toggleNode = document.getElementById("cmd_zenToggleUnloadedCycling"); + if (toggleNode) { + toggleNode.addEventListener("command", () => { + + const currentMode = Services.prefs.getCharPref('zen.tabs.unloaded-navigation-mode', 'always'); + const nextMode = currentMode === 'always' ? 'never' : 'always'; + + Services.prefs.setCharPref('zen.tabs.unloaded-navigation-mode', nextMode); + + const message = nextMode === 'always' ? 'Including unloaded tabs' : 'Skipping unloaded tabs'; + + gNotificationBox.appendNotification( + 'zen-unloaded-cycle-toggle-notification', + { + label: message, + priority: gNotificationBox.PRIORITY_INFO_MEDIUM, + }, + [] + ); + }); + } + } + _setupAppCommandHandlers() { // Remove existing handler temporarily - this is needed so that _handleAppCommand is called before the original window.removeEventListener('AppCommand', HandleAppCommandEvent, true); @@ -2945,3 +2987,83 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature { } } })(); + +function zenNavigateTab(direction) { + const basePref = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'never'); + const invertedState = Services.prefs.getBoolPref('zen.tabs.unloaded-navigation-mode.inverted', false); + + const cycleUnloaded = (basePref === 'always' && !invertedState) || (basePref === 'never' && invertedState); + + let allTabs = gZenWorkspaces.tabboxChildren.filter( + (t) => !t.hidden && !t.hasAttribute('zen-empty-tab') + ); + + let tabs = cycleUnloaded ? allTabs : allTabs.filter(t => !t.hasAttribute('pending')); + + if (tabs.length === 0) { + return; + } + + const selectedIndex = tabs.indexOf(gBrowser.selectedTab); + + let newIndex; + + if (selectedIndex === -1) { + newIndex = direction > 0 ? 0 : tabs.length - 1; + } else { + newIndex = selectedIndex + direction; + + if (newIndex < 0) { + newIndex = tabs.length - 1; + } else if (newIndex >= tabs.length) { + newIndex = 0; + } + } + + gBrowser.selectedTab = tabs[newIndex]; +} + +window.cmd_zenTabNext = function () { + console.log('[ZenDebug] window.cmd_zenTabNext called'); + if (typeof zenNavigateTab === 'function') { + zenNavigateTab(1); + } else { + console.error('[ZenDebug] zenNavigateTab is not defined!'); + } +}; +window.cmd_zenTabPrev = function () { + console.log('[ZenDebug] window.cmd_zenTabPrev called'); + if (typeof zenNavigateTab === 'function') { + zenNavigateTab(-1); + } else { + console.error('[ZenDebug] zenNavigateTab is not defined!'); + } +}; + +// if (window && window.Commands) { +// window.Commands.cmd_zenTabNext = window.cmd_zenTabNext; +// window.Commands.cmd_zenTabPrev = window.cmd_zenTabPrev; +// } + +// if (typeof zenNavigateTab === 'function') { +// const _zenNavigateTabOrig = zenNavigateTab; +// window.zenNavigateTab = function (direction) { +// console.log('[ZenDebug] zenNavigateTab called with direction:', direction); +// return _zenNavigateTabOrig(direction); +// }; +// } + +// // Global keydown logger to debug event flow +// window.addEventListener('keydown', function (e) { +// // Only log if not in an input/textarea +// if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) return; +// console.log('[ZenDebug] keydown:', { +// key: e.key, +// code: e.code, +// ctrlKey: e.ctrlKey, +// shiftKey: e.shiftKey, +// altKey: e.altKey, +// metaKey: e.metaKey, +// defaultPrevented: e.defaultPrevented +// }); +// }, true); From 42fd45d0cc59052b612a4acdef13d147fc19ee3d Mon Sep 17 00:00:00 2001 From: nikvnt Date: Sun, 13 Jul 2025 00:58:55 -0300 Subject: [PATCH 04/16] change: trail some trash code and add more modifier options --- src/zen/kbs/ZenKeyboardShortcuts.mjs | 8 ++++---- src/zen/workspaces/ZenWorkspaces.mjs | 30 +--------------------------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 3941c57da0..707039879f 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -986,14 +986,14 @@ class nsZenKeyboardShortcutsVersioner { } if (version < 10) { // Migrate from version 9 to 10 - // In this new version, we add customizable shortcuts for switching to the next/previous tab. + // In this new version, we add customizable shortcuts for switching to the next/previous tab and toggling unloaded tab cycling. data.push( new KeyShortcut( 'zen-tab-next-shortcut', '', 'VK_TAB', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({ accel: true }), + nsKeyShortcutModifiers.fromObject({ accel: true, shift: true, alt: true }), 'cmd_zenTabNext', 'zen-tab-next-shortcut' ) @@ -1004,7 +1004,7 @@ class nsZenKeyboardShortcutsVersioner { '', 'VK_TAB', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({ accel: true, shift: true }), + nsKeyShortcutModifiers.fromObject({ accel: true, shift: true, alt: true }), 'cmd_zenTabPrev', 'zen-tab-prev-shortcut' ) @@ -1015,7 +1015,7 @@ class nsZenKeyboardShortcutsVersioner { '', '', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({ alt: true }), + nsKeyShortcutModifiers.fromObject({ accel: true, shift: true, alt: true }), 'cmd_zenToggleUnloadedCycling', 'zen-toggle-unloaded-cycling-shortcut' ) diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 8cc270063c..2de44c9334 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -3038,32 +3038,4 @@ window.cmd_zenTabPrev = function () { } else { console.error('[ZenDebug] zenNavigateTab is not defined!'); } -}; - -// if (window && window.Commands) { -// window.Commands.cmd_zenTabNext = window.cmd_zenTabNext; -// window.Commands.cmd_zenTabPrev = window.cmd_zenTabPrev; -// } - -// if (typeof zenNavigateTab === 'function') { -// const _zenNavigateTabOrig = zenNavigateTab; -// window.zenNavigateTab = function (direction) { -// console.log('[ZenDebug] zenNavigateTab called with direction:', direction); -// return _zenNavigateTabOrig(direction); -// }; -// } - -// // Global keydown logger to debug event flow -// window.addEventListener('keydown', function (e) { -// // Only log if not in an input/textarea -// if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) return; -// console.log('[ZenDebug] keydown:', { -// key: e.key, -// code: e.code, -// ctrlKey: e.ctrlKey, -// shiftKey: e.shiftKey, -// altKey: e.altKey, -// metaKey: e.metaKey, -// defaultPrevented: e.defaultPrevented -// }); -// }, true); +}; \ No newline at end of file From 2e7ef057ef9d4d4da1a7a079262873ff300ffa25 Mon Sep 17 00:00:00 2001 From: nikvnt Date: Sun, 13 Jul 2025 05:12:11 -0300 Subject: [PATCH 05/16] change: leaving inputs for navigation unset + setting a default input for cycling --- src/zen/kbs/ZenKeyboardShortcuts.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 707039879f..6e2f3868fd 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -991,9 +991,9 @@ class nsZenKeyboardShortcutsVersioner { new KeyShortcut( 'zen-tab-next-shortcut', '', - 'VK_TAB', + '', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({ accel: true, shift: true, alt: true }), + nsKeyShortcutModifiers.fromObject({}), 'cmd_zenTabNext', 'zen-tab-next-shortcut' ) @@ -1002,9 +1002,9 @@ class nsZenKeyboardShortcutsVersioner { new KeyShortcut( 'zen-tab-prev-shortcut', '', - 'VK_TAB', + '', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({ accel: true, shift: true, alt: true }), + nsKeyShortcutModifiers.fromObject({}), 'cmd_zenTabPrev', 'zen-tab-prev-shortcut' ) @@ -1012,10 +1012,10 @@ class nsZenKeyboardShortcutsVersioner { data.push( new KeyShortcut( 'zen-toggle-unloaded-cycling-shortcut', - '', + 'U', '', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({ accel: true, shift: true, alt: true }), + nsKeyShortcutModifiers.fromObject({ alt: true }), 'cmd_zenToggleUnloadedCycling', 'zen-toggle-unloaded-cycling-shortcut' ) From b030f0117840aab935714b32adedccfabc40a08b Mon Sep 17 00:00:00 2001 From: nikvnt Date: Tue, 22 Jul 2025 01:26:36 -0300 Subject: [PATCH 06/16] change: move functionalities from workspaces to commonUtils & remove comments on tabbox-js patch --- src/toolkit/content/widgets/tabbox-js.patch | 30 +------ src/zen/common/ZenCommonUtils.mjs | 52 +++++++++++ src/zen/common/zen-sets.js | 9 ++ src/zen/kbs/ZenKeyboardShortcuts.mjs | 14 +-- src/zen/workspaces/ZenWorkspaces.mjs | 96 +-------------------- 5 files changed, 71 insertions(+), 130 deletions(-) diff --git a/src/toolkit/content/widgets/tabbox-js.patch b/src/toolkit/content/widgets/tabbox-js.patch index adea3d6065..9abb68b947 100644 --- a/src/toolkit/content/widgets/tabbox-js.patch +++ b/src/toolkit/content/widgets/tabbox-js.patch @@ -5,7 +5,7 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..1678c1d34873df2cb97d75d8d3a6cc49 @@ -126,32 +126,32 @@ const { ShortcutUtils } = imports; - switch (ShortcutUtils.getSystemActionForEvent(event)) { +- switch (ShortcutUtils.getSystemActionForEvent(event)) { - case ShortcutUtils.CYCLE_TABS: - Glean.browserUiInteraction.keyboard["ctrl-tab"].add(1); - Services.prefs.setBoolPref( @@ -32,33 +32,7 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..1678c1d34873df2cb97d75d8d3a6cc49 - event.preventDefault(); - } - break; -+ // case ShortcutUtils.CYCLE_TABS: -+ // Glean.browserUiInteraction.keyboard["ctrl-tab"].add(1); -+ // Services.prefs.setBoolPref( -+ // "browser.engagement.ctrlTab.has-used", -+ // true -+ // ); -+ // if (this.tabs && this.handleCtrlTab) { -+ // this.tabs.advanceSelectedTab( -+ // event.shiftKey ? DIRECTION_BACKWARD : DIRECTION_FORWARD, -+ // true -+ // ); -+ // event.preventDefault(); -+ // } -+ // break; -+ // case ShortcutUtils.PREVIOUS_TAB: -+ // if (this.tabs) { -+ // this.tabs.advanceSelectedTab(DIRECTION_BACKWARD, true); -+ // event.preventDefault(); -+ // } -+ // break; -+ // case ShortcutUtils.NEXT_TAB: -+ // if (this.tabs) { -+ // this.tabs.advanceSelectedTab(DIRECTION_FORWARD, true); -+ // event.preventDefault(); -+ // } -+ // break; - } +- } } } @@ -213,7 +213,7 @@ diff --git a/src/zen/common/ZenCommonUtils.mjs b/src/zen/common/ZenCommonUtils.mjs index fab73404e4..b13a77fe98 100644 --- a/src/zen/common/ZenCommonUtils.mjs +++ b/src/zen/common/ZenCommonUtils.mjs @@ -118,6 +118,58 @@ var gZenCommonActions = { } }, + zenNavigateTab(direction) { + try { + const basePref = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'never'); + const invertedState = Services.prefs.getBoolPref('zen.tabs.unloaded-navigation-mode.inverted', false); + const cycleUnloaded = (basePref === 'always' && !invertedState) || (basePref === 'never' && invertedState); + + let allTabs = window.gZenWorkspaces?.tabboxChildren?.filter( + (t) => !t.hidden && !t.hasAttribute('zen-empty-tab') + ) || []; + let tabs = cycleUnloaded ? allTabs : allTabs.filter(t => !t.hasAttribute('pending')); + if (tabs.length === 0) return; + const selectedIndex = tabs.indexOf(window.gBrowser?.selectedTab); + let newIndex; + if (selectedIndex === -1) { + newIndex = direction > 0 ? 0 : tabs.length - 1; + } else { + newIndex = selectedIndex + direction; + if (newIndex < 0) newIndex = tabs.length - 1; + else if (newIndex >= tabs.length) newIndex = 0; + } + if (window.gBrowser) window.gBrowser.selectedTab = tabs[newIndex]; + } catch (e) { + console.error('Error in gZenCommonActions.zenNavigateTab:', e); + } + }, + + nextTab() { + this.zenNavigateTab(1); + }, + previousTab() { + this.zenNavigateTab(-1); + }, + + toggleUnloadedCycling() { + try { + const currentMode = Services.prefs.getCharPref('zen.tabs.unloaded-navigation-mode', 'always'); + const nextMode = currentMode === 'always' ? 'never' : 'always'; + Services.prefs.setCharPref('zen.tabs.unloaded-navigation-mode', nextMode); + const message = nextMode === 'always' ? 'Including unloaded tabs' : 'Skipping unloaded tabs'; + gNotificationBox.appendNotification( + 'zen-unloaded-cycle-toggle-notification', + { + label: message, + priority: gNotificationBox.PRIORITY_INFO_MEDIUM, + }, + [] + ); + } catch (e) { + console.error('[gZenCommonActions] Error on unloaded cycling:', e); + } + }, + throttle(f, delay) { let timer = 0; return function (...args) { diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 14e2eb09f7..57db700d84 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -47,6 +47,15 @@ document.addEventListener( case 'cmd_zenSplitViewUnsplit': gZenViewSplitter.toggleShortcut('unsplit'); break; + case 'cmd_zenTabNext': + gZenCommonActions.nextTab(); + break; + case 'cmd_zenTabPrev': + gZenCommonActions.previousTab(); + break; + case 'cmd_zenToggleUnloadedCycling': + gZenCommonActions.toggleUnloadedCycling(); + break; case 'cmd_zenSplitViewContextMenu': gZenViewSplitter.contextSplitTabs(); break; diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 6e2f3868fd..7653cd9d94 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -990,10 +990,10 @@ class nsZenKeyboardShortcutsVersioner { data.push( new KeyShortcut( 'zen-tab-next-shortcut', - '', - '', + 'TAB', + 'VK_TAB', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({}), + nsKeyShortcutModifiers.fromObject({ accel: true }), 'cmd_zenTabNext', 'zen-tab-next-shortcut' ) @@ -1001,10 +1001,10 @@ class nsZenKeyboardShortcutsVersioner { data.push( new KeyShortcut( 'zen-tab-prev-shortcut', - '', - '', + 'TAB', + 'VK_TAB', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({}), + nsKeyShortcutModifiers.fromObject({ accel: true, shift: true }), 'cmd_zenTabPrev', 'zen-tab-prev-shortcut' ) @@ -1298,4 +1298,4 @@ document.addEventListener( } }, { once: true } -); +); \ No newline at end of file diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 7d73654fff..4e45e37933 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -120,14 +120,6 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature { window.addEventListener('resize', this.onWindowResize.bind(this)); this.addPopupListeners(); - if (document.readyState === "complete") { - this.initializeTabNavigationCommands(); - } else { - window.addEventListener("load", () => { - this.initializeTabNavigationCommands(); - }, { once: true }); - } - await this.#waitForPromises(); await this._workspaces(); @@ -534,40 +526,6 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature { this._setupSidebarHandlers(); } - initializeTabNavigationCommands() { - const tabNextNode = document.getElementById("cmd_zenTabNext"); - if (tabNextNode) { - tabNextNode.addEventListener("command", () => zenNavigateTab(1)); - } - - const tabPrevNode = document.getElementById("cmd_zenTabPrev"); - if (tabPrevNode) { - tabPrevNode.addEventListener("command", () => zenNavigateTab(-1)); - } - - const toggleNode = document.getElementById("cmd_zenToggleUnloadedCycling"); - if (toggleNode) { - toggleNode.addEventListener("command", () => { - - const currentMode = Services.prefs.getCharPref('zen.tabs.unloaded-navigation-mode', 'always'); - const nextMode = currentMode === 'always' ? 'never' : 'always'; - - Services.prefs.setCharPref('zen.tabs.unloaded-navigation-mode', nextMode); - - const message = nextMode === 'always' ? 'Including unloaded tabs' : 'Skipping unloaded tabs'; - - gNotificationBox.appendNotification( - 'zen-unloaded-cycle-toggle-notification', - { - label: message, - priority: gNotificationBox.PRIORITY_INFO_MEDIUM, - }, - [] - ); - }); - } - } - _setupAppCommandHandlers() { // Remove existing handler temporarily - this is needed so that _handleAppCommand is called before the original window.removeEventListener('AppCommand', HandleAppCommandEvent, true); @@ -2992,56 +2950,4 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature { document.getElementById('cmd_closeWindow').doCommand(); } } -})(); - -function zenNavigateTab(direction) { - const basePref = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'never'); - const invertedState = Services.prefs.getBoolPref('zen.tabs.unloaded-navigation-mode.inverted', false); - - const cycleUnloaded = (basePref === 'always' && !invertedState) || (basePref === 'never' && invertedState); - - let allTabs = gZenWorkspaces.tabboxChildren.filter( - (t) => !t.hidden && !t.hasAttribute('zen-empty-tab') - ); - - let tabs = cycleUnloaded ? allTabs : allTabs.filter(t => !t.hasAttribute('pending')); - - if (tabs.length === 0) { - return; - } - - const selectedIndex = tabs.indexOf(gBrowser.selectedTab); - - let newIndex; - - if (selectedIndex === -1) { - newIndex = direction > 0 ? 0 : tabs.length - 1; - } else { - newIndex = selectedIndex + direction; - - if (newIndex < 0) { - newIndex = tabs.length - 1; - } else if (newIndex >= tabs.length) { - newIndex = 0; - } - } - - gBrowser.selectedTab = tabs[newIndex]; -} - -window.cmd_zenTabNext = function () { - console.log('[ZenDebug] window.cmd_zenTabNext called'); - if (typeof zenNavigateTab === 'function') { - zenNavigateTab(1); - } else { - console.error('[ZenDebug] zenNavigateTab is not defined!'); - } -}; -window.cmd_zenTabPrev = function () { - console.log('[ZenDebug] window.cmd_zenTabPrev called'); - if (typeof zenNavigateTab === 'function') { - zenNavigateTab(-1); - } else { - console.error('[ZenDebug] zenNavigateTab is not defined!'); - } -}; \ No newline at end of file +})(); \ No newline at end of file From f582029454f2a9b42f277474fd7a7a189c32f61e Mon Sep 17 00:00:00 2001 From: nikvnt Date: Tue, 9 Sep 2025 07:51:48 -0300 Subject: [PATCH 07/16] test: add tests for unloaded navigation pref --- src/zen/tests/tabs/browser.toml | 4 +- .../tabs/browser_tab_unloaded_navigation.js | 225 ++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/zen/tests/tabs/browser_tab_unloaded_navigation.js diff --git a/src/zen/tests/tabs/browser.toml b/src/zen/tests/tabs/browser.toml index 1b03625c58..ce227c6e61 100644 --- a/src/zen/tests/tabs/browser.toml +++ b/src/zen/tests/tabs/browser.toml @@ -7,8 +7,10 @@ support-files = [ "head.js", ] +["browser_tab_unloaded_navigation.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_tab_unloaded_navigation.js b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js new file mode 100644 index 0000000000..593bd9c246 --- /dev/null +++ b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js @@ -0,0 +1,225 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests tab navigation between loaded/unloaded tabs based on user preference + */ + +const URL1 = 'http://example.com/page/1'; +const URL2 = 'http://example.com/page/2_unloaded'; +const URL3 = 'http://example.com/page/3'; + +// Helper function to create a normal loaded tab +async function createLoadedTab(url) { + info(`Creating loaded tab predictably with URL ${url}`); + const tab = BrowserTestUtils.addTab(gBrowser, url, { + inBackground: true, + }); + + + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + return tab; +} + +function createUnloadedTab(url) { + const tab = BrowserTestUtils.addTab(gBrowser, url, { + inBackground: true, + skipAnimation: true + }); + + tab.linkedBrowser.setAttribute("pending", "true"); + tab.setAttribute("pending", "true"); + + info(`New unloaded tab created at index ${gBrowser.tabs.indexOf(tab)} with URL ${url}`); + return tab; +} + +function resetPreferences() { + Services.prefs.clearUserPref("zen.tabs.unloaded-navigation-mode"); +} + +async function waitForTabSelection(expectedTab) { + await BrowserTestUtils.waitForCondition( + () => gBrowser.selectedTab === expectedTab, + "Waiting for tab to be selected" + ); +} + +add_setup(async () => { + resetPreferences(); +}); + +add_task(async function test_basic_unloaded_navigation() { + info("Basic test to verify the test infrastructure works"); + + const tab1 = await createLoadedTab(URL1); + const tab2 = createUnloadedTab(URL2); + + ok(tab1, "Loaded tab should be created"); + ok(tab2, "Unloaded tab should be created"); + is(tab2.hasAttribute("pending"), true, "Tab should have pending attribute"); + is(tab1.hasAttribute("pending"), false, "Loaded tab should not have pending attribute"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +add_task(async function test_unloaded_navigation_always_mode() { + info("Testing navigation with 'always' mode (includes unloaded tabs)"); + + await SpecialPowers.pushPrefEnv({ + set: [["zen.tabs.unloaded-navigation-mode", "always"]], + }); + + const navigateAndAssert = async (direction, expectedTab, message) => { + gBrowser.tabContainer.advanceSelectedTab(direction, false); + await TestUtils.waitForTick(); + info(`After navigating by ${direction}, selected tab is at index ${gBrowser.tabs.indexOf(gBrowser.selectedTab)}`); + is(gBrowser.selectedTab, expectedTab, message); + }; + + const loadedTab = await createLoadedTab(URL1); + const unloadedTab = createUnloadedTab(URL2); + const initialIndex = gBrowser.tabs.indexOf(loadedTab); + + gBrowser.selectedTab = loadedTab; + await waitForTabSelection(loadedTab); + + await navigateAndAssert(1, gBrowser.tabs[initialIndex + 1], + "Should navigate forward to the next tab (unloaded)"); + + await navigateAndAssert(-1, loadedTab, + "Should navigate backward to the previous tab (loaded)"); + + BrowserTestUtils.removeTab(loadedTab); + BrowserTestUtils.removeTab(unloadedTab); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_unloaded_navigation_never_mode() { + info("Testing navigation with 'never' mode (skips unloaded tabs) using URL comparison"); + + await SpecialPowers.pushPrefEnv({ + set: [["zen.tabs.unloaded-navigation-mode", "never"]], + }); + + await TestUtils.waitForTick(); + + /** + * Helper to test navigation scenarios. Each run is isolated by creating and + * then cleaning up its own set of tabs. + */ + async function runNavTest({ setup, startIndex, direction, expectedURL, description }) { + info(`Running test: ${description}`); + + let allTestTabs = []; + try { + + // Create all the tabs required for this specific test scenario + for (const tabConfig of setup) { + let newTab; + if (tabConfig.type === "loaded") { + newTab = await createLoadedTab(tabConfig.url); + } else { + newTab = createUnloadedTab(tabConfig.url); + } + allTestTabs.push(newTab); + } + + // put tabs in ascending order for the tests to make sense + for (let i = 0; i < allTestTabs.length; i++) { + const targetIndex = gBrowser.tabs.length - allTestTabs.length + i; + gBrowser.moveTabTo(allTestTabs[i], targetIndex); + } + + //trimming unnecessary about:blanks + for (let i = gBrowser.tabs.length - 1; i >= 0; i--) { + const tab = gBrowser.tabs[i]; + if (tab.linkedBrowser.currentURI.spec === "about:blank" && + tab.visible && !tab.hasAttribute('pending') && + !allTestTabs.includes(tab)) { + gBrowser.removeTab(tab); + } + } + + const startTab = allTestTabs[startIndex]; + gBrowser.selectedTab = startTab; + await waitForTabSelection(startTab); + + const startURL = gBrowser.selectedTab.linkedBrowser.currentURI.spec; + const startTabIndex = gBrowser.tabs.indexOf(gBrowser.selectedTab); + + info(`Starting navigation from tab index ${startTabIndex}, direction ${direction}`); + + gBrowser.tabContainer.advanceSelectedTab(direction, false); + await TestUtils.waitForTick(); + + const finalURL = gBrowser.selectedTab.linkedBrowser.currentURI.spec; + const finalTabIndex = gBrowser.tabs.indexOf(gBrowser.selectedTab); + + info(`--> Navigation result: Started on tab ${startTabIndex} [${startURL}], ended on tab ${finalTabIndex} [${finalURL}]`); + + is(finalURL, expectedURL, description); + + } finally { + // just remove all tabs created for a specific test run before moving on to the next. Trying to 'KISS' + for (const tab of allTestTabs) { + if (tab && tab.isConnected) { + BrowserTestUtils.removeTab(tab); + } + } + } + } + + // test scenarios + await runNavTest({ + setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }], + startIndex: 0, + direction: 1, + expectedURL: URL3, + description: "Should skip one unloaded tab and land on the correct URL.", + }); + + await runNavTest({ + setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }], + startIndex: 2, + direction: -1, + expectedURL: URL1, + description: "Should skip one unloaded tab backward and land on the correct URL.", + }); + + await runNavTest({ + setup: [ + { type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }, + ], + startIndex: 0, + direction: 1, + expectedURL: URL3, + description: "Should skip multiple unloaded tabs and land on the correct URL.", + }); + + await runNavTest({ + setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "unloaded", url: URL2 }], + startIndex: 0, + direction: 1, + expectedURL: URL1, + description: "Should not move if there is no next loaded tab.", + }); + + await runNavTest({ + setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }], + startIndex: 2, + direction: -1, + expectedURL: URL1, + description: "Should wrap around to the first loaded tab's URL.", + }); + + await SpecialPowers.popPrefEnv(); +}); + +registerCleanupFunction(() => { + resetPreferences(); +}); From 45503222101d09a45ed09255d4230336053d16fa Mon Sep 17 00:00:00 2001 From: nikvnt Date: Tue, 9 Sep 2025 08:01:07 -0300 Subject: [PATCH 08/16] feat: use firefox's native tab navigation + other adjusts for PR --- .../base/content/zen-commands.inc.xhtml | 6 +- src/toolkit/content/widgets/tabbox-js.patch | 79 ++++++++++--------- src/zen/common/ZenCommonUtils.mjs | 45 ++--------- src/zen/kbs/ZenKeyboardShortcuts.mjs | 1 - 4 files changed, 52 insertions(+), 79 deletions(-) diff --git a/src/browser/base/content/zen-commands.inc.xhtml b/src/browser/base/content/zen-commands.inc.xhtml index d212468929..dcb715c956 100644 --- a/src/browser/base/content/zen-commands.inc.xhtml +++ b/src/browser/base/content/zen-commands.inc.xhtml @@ -54,9 +54,13 @@ + + + + - + \ No newline at end of file diff --git a/src/toolkit/content/widgets/tabbox-js.patch b/src/toolkit/content/widgets/tabbox-js.patch index 8e745a2404..9bdf480f50 100644 --- a/src/toolkit/content/widgets/tabbox-js.patch +++ b/src/toolkit/content/widgets/tabbox-js.patch @@ -2,40 +2,15 @@ diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox. index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5eee090b258 100644 --- a/toolkit/content/widgets/tabbox.js +++ b/toolkit/content/widgets/tabbox.js -@@ -126,32 +126,32 @@ +@@ -125,6 +125,7 @@ + const { ShortcutUtils } = imports; - -- switch (ShortcutUtils.getSystemActionForEvent(event)) { -- case ShortcutUtils.CYCLE_TABS: -- Glean.browserUiInteraction.keyboard["ctrl-tab"].add(1); -- Services.prefs.setBoolPref( -- "browser.engagement.ctrlTab.has-used", -- true -- ); -- if (this.tabs && this.handleCtrlTab) { -- this.tabs.advanceSelectedTab( -- event.shiftKey ? DIRECTION_BACKWARD : DIRECTION_FORWARD, -- true -- ); -- event.preventDefault(); -- } -- break; -- case ShortcutUtils.PREVIOUS_TAB: -- if (this.tabs) { -- this.tabs.advanceSelectedTab(DIRECTION_BACKWARD, true); -- event.preventDefault(); -- } -- break; -- case ShortcutUtils.NEXT_TAB: -- if (this.tabs) { -- this.tabs.advanceSelectedTab(DIRECTION_FORWARD, true); -- event.preventDefault(); -- } -- break; -- } - } - } -@@ -213,7 +213,7 @@ + ++ return; + switch (ShortcutUtils.getSystemActionForEvent(event)) { + case ShortcutUtils.CYCLE_TABS: + Glean.browserUiInteraction.keyboard["ctrl-tab"].add(1); +@@ -213,7 +214,7 @@ ) { this._inAsyncOperation = false; if (oldPanel != this._selectedPanel) { @@ -44,7 +19,7 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee this._selectedPanel?.classList.add("deck-selected"); } this.setAttribute("selectedIndex", val); -@@ -610,7 +610,7 @@ +@@ -610,7 +611,7 @@ if (!tab) { return; } @@ -53,7 +28,7 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee if (otherTab != tab && otherTab.selected) { otherTab._selected = false; } -@@ -823,7 +823,7 @@ +@@ -823,7 +824,7 @@ if (tab == startTab) { return null; } @@ -62,7 +37,18 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee return tab; } } -@@ -888,7 +888,7 @@ +@@ -885,10 +886,18 @@ + * @param {boolean} [aWrap] + */ + advanceSelectedTab(aDir, aWrap) { ++ ++ const basePref = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'never'); ++ const invertedState = Services.prefs.getBoolPref('zen.tabs.unloaded-navigation-mode.inverted', false); ++ const includeUnloaded = (basePref === 'always' && !invertedState) || (basePref === 'never' && invertedState); ++ const tabFilter = includeUnloaded ++ ? (tab => tab.visible) ++ : (tab => tab.visible && !tab.hasAttribute('pending')); ++ let { ariaFocusedItem } = this; let startTab = ariaFocusedItem; if (!ariaFocusedItem || !this.allTabs.includes(ariaFocusedItem)) { @@ -70,4 +56,23 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee + startTab = gZenGlanceManager.getFocusedTab(aDir) || this.selectedItem; } let newTab = null; - + +@@ -896,15 +905,15 @@ + // which has a random placement in this.allTabs. + if (startTab.hidden) { + if (aDir == 1) { +- newTab = this.allTabs.find(tab => tab.visible); ++ newTab = this.allTabs.find(tabFilter); + } else { +- newTab = this.allTabs.findLast(tab => tab.visible); ++ newTab = this.allTabs.findLast(tabFilter); + } + } else { + newTab = this.findNextTab(startTab, { + direction: aDir, + wrap: aWrap, +- filter: tab => tab.visible, ++ filter: tabFilter, + }); + } + diff --git a/src/zen/common/ZenCommonUtils.mjs b/src/zen/common/ZenCommonUtils.mjs index d2f1697dce..d15734a8ac 100644 --- a/src/zen/common/ZenCommonUtils.mjs +++ b/src/zen/common/ZenCommonUtils.mjs @@ -17,7 +17,7 @@ window.gZenOperatingSystemCommonUtils = { /* eslint-disable no-unused-vars */ class nsZenMultiWindowFeature { - constructor() {} + constructor() { } static get browsers() { return Services.wm.getEnumerator('navigator:browser'); @@ -121,53 +121,18 @@ var gZenCommonActions = { } }, - zenNavigateTab(direction) { - try { - const basePref = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'never'); - const invertedState = Services.prefs.getBoolPref('zen.tabs.unloaded-navigation-mode.inverted', false); - const cycleUnloaded = (basePref === 'always' && !invertedState) || (basePref === 'never' && invertedState); - - let allTabs = window.gZenWorkspaces?.tabboxChildren?.filter( - (t) => !t.hidden && !t.hasAttribute('zen-empty-tab') - ) || []; - let tabs = cycleUnloaded ? allTabs : allTabs.filter(t => !t.hasAttribute('pending')); - if (tabs.length === 0) return; - const selectedIndex = tabs.indexOf(window.gBrowser?.selectedTab); - let newIndex; - if (selectedIndex === -1) { - newIndex = direction > 0 ? 0 : tabs.length - 1; - } else { - newIndex = selectedIndex + direction; - if (newIndex < 0) newIndex = tabs.length - 1; - else if (newIndex >= tabs.length) newIndex = 0; - } - if (window.gBrowser) window.gBrowser.selectedTab = tabs[newIndex]; - } catch (e) { - console.error('Error in gZenCommonActions.zenNavigateTab:', e); - } - }, - nextTab() { - this.zenNavigateTab(1); + gBrowser.tabContainer.advanceSelectedTab(1, false); }, previousTab() { - this.zenNavigateTab(-1); + gBrowser.tabContainer.advanceSelectedTab(-1, false); }, toggleUnloadedCycling() { try { - const currentMode = Services.prefs.getCharPref('zen.tabs.unloaded-navigation-mode', 'always'); + const currentMode = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'always'); const nextMode = currentMode === 'always' ? 'never' : 'always'; - Services.prefs.setCharPref('zen.tabs.unloaded-navigation-mode', nextMode); - const message = nextMode === 'always' ? 'Including unloaded tabs' : 'Skipping unloaded tabs'; - gNotificationBox.appendNotification( - 'zen-unloaded-cycle-toggle-notification', - { - label: message, - priority: gNotificationBox.PRIORITY_INFO_MEDIUM, - }, - [] - ); + Services.prefs.setStringPref('zen.tabs.unloaded-navigation-mode', nextMode); } catch (e) { console.error('[gZenCommonActions] Error on unloaded cycling:', e); } diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 53d52e390c..8e949db2de 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -1051,7 +1051,6 @@ var gZenKeyboardShortcutsManager = { async init() { if (this.inBrowserView) { const loadedShortcuts = await this._loadSaved(); - this.versioner = new nsZenKeyboardShortcutsVersioner(loadedShortcuts); this._currentShortcutList = this.versioner.fixedKeyboardShortcuts(loadedShortcuts) || []; this._applyShortcuts(); await this._saveShortcuts(); From f6f6b5675413ae46d2bb3d35a8cb54d9b2ca06c7 Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 10 Sep 2025 05:51:59 -0300 Subject: [PATCH 09/16] feat: make it so that unloaded cycling shortcut is unset by default --- src/zen/kbs/ZenKeyboardShortcuts.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index c395850b51..53f4af3424 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -1021,10 +1021,10 @@ class nsZenKeyboardShortcutsVersioner { data.push( new KeyShortcut( 'zen-toggle-unloaded-cycling-shortcut', - 'U', + '', '', 'windowAndTabManagement', - nsKeyShortcutModifiers.fromObject({ alt: true }), + nsKeyShortcutModifiers.fromObject({}), 'cmd_zenToggleUnloadedCycling', 'zen-toggle-unloaded-cycling-shortcut' ) @@ -1306,4 +1306,4 @@ document.addEventListener( } }, { once: true } -); \ No newline at end of file +); From cf89eba59348aa1609f7b3761f4408093a021b9f Mon Sep 17 00:00:00 2001 From: nikvnt Date: Thu, 11 Sep 2025 04:50:33 -0300 Subject: [PATCH 10/16] feat: make tab navigation shortcuts take precedence over sites --- src/zen/kbs/ZenKeyboardShortcuts.mjs | 56 +++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 53f4af3424..1416124d07 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -1166,6 +1166,48 @@ var gZenKeyboardShortcutsManager = { this.triggerShortcutRebuild(); }, + _registerPrecedentShortcut(shortcut, browser) { + const listener = (event) => { + + let keyMatch = false; + if (shortcut.getKeyName()) { + keyMatch = event.key.toLowerCase() === shortcut.getKeyName().toLowerCase(); + } else if (shortcut.getKeyCode()) { + for (const [mapKey, mapValue] of Object.entries(KEYCODE_MAP)) { + if (mapValue === shortcut.getKeyCode()) { + if (mapKey.toLowerCase() === event.code.toLowerCase()) { + keyMatch = true; + } + break; + } + } + } + + if (!keyMatch) { + return; + } + + const modifiers = shortcut.getModifiers(); + const accelPressed = AppConstants.platform === 'macosx' ? event.metaKey : event.ctrlKey; + const modifiersMatch = + modifiers.accel === accelPressed && + modifiers.alt === event.altKey && + modifiers.shift === event.shiftKey; + + if (modifiersMatch) { + event.preventDefault(); + event.stopImmediatePropagation(); + const command = browser.document.getElementById(shortcut.getAction()); + if (command) { + command.doCommand(); + } + } + }; + + browser.addEventListener('keydown', listener, true); + browser.gZenKeyboardShortcutsManager._precedentListeners.push(listener); + }, + _applyShortcuts() { for (const browser of nsZenMultiWindowFeature.browsers) { let mainKeyset = browser.document.getElementById(ZEN_MAIN_KEYSET_ID); @@ -1182,12 +1224,22 @@ var gZenKeyboardShortcutsManager = { // throw new Error('Child list not empty'); //} + browser.gZenKeyboardShortcutsManager._precedentListeners = []; + for (let key of this._currentShortcutList) { if (key.isInternal()) { continue; } - let child = key.toXHTMLElement(browser); - keyset.appendChild(child); + + if ( + key.getID() === 'zen-tab-next-shortcut' || + key.getID() === 'zen-tab-prev-shortcut' + ) { + this._registerPrecedentShortcut(key, browser); + } else { + let child = key.toXHTMLElement(browser); + keyset.appendChild(child); + } } this._applyDevtoolsShortcuts(browser); From 8237ee663b022b51a0003541c0fd8e3fd998f593 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sat, 13 Sep 2025 20:12:04 +0200 Subject: [PATCH 11/16] chore: Updated to dev, b=no-bug, c=common, kbs, tests, tabs, workspaces --- .../components/preferences/zen-settings.js | 4 +- src/zen/common/ZenCommonUtils.mjs | 7 +- src/zen/kbs/ZenKeyboardShortcuts.mjs | 10 +- .../tabs/browser_tab_unloaded_navigation.js | 95 ++++++++++++------- src/zen/workspaces/ZenWorkspaces.mjs | 2 +- 5 files changed, 72 insertions(+), 46 deletions(-) diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 6925211fc8..a901c1bc94 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -778,7 +778,7 @@ var zenMissingKeyboardShortcutL10n = { key_zenTabNext: 'zen-key-tab-next-shortcut', key_zenTabPrevious: 'zen-key-tab-prev-shortcut', key_toggleUnloadedCycling: 'zen-toggle-unloaded-cycling-shortcut', - + 'zen-glance-expand': 'zen-glance-expand', key_selectTab1: 'zen-key-select-tab-1', @@ -1169,7 +1169,7 @@ Preferences.addAll([ type: 'bool', default: true, }, - { + { id: 'zen.tabs.unloaded-navigation-mode', type: 'string', default: 'always', diff --git a/src/zen/common/ZenCommonUtils.mjs b/src/zen/common/ZenCommonUtils.mjs index d15734a8ac..d98b5d98fb 100644 --- a/src/zen/common/ZenCommonUtils.mjs +++ b/src/zen/common/ZenCommonUtils.mjs @@ -17,7 +17,7 @@ window.gZenOperatingSystemCommonUtils = { /* eslint-disable no-unused-vars */ class nsZenMultiWindowFeature { - constructor() { } + constructor() {} static get browsers() { return Services.wm.getEnumerator('navigator:browser'); @@ -130,7 +130,10 @@ var gZenCommonActions = { toggleUnloadedCycling() { try { - const currentMode = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'always'); + const currentMode = Services.prefs.getStringPref( + 'zen.tabs.unloaded-navigation-mode', + 'always' + ); const nextMode = currentMode === 'always' ? 'never' : 'always'; Services.prefs.setStringPref('zen.tabs.unloaded-navigation-mode', nextMode); } catch (e) { diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index a48a618f94..5ad54c6176 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -1084,7 +1084,9 @@ class nsZenKeyboardShortcutsVersioner { nsKeyShortcutModifiers.fromObject({}), 'cmd_zenToggleUnloadedCycling', 'zen-toggle-unloaded-cycling-shortcut' - + ) + ); + // 2) Add the new pin/unpin tab toggle shortcut with Ctrl+Shift+D data.push( new KeyShortcut( @@ -1249,7 +1251,6 @@ var gZenKeyboardShortcutsManager = { _registerPrecedentShortcut(shortcut, browser) { const listener = (event) => { - let keyMatch = false; if (shortcut.getKeyName()) { keyMatch = event.key.toLowerCase() === shortcut.getKeyName().toLowerCase(); @@ -1312,10 +1313,7 @@ var gZenKeyboardShortcutsManager = { continue; } - if ( - key.getID() === 'zen-tab-next-shortcut' || - key.getID() === 'zen-tab-prev-shortcut' - ) { + if (key.getID() === 'zen-tab-next-shortcut' || key.getID() === 'zen-tab-prev-shortcut') { this._registerPrecedentShortcut(key, browser); } else { let child = key.toXHTMLElement(browser); diff --git a/src/zen/tests/tabs/browser_tab_unloaded_navigation.js b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js index 593bd9c246..5873df0ffb 100644 --- a/src/zen/tests/tabs/browser_tab_unloaded_navigation.js +++ b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -"use strict"; +'use strict'; /** * Tests tab navigation between loaded/unloaded tabs based on user preference @@ -18,7 +18,6 @@ async function createLoadedTab(url) { inBackground: true, }); - await BrowserTestUtils.browserLoaded(tab.linkedBrowser); return tab; } @@ -26,24 +25,24 @@ async function createLoadedTab(url) { function createUnloadedTab(url) { const tab = BrowserTestUtils.addTab(gBrowser, url, { inBackground: true, - skipAnimation: true + skipAnimation: true, }); - tab.linkedBrowser.setAttribute("pending", "true"); - tab.setAttribute("pending", "true"); + tab.linkedBrowser.setAttribute('pending', 'true'); + tab.setAttribute('pending', 'true'); info(`New unloaded tab created at index ${gBrowser.tabs.indexOf(tab)} with URL ${url}`); return tab; } function resetPreferences() { - Services.prefs.clearUserPref("zen.tabs.unloaded-navigation-mode"); + Services.prefs.clearUserPref('zen.tabs.unloaded-navigation-mode'); } async function waitForTabSelection(expectedTab) { await BrowserTestUtils.waitForCondition( () => gBrowser.selectedTab === expectedTab, - "Waiting for tab to be selected" + 'Waiting for tab to be selected' ); } @@ -52,15 +51,15 @@ add_setup(async () => { }); add_task(async function test_basic_unloaded_navigation() { - info("Basic test to verify the test infrastructure works"); + info('Basic test to verify the test infrastructure works'); const tab1 = await createLoadedTab(URL1); const tab2 = createUnloadedTab(URL2); - ok(tab1, "Loaded tab should be created"); - ok(tab2, "Unloaded tab should be created"); - is(tab2.hasAttribute("pending"), true, "Tab should have pending attribute"); - is(tab1.hasAttribute("pending"), false, "Loaded tab should not have pending attribute"); + ok(tab1, 'Loaded tab should be created'); + ok(tab2, 'Unloaded tab should be created'); + is(tab2.hasAttribute('pending'), true, 'Tab should have pending attribute'); + is(tab1.hasAttribute('pending'), false, 'Loaded tab should not have pending attribute'); BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); @@ -70,13 +69,15 @@ add_task(async function test_unloaded_navigation_always_mode() { info("Testing navigation with 'always' mode (includes unloaded tabs)"); await SpecialPowers.pushPrefEnv({ - set: [["zen.tabs.unloaded-navigation-mode", "always"]], + set: [['zen.tabs.unloaded-navigation-mode', 'always']], }); const navigateAndAssert = async (direction, expectedTab, message) => { gBrowser.tabContainer.advanceSelectedTab(direction, false); await TestUtils.waitForTick(); - info(`After navigating by ${direction}, selected tab is at index ${gBrowser.tabs.indexOf(gBrowser.selectedTab)}`); + info( + `After navigating by ${direction}, selected tab is at index ${gBrowser.tabs.indexOf(gBrowser.selectedTab)}` + ); is(gBrowser.selectedTab, expectedTab, message); }; @@ -87,11 +88,13 @@ add_task(async function test_unloaded_navigation_always_mode() { gBrowser.selectedTab = loadedTab; await waitForTabSelection(loadedTab); - await navigateAndAssert(1, gBrowser.tabs[initialIndex + 1], - "Should navigate forward to the next tab (unloaded)"); + await navigateAndAssert( + 1, + gBrowser.tabs[initialIndex + 1], + 'Should navigate forward to the next tab (unloaded)' + ); - await navigateAndAssert(-1, loadedTab, - "Should navigate backward to the previous tab (loaded)"); + await navigateAndAssert(-1, loadedTab, 'Should navigate backward to the previous tab (loaded)'); BrowserTestUtils.removeTab(loadedTab); BrowserTestUtils.removeTab(unloadedTab); @@ -103,7 +106,7 @@ add_task(async function test_unloaded_navigation_never_mode() { info("Testing navigation with 'never' mode (skips unloaded tabs) using URL comparison"); await SpecialPowers.pushPrefEnv({ - set: [["zen.tabs.unloaded-navigation-mode", "never"]], + set: [['zen.tabs.unloaded-navigation-mode', 'never']], }); await TestUtils.waitForTick(); @@ -117,11 +120,10 @@ add_task(async function test_unloaded_navigation_never_mode() { let allTestTabs = []; try { - // Create all the tabs required for this specific test scenario for (const tabConfig of setup) { let newTab; - if (tabConfig.type === "loaded") { + if (tabConfig.type === 'loaded') { newTab = await createLoadedTab(tabConfig.url); } else { newTab = createUnloadedTab(tabConfig.url); @@ -138,9 +140,12 @@ add_task(async function test_unloaded_navigation_never_mode() { //trimming unnecessary about:blanks for (let i = gBrowser.tabs.length - 1; i >= 0; i--) { const tab = gBrowser.tabs[i]; - if (tab.linkedBrowser.currentURI.spec === "about:blank" && - tab.visible && !tab.hasAttribute('pending') && - !allTestTabs.includes(tab)) { + if ( + tab.linkedBrowser.currentURI.spec === 'about:blank' && + tab.visible && + !tab.hasAttribute('pending') && + !allTestTabs.includes(tab) + ) { gBrowser.removeTab(tab); } } @@ -160,10 +165,11 @@ add_task(async function test_unloaded_navigation_never_mode() { const finalURL = gBrowser.selectedTab.linkedBrowser.currentURI.spec; const finalTabIndex = gBrowser.tabs.indexOf(gBrowser.selectedTab); - info(`--> Navigation result: Started on tab ${startTabIndex} [${startURL}], ended on tab ${finalTabIndex} [${finalURL}]`); + info( + `--> Navigation result: Started on tab ${startTabIndex} [${startURL}], ended on tab ${finalTabIndex} [${finalURL}]` + ); is(finalURL, expectedURL, description); - } finally { // just remove all tabs created for a specific test run before moving on to the next. Trying to 'KISS' for (const tab of allTestTabs) { @@ -176,41 +182,60 @@ add_task(async function test_unloaded_navigation_never_mode() { // test scenarios await runNavTest({ - setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }], + setup: [ + { type: 'loaded', url: URL1 }, + { type: 'unloaded', url: URL2 }, + { type: 'loaded', url: URL3 }, + ], startIndex: 0, direction: 1, expectedURL: URL3, - description: "Should skip one unloaded tab and land on the correct URL.", + description: 'Should skip one unloaded tab and land on the correct URL.', }); await runNavTest({ - setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }], + setup: [ + { type: 'loaded', url: URL1 }, + { type: 'unloaded', url: URL2 }, + { type: 'loaded', url: URL3 }, + ], startIndex: 2, direction: -1, expectedURL: URL1, - description: "Should skip one unloaded tab backward and land on the correct URL.", + description: 'Should skip one unloaded tab backward and land on the correct URL.', }); await runNavTest({ setup: [ - { type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }, + { type: 'loaded', url: URL1 }, + { type: 'unloaded', url: URL2 }, + { type: 'unloaded', url: URL2 }, + { type: 'loaded', url: URL3 }, ], startIndex: 0, direction: 1, expectedURL: URL3, - description: "Should skip multiple unloaded tabs and land on the correct URL.", + description: 'Should skip multiple unloaded tabs and land on the correct URL.', }); await runNavTest({ - setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "unloaded", url: URL2 }], + setup: [ + { type: 'loaded', url: URL1 }, + { type: 'unloaded', url: URL2 }, + { type: 'unloaded', url: URL2 }, + ], startIndex: 0, direction: 1, expectedURL: URL1, - description: "Should not move if there is no next loaded tab.", + description: 'Should not move if there is no next loaded tab.', }); await runNavTest({ - setup: [{ type: "loaded", url: URL1 }, { type: "unloaded", url: URL2 }, { type: "loaded", url: URL3 }], + setup: [ + { type: 'loaded', url: URL1 }, + { type: 'unloaded', url: URL2 }, + { type: 'loaded', url: URL3 }, + ], startIndex: 2, direction: -1, expectedURL: URL1, diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index c156e9ac5d..47f541bc73 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -3068,4 +3068,4 @@ var gZenWorkspaces = new (class extends nsZenMultiWindowFeature { document.getElementById('cmd_closeWindow').doCommand(); } } -})(); \ No newline at end of file +})(); From 9d4d9e31222def97f53922c5af8e9d85dcc992f4 Mon Sep 17 00:00:00 2001 From: nikvnt Date: Tue, 16 Sep 2025 14:46:56 -0300 Subject: [PATCH 12/16] chore: change key names so that they match l10n translations --- src/browser/components/preferences/zen-settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 9b3442f736..fa9594776f 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -776,8 +776,8 @@ var zenMissingKeyboardShortcutL10n = { key_wrToggleCaptureSequenceCmd: 'zen-key-wr-toggle-capture-sequence-cmd', key_undoCloseWindow: 'zen-key-undo-close-window', - key_zenTabNext: 'zen-key-tab-next-shortcut', - key_zenTabPrevious: 'zen-key-tab-prev-shortcut', + key_zenTabNext: 'zen-tab-next-shortcut', + key_zenTabPrevious: 'zen-tab-prev-shortcut', key_toggleUnloadedCycling: 'zen-toggle-unloaded-cycling-shortcut', key_selectTab1: 'zen-key-select-tab-1', From 0b0ef8fd29cf043fe4da36a3e82a310e0550bcfd Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 17 Sep 2025 17:01:49 -0300 Subject: [PATCH 13/16] chore: improve identation --- src/zen/common/ZenCommonUtils.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/zen/common/ZenCommonUtils.mjs b/src/zen/common/ZenCommonUtils.mjs index d98b5d98fb..418cdcbae7 100644 --- a/src/zen/common/ZenCommonUtils.mjs +++ b/src/zen/common/ZenCommonUtils.mjs @@ -17,7 +17,7 @@ window.gZenOperatingSystemCommonUtils = { /* eslint-disable no-unused-vars */ class nsZenMultiWindowFeature { - constructor() {} + constructor() { } static get browsers() { return Services.wm.getEnumerator('navigator:browser'); @@ -122,10 +122,15 @@ var gZenCommonActions = { }, nextTab() { - gBrowser.tabContainer.advanceSelectedTab(1, false); + gBrowser + .tabContainer + .advanceSelectedTab(1, false); }, + previousTab() { - gBrowser.tabContainer.advanceSelectedTab(-1, false); + gBrowser + .tabContainer + .advanceSelectedTab(-1, false); }, toggleUnloadedCycling() { From f1bb8f8f3bc55b5c4f7646225708b777f5e4e80d Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 17 Sep 2025 19:05:17 -0300 Subject: [PATCH 14/16] feat: make it so that cycling tabs wrap around + add a test case for that scenario --- src/zen/common/ZenCommonUtils.mjs | 4 ++-- .../tabs/browser_tab_unloaded_navigation.js | 24 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/zen/common/ZenCommonUtils.mjs b/src/zen/common/ZenCommonUtils.mjs index 418cdcbae7..67663de9c8 100644 --- a/src/zen/common/ZenCommonUtils.mjs +++ b/src/zen/common/ZenCommonUtils.mjs @@ -124,13 +124,13 @@ var gZenCommonActions = { nextTab() { gBrowser .tabContainer - .advanceSelectedTab(1, false); + .advanceSelectedTab(1, true); }, previousTab() { gBrowser .tabContainer - .advanceSelectedTab(-1, false); + .advanceSelectedTab(-1, true); }, toggleUnloadedCycling() { diff --git a/src/zen/tests/tabs/browser_tab_unloaded_navigation.js b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js index 5873df0ffb..93e4a109a1 100644 --- a/src/zen/tests/tabs/browser_tab_unloaded_navigation.js +++ b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js @@ -115,7 +115,7 @@ add_task(async function test_unloaded_navigation_never_mode() { * Helper to test navigation scenarios. Each run is isolated by creating and * then cleaning up its own set of tabs. */ - async function runNavTest({ setup, startIndex, direction, expectedURL, description }) { + async function runNavTest({ setup, startIndex, direction, shouldWrap, expectedURL, description }) { info(`Running test: ${description}`); let allTestTabs = []; @@ -159,7 +159,7 @@ add_task(async function test_unloaded_navigation_never_mode() { info(`Starting navigation from tab index ${startTabIndex}, direction ${direction}`); - gBrowser.tabContainer.advanceSelectedTab(direction, false); + gBrowser.tabContainer.advanceSelectedTab(direction, shouldWrap); await TestUtils.waitForTick(); const finalURL = gBrowser.selectedTab.linkedBrowser.currentURI.spec; @@ -189,6 +189,7 @@ add_task(async function test_unloaded_navigation_never_mode() { ], startIndex: 0, direction: 1, + shouldWrap: false, expectedURL: URL3, description: 'Should skip one unloaded tab and land on the correct URL.', }); @@ -201,6 +202,7 @@ add_task(async function test_unloaded_navigation_never_mode() { ], startIndex: 2, direction: -1, + shouldWrap: false, expectedURL: URL1, description: 'Should skip one unloaded tab backward and land on the correct URL.', }); @@ -214,6 +216,7 @@ add_task(async function test_unloaded_navigation_never_mode() { ], startIndex: 0, direction: 1, + shouldWrap: false, expectedURL: URL3, description: 'Should skip multiple unloaded tabs and land on the correct URL.', }); @@ -226,6 +229,7 @@ add_task(async function test_unloaded_navigation_never_mode() { ], startIndex: 0, direction: 1, + shouldWrap: false, expectedURL: URL1, description: 'Should not move if there is no next loaded tab.', }); @@ -238,8 +242,22 @@ add_task(async function test_unloaded_navigation_never_mode() { ], startIndex: 2, direction: -1, + shouldWrap: false, expectedURL: URL1, - description: "Should wrap around to the first loaded tab's URL.", + description: "Should skip the unloaded tab and reach the correct URLto the first loaded tab's URL, by moving backwards.", + }); + + await runNavTest({ + setup: [ + { type: 'loaded', url: URL1 }, + { type: 'loaded', url: URL2 }, + { type: 'loaded', url: URL3 }, + ], + startIndex: 2, + direction: 1, + shouldWrap: true, + expectedURL: URL1, + description: "Should wrap from the last tab to the first one, by moving forward.", }); await SpecialPowers.popPrefEnv(); From 47e37790d6236e84c144b21ff33df9508dbe2ac7 Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 17 Sep 2025 19:06:29 -0300 Subject: [PATCH 15/16] fix: remove _precedentListeners array --- src/zen/kbs/ZenKeyboardShortcuts.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 5ad54c6176..2d8a3e07bb 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -1287,7 +1287,6 @@ var gZenKeyboardShortcutsManager = { }; browser.addEventListener('keydown', listener, true); - browser.gZenKeyboardShortcutsManager._precedentListeners.push(listener); }, _applyShortcuts() { @@ -1306,8 +1305,6 @@ var gZenKeyboardShortcutsManager = { // throw new Error('Child list not empty'); //} - browser.gZenKeyboardShortcutsManager._precedentListeners = []; - for (let key of this._currentShortcutList) { if (key.isInternal()) { continue; From 6f608dbe55bec2163974de288a01c207ee1efd9b Mon Sep 17 00:00:00 2001 From: nikvnt Date: Wed, 17 Sep 2025 23:35:58 -0300 Subject: [PATCH 16/16] feat: make it so that every key has isPrecedent as an attribute + set it as true for tab navigation keys --- src/zen/kbs/ZenKeyboardShortcuts.mjs | 53 ++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 2d8a3e07bb..89d1df2ec4 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -229,10 +229,10 @@ class nsKeyShortcutModifiers { this.#control == other.#control && (AppConstants.platform == 'macosx' ? (this.#meta || this.#accel) == (other.#meta || other.#accel) && - this.#control == other.#control + this.#control == other.#control : // In other platforms, we can have control and accel counting as the same thing - this.#meta == other.#meta && - (this.#control || this.#accel) == (other.#control || other.#accel)) + this.#meta == other.#meta && + (this.#control || this.#accel) == (other.#control || other.#accel)) ); } @@ -303,6 +303,7 @@ class KeyShortcut { #reserved = false; #internal = false; #shouldBeEmpty = false; + #isPrecedent = false; constructor( id, @@ -314,7 +315,8 @@ class KeyShortcut { l10nId, disabled = false, reserved = false, - internal = false + internal = false, + isPrecedent = false ) { this.#id = id; this.#key = key?.toLowerCase(); @@ -331,6 +333,7 @@ class KeyShortcut { this.#disabled = disabled; this.#reserved = reserved; this.#internal = internal; + this.#isPrecedent = isPrecedent; } isEmpty() { @@ -369,7 +372,8 @@ class KeyShortcut { json['l10nId'], json['disabled'], json['reserved'], - json['internal'] + json['internal'], + json['isPrecedent'] ); } @@ -379,16 +383,17 @@ class KeyShortcut { key.getAttribute('key'), key.getAttribute('keycode'), group ?? - KeyShortcut.getGroupFromL10nId( - KeyShortcut.sanitizeL10nId(key.getAttribute('data-l10n-id')), - key.getAttribute('id') - ), + KeyShortcut.getGroupFromL10nId( + KeyShortcut.sanitizeL10nId(key.getAttribute('data-l10n-id')), + key.getAttribute('id') + ), nsKeyShortcutModifiers.parseFromXHTMLAttribute(key.getAttribute('modifiers')), key.getAttribute('command'), key.getAttribute('data-l10n-id'), key.getAttribute('disabled') == 'true', key.getAttribute('reserved') == 'true', - key.getAttribute('internal') == 'true' + key.getAttribute('internal') == 'true', + key.getAttribute('isPrecedent') == 'true' ); } @@ -452,6 +457,9 @@ class KeyShortcut { if (this.#internal) { key.setAttribute('internal', this.#internal); } + if (this.#isPrecedent) { + key.setAttribute('isPrecedent', this.#isPrecedent); + } key.setAttribute('zen-keybind', 'true'); return key; @@ -517,6 +525,10 @@ class KeyShortcut { return this.#internal; } + isPrecedent() { + return this.#isPrecedent; + } + isInvalid() { return this.#key == '' && this.#keycode == '' && this.#l10nId == null; } @@ -540,6 +552,7 @@ class KeyShortcut { disabled: this.#disabled, reserved: this.#reserved, internal: this.#internal, + isPrecedent: this.#isPrecedent, }; } @@ -1061,7 +1074,11 @@ class nsZenKeyboardShortcutsVersioner { 'windowAndTabManagement', nsKeyShortcutModifiers.fromObject({ accel: true }), 'cmd_zenTabNext', - 'zen-tab-next-shortcut' + 'zen-tab-next-shortcut', + false, // disabled + false, // reserved + false, // internal + true // isPrecedent ) ); data.push( @@ -1072,7 +1089,11 @@ class nsZenKeyboardShortcutsVersioner { 'windowAndTabManagement', nsKeyShortcutModifiers.fromObject({ accel: true, shift: true }), 'cmd_zenTabPrev', - 'zen-tab-prev-shortcut' + 'zen-tab-prev-shortcut', + false, // disabled + false, // reserved + false, // internal + true // isPrecedent ) ); data.push( @@ -1083,7 +1104,11 @@ class nsZenKeyboardShortcutsVersioner { 'windowAndTabManagement', nsKeyShortcutModifiers.fromObject({}), 'cmd_zenToggleUnloadedCycling', - 'zen-toggle-unloaded-cycling-shortcut' + 'zen-toggle-unloaded-cycling-shortcut', + false, // disabled + false, // reserved + false, // internal + true // isPrecedent ) ); @@ -1310,7 +1335,7 @@ var gZenKeyboardShortcutsManager = { continue; } - if (key.getID() === 'zen-tab-next-shortcut' || key.getID() === 'zen-tab-prev-shortcut') { + if (key.isPrecedent()) { this._registerPrecedentShortcut(key, browser); } else { let child = key.toXHTMLElement(browser);