From fa1693d16b11ec2140cf3239106257da84596565 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Sun, 14 Dec 2025 12:03:37 +0100 Subject: [PATCH] feat: Added partial support for split views, b=no-bug, c=split-view --- src/zen/sessionstore/ZenWindowSync.sys.mjs | 62 ++++++++++++++++++---- src/zen/split-view/ZenViewSplitter.mjs | 31 +++++++++-- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index e724de6d9..555ca93bd 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -36,6 +36,9 @@ const EVENTS = [ 'TabGroupRemoved', 'TabGroupMoved', + //'ZenTabRemovedFromSplit', + //'ZenSplitViewTabsSplit', + 'TabSelect', 'focus', @@ -358,6 +361,8 @@ class nsZenWindowSync { gBrowser.unpinTab(aTargetItem); } } + } else { + aTargetItem.pinned = aOriginalItem.pinned; } this.#moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, { @@ -673,10 +678,9 @@ class nsZenWindowSync { * @param {Object} aPreviousTab - The previously selected tab. */ async #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) { - const selectedTab = aWindow.gBrowser.selectedTab; // On some occasions, such as when closing a window, this // function might be called multiple times for the same tab. - if (selectedTab === this.#lastSelectedTab) { + if (aWindow.gBrowser.selectedTab === this.#lastSelectedTab) { return; } if (aPreviousTab?._zenContentsVisible) { @@ -691,15 +695,20 @@ class nsZenWindowSync { await this.#swapBrowserDocShellsAsync(otherTabToShow, aPreviousTab); } } - if (selectedTab._zenContentsVisible || selectedTab.hasAttribute('zen-empty-tab')) { - return; - } - const otherSelectedTab = this.#getActiveTabFromOtherWindows(aWindow, selectedTab.id); - selectedTab._zenContentsVisible = true; - if (otherSelectedTab) { - delete otherSelectedTab._zenContentsVisible; - await this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab); + let promises = []; + for (const browserView of aWindow.gBrowser.selectedBrowsers) { + const selectedTab = aWindow.gBrowser.getTabForBrowser(browserView); + if (selectedTab._zenContentsVisible || selectedTab.hasAttribute('zen-empty-tab')) { + continue; + } + const otherSelectedTab = this.#getActiveTabFromOtherWindows(aWindow, selectedTab.id); + selectedTab._zenContentsVisible = true; + if (otherSelectedTab) { + delete otherSelectedTab._zenContentsVisible; + promises.push(this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab)); + } } + await Promise.all(promises); } /** @@ -931,11 +940,14 @@ class nsZenWindowSync { const window = tabGroup.ownerGlobal; const isFolder = tabGroup.isZenFolder; const isSplitView = tabGroup.hasAttribute('split-view-group'); + if (isSplitView) { + return; // Split view groups are synced via ZenSplitViewTabsSplit event. + } // Tab groups already have an ID upon creation. this.#runOnAllWindows(window, (win) => { const newGroup = isFolder ? win.gZenFolders.createFolder([], {}) - : win.gBrowser.addTabGroup({ splitView: isSplitView }); + : win.gBrowser.addTabGroup([]); newGroup.id = tabGroup.id; newGroup.alreadySynced = true; this.#syncItemWithOriginal( @@ -969,6 +981,34 @@ class nsZenWindowSync { on_TabGroupUpdate(aEvent) { return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON | SYNC_FLAG_LABEL); } + + on_ZenTabRemovedFromSplit(aEvent) { + const tab = aEvent.target; + const window = tab.ownerGlobal; + this.#runOnAllWindows(window, (win) => { + const targetTab = this.#getItemFromWindow(win, tab.id); + if (targetTab && win.gZenViewSplitter) { + win.gZenViewSplitter.removeTabFromGroup(targetTab); + } + }); + } + + on_ZenSplitViewTabsSplit(aEvent) { + const tabGroup = aEvent.target; + const window = tabGroup.ownerGlobal; + const tabs = tabGroup.tabs; + this.#runOnAllWindows(window, (win) => { + const otherWindowTabs = tabs + .map((tab) => this.#getItemFromWindow(win, tab.id)) + .filter(Boolean); + if (otherWindowTabs.length > 0 && win.gZenViewSplitter) { + const group = win.gZenViewSplitter.splitTabs(otherWindowTabs, 'grid'); + if (group) { + group.id = tabGroup.id; + } + } + }); + } } export const ZenWindowSync = new nsZenWindowSync(); diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 89488cd43..4fc621f49 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -202,6 +202,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { this.resetTabState(tab, forUnsplit); if (tab.group && tab.group.hasAttribute('split-view-group')) { gBrowser.ungroupTab(tab); + this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab); } if (group.tabs.length < 2) { // We need to remove all remaining tabs from the group when unsplitting @@ -895,6 +896,21 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { } } + /** + * Dispatches a custom event on a tab. + * + * @param {string} eventName - The name of the event to dispatch. + * @param {HTMLElement} item - The item on which to dispatch the event. + */ + #dispatchItemEvent(eventName, item) { + const event = new CustomEvent(eventName, { + detail: { item }, + bubbles: true, + cancelable: false, + }); + item.dispatchEvent(event); + } + /** * Removes a group. * @@ -905,6 +921,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { for (const tab of group.tabs.reverse()) { if (tab.group?.hasAttribute('split-view-group')) { gBrowser.ungroupTab(tab); + this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab); } } if (this.currentView === groupIndex) { @@ -1069,7 +1086,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { * @param {string|undefined} gridType - The type of grid layout. */ splitTabs(tabs, gridType, initialIndex = 0) { - this.#withoutSplitViewTransition(() => { + return this.#withoutSplitViewTransition(() => { // TODO: Add support for splitting essential tabs tabs = tabs.filter((t) => !t.hidden && !t.hasAttribute('zen-empty-tab')); if (tabs.length < 2 || tabs.length > this.MAX_TABS) { @@ -1105,7 +1122,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { return; } this.activateSplitView(group, true); - return; + this.#dispatchItemEvent('ZenSplitViewTabsSplit', group); + return group; } // We are here if none of the tabs have been previously split @@ -1150,6 +1168,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { return; } this.activateSplitView(splitData); + this.#dispatchItemEvent('ZenSplitViewTabsSplit', splitGroup); + return splitData; }); } @@ -1855,9 +1875,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { } // We can't create an empty group, so only create if we have tabs + let group = null; if (tabs?.length) { // Create a new group with the initial tabs - gBrowser.addTabGroup(tabs, { + group = gBrowser.addTabGroup(tabs, { label: '', showCreateUI: false, insertBefore: tabs[0], @@ -1865,7 +1886,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { }); } - return null; + return group; } storeDataForSessionStore() { @@ -1936,7 +1957,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { #withoutSplitViewTransition(callback) { this.tabBrowserPanel.classList.add('zen-split-view-no-transition'); try { - callback(); + return callback(); } finally { requestAnimationFrame(() => { this.tabBrowserPanel.classList.remove('zen-split-view-no-transition');