From e6882a42d60468eeb5392069d8379f53e9dde82e Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 14 Sep 2025 17:41:12 +0200 Subject: [PATCH 1/4] feat: Added empty splits support and more urlbar actions, b=no-bug, c=workspaces, common, kbs, split-view, tests --- .../browser/preferences/zen-preferences.ftl | 1 + .../en-US/browser/browser/zen-workspaces.ftl | 3 + .../base/content/zen-commands.inc.xhtml | 1 + .../base/content/zen-panels/popups.inc | 7 +- .../tabbrowser/content/tabbrowser-js.patch | 69 ++++++++++--------- .../urlbar/UrlbarView-sys-mjs.patch | 4 +- src/zen/common/ZenCustomizableUI.sys.mjs | 2 +- src/zen/common/ZenUIManager.mjs | 18 +++-- src/zen/common/zen-sets.js | 5 ++ src/zen/kbs/ZenKeyboardShortcuts.mjs | 17 ++++- src/zen/split-view/ZenViewSplitter.mjs | 45 ++++++++++++ src/zen/tests/split_view/browser.toml | 1 + .../split_view/browser_split_view_empty.js | 49 +++++++++++++ src/zen/urlbar/ZenUBActionsProvider.sys.mjs | 17 +++-- src/zen/urlbar/ZenUBGlobalActions.sys.mjs | 31 ++++++++- 15 files changed, 217 insertions(+), 53 deletions(-) create mode 100644 src/zen/tests/split_view/browser_split_view_empty.js diff --git a/locales/en-US/browser/browser/preferences/zen-preferences.ftl b/locales/en-US/browser/browser/preferences/zen-preferences.ftl index 5152d2044..5ab322e34 100644 --- a/locales/en-US/browser/browser/preferences/zen-preferences.ftl +++ b/locales/en-US/browser/browser/preferences/zen-preferences.ftl @@ -311,6 +311,7 @@ zen-split-view-shortcut-grid = Toggle Split View Grid zen-split-view-shortcut-vertical = Toggle Split View Vertical zen-split-view-shortcut-horizontal = Toggle Split View Horizontal zen-split-view-shortcut-unsplit = Close Split View +zen-new-empty-split-view-shortcut = New Empty Split View zen-key-select-tab-1 = Select tab #1 zen-key-select-tab-2 = Select tab #2 zen-key-select-tab-3 = Select tab #3 diff --git a/locales/en-US/browser/browser/zen-workspaces.ftl b/locales/en-US/browser/browser/zen-workspaces.ftl index cae5d179b..5b6452475 100644 --- a/locales/en-US/browser/browser/zen-workspaces.ftl +++ b/locales/en-US/browser/browser/zen-workspaces.ftl @@ -7,6 +7,9 @@ zen-panel-ui-workspaces-create = zen-panel-ui-folder-create = .label = Create Folder +zen-panel-ui-new-empty-split = + .label = New Split + zen-workspaces-panel-context-delete = .label = Delete Space .accesskey = D diff --git a/src/browser/base/content/zen-commands.inc.xhtml b/src/browser/base/content/zen-commands.inc.xhtml index 68f96a127..aad8b0c74 100644 --- a/src/browser/base/content/zen-commands.inc.xhtml +++ b/src/browser/base/content/zen-commands.inc.xhtml @@ -19,6 +19,7 @@ + diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc index a84187bf1..955609aa4 100644 --- a/src/browser/base/content/zen-panels/popups.inc +++ b/src/browser/base/content/zen-panels/popups.inc @@ -3,10 +3,11 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. - - - + + + + diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index c810f9de4..7f75e9b73 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010eedf8dcc29 100644 +index 3204f253c23551650991d3385dd256d55892a012..e5a907a81526fde51087a0c33599fbb2948420ad 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -427,15 +427,64 @@ @@ -395,10 +395,10 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee + gZenWorkspaces._initialTab._shouldRemove = true; + } + } -+ } + } + else { + gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab; - } ++ } + this._hasAlreadyInitializedZenSessionStore = true; if (tabs.length > 1 || !tabs[0].selected) { @@ -522,17 +522,22 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -4796,7 +4937,9 @@ +@@ -4796,7 +4937,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; -+ gZenVerticalTabsManager.animateTabClose(aTab, (animate && !gReduceMotion)).then(() => { - this._endRemoveTab(aTab); -+ }); +- this._endRemoveTab(aTab); ++ if (animate && !gReduceMotion && !gZenUIManager.testingEnabled) { ++ gZenVerticalTabsManager.animateTabClose(aTab, (animate && !gReduceMotion)).then(() => { ++ this._endRemoveTab(aTab); ++ }); ++ } else { ++ this._endRemoveTab(aTab); ++ } return; } -@@ -4930,7 +5073,7 @@ +@@ -4930,7 +5077,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -541,7 +546,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -4954,6 +5097,7 @@ +@@ -4954,6 +5101,7 @@ newTab = true; } @@ -549,7 +554,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -4994,13 +5138,7 @@ +@@ -4994,13 +5142,7 @@ aTab._mouseleave(); if (newTab) { @@ -564,7 +569,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee } else { TabBarVisibility.update(); } -@@ -5133,6 +5271,7 @@ +@@ -5133,6 +5275,7 @@ this.tabs[i]._tPos = i; } @@ -572,7 +577,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -5345,6 +5484,7 @@ +@@ -5345,6 +5488,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -580,7 +585,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -5357,13 +5497,13 @@ +@@ -5357,13 +5501,13 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -596,7 +601,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee ); let tab = this.tabContainer.findNextTab(aTab, { -@@ -5379,7 +5519,7 @@ +@@ -5379,7 +5523,7 @@ } if (tab) { @@ -605,7 +610,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee } // If no qualifying visible tab was found, see if there is a tab in -@@ -5400,7 +5540,7 @@ +@@ -5400,7 +5544,7 @@ }); } @@ -614,7 +619,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee } _blurTab(aTab) { -@@ -5802,10 +5942,10 @@ +@@ -5802,10 +5946,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -627,7 +632,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -5864,6 +6004,7 @@ +@@ -5864,6 +6008,7 @@ * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab */ replaceTabWithWindow(aTab, aOptions) { @@ -635,7 +640,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee if (this.tabs.length == 1) { return null; } -@@ -5997,7 +6138,7 @@ +@@ -5997,7 +6142,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -644,7 +649,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee } /** -@@ -6073,8 +6214,8 @@ +@@ -6073,8 +6218,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -655,7 +660,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -6100,10 +6241,16 @@ +@@ -6100,10 +6245,16 @@ this.#handleTabMove( element, () => { @@ -674,7 +679,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee if (neighbor && this.isTab(element) && tabIndex > element._tPos) { neighbor.after(element); } else { -@@ -6161,23 +6308,28 @@ +@@ -6161,23 +6312,28 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -709,7 +714,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -6190,14 +6342,34 @@ +@@ -6190,14 +6346,34 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -745,7 +750,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -6206,7 +6378,7 @@ +@@ -6206,7 +6382,7 @@ element, () => { if (moveBefore) { @@ -754,7 +759,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee } else if (targetElement) { targetElement.after(element); } else { -@@ -6252,10 +6424,10 @@ +@@ -6252,10 +6428,10 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToGroup(aTab, aGroup, metricsContext) { @@ -767,7 +772,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee return; } if (aTab.group && aTab.group.id === aGroup.id) { -@@ -6285,6 +6457,7 @@ +@@ -6285,6 +6461,7 @@ let state = { tabIndex: tab._tPos, @@ -775,7 +780,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -6311,7 +6484,7 @@ +@@ -6311,7 +6488,7 @@ let changedTabGroup = previousTabState.tabGroupId != currentTabState.tabGroupId; @@ -784,7 +789,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -6348,6 +6521,10 @@ +@@ -6348,6 +6525,10 @@ moveActionCallback(); @@ -795,7 +800,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7249,7 +7426,7 @@ +@@ -7249,7 +7430,7 @@ // preventDefault(). It will still raise the window if appropriate. break; } @@ -804,7 +809,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee window.focus(); aEvent.preventDefault(); break; -@@ -7264,7 +7441,6 @@ +@@ -7264,7 +7445,6 @@ } case "TabGroupCollapse": aEvent.target.tabs.forEach(tab => { @@ -812,7 +817,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee }); break; case "TabGroupCreateByUser": -@@ -8199,6 +8375,7 @@ +@@ -8199,6 +8379,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -820,7 +825,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9200,7 +9377,7 @@ var TabContextMenu = { +@@ -9200,7 +9381,7 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; @@ -829,7 +834,7 @@ index 3204f253c23551650991d3385dd256d55892a012..0285b0bcf1e5ba769011c82729e010ee // Build Ask Chat items TabContextMenu.GenAI.buildTabMenu( document.getElementById("context_askChat"), -@@ -9520,6 +9697,7 @@ var TabContextMenu = { +@@ -9520,6 +9701,7 @@ var TabContextMenu = { ) ); } else { diff --git a/src/browser/components/urlbar/UrlbarView-sys-mjs.patch b/src/browser/components/urlbar/UrlbarView-sys-mjs.patch index 0ece37d21..acf8fdfd8 100644 --- a/src/browser/components/urlbar/UrlbarView-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarView-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs -index fdbab8806fd320f4aacec46a42c8ef953580d00c..40568280c3ba2f0a36f4443a5116430d3c502ec1 100644 +index fdbab8806fd320f4aacec46a42c8ef953580d00c..5ed31d5dbfa2e2041e6616f6b036d67928e64114 100644 --- a/browser/components/urlbar/UrlbarView.sys.mjs +++ b/browser/components/urlbar/UrlbarView.sys.mjs @@ -613,7 +613,7 @@ export class UrlbarView { @@ -27,7 +27,7 @@ index fdbab8806fd320f4aacec46a42c8ef953580d00c..40568280c3ba2f0a36f4443a5116430d + setAccessibleFocus: + this.controller._userSelectionBehavior == "arrow", + }); -+ }, 150); ++ }, 140); } } diff --git a/src/zen/common/ZenCustomizableUI.sys.mjs b/src/zen/common/ZenCustomizableUI.sys.mjs index 2279b4074..04304dec0 100644 --- a/src/zen/common/ZenCustomizableUI.sys.mjs +++ b/src/zen/common/ZenCustomizableUI.sys.mjs @@ -111,7 +111,7 @@ export var ZenCustomizableUI = new (class { return; } const popup = window.document.getElementById('zenCreateNewPopup'); - popup.openPopup(button, 'after_start'); + popup.openPopup(button, 'before_start'); }); } diff --git a/src/zen/common/ZenUIManager.mjs b/src/zen/common/ZenUIManager.mjs index 365e13fee..5bebdfe91 100644 --- a/src/zen/common/ZenUIManager.mjs +++ b/src/zen/common/ZenUIManager.mjs @@ -274,22 +274,23 @@ var gZenUIManager = { return true; }, - handleNewTab(werePassedURL, searchClipboard, where) { + handleNewTab(werePassedURL, searchClipboard, where, overridePreferance = false) { // Validate browser state first if (!this._validateBrowserState()) { console.warn('Browser state invalid for new tab operation'); return false; } - if (this.testingEnabled) { + if (this.testingEnabled && !overridePreferance) { return false; } const shouldOpenURLBar = - gZenVerticalTabsManager._canReplaceNewTab && - !werePassedURL && - !searchClipboard && - where === 'tab'; + overridePreferance || + (gZenVerticalTabsManager._canReplaceNewTab && + !werePassedURL && + !searchClipboard && + where === 'tab'); if (!shouldOpenURLBar) { return false; @@ -399,7 +400,10 @@ var gZenUIManager = { if (gURLBar.focused) { setTimeout(() => { - gURLBar.view.close({ elementPicked: onSwitch }); + window.dispatchEvent( + new CustomEvent('ZenURLBarClosed', { detail: { onSwitch, onElementPicked } }) + ); + gURLBar.view.close({ elementPicked: onElementPicked }); gURLBar.updateTextOverflow(); // Ensure tab and browser are valid before updating state diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 78a4cdc50..5d8316589 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -79,6 +79,11 @@ document.addEventListener( case 'cmd_zenSplitViewLinkInNewTab': gZenViewSplitter.splitLinkInNewTab(); break; + case 'cmd_zenNewEmptySplit': + setTimeout(() => { + gZenViewSplitter.createEmptySplit(); + }, 0); + break; case 'cmd_zenReplacePinnedUrlWithCurrent': gZenPinnedTabManager.replacePinnedUrlWithCurrent(); break; diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 8d309e357..245de5c0e 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -816,7 +816,7 @@ class nsZenKeyboardShortcutsLoader { } class nsZenKeyboardShortcutsVersioner { - static LATEST_KBS_VERSION = 10; + static LATEST_KBS_VERSION = 11; constructor() {} @@ -1078,6 +1078,21 @@ class nsZenKeyboardShortcutsVersioner { ) ); } + + if (version < 11) { + // Migrate from version 10 to 11 + data.push( + new KeyShortcut( + 'zen-new-empty-split-view', + '+', + '', + ZEN_SPLIT_VIEW_SHORTCUTS_GROUP, + nsKeyShortcutModifiers.fromObject({ accel: true, alt: true }), + 'cmd_zenNewEmptySplit', + 'zen-new-empty-split-view-shortcut' + ) + ); + } return data; } } diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 39c8b4edc..96023bbe9 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -1008,6 +1008,15 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { tab.linkedBrowser.docShellIsActive = true; } this._maybeRemoveFakeBrowser(); + { + const shouldDisableEmptySplits = tab.hasAttribute('zen-empty-tab') || tab.splitView; + const command = document.getElementById('cmd_zenNewEmptySplit'); + if (shouldDisableEmptySplits) { + command.setAttribute('disabled', 'true'); + } else { + command.removeAttribute('disabled'); + } + } } /** @@ -1913,6 +1922,42 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { } return true; } + + createEmptySplit() { + const selectedTab = gBrowser.selectedTab; + const emptyTab = gZenWorkspaces._emptyTab; + const data = { + tabs: [selectedTab, emptyTab], + gridType: 'grid', + layoutTree: this.calculateLayoutTree([selectedTab, emptyTab], 'grid'), + }; + this._data.push(data); + this.activateSplitView(data); + gBrowser.selectedTab = emptyTab; + window.addEventListener( + 'ZenURLBarClosed', + (event) => { + const { onElementPicked } = event.detail; + const groupIndex = this._data.findIndex((group) => group.tabs.includes(emptyTab)); + const newSelectedTab = gBrowser.selectedTab; + requestAnimationFrame(() => { + this.removeTabFromGroup(emptyTab, groupIndex); + if (onElementPicked) { + if (newSelectedTab === emptyTab || newSelectedTab === selectedTab) { + return; + } + this.splitTabs([selectedTab, newSelectedTab], 'grid', 1); + } else { + gBrowser.selectedTab = selectedTab; + } + }); + }, + { once: true } + ); + setTimeout(() => { + gZenUIManager.handleNewTab(false, false, 'tab', true); + }, 0); + } } window.gZenViewSplitter = new nsZenViewSplitter(); diff --git a/src/zen/tests/split_view/browser.toml b/src/zen/tests/split_view/browser.toml index 173a8f13a..d2b498968 100644 --- a/src/zen/tests/split_view/browser.toml +++ b/src/zen/tests/split_view/browser.toml @@ -14,3 +14,4 @@ support-files = [ ["browser_split_browser_duplication.js"] ["browser_split_view_with_glance.js"] ["browser_split_view_with_folders.js"] +["browser_split_view_empty.js"] diff --git a/src/zen/tests/split_view/browser_split_view_empty.js b/src/zen/tests/split_view/browser_split_view_empty.js new file mode 100644 index 000000000..a019524ee --- /dev/null +++ b/src/zen/tests/split_view/browser_split_view_empty.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + 'resource://testing-common/UrlbarTestUtils.sys.mjs' +); + +add_task(async function test_Split_View_Empty() { + await BrowserTestUtils.withNewTab('https://example.com', async function () { + const originalTab = gBrowser.selectedTab; + const command = document.getElementById('cmd_zenNewEmptySplit'); + command.doCommand(); + await UrlbarTestUtils.promisePopupOpen(window, () => {}); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: 'https://example.com', + }); + const waitForActivationPromise = BrowserTestUtils.waitForEvent( + window, + 'ZenViewSplitter:SplitViewActivated' + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(result.element.row, {}); + await waitForActivationPromise; + await new Promise((resolve) => { + setTimeout(async () => { + resolve(); + }, 100); + }); + const selectedTab = gBrowser.selectedTab; + ok( + gBrowser.tabpanels.hasAttribute('zen-split-view'), + 'The split view should not have crashed with two tabs in it' + ); + ok(!gZenWorkspaces._emptyTab.splitView, 'The empty tab should not be in split view'); + ok(!gZenWorkspaces._emptyTab.group, 'The empty tab should not be in a group'); + ok(selectedTab.splitView, 'The selected tab should be in split view'); + ok(originalTab.splitView, 'The original tab should be in split view'); + Assert.equal( + gBrowser.tabpanels.querySelectorAll('[zen-split="true"]').length, + 2, + 'There should be two split views present' + ); + await BrowserTestUtils.removeTab(selectedTab); + }); +}); diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs index 18283e4f0..ccda325d0 100644 --- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs +++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs @@ -64,21 +64,22 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { } /** + * @param {Window} window The window to check available actions for. * @returns All the available global actions. */ - get #availableActions() { - return globalActions.filter((a) => - typeof a.isAvailable === 'function' ? a.isAvailable() : true - ); + #getAvailableActions(window) { + return globalActions.filter((a) => a.isAvailable(window)); } /** * Starts a search query amongst the available global actions. * - * @param {string} queryContext The query context object + * @param {string} query The user's search query. + * */ #findMatchingActions(query) { - const actions = this.#availableActions; + const window = lazy.BrowserWindowTracker.getTopWindow(); + const actions = this.#getAvailableActions(window); let results = []; for (let action of actions) { const label = action.label; @@ -261,6 +262,10 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { return; } const ownerGlobal = details.element.ownerGlobal; + if (typeof command === 'function') { + command(ownerGlobal); + return; + } const commandToRun = ownerGlobal.document.getElementById(command); if (commandToRun) { ownerGlobal.gBrowser.selectedBrowser.focus(); diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs index 1b831ba03..40a273731 100644 --- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export const globalActions = [ +const globalActionsTemplate = [ { label: 'Toggle Compact Mode', command: 'cmd_zenCompactModeToggle', @@ -15,4 +15,33 @@ export const globalActions = [ icon: 'chrome://browser/skin/zen-icons/edit-theme.svg', suggestedIndex: 4, }, + { + label: 'New Split View', + command: 'cmd_zenNewEmptySplit', + icon: 'chrome://browser/skin/zen-icons/split.svg', + suggestedIndex: 0, + }, + { + label: 'New Folder', + command: 'cmd_zenOpenFolderCreation', + icon: 'chrome://browser/skin/zen-icons/folder.svg', + }, + { + label: 'Copy Current URL', + command: 'cmd_zenCopyCurrentURL', + icon: 'chrome://browser/skin/zen-icons/edit-copy.svg', + suggestedIndex: 0, + }, + { + label: 'Settings', + command: (window) => window.openPreferences(), + icon: 'chrome://browser/skin/zen-icons/settings.svg', + }, ]; + +export const globalActions = globalActionsTemplate.map((action) => ({ + ...action, + isAvailable: (window) => { + return window.document.getElementById(action.command)?.getAttribute('disabled') !== 'true'; + }, +})); From 148e63e2265b24b2eff9462de45dd8b146cfda29 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 14 Sep 2025 20:02:20 +0200 Subject: [PATCH 2/4] fix: Fixed temporary containers not working on zen, b=closes #172, c=no-component --- src/toolkit/components/extensions/parent/ext-runtime-js.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/toolkit/components/extensions/parent/ext-runtime-js.patch b/src/toolkit/components/extensions/parent/ext-runtime-js.patch index cc8d19e1a..3e3a78344 100644 --- a/src/toolkit/components/extensions/parent/ext-runtime-js.patch +++ b/src/toolkit/components/extensions/parent/ext-runtime-js.patch @@ -1,5 +1,5 @@ diff --git a/toolkit/components/extensions/parent/ext-runtime.js b/toolkit/components/extensions/parent/ext-runtime.js -index 0d7a3e505b6bd30548c6dda1504dd343a517b083..71bab6f1562ef6ec43541e52573d2ed5c4e8e3af 100644 +index 0d7a3e505b6bd30548c6dda1504dd343a517b083..54400def5e02e886765fab68c3854a6b3c24ef2b 100644 --- a/toolkit/components/extensions/parent/ext-runtime.js +++ b/toolkit/components/extensions/parent/ext-runtime.js @@ -333,7 +333,7 @@ this.runtime = class extends ExtensionAPIPersistent { @@ -7,7 +7,7 @@ index 0d7a3e505b6bd30548c6dda1504dd343a517b083..71bab6f1562ef6ec43541e52573d2ed5 getBrowserInfo: function () { const { name, vendor, version, appBuildID } = Services.appinfo; - const info = { name, vendor, version, buildID: appBuildID }; -+ const info = { name, vendor, version: AppConstants.ZEN_FIREFOX_VERSION, buildID: appBuildID }; ++ const info = { name: 'firefox', vendor, version: AppConstants.ZEN_FIREFOX_VERSION, buildID: appBuildID }; return Promise.resolve(info); }, From d3cca04c15b38d796547ff3d8ca982772fb025a3 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 14 Sep 2025 21:31:04 +0200 Subject: [PATCH 3/4] feat: Urlbar now supports 'switch space' and 'extensions', b=no-bug, c=common, kbs --- .../urlbar/UrlbarView-sys-mjs.patch | 24 ++-- src/zen/common/styles/zen-urlbar.css | 64 +++++++-- src/zen/kbs/ZenKeyboardShortcuts.mjs | 3 + src/zen/urlbar/ZenUBActionsProvider.sys.mjs | 132 ++++++++++++++++-- src/zen/urlbar/ZenUBGlobalActions.sys.mjs | 13 +- 5 files changed, 199 insertions(+), 37 deletions(-) diff --git a/src/browser/components/urlbar/UrlbarView-sys-mjs.patch b/src/browser/components/urlbar/UrlbarView-sys-mjs.patch index acf8fdfd8..43515e50a 100644 --- a/src/browser/components/urlbar/UrlbarView-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarView-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs -index fdbab8806fd320f4aacec46a42c8ef953580d00c..5ed31d5dbfa2e2041e6616f6b036d67928e64114 100644 +index fdbab8806fd320f4aacec46a42c8ef953580d00c..f327ca63ff4c99f7aa9acbddc69d2d8b80442d35 100644 --- a/browser/components/urlbar/UrlbarView.sys.mjs +++ b/browser/components/urlbar/UrlbarView.sys.mjs @@ -613,7 +613,7 @@ export class UrlbarView { @@ -11,27 +11,25 @@ index fdbab8806fd320f4aacec46a42c8ef953580d00c..5ed31d5dbfa2e2041e6616f6b036d679 // Try to reuse the cached top-sites context. If it's not cached, then // there will be a gap of time between when the input is focused and // when the view opens that can be perceived as flicker. -@@ -823,6 +823,19 @@ export class UrlbarView { +@@ -823,7 +823,16 @@ export class UrlbarView { // them, resembling tab-to-search. In that case, the input value is // still associated with the first result. this.input.setResultForCurrentValue(firstResult); -+ } else if (firstResult.payload.zenAction) { -+ this.#selectElement(this.getFirstSelectableElement(), { -+ updateInput: false, -+ setAccessibleFocus: -+ this.controller._userSelectionBehavior == "arrow", -+ }); -+ this.window.setTimeout(() => { +- } ++ } ++ this.window.setTimeout(() => { ++ if (queryContext.results[0].payload.zenAction) { + this.#selectElement(this.getFirstSelectableElement(), { + updateInput: false, + setAccessibleFocus: + this.controller._userSelectionBehavior == "arrow", + }); -+ }, 140); - } ++ } ++ }, 10); } -@@ -1341,7 +1354,7 @@ export class UrlbarView { + // Announce tab-to-search results to screen readers as the user types. +@@ -1341,7 +1350,7 @@ export class UrlbarView { includeHiddenExposures: true, }); let canBeVisible = @@ -40,7 +38,7 @@ index fdbab8806fd320f4aacec46a42c8ef953580d00c..5ed31d5dbfa2e2041e6616f6b036d679 if (result.isHiddenExposure) { if (canBeVisible) { this.controller.engagementEvent.addExposure( -@@ -3189,7 +3202,7 @@ export class UrlbarView { +@@ -3189,7 +3198,7 @@ export class UrlbarView { } #enableOrDisableRowWrap() { diff --git a/src/zen/common/styles/zen-urlbar.css b/src/zen/common/styles/zen-urlbar.css index fe49658f8..9ac50f559 100644 --- a/src/zen/common/styles/zen-urlbar.css +++ b/src/zen/common/styles/zen-urlbar.css @@ -563,21 +563,52 @@ button.popup-notification-dropmarker { } .urlbarView-shortcutContent { - border-radius: 4px; - padding: 6px 8px; - font-size: 10px; - font-weight: 600; text-transform: uppercase; margin-left: auto; - margin-top: auto; - margin-bottom: auto; + background-color: color-mix(in srgb, var(--zen-branding-bg-reverse), transparent 95%); + padding: 6px 8px; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); + font-size: 10px; &:empty { display: none !important; } } +.urlbarView-prettyName, +.urlbarView-shortcutContent { + border-radius: 4px; + font-weight: 600; + margin-top: auto; + margin-bottom: auto; +} + +.urlbarView-prettyName { + padding: 4px 6px; + background-color: color-mix(in srgb, var(--zen-branding-bg-reverse), transparent 90%); + margin-left: 6px; + font-size: 12px; + align-items: center; + gap: 6px; + display: flex; + color: color-mix(in srgb, var(--zen-primary-color), currentColor 95%); + + & img { + -moz-context-properties: fill; + fill: currentColor; + width: 16px; + height: 16px; + + &[workspaceIcon] { + scale: 1.4; + } + } + + &[hidden] { + display: none !important; + } +} + .urlbarView-row[has-action]:is([type='switchtab'], [type='remotetab'], [type='clipboard']) { & .urlbarView-action:last-child { margin-left: auto !important; @@ -635,14 +666,15 @@ button.popup-notification-dropmarker { --urlbarView-item-inline-padding: 8px; --urlbarView-item-block-padding: 10px; - &:hover.urlbarView-favicon, - &:hover, - & .urlbarView-shortcutContent { - background-color: color-mix( - in srgb, - var(--zen-branding-bg-reverse) 5%, - transparent 95% - ) !important; + &:hover { + &, + & .urlbarView-favicon { + background-color: color-mix( + in srgb, + var(--zen-branding-bg-reverse) 5%, + transparent 95% + ) !important; + } } &[selected] { @@ -669,6 +701,10 @@ button.popup-notification-dropmarker { & .urlbarView-shortcutContent { background-color: rgba(255, 255, 255, 0.9) !important; } + + & .urlbarView-prettyName { + background-color: color-mix(in srgb, var(--zen-branding-bg-reverse), transparent 80%); + } } } diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 245de5c0e..b8630ad6e 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -1377,6 +1377,9 @@ var gZenKeyboardShortcutsManager = { * @returns {string|null} The shortcut as a string or null if not found */ getShortcutDisplayFromCommand(command) { + if (!command) { + return null; + } const shortcut = this.getShortcutFromCommand(command); if (shortcut) { return shortcut.toUserString(); diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs index ccda325d0..26cc09228 100644 --- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs +++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs @@ -5,6 +5,7 @@ import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; import { UrlbarProvider, UrlbarUtils } from 'resource:///modules/UrlbarUtils.sys.mjs'; import { globalActions } from 'resource:///modules/ZenUBGlobalActions.sys.mjs'; +import { ExtensionCommon } from 'resource://gre/modules/ExtensionCommon.sys.mjs'; const lazy = {}; @@ -21,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, { UrlbarTokenizer: 'resource:///modules/UrlbarTokenizer.sys.mjs', QueryScorer: 'resource:///modules/UrlbarProviderInterventions.sys.mjs', BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', + AddonManager: 'resource://gre/modules/AddonManager.sys.mjs', }); XPCOMUtils.defineLazyPreferenceGetter( @@ -30,6 +32,8 @@ XPCOMUtils.defineLazyPreferenceGetter( true ); +let { makeWidgetId } = ExtensionCommon; + /** * A provider that lets the user view all available global actions for a query. */ @@ -63,12 +67,67 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { ); } + #getWorkspaceActions(window) { + if (window.gZenWorkspaces.privateWindowOrDisabled) { + return []; + } + const workspaces = window.gZenWorkspaces._workspaceCache?.workspaces; + if (!workspaces?.length) { + return []; + } + let actions = []; + const activeSpaceUUID = window.gZenWorkspaces.activeWorkspace; + for (const workspace of workspaces) { + if (workspace.uuid !== activeSpaceUUID) { + const accentColor = window.gZenWorkspaces + .workspaceElement(workspace.uuid) + ?.style.getPropertyValue('--zen-primary-color'); + actions.push({ + label: 'Focus on', + extraPayload: { + workspaceId: workspace.uuid, + prettyName: workspace.name, + prettyIcon: workspace.icon, + accentColor, + }, + icon: 'chrome://browser/skin/zen-icons/forward.svg', + }); + } + } + return actions; + } + + async #getExtensionActions(window) { + const addons = await lazy.AddonManager.getAddonsByTypes(['extension']); + return addons + .filter( + (addon) => + addon.isActive && + !addon.isSystem && + window.document.getElementById(makeWidgetId(addon.id) + '-BAP') + ) + .map((addon) => { + return { + icon: 'chrome://browser/skin/zen-icons/extension.svg', + label: 'Extension', + extraPayload: { + extensionId: addon.id, + prettyName: addon.name, + prettyIcon: addon.iconURL, + }, + }; + }); + } + /** * @param {Window} window The window to check available actions for. * @returns All the available global actions. */ - #getAvailableActions(window) { - return globalActions.filter((a) => a.isAvailable(window)); + async #getAvailableActions(window) { + return globalActions + .filter((a) => a.isAvailable(window)) + .concat(this.#getWorkspaceActions(window)) + .concat(await this.#getExtensionActions(window)); } /** @@ -77,12 +136,12 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { * @param {string} query The user's search query. * */ - #findMatchingActions(query) { + async #findMatchingActions(query) { const window = lazy.BrowserWindowTracker.getTopWindow(); - const actions = this.#getAvailableActions(window); + const actions = await this.#getAvailableActions(window); let results = []; for (let action of actions) { - const label = action.label; + const label = action.extraPayload?.prettyName || action.label; const score = this.#calculateFuzzyScore(label, query); if (score > MINIMUM_QUERY_SCORE) { results.push({ @@ -156,7 +215,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { return; } - const actionsResults = this.#findMatchingActions(query); + const actionsResults = await this.#findMatchingActions(query); if (!actionsResults.length) { return; } @@ -174,6 +233,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { shortcutContent: ownerGlobal.gZenKeyboardShortcutsManager.getShortcutDisplayFromCommand( action.command ), + ...action.extraPayload, }); let result = new lazy.UrlbarResult( @@ -207,6 +267,9 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { * @returns {object} An object describing the view update. */ getViewUpdate(result) { + const prettyIconIsSvg = + result.payload.prettyIcon && + (result.payload.prettyIcon.endsWith('.svg') || result.payload.prettyIcon.endsWith('.png')); return { icon: { attributes: { @@ -220,6 +283,27 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { shortcutContent: { textContent: result.payload.shortcutContent || '', }, + prettyName: { + attributes: { + hidden: !result.payload.prettyName, + style: `--zen-primary-color: ${result.payload.accentColor || 'currentColor'}`, + }, + }, + prettyNameStrong: { + textContent: result.payload.prettyName + ? prettyIconIsSvg || !result.payload.prettyIcon + ? result.payload.prettyName + : `${result.payload.prettyIcon} ${result.payload.prettyName}` + : '', + attributes: { dir: 'ltr' }, + }, + prettyNameIcon: { + attributes: { + src: result.payload.prettyIcon || '', + hidden: !prettyIconIsSvg || !result.payload.prettyIcon, + workspaceIcon: !!result.payload.workspaceId, + }, + }, }; } @@ -245,6 +329,23 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { }, ], }, + { + tag: 'span', + classList: ['urlbarView-prettyName'], + hidden: true, + name: 'prettyName', + children: [ + { + tag: 'img', + name: 'prettyNameIcon', + attributes: { hidden: true }, + }, + { + name: 'prettyNameStrong', + tag: 'strong', + }, + ], + }, { name: 'shortcutContent', tag: 'span', @@ -258,14 +359,27 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { const result = details.result; const payload = result.payload; const command = payload.zenCommand; - if (!command) { - return; - } const ownerGlobal = details.element.ownerGlobal; if (typeof command === 'function') { command(ownerGlobal); return; } + // Switch workspace if theres a workspaceId in the payload. + if (payload.workspaceId) { + ownerGlobal.gZenWorkspaces.changeWorkspaceWithID(payload.workspaceId); + return; + } + if (payload.extensionId) { + const widgetId = makeWidgetId(payload.extensionId) + '-BAP'; + const node = ownerGlobal.document.getElementById(widgetId); + if (node) { + node.doCommand(); + } + return; + } + if (!command) { + return; + } const commandToRun = ownerGlobal.document.getElementById(command); if (commandToRun) { ownerGlobal.gBrowser.selectedBrowser.focus(); diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs index 40a273731..0656d1834 100644 --- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -37,11 +37,22 @@ const globalActionsTemplate = [ command: (window) => window.openPreferences(), icon: 'chrome://browser/skin/zen-icons/settings.svg', }, + { + label: 'Open New Window', + command: 'cmd_newNavigator', + icon: 'chrome://browser/skin/zen-icons/window.svg', + }, + { + label: 'Open Private Window', + command: 'Tools:PrivateBrowsing', + icon: 'chrome://browser/skin/zen-icons/private-window.svg', + }, ]; export const globalActions = globalActionsTemplate.map((action) => ({ - ...action, isAvailable: (window) => { return window.document.getElementById(action.command)?.getAttribute('disabled') !== 'true'; }, + extraPayload: {}, + ...action, })); From 9b25a3c950c508351a707c1d822eeaa885f4a030 Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Sun, 14 Sep 2025 21:48:54 +0200 Subject: [PATCH 4/4] feat: Improve addon panel opening method, b=no-bug, c=no-component --- src/zen/urlbar/ZenUBActionsProvider.sys.mjs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs index 26cc09228..52dfe8cc2 100644 --- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs +++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs @@ -5,7 +5,6 @@ import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; import { UrlbarProvider, UrlbarUtils } from 'resource:///modules/UrlbarUtils.sys.mjs'; import { globalActions } from 'resource:///modules/ZenUBGlobalActions.sys.mjs'; -import { ExtensionCommon } from 'resource://gre/modules/ExtensionCommon.sys.mjs'; const lazy = {}; @@ -32,8 +31,6 @@ XPCOMUtils.defineLazyPreferenceGetter( true ); -let { makeWidgetId } = ExtensionCommon; - /** * A provider that lets the user view all available global actions for a query. */ @@ -104,7 +101,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { (addon) => addon.isActive && !addon.isSystem && - window.document.getElementById(makeWidgetId(addon.id) + '-BAP') + window.gUnifiedExtensions.browserActionFor(window.WebExtensionPolicy.getByID(addon.id)) ) .map((addon) => { return { @@ -370,10 +367,11 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { return; } if (payload.extensionId) { - const widgetId = makeWidgetId(payload.extensionId) + '-BAP'; - const node = ownerGlobal.document.getElementById(widgetId); - if (node) { - node.doCommand(); + const action = ownerGlobal.gUnifiedExtensions.browserActionFor( + ownerGlobal.WebExtensionPolicy.getByID(payload.extensionId) + ); + if (action) { + action.openPopup(ownerGlobal, /* without user interaction = */ true); } return; }