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 cedcda356..9806095d5 100644
--- a/src/browser/components/urlbar/UrlbarView-sys-mjs.patch
+++ b/src/browser/components/urlbar/UrlbarView-sys-mjs.patch
@@ -11,24 +11,21 @@ index fdbab8806fd320f4aacec46a42c8ef953580d00c..5501c329a9fd12bcc8e12de396113497
// 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",
+ });
-+ }, 150);
- }
++ }
++ }, 10);
}
@@ -3189,7 +3202,7 @@ export class UrlbarView {
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);
},
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 0ec0960bb..d1a6f535e 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/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css
index d6171d4df..1073f1c93 100644
--- a/src/zen/common/styles/zen-omnibox.css
+++ b/src/zen/common/styles/zen-omnibox.css
@@ -502,21 +502,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;
@@ -575,14 +606,15 @@ button.popup-notification-dropmarker {
--urlbarView-item-block-padding: 10px;
color: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)) !important;
- &: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] {
@@ -612,6 +644,10 @@ button.popup-notification-dropmarker {
fill: var(--zen-selected-color) !important;
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/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..b8630ad6e 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;
}
}
@@ -1362,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/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..52dfe8cc2 100644
--- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
+++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
@@ -21,6 +21,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(
@@ -63,25 +64,81 @@ 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.gUnifiedExtensions.browserActionFor(window.WebExtensionPolicy.getByID(addon.id))
+ )
+ .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.
*/
- get #availableActions() {
- return globalActions.filter((a) =>
- typeof a.isAvailable === 'function' ? a.isAvailable() : true
- );
+ async #getAvailableActions(window) {
+ return globalActions
+ .filter((a) => a.isAvailable(window))
+ .concat(this.#getWorkspaceActions(window))
+ .concat(await this.#getExtensionActions(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;
+ async #findMatchingActions(query) {
+ const window = lazy.BrowserWindowTracker.getTopWindow();
+ 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({
@@ -155,7 +212,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
return;
}
- const actionsResults = this.#findMatchingActions(query);
+ const actionsResults = await this.#findMatchingActions(query);
if (!actionsResults.length) {
return;
}
@@ -173,6 +230,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
shortcutContent: ownerGlobal.gZenKeyboardShortcutsManager.getShortcutDisplayFromCommand(
action.command
),
+ ...action.extraPayload,
});
let result = new lazy.UrlbarResult(
@@ -206,6 +264,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: {
@@ -219,6 +280,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,
+ },
+ },
};
}
@@ -244,6 +326,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',
@@ -257,10 +356,28 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
const result = details.result;
const payload = result.payload;
const command = payload.zenCommand;
+ 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 action = ownerGlobal.gUnifiedExtensions.browserActionFor(
+ ownerGlobal.WebExtensionPolicy.getByID(payload.extensionId)
+ );
+ if (action) {
+ action.openPopup(ownerGlobal, /* without user interaction = */ true);
+ }
+ return;
+ }
if (!command) {
return;
}
- const ownerGlobal = details.element.ownerGlobal;
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..0656d1834 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,44 @@ 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',
+ },
+ {
+ 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) => ({
+ isAvailable: (window) => {
+ return window.document.getElementById(action.command)?.getAttribute('disabled') !== 'true';
+ },
+ extraPayload: {},
+ ...action,
+}));